Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support of openzeppelin v5 transparent proxy #298

Merged
merged 2 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/proxyAdmin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {AddressLike, Contract, Transaction} from "ethers";
import {ethers, upgrades} from "hardhat";
import chalk from "chalk";

export const getProxyAdmin = async (proxy: AddressLike) => {
const proxyAdminAddress = await upgrades.erc1967.getAdminAddress(
await ethers.resolveAddress(proxy)
);
const generalProxyAdminAbi = [
"function UPGRADE_INTERFACE_VERSION() view returns (string)",
"function upgrade(address,address)",
"function upgradeAndCall(address,address,bytes) payable",
"function owner() view returns (address)"
];
return new ethers.Contract(
proxyAdminAddress,
generalProxyAdminAbi,
await ethers.provider.getSigner()
);
}

export const isNewProxyAdmin = async (proxyAdmin: Contract) => {
try {
console.log(chalk.gray(`ProxyAdmin version ${
// This function name is set in external library
// eslint-disable-next-line new-cap
await proxyAdmin.UPGRADE_INTERFACE_VERSION()
}`));
return true;
} catch (error) {
console.log(chalk.gray("Use old ProxyAdmin"));
return false;
}
}

export const getUpgradeTransaction = async (proxy: AddressLike, implementation: AddressLike) => {
const proxyAdmin = await getProxyAdmin(proxy);
if (await isNewProxyAdmin(proxyAdmin)) {
return Transaction.from({
"data": proxyAdmin.interface.encodeFunctionData(
"upgradeAndCall",
[
await ethers.resolveAddress(proxy),
await ethers.resolveAddress(implementation),
"0x"
]
),
"to": await ethers.resolveAddress(proxyAdmin)
});
}
return Transaction.from({
"data": proxyAdmin.interface.encodeFunctionData(
"upgrade",
[
await ethers.resolveAddress(proxy),
await ethers.resolveAddress(implementation),
]
),
"to": await ethers.resolveAddress(proxyAdmin)
});
}
16 changes: 11 additions & 5 deletions src/submitters/auto-submitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
} from "./safe-ima-legacy-marionette-submitter";
import {SafeSubmitter} from "./safe-submitter";
import {Submitter} from "./submitter";

import {Upgrader} from "../upgrader";
import chalk from "chalk";
import hre from "hardhat";
Expand All @@ -16,6 +15,14 @@ import {skaleContracts} from "@skalenetwork/skale-contracts-ethers-v6";

export class AutoSubmitter extends Submitter {
name = "Auto Submitter";
upgrader: Upgrader

constructor (
upgrader: Upgrader
) {
super();
this.upgrader = upgrader
}

static marionetteInterface = [
{
Expand All @@ -35,15 +42,14 @@ export class AutoSubmitter extends Submitter {

async submit (transactions: Transaction[]) {
console.log(`Submit via ${this.name}`);
const submitter = await AutoSubmitter.getSubmitter();
const submitter = await this.getSubmitter();
await submitter.submit(transactions);
}

// Private

private static async getSubmitter () {
const proxyAdmin = await Upgrader.getProxyAdmin();
const owner = await proxyAdmin.owner();
private async getSubmitter () {
const owner = await this.upgrader.getOwner();
if (await hre.ethers.provider.getCode(owner) === "0x") {
console.log("Owner is not a contract");
return new EoaSubmitter();
Expand Down
120 changes: 46 additions & 74 deletions src/upgrader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Contract, ContractFactory, Transaction} from "ethers";
import {ContractFactory, Transaction} from "ethers";
import {ContractToUpgrade, Project} from "./types/upgrader";
import {Manifest, getImplementationAddress} from "@openzeppelin/upgrades-core";
import {ethers, network, upgrades} from "hardhat";
import {getProxyAdmin, getUpgradeTransaction} from "./proxyAdmin";
import {AutoSubmitter} from "./submitters/auto-submitter";
import {EXIT_CODES} from "./exitCodes";
import {Instance} from "@skalenetwork/skale-contracts-ethers-v6";
Expand All @@ -11,6 +11,7 @@
import chalk from "chalk";
import {promises as fs} from "fs";
import {getContractFactoryAndUpdateManifest} from "./contractFactory";
import {getImplementationAddress} from "@openzeppelin/upgrades-core";
import {getVersion} from "./version";
import {verify} from "./verification";

Expand All @@ -18,25 +19,26 @@
const withoutNull = <T>(array: Array<T | null>) => array.
filter((element) => element !== null) as Array<T>;

// TODO: Set to 8 when upgrade plugins become thread safe

Check warning on line 22 in src/upgrader.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Unexpected 'todo' comment: 'TODO: Set to 8 when upgrade plugins...'

Check warning on line 22 in src/upgrader.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected 'todo' comment: 'TODO: Set to 8 when upgrade plugins...'

Check warning on line 22 in src/upgrader.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Unexpected 'todo' comment: 'TODO: Set to 8 when upgrade plugins...'

Check warning on line 22 in src/upgrader.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected 'todo' comment: 'TODO: Set to 8 when upgrade plugins...'
const maxSimultaneousDeployments = 1;
// 10 minutes
const deployTimeout = 60e4;


export abstract class Upgrader {
instance: Instance;
targetVersion: string;
contractNamesToUpgrade: string[];
projectName: string;
transactions: Transaction[];
submitter: Submitter;
nonceProvider?: NonceProvider;
deploySemaphore: Semaphore;
private targetVersion: string;
private contractNamesToUpgrade: string[];
private projectName: string;
private submitter: Submitter;
private nonceProvider?: NonceProvider;
private deploySemaphore: Semaphore;

protected instance: Instance;
protected transactions: Transaction[];

constructor (
project: Project,
submitter: Submitter = new AutoSubmitter()
submitter?: Submitter
) {
this.targetVersion = project.version;
if (!project.version.includes("-")) {
Expand All @@ -46,7 +48,7 @@
this.contractNamesToUpgrade = project.contractNamesToUpgrade;
this.projectName = project.name;
this.transactions = [];
this.submitter = submitter;
this.submitter = submitter ?? new AutoSubmitter(this);
this.deploySemaphore = new Semaphore(maxSimultaneousDeployments);
}

Expand All @@ -69,8 +71,7 @@
await this.callDeployNewContracts();
const contractsToUpgrade = await this.deployNewImplementations();
await this.switchToNewImplementations(
contractsToUpgrade,
await Upgrader.getProxyAdmin()
contractsToUpgrade
);
await this.callInitialize();
// Write version
Expand All @@ -81,23 +82,29 @@
console.log("Done");
}

static async getProxyAdmin() {
const manifest = await Manifest.forNetwork(network.provider);
const adminDeployment = await manifest.getAdmin();
if (!adminDeployment) {
throw new Error("Can't load ProxyAdmin address");
}
const generalProxyAdminAbi = [
"function UPGRADE_INTERFACE_VERSION() view returns (string)",
"function upgrade(address,address)",
"function upgradeAndCall(address,address,bytes) payable",
"function owner() view returns (address)"
];
return new ethers.Contract(
adminDeployment.address,
generalProxyAdminAbi,
await ethers.provider.getSigner()
async getOwner() {
const proxyAddresses = await Promise.all(
this.contractNamesToUpgrade.map(
(contract) => this.instance.getContractAddress(contract),
this
)
);
const admins = await Promise.all(
proxyAddresses.map(
(proxy) => getProxyAdmin(proxy)
)
);
const owners = await Promise.all(
admins.map(
(admin) => admin.owner() as Promise<string>
)
);
return owners.reduce( (owner1, owner2) => {
if (owner1 !== owner2) {
throw Error("Proxies have different owners");
}
return owner1;
})
}

// Private
Expand Down Expand Up @@ -148,42 +155,21 @@
}

private async switchToNewImplementations (
contractsToUpgrade: ContractToUpgrade[],
proxyAdmin: Contract
contractsToUpgrade: ContractToUpgrade[]
) {
const proxyAdminAddress = await proxyAdmin.getAddress();
const newProxyAdmin = await Upgrader.isNewProxyAdmin(proxyAdmin);
for (const contract of contractsToUpgrade) {
const upgradeTransactions = await Promise.all(
contractsToUpgrade.map(
(contract) => getUpgradeTransaction(contract.proxyAddress, contract.implementationAddress)
)
);
contractsToUpgrade.forEach((contract, index) => {
const infoMessage =
`Prepare transaction to upgrade ${contract.name}` +
` at ${contract.proxyAddress}` +
` to ${contract.implementationAddress}`;
console.log(chalk.yellowBright(infoMessage));
let transaction = Transaction.from({
"data": proxyAdmin.interface.encodeFunctionData(
"upgradeAndCall",
[
contract.proxyAddress,
contract.implementationAddress,
"0x"
]
),
"to": proxyAdminAddress
});
if (!newProxyAdmin) {
transaction = Transaction.from({
"data": proxyAdmin.interface.encodeFunctionData(
"upgrade",
[
contract.proxyAddress,
contract.implementationAddress
]
),
"to": proxyAdminAddress
});
}
this.transactions.push(transaction);
}
this.transactions.push(upgradeTransactions[index]);
});
}

private async deployNewImplementations () {
Expand Down Expand Up @@ -280,18 +266,4 @@
console.log(chalk.yellow(cannotCheckMessage));
}
}

private static async isNewProxyAdmin(proxyAdmin: Contract) {
try {
console.log(chalk.gray(`ProxyAdmin version ${
// This function name is set in external library
// eslint-disable-next-line new-cap
await proxyAdmin.UPGRADE_INTERFACE_VERSION()
}`));
return true;
} catch (error) {
console.log(chalk.gray("Use old ProxyAdmin"));
return false;
}
}
}
Loading