From 9bbe3f975eec514fef9c6e0d86e26f424b77d435 Mon Sep 17 00:00:00 2001 From: Shah <10547529+shahthepro@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:18:11 +0400 Subject: [PATCH] Add deployment script for Timelock governance (#2212) * Add deployment script for Timelock governance * Add test * Fix base tests * Prettify * Lint * Fix mainnet tests --- contracts/contracts/interfaces/IVault.sol | 2 + .../contracts/vault/OETHBaseVaultCore.sol | 9 +- .../deploy/base/011_transfer_governance.js | 87 +++++++++++++++++++ contracts/deploy/base/012_claim_governance.js | 74 ++++++++++++++++ contracts/hardhat.config.js | 12 ++- contracts/test/_fixture-base.js | 35 ++++---- contracts/test/_fixture.js | 6 +- contracts/test/buyback/buyback.js | 6 +- .../oethb-timelock.base.fork-test.js | 46 ++++++++++ contracts/test/token/oeth.base.fork-test.js | 2 +- .../test/vault/oethb-vault.base.fork-test.js | 10 ++- .../zapper/oethb-zapper.base.fork-test.js | 40 ++++++--- contracts/utils/addresses.js | 1 + contracts/utils/deploy-l2.js | 69 ++++++++------- contracts/utils/deploy.js | 6 +- 15 files changed, 332 insertions(+), 73 deletions(-) create mode 100644 contracts/deploy/base/011_transfer_governance.js create mode 100644 contracts/deploy/base/012_claim_governance.js create mode 100644 contracts/test/governance/oethb-timelock.base.fork-test.js diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index 91fae6480c..e1c4e78160 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -216,6 +216,8 @@ interface IVault { function setDripper(address _dripper) external; + function dripper() external view returns (address); + function weth() external view returns (address); function cacheWETHAssetIndex() external; diff --git a/contracts/contracts/vault/OETHBaseVaultCore.sol b/contracts/contracts/vault/OETHBaseVaultCore.sol index e60e38a0a5..4e2ab6fc6b 100644 --- a/contracts/contracts/vault/OETHBaseVaultCore.sol +++ b/contracts/contracts/vault/OETHBaseVaultCore.sol @@ -79,31 +79,34 @@ contract OETHBaseVaultCore is OETHVaultCore { } // @inheritdoc OETHVaultCore + // solhint-disable-next-line no-unused-vars function requestWithdrawal(uint256 _amount) external virtual override - returns (uint256 requestId, uint256 queued) + returns (uint256, uint256) { revert("Async withdrawals disabled"); } // @inheritdoc OETHVaultCore + // solhint-disable-next-line no-unused-vars function claimWithdrawal(uint256 _requestId) external virtual override - returns (uint256 amount) + returns (uint256) { revert("Async withdrawals disabled"); } // @inheritdoc OETHVaultCore + // solhint-disable-next-line no-unused-vars function claimWithdrawals(uint256[] memory _requestIds) external virtual override - returns (uint256[] memory amounts, uint256 totalAmount) + returns (uint256[] memory, uint256) { revert("Async withdrawals disabled"); } diff --git a/contracts/deploy/base/011_transfer_governance.js b/contracts/deploy/base/011_transfer_governance.js new file mode 100644 index 0000000000..2e7029d0e9 --- /dev/null +++ b/contracts/deploy/base/011_transfer_governance.js @@ -0,0 +1,87 @@ +const { deployOnBaseWithGuardian } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); + +const ADMIN_ROLE = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + +module.exports = deployOnBaseWithGuardian( + { + deployName: "011_transfer_governance", + }, + async ({ ethers }) => { + const cBridgedWOETHProxy = await ethers.getContract( + "BridgedBaseWOETHProxy" + ); + const cBridgedWOETH = await ethers.getContractAt( + "BridgedWOETH", + cBridgedWOETHProxy.address + ); + + const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + const cOETHbProxy = await ethers.getContract("OETHBaseProxy"); + const cWOETHbProxy = await ethers.getContract("WOETHBaseProxy"); + + const cDripperProxy = await ethers.getContract("OETHBaseDripperProxy"); + + const cAMOStrategyProxy = await ethers.getContract( + "AerodromeAMOStrategyProxy" + ); + const cWOETHStrategyProxy = await ethers.getContract( + "BridgedWOETHStrategyProxy" + ); + + return { + actions: [ + { + // 1. Grant admin role to Timelock on Bridged wOETH + // TODO: Revoke role later when everything works fine + contract: cBridgedWOETH, + signature: "grantRole(bytes32,address)", + args: [ADMIN_ROLE, addresses.base.timelock], + }, + { + // 2. Bridged wOETH proxy + contract: cBridgedWOETHProxy, + signature: "transferGovernance(address)", + args: [addresses.base.timelock], + }, + { + // 3. Vault proxy + contract: cOETHbVaultProxy, + signature: "transferGovernance(address)", + args: [addresses.base.timelock], + }, + { + // 4. OETHb proxy + contract: cOETHbProxy, + signature: "transferGovernance(address)", + args: [addresses.base.timelock], + }, + { + // 5. WOETHb proxy + contract: cWOETHbProxy, + signature: "transferGovernance(address)", + args: [addresses.base.timelock], + }, + { + // 6. Dripper proxy + contract: cDripperProxy, + signature: "transferGovernance(address)", + args: [addresses.base.timelock], + }, + { + // 7. AMO Strategy proxy + contract: cAMOStrategyProxy, + signature: "transferGovernance(address)", + args: [addresses.base.timelock], + }, + { + // 8. Bridged WOETH Strategy Proxy + contract: cWOETHStrategyProxy, + signature: "transferGovernance(address)", + args: [addresses.base.timelock], + }, + ], + }; + } +); diff --git a/contracts/deploy/base/012_claim_governance.js b/contracts/deploy/base/012_claim_governance.js new file mode 100644 index 0000000000..aaf643fd45 --- /dev/null +++ b/contracts/deploy/base/012_claim_governance.js @@ -0,0 +1,74 @@ +const { deployOnBaseWithGuardian } = require("../../utils/deploy-l2"); + +module.exports = deployOnBaseWithGuardian( + { + deployName: "012_claim_governance", + useTimelock: true, + }, + async ({ ethers }) => { + const cBridgedWOETHProxy = await ethers.getContract( + "BridgedBaseWOETHProxy" + ); + + const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + const cOETHbProxy = await ethers.getContract("OETHBaseProxy"); + const cWOETHbProxy = await ethers.getContract("WOETHBaseProxy"); + + const cDripperProxy = await ethers.getContract("OETHBaseDripperProxy"); + + const cAMOStrategyProxy = await ethers.getContract( + "AerodromeAMOStrategyProxy" + ); + const cWOETHStrategyProxy = await ethers.getContract( + "BridgedWOETHStrategyProxy" + ); + + return { + name: "Claim Governance on superOETHb contracts", + actions: [ + { + // 1. Bridged wOETH proxy + contract: cBridgedWOETHProxy, + signature: "claimGovernance()", + args: [], + }, + { + // 2. Vault proxy + contract: cOETHbVaultProxy, + signature: "claimGovernance()", + args: [], + }, + { + // 3. OETHb proxy + contract: cOETHbProxy, + signature: "claimGovernance()", + args: [], + }, + { + // 4. WOETHb proxy + contract: cWOETHbProxy, + signature: "claimGovernance()", + args: [], + }, + { + // 5. Dripper proxy + contract: cDripperProxy, + signature: "claimGovernance()", + args: [], + }, + { + // 6. AMO Strategy proxy + contract: cAMOStrategyProxy, + signature: "claimGovernance()", + args: [], + }, + { + // 7. Bridged WOETH Strategy Proxy + contract: cWOETHStrategyProxy, + signature: "claimGovernance()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js index 9218077d91..79b8df3bd7 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -265,15 +265,21 @@ module.exports = { default: ethers.constants.AddressZero, // On Mainnet and fork, the governor is the Governor contract. localhost: - process.env.FORK === "true" + process.env.FORK_NETWORK_NAME == "base" + ? addresses.base.timelock + : process.env.FORK_NETWORK_NAME == "mainnet" || + (!process.env.FORK_NETWORK_NAME && process.env.FORK == "true") ? MAINNET_TIMELOCK : ethers.constants.AddressZero, hardhat: - process.env.FORK === "true" + process.env.FORK_NETWORK_NAME == "base" + ? addresses.base.timelock + : process.env.FORK_NETWORK_NAME == "mainnet" || + (!process.env.FORK_NETWORK_NAME && process.env.FORK == "true") ? MAINNET_TIMELOCK : ethers.constants.AddressZero, mainnet: MAINNET_TIMELOCK, - // Base has no timelock + base: addresses.base.timelock, }, guardianAddr: { default: 1, diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index a3d1c1abdb..75d7b2837e 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -108,31 +108,29 @@ const defaultBaseFixture = deployments.createFixture(async () => { const signers = await hre.ethers.getSigners(); const [minter, burner, rafael, nick, clement] = signers.slice(4); // Skip first 4 addresses to avoid conflict - const { governorAddr, strategistAddr } = await getNamedAccounts(); - const governor = await ethers.getSigner(governorAddr); - const woethGovernor = await ethers.getSigner(await woethProxy.governor()); + const { governorAddr, strategistAddr, timelockAddr } = + await getNamedAccounts(); + const governor = await ethers.getSigner(isFork ? timelockAddr : governorAddr); + + const guardian = await ethers.getSigner(governorAddr); + const timelock = await ethers.getContractAt( + "ITimelockController", + timelockAddr + ); let strategist; if (isFork) { // Impersonate strategist on Fork strategist = await impersonateAndFund(strategistAddr); strategist.address = strategistAddr; - } - - // Make sure we can print bridged WOETH for tests - if (isBaseFork) { - await impersonateAndFund(woethGovernor.address); - - const woethImplAddr = await woethProxy.implementation(); - const latestImplAddr = (await ethers.getContract("BridgedWOETH")).address; - if (woethImplAddr != latestImplAddr) { - await woethProxy.connect(woethGovernor).upgradeTo(latestImplAddr); - } + await impersonateAndFund(governor.address); + await impersonateAndFund(timelock.address); } - await woeth.connect(woethGovernor).grantRole(MINTER_ROLE, minter.address); - await woeth.connect(woethGovernor).grantRole(BURNER_ROLE, burner.address); + // Make sure we can print bridged WOETH for tests + await woeth.connect(governor).grantRole(MINTER_ROLE, minter.address); + await woeth.connect(governor).grantRole(BURNER_ROLE, burner.address); for (const user of [rafael, nick]) { // Mint some bridged WOETH @@ -143,7 +141,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { await weth.connect(user).approve(oethbVault.address, oethUnits("50")); } - await woeth.connect(minter).mint(woethGovernor.address, oethUnits("1")); + await woeth.connect(minter).mint(governor.address, oethUnits("1")); if (isFork) { // Governor opts in for rebasing @@ -189,8 +187,9 @@ const defaultBaseFixture = deployments.createFixture(async () => { // Signers governor, + guardian, + timelock, strategist, - woethGovernor, minter, burner, diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index b70b4d9681..67d0bd70bb 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -667,10 +667,8 @@ const defaultFixture = deployments.createFixture(async () => { if (isFork) { governor = await ethers.provider.getSigner(governorAddr); strategist = await ethers.provider.getSigner(strategistAddr); - timelock = await ethers.provider.getSigner(timelockAddr); - oldTimelock = await ethers.provider.getSigner( - addresses.mainnet.OldTimelock - ); + timelock = await impersonateAndFund(timelockAddr); + oldTimelock = await impersonateAndFund(addresses.mainnet.OldTimelock); } else { timelock = governor; } diff --git a/contracts/test/buyback/buyback.js b/contracts/test/buyback/buyback.js index 38bb2aeb20..94d4ebd970 100644 --- a/contracts/test/buyback/buyback.js +++ b/contracts/test/buyback/buyback.js @@ -1,7 +1,7 @@ const { expect } = require("chai"); const { createFixtureLoader, buybackFixture } = require("../_fixture"); -const { ousdUnits, usdcUnits, oethUnits } = require("../helpers"); +const { ousdUnits, usdcUnits, oethUnits, isCI } = require("../helpers"); const addresses = require("../../utils/addresses"); const { impersonateAndFund } = require("../../utils/signers"); const { setERC20TokenBalance } = require("../_fund"); @@ -11,6 +11,10 @@ const loadFixture = createFixtureLoader(buybackFixture); describe("Buyback", function () { let fixture; + + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + beforeEach(async () => { fixture = await loadFixture(); }); diff --git a/contracts/test/governance/oethb-timelock.base.fork-test.js b/contracts/test/governance/oethb-timelock.base.fork-test.js new file mode 100644 index 0000000000..279e23db98 --- /dev/null +++ b/contracts/test/governance/oethb-timelock.base.fork-test.js @@ -0,0 +1,46 @@ +const { createFixtureLoader } = require("../_fixture"); +const { defaultBaseFixture } = require("../_fixture-base"); +const { expect } = require("chai"); +const addresses = require("../../utils/addresses"); +const { advanceTime, advanceBlocks } = require("../helpers"); + +const baseFixture = createFixtureLoader(defaultBaseFixture); + +describe("ForkTest: OETHb Timelock", function () { + let fixture; + beforeEach(async () => { + fixture = await baseFixture(); + }); + + it("Multisig can propose and execute on Timelock", async () => { + const { guardian, timelock, oethbVault } = fixture; + + const calldata = oethbVault.interface.encodeFunctionData( + "setDripper(address)", + [addresses.dead] + ); + + const args = [ + [oethbVault.address], // Targets + [0], // Values + [calldata], // Calldata, + "0x0000000000000000000000000000000000000000000000000000000000000000", // Predecessor + "0x0000000000000000000000000000000000000000000000000000000000000001", // Salt + ]; + + const minDelay = await timelock.getMinDelay(); + + await timelock.connect(guardian).scheduleBatch( + ...args, + minDelay // minDelay + ); + + // Wait for timelock + await advanceTime(minDelay.toNumber() + 10); + await advanceBlocks(2); + + await timelock.connect(guardian).executeBatch(...args); + + expect(await oethbVault.dripper()).to.eq(addresses.dead); + }); +}); diff --git a/contracts/test/token/oeth.base.fork-test.js b/contracts/test/token/oeth.base.fork-test.js index 5c29cf18d2..800d9a260e 100644 --- a/contracts/test/token/oeth.base.fork-test.js +++ b/contracts/test/token/oeth.base.fork-test.js @@ -27,7 +27,7 @@ describe("ForkTest: OETHb", function () { it("Should have the right governor", async () => { const { oethb } = fixture; - expect(await oethb.governor()).to.eq(addresses.base.governor); + expect(await oethb.governor()).to.eq(addresses.base.timelock); }); it("Should have the right Vault address", async () => { diff --git a/contracts/test/vault/oethb-vault.base.fork-test.js b/contracts/test/vault/oethb-vault.base.fork-test.js index 08a00bf42d..2ab0dc8aa2 100644 --- a/contracts/test/vault/oethb-vault.base.fork-test.js +++ b/contracts/test/vault/oethb-vault.base.fork-test.js @@ -37,9 +37,13 @@ describe("ForkTest: OETHb Vault", function () { const userBalanceAfter = await oethb.balanceOf(nick.address); const totalSupplyAfter = await oethb.totalSupply(); - expect(totalSupplyAfter).to.equal(totalSupplyBefore.add(oethUnits("1"))); - expect(userBalanceAfter).to.equal(userBalanceBefore.add(oethUnits("1"))); - expect(vaultBalanceAfter).to.equal( + expect(totalSupplyAfter).to.approxEqual( + totalSupplyBefore.add(oethUnits("1")) + ); + expect(userBalanceAfter).to.approxEqual( + userBalanceBefore.add(oethUnits("1")) + ); + expect(vaultBalanceAfter).to.approxEqual( vaultBalanceBefore.add(oethUnits("1")) ); }); diff --git a/contracts/test/zapper/oethb-zapper.base.fork-test.js b/contracts/test/zapper/oethb-zapper.base.fork-test.js index 0bbf6d466e..c4eb1398ca 100644 --- a/contracts/test/zapper/oethb-zapper.base.fork-test.js +++ b/contracts/test/zapper/oethb-zapper.base.fork-test.js @@ -26,8 +26,14 @@ describe("ForkTest: OETHb Zapper", function () { const supplyAfter = await oethb.totalSupply(); const balanceAfter = await hre.ethers.provider.getBalance(clement.address); - expect(supplyAfter).to.eq(supplyBefore.add(oethUnits("1"))); - expect(balanceAfter).to.approxEqual(balanceBefore.sub(oethUnits("1"))); + expect(supplyAfter).to.approxEqualTolerance( + supplyBefore.add(oethUnits("1")), + 2 + ); + expect(balanceAfter).to.approxEqualTolerance( + balanceBefore.sub(oethUnits("1")), + 2 + ); }); it("Should mint wsuperOETHb with ETH", async () => { @@ -53,11 +59,18 @@ describe("ForkTest: OETHb Zapper", function () { ); const woethbBalanceAfter = await wOETHb.balanceOf(clement.address); - expect(supplyAfter).to.eq(supplyBefore.add(oethUnits("1"))); - expect(ethBalanceAfter).to.approxEqual( - ethBalanceBefore.sub(oethUnits("1")) + expect(supplyAfter).to.approxEqualTolerance( + supplyBefore.add(oethUnits("1")), + 2 + ); + expect(ethBalanceAfter).to.approxEqualTolerance( + ethBalanceBefore.sub(oethUnits("1")), + 2 + ); + expect(woethbBalanceAfter).to.approxEqualTolerance( + woethbBalanceBefore.add(expected), + 2 ); - expect(woethbBalanceAfter).to.eq(woethbBalanceBefore.add(expected)); }); it("Should mint wsuperOETHb with WETH", async () => { @@ -82,10 +95,17 @@ describe("ForkTest: OETHb Zapper", function () { const wethBalanceAfter = await weth.balanceOf(clement.address); const woethbBalanceAfter = await wOETHb.balanceOf(clement.address); - expect(supplyAfter).to.eq(supplyBefore.add(oethUnits("1"))); - expect(wethBalanceAfter).to.approxEqual( - wethBalanceBefore.sub(oethUnits("1")) + expect(supplyAfter).to.approxEqualTolerance( + supplyBefore.add(oethUnits("1")), + 2 + ); + expect(wethBalanceAfter).to.approxEqualTolerance( + wethBalanceBefore.sub(oethUnits("1")), + 2 + ); + expect(woethbBalanceAfter).to.approxEqualTolerance( + woethbBalanceBefore.add(expected), + 2 ); - expect(woethbBalanceAfter).to.eq(woethbBalanceBefore.add(expected)); }); }); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index e2a72a5e6d..66121720d9 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -300,6 +300,7 @@ addresses.base.wethAeroPoolAddress = addresses.base.governor = "0x92A19381444A001d62cE67BaFF066fA1111d7202"; // 2/8 Multisig addresses.base.strategist = "0x28bce2eE5775B652D92bB7c2891A89F036619703"; +addresses.base.timelock = "0xf817cb3092179083c48c014688D98B72fB61464f"; // Chainlink: https://data.chain.link/feeds/base/base/woeth-oeth-exchange-rate addresses.base.BridgedWOETHOracleFeed = diff --git a/contracts/utils/deploy-l2.js b/contracts/utils/deploy-l2.js index 45c3a6af76..d05530162a 100644 --- a/contracts/utils/deploy-l2.js +++ b/contracts/utils/deploy-l2.js @@ -4,7 +4,9 @@ const { deployWithConfirmation, withConfirmation, impersonateGuardian, + handleTransitionGovernance, } = require("./deploy"); +const { proposeGovernanceArgs } = require("./governor"); const { impersonateAndFund } = require("./signers"); const { getTxOpts } = require("./tx"); @@ -106,7 +108,7 @@ function deployOnBase(opts, fn) { return main; } function deployOnBaseWithGuardian(opts, fn) { - const { deployName, dependencies, onlyOnFork, forceSkip } = opts; + const { deployName, dependencies, onlyOnFork, forceSkip, useTimelock } = opts; const runDeployment = async (hre) => { const tools = { @@ -132,37 +134,45 @@ function deployOnBaseWithGuardian(opts, fn) { const proposal = await fn(tools); - const sGuardian = !isFork - ? undefined - : await ethers.provider.getSigner(guardianAddr); - console.log("guardianAddr", guardianAddr); - - const guardianActions = []; - for (const action of proposal.actions) { - const { contract, signature, args } = action; - - if (isFork) { - log(`Sending governance action ${signature} to ${contract.address}`); - await withConfirmation( - contract.connect(sGuardian)[signature](...args, await getTxOpts()) - ); + if (useTimelock) { + const propDescription = proposal.name || deployName; + const propArgs = await proposeGovernanceArgs(proposal.actions); + + await handleTransitionGovernance(propDescription, propArgs); + } else { + // Handle Guardian governance + const sGuardian = !isFork + ? undefined + : await ethers.provider.getSigner(guardianAddr); + console.log("guardianAddr", guardianAddr); + + const guardianActions = []; + for (const action of proposal.actions) { + const { contract, signature, args } = action; + + if (isFork) { + log(`Sending governance action ${signature} to ${contract.address}`); + await withConfirmation( + contract.connect(sGuardian)[signature](...args, await getTxOpts()) + ); + } + + guardianActions.push({ + sig: signature, + args: args, + to: contract.address, + data: contract.interface.encodeFunctionData(signature, args), + value: "0", + }); + + console.log(`... ${signature} completed`); } - guardianActions.push({ - sig: signature, - args: args, - to: contract.address, - data: contract.interface.encodeFunctionData(signature, args), - value: "0", - }); - - console.log(`... ${signature} completed`); + console.log( + "Execute the following actions using guardian safe: ", + guardianActions + ); } - - console.log( - "Execute the following actions using guardian safe: ", - guardianActions - ); }; const main = async (hre) => { @@ -190,6 +200,7 @@ function deployOnBaseWithGuardian(opts, fn) { return main; } + module.exports = { deployOnArb, deployOnBase, diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index ad3d50546f..0d5e1315bb 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -1049,7 +1049,9 @@ async function handleTransitionGovernance(propDesc, propArgs) { const guardian = !isFork ? undefined - : await impersonateAndFund(addresses.mainnet.Guardian); + : await impersonateAndFund( + isBaseFork ? addresses.base.governor : addresses.mainnet.Guardian + ); if (!isScheduled) { // Needs to be scheduled @@ -1577,4 +1579,6 @@ module.exports = { deploymentWithProposal, deploymentWithGovernanceProposal, deploymentWithGuardianGovernor, + + handleTransitionGovernance, };