diff --git a/hardhat/tasks/sips/args/sipArgs.js b/hardhat/tasks/sips/args/sipArgs.js index 51bfbc100..8e24a4d94 100644 --- a/hardhat/tasks/sips/args/sipArgs.js +++ b/hardhat/tasks/sips/args/sipArgs.js @@ -1180,6 +1180,395 @@ const getArgsSip0084Part2 = async (hre) => { return { args, governor: "GovernorOwner" }; }; +// --------------------------------------------------------------------------- +// Revert AMM contract ownership from governance (Timelock) back to Exchequer (MultiSig). +// Inverse of SIP-0046 parts 1–4. +// +// Each part mirrors the original SIP-0046 contract grouping exactly. +// Two-step process: +// Step 1 (on-chain, via SIP): Timelock calls transferOwnership(multisig) on each contract. +// Step 2 (off-chain, after SIP passes): Multisig calls acceptOwnership() on each contract. +// --------------------------------------------------------------------------- + +// Shared helper to build the targets/values/signatures/datas arrays for a set of +// AMM contracts that should have their ownership reverted to the multisig. +// `expectedOwner` – the timelock address that must currently own each contract. +// `deploymentTargets` – same shape as SIP-0046 (deployment, contractName, optional validation). +const _buildRevertOwnershipArgs = async ( + hre, + deploymentTargets, + expectedOwner, + expectedOwnerLabel +) => { + const { ethers } = hre; + const { deployments } = hre; + const abiCoder = new ethers.utils.AbiCoder(); + const ownershipABI = [ + "function owner() view returns(address)", + "function newOwner() view returns(address)", + ]; + const ownershipInterface = new ethers.utils.Interface(ownershipABI); + const multisigDeployment = await deployments.get("MultiSigWallet"); + + const targets = []; + const values = []; + const signatures = []; + const datas = []; + + for (let i = 0; i < deploymentTargets.length; i++) { + let deploymentTarget = deploymentTargets[i]; + + if (deploymentTarget.deployment === "oracle") { + const oracleAddress = await getAmmOracleAddress( + deploymentTarget.sourceContractNameToValidate, + deploymentTarget.sourceContractTypeToValidate + ); + if (oracleAddress === ethers.constants.AddressZero) { + logger.error(`Zero address for oracle converter ${deploymentTarget.contractName}`); + return process.exit; + } + const oracleArtifact = await deployments.getArtifact("Oracle"); + deploymentTarget.deployment = await ethers.getContractAt( + oracleArtifact.abi, + oracleAddress + ); + } else if (deploymentTarget.sourceContractNameToValidate) { + const isValid = await validateAmmOnchainAddresses(deploymentTarget); + if (!isValid) { + logger.error( + `Validation of AMM on-chain address failed for ${deploymentTarget.contractName}` + ); + return process.exit; + } + } + + const ammContract = await ethers.getContractAt( + ownershipInterface, + deploymentTarget.deployment.address + ); + const currentOwner = await ammContract.owner(); + const pendingOwner = await ammContract.newOwner(); + + if (currentOwner.toLowerCase() !== expectedOwner.toLowerCase()) { + logger.error( + `${deploymentTarget.contractName} - Current owner (${currentOwner}) is not ${expectedOwnerLabel} (${expectedOwner}). ` + + `Ownership may not have been transferred to governance yet, or has already been reverted.` + ); + return process.exit; + } + + if ( + pendingOwner !== ethers.constants.AddressZero && + pendingOwner.toLowerCase() !== multisigDeployment.address.toLowerCase() + ) { + logger.warn( + `${deploymentTarget.contractName} - Pending new owner (${pendingOwner}) is not the multisig (${multisigDeployment.address}). ` + + `transferOwnership() will overwrite it.` + ); + } + + targets.push(deploymentTarget.deployment.address); + values.push(0); + signatures.push("transferOwnership(address)"); + datas.push(abiCoder.encode(["address"], [multisigDeployment.address])); + } + + return { targets, values, signatures, datas }; +}; + +// Inverse of SIP-0046 Part 1 (GovernorAdmin): +// SovrynSwapNetwork, SwapSettings, MocOracle, SovOracle, EthOracle, BnbOracle, +// XusdOracle, FishOracle, RifOracle +const getArgsSipRevertAmmOwnershipPart1 = async (hre) => { + const { + deployments: { get }, + } = hre; + const timeLockAdminDeployment = await get("TimelockAdmin"); + + const deploymentTargets = [ + { + deployment: await get("AmmSovrynSwapNetwork"), + contractName: "SovrynSwapNetwork", + sourceContractTypeToValidate: "ContractRegistry", + sourceContractNameToValidate: "AmmContractRegistry", + }, + { + deployment: await get("AmmSwapSettings"), + contractName: "SwapSettings", + sourceContractTypeToValidate: "ContractRegistry", + sourceContractNameToValidate: "AmmContractRegistry", + }, + { + deployment: "oracle", + contractName: "MocOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterMoc", + }, + { + deployment: "oracle", + contractName: "SovOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterSov", + }, + { + deployment: "oracle", + contractName: "EthOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterEth", + }, + { + deployment: "oracle", + contractName: "BnbOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterBnb", + }, + { + deployment: "oracle", + contractName: "XusdOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterXusd", + }, + { + deployment: "oracle", + contractName: "FishOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterFish", + }, + { + deployment: "oracle", + contractName: "RifOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterRif", + }, + ]; + + const { targets, values, signatures, datas } = await _buildRevertOwnershipArgs( + hre, + deploymentTargets, + timeLockAdminDeployment.address, + "TimelockAdmin" + ); + + const args = { + targets, + values, + signatures, + data: datas, + description: + "SIP-XXXX: Revert AMM Contract Ownership to Exchequer (Part 1), Details: https://github.com/DistributedCollective/SIPS/blob/XXXXXXX/SIP-XXXX_part-1.md, sha256: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + }; + + return { args, governor: "GovernorAdmin" }; +}; + +// Inverse of SIP-0046 Part 2 (GovernorAdmin): +// MyntOracle, DllrOracle, ConversionPathFinder, ConverterUpgrader, +// ConverterRegistryData, OracleWhitelist, RbtcWrapperProxy +const getArgsSipRevertAmmOwnershipPart2 = async (hre) => { + const { + deployments: { get }, + } = hre; + const timeLockAdminDeployment = await get("TimelockAdmin"); + + const deploymentTargets = [ + { + deployment: "oracle", + contractName: "MyntOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterMynt", + }, + { + deployment: "oracle", + contractName: "DllrOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterDllr", + }, + { + deployment: await get("AmmConversionPathFinder"), + contractName: "ConversionPathFinder", + sourceContractTypeToValidate: "ContractRegistry", + sourceContractNameToValidate: "AmmContractRegistry", + }, + { + deployment: await get("AmmConverterUpgrader"), + contractName: "ConverterUpgrader", + }, + { + deployment: await get("AmmConverterRegistryData"), + contractName: "ConverterRegistryData", + }, + { + deployment: await get("AmmOracleWhitelist"), + contractName: "OracleWhitelist", + }, + { + deployment: await get("AmmRbtcWrapperProxy"), + contractName: "RbtcWrapperProxy", + }, + ]; + + const { targets, values, signatures, datas } = await _buildRevertOwnershipArgs( + hre, + deploymentTargets, + timeLockAdminDeployment.address, + "TimelockAdmin" + ); + + const args = { + targets, + values, + signatures, + data: datas, + description: + "SIP-XXXX: Revert AMM Contract Ownership to Exchequer (Part 2), Details: https://github.com/DistributedCollective/SIPS/blob/XXXXXXX/SIP-XXXX_part-2.md, sha256: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + }; + + return { args, governor: "GovernorAdmin" }; +}; + +// Inverse of SIP-0046 Part 3 (GovernorOwner): +// ConverterDoc, ConverterUsdt, ConverterBpro, ConverterBnb, ConverterMoc, +// ConverterXusd, ConverterSov, ConverterEth, ConverterFish, ConverterMynt +const getArgsSipRevertAmmOwnershipPart3 = async (hre) => { + const { + deployments: { get }, + } = hre; + const timeLockOwnerDeployment = await get("TimelockOwner"); + + const deploymentTargets = [ + { + deployment: await get("AmmConverterDoc"), + contractName: "ConverterDoc", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmConverterUsdt"), + contractName: "ConverterUsdt", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmConverterBpro"), + contractName: "ConverterBpro", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmConverterBnb"), + contractName: "ConverterBnb", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmConverterMoc"), + contractName: "ConverterMoc", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmConverterXusd"), + contractName: "ConverterXusd", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmConverterSov"), + contractName: "ConverterSov", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmConverterEth"), + contractName: "ConverterEth", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmConverterFish"), + contractName: "ConverterFish", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmConverterMynt"), + contractName: "ConverterMynt", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + ]; + + const { targets, values, signatures, datas } = await _buildRevertOwnershipArgs( + hre, + deploymentTargets, + timeLockOwnerDeployment.address, + "TimelockOwner" + ); + + const args = { + targets, + values, + signatures, + data: datas, + description: + "SIP-XXXX: Revert AMM Contract Ownership to Exchequer (Part 3), Details: https://github.com/DistributedCollective/SIPS/blob/XXXXXXX/SIP-XXXX_part-3.md, sha256: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + }; + + return { args, governor: "GovernorOwner" }; +}; + +// Inverse of SIP-0046 Part 4 (GovernorOwner): +// ConverterRif, ConverterDllr, ContractRegistry, ConverterFactory +const getArgsSipRevertAmmOwnershipPart4 = async (hre) => { + const { + deployments: { get }, + } = hre; + const timeLockOwnerDeployment = await get("TimelockOwner"); + + const deploymentTargets = [ + { + deployment: await get("AmmConverterRif"), + contractName: "ConverterRif", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmConverterDllr"), + contractName: "ConverterDllr", + sourceContractTypeToValidate: "ConverterRegistry", + sourceContractNameToValidate: "AmmConverterRegistry", + }, + { + deployment: await get("AmmContractRegistry"), + contractName: "ContractRegistry", + }, + { + deployment: await get("AmmConverterFactory"), + contractName: "ConverterFactory", + sourceContractTypeToValidate: "ContractRegistry", + sourceContractNameToValidate: "AmmContractRegistry", + }, + ]; + + const { targets, values, signatures, datas } = await _buildRevertOwnershipArgs( + hre, + deploymentTargets, + timeLockOwnerDeployment.address, + "TimelockOwner" + ); + + const args = { + targets, + values, + signatures, + data: datas, + description: + "SIP-XXXX: Revert AMM Contract Ownership to Exchequer (Part 4), Details: https://github.com/DistributedCollective/SIPS/blob/XXXXXXX/SIP-XXXX_part-4.md, sha256: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + }; + + return { args, governor: "GovernorOwner" }; +}; + const getArgsSip0087 = async (hre) => { // SOV-5158 Fix Liquidation Blocking Vulnerability const { @@ -1272,4 +1661,8 @@ module.exports = { getArgsSip0084Part1, getArgsSip0084Part2, getArgsSip0087, + getArgsSipRevertAmmOwnershipPart1, + getArgsSipRevertAmmOwnershipPart2, + getArgsSipRevertAmmOwnershipPart3, + getArgsSipRevertAmmOwnershipPart4, }; diff --git a/scripts/contractInteraction/contract_interaction.py b/scripts/contractInteraction/contract_interaction.py index 883edc1c1..712738c78 100644 --- a/scripts/contractInteraction/contract_interaction.py +++ b/scripts/contractInteraction/contract_interaction.py @@ -342,6 +342,57 @@ def main(): # setupTorqueLoanParams(conf.contracts['iXUSD'], "0x0000000000000000000000000000000000000000", conf.contracts['SOV'], Wei("20 ether"), ) # checkTx(1344) + # ----------------------------------------------------------------------- + # Revert AMM ownership from Governance (Timelock) back to Exchequer (Multisig) + # Step 2 of 2: multisig calls acceptOwnership() after the SIP has been executed + # and the Timelock has already called transferOwnership(multisig) on each contract. + # Run each block only after its corresponding SIP part has been fully executed. + # ----------------------------------------------------------------------- + + # --- Part 1 (inverse of SIP-0046 Part 1, GovernorAdmin) --- + # SovrynSwapNetwork, SwapSettings, and AMM pool oracles: + # Moc, Sov, Eth, Bnb, Xusd, Fish, Rif + #acceptOwnershipWithMultisig(conf.contracts['swapNetwork']) + #acceptOwnershipWithMultisig(conf.contracts['ammSwapSettings']) + #acceptOwnershipWithMultisig(conf.contracts['MOCPoolOracle']) + #acceptOwnershipWithMultisig(conf.contracts['SOVPoolOracle']) + #acceptOwnershipWithMultisig(conf.contracts['ETHPoolOracle']) + #acceptOwnershipWithMultisig(conf.contracts['BNBPoolOracle']) + #acceptOwnershipWithMultisig(conf.contracts['XUSDPoolOracle']) + #acceptOwnershipWithMultisig(conf.contracts['FishPoolOracle']) + #acceptOwnershipWithMultisig(conf.contracts['RIFPoolOracle']) + + # --- Part 2 (inverse of SIP-0046 Part 2, GovernorAdmin) --- + # Mynt & Dllr oracles, ConversionPathFinder, ConverterUpgrader, + # ConverterRegistryData, OracleWhitelist, RBTCWrapperProxy + #acceptOwnershipWithMultisig(conf.contracts['MYNTPoolOracle']) + #acceptOwnershipWithMultisig(conf.contracts['DLLRPoolOracle']) + #acceptOwnershipWithMultisig(conf.contracts['ConversionPathFinder']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterUpgrader']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterRegistryData']) + #acceptOwnershipWithMultisig(conf.contracts['ammOracleWhitelist']) + #acceptOwnershipWithMultisig(conf.contracts['RBTCWrapperProxy']) + + # --- Part 3 (inverse of SIP-0046 Part 3, GovernorOwner) --- + # Converters: DOC, USDT, BPRO, BNBs, MOC, XUSD, SOV, ETHs, FISH, MYNT + #acceptOwnershipWithMultisig(conf.contracts['ConverterDOC']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterUSDT']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterBPRO']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterBNBs']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterMOC']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterXUSD']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterSOV']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterETHs']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterFISH']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterMYNT']) + + # --- Part 4 (inverse of SIP-0046 Part 4, GovernorOwner) --- + # Converters: RIF, DLLR, ContractRegistry, ConverterFactory + #acceptOwnershipWithMultisig(conf.contracts['ConverterRIF']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterDLLR']) + #acceptOwnershipWithMultisig(conf.contracts['ammContractRegistry']) + #acceptOwnershipWithMultisig(conf.contracts['ConverterFactory']) + def guardiansTransfer(): #################################################################### ### THIS SCRIPT SHOULD RUN STRICTLY AFTER THE SIP-0047 EXECUTION ### diff --git a/tests-onchain/sipRevertAmmOwnershipPart1.test.js b/tests-onchain/sipRevertAmmOwnershipPart1.test.js new file mode 100644 index 000000000..f1767b2d6 --- /dev/null +++ b/tests-onchain/sipRevertAmmOwnershipPart1.test.js @@ -0,0 +1,228 @@ +// first run a local forked mainnet node in a separate terminal window: +// npx hardhat node --fork https://mainnet-dev.sovryn.app/rpc --no-deploy +// now run the test: +// npx hardhat test tests-onchain/sipRevertAmmOwnershipPart1.test.js --network rskForkedMainnet + +const { + impersonateAccount, + mine, + time, + setBalance, +} = require("@nomicfoundation/hardhat-network-helpers"); +const hre = require("hardhat"); +const { validateAmmOnchainAddresses, getAmmOracleAddress } = require("../hardhat/helpers"); + +const { + ethers, + deployments: { createFixture, get }, +} = hre; + +const MAX_DURATION = ethers.BigNumber.from(24 * 60 * 60).mul(1092); + +const ONE_RBTC = ethers.utils.parseEther("1.0"); + +const getImpersonatedSigner = async (addressToImpersonate) => { + await impersonateAccount(addressToImpersonate); + return await ethers.getSigner(addressToImpersonate); +}; + +describe("Revert AMM Ownership Part 1 (GovernorAdmin) - Inverse of SIP-0046 Part 1", () => { + const getImpersonatedSignerFromJsonRpcProvider = async (addressToImpersonate) => { + const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545"); + await provider.send("hardhat_impersonateAccount", [addressToImpersonate]); + return provider.getSigner(addressToImpersonate); + }; + + const setupTest = createFixture(async ({ deployments }) => { + const deployer = (await ethers.getSigners())[0].address; + const deployerSigner = await ethers.getSigner(deployer); + + const multisigAddress = (await get("MultiSigWallet")).address; + const multisigSigner = await getImpersonatedSignerFromJsonRpcProvider(multisigAddress); + + await setBalance(deployer, ONE_RBTC.mul(10)); + await deployments.fixture(["ProtocolModules"], { + keepExistingDeployments: true, + }); + + const staking = await ethers.getContract("Staking", deployerSigner); + + const god = await deployments.get("GovernorAdmin"); + const governorAdmin = await ethers.getContractAt( + "GovernorAlpha", + god.address, + deployerSigner + ); + const governorAdminSigner = await getImpersonatedSigner(god.address); + + await setBalance(governorAdminSigner.address, ONE_RBTC); + const timelockOwner = await ethers.getContract("TimelockOwner", governorAdminSigner); + + const timelockOwnerSigner = await getImpersonatedSignerFromJsonRpcProvider( + timelockOwner.address + ); + await setBalance(timelockOwnerSigner._address, ONE_RBTC); + + return { + deployer, + deployerSigner, + staking, + governorAdmin, + governorAdminSigner, + timelockOwner, + timelockOwnerSigner, + multisigAddress, + multisigSigner, + }; + }); + + describe("SIP Revert AMM Ownership Part 1 - Test creation and execution", () => { + it("SIP Revert AMM Ownership Part 1 is executable and valid", async () => { + expect(hre.network.tags["forked"], "ERROR: Must run on a forked net").equal(true); + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: "https://mainnet-dev.sovryn.app/rpc", + }, + }, + ], + }); + + const { + deployer, + deployerSigner, + staking, + governorAdmin, + timelockOwnerSigner, + multisigAddress, + multisigSigner, + } = await setupTest(); + + // CREATE PROPOSAL + const sov = await ethers.getContract("SOV", timelockOwnerSigner); + const whaleAmount = (await sov.totalSupply()).mul(ethers.BigNumber.from(5)); + await sov.mint(deployer, whaleAmount); + + await sov.connect(deployerSigner).approve(staking.address, whaleAmount); + + if (await staking.paused()) await staking.connect(multisigSigner).pauseUnpause(false); + const currentTS = ethers.BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + await staking.stake(whaleAmount, currentTS.add(MAX_DURATION), deployer, deployer); + await mine(); + + // CREATE PROPOSAL AND VERIFY + const proposalIdBeforeSIP = await governorAdmin.latestProposalIds(deployer); + await hre.run("sips:create", { argsFunc: "getArgsSipRevertAmmOwnershipPart1" }); + const proposalId = await governorAdmin.latestProposalIds(deployer); + expect( + proposalId, + "Proposal was not created. Check the SIP creation is not commented out." + ).is.gt(proposalIdBeforeSIP); + + // VOTE FOR PROPOSAL + await mine(); + await governorAdmin.connect(deployerSigner).castVote(proposalId, true); + + // QUEUE PROPOSAL + let proposal = await governorAdmin.proposals(proposalId); + await mine(proposal.endBlock); + await governorAdmin.queue(proposalId); + + // EXECUTE PROPOSAL + proposal = await governorAdmin.proposals(proposalId); + await time.increaseTo(proposal.eta); + await expect(governorAdmin.execute(proposalId)) + .to.emit(governorAdmin, "ProposalExecuted") + .withArgs(proposalId); + + // VERIFY execution + expect((await governorAdmin.proposals(proposalId)).executed).to.be.true; + + // Verify transferOwnership was called: newOwner() should be multisig for each contract + const ownershipABI = [ + "function owner() view returns(address)", + "function newOwner() view returns(address)", + ]; + + const deploymentTargets = [ + { + deployment: await get("AmmSovrynSwapNetwork"), + contractName: "SovrynSwapNetwork", + sourceContractTypeToValidate: "ContractRegistry", + sourceContractNameToValidate: "AmmContractRegistry", + }, + { + deployment: await get("AmmSwapSettings"), + contractName: "SwapSettings", + sourceContractTypeToValidate: "ContractRegistry", + sourceContractNameToValidate: "AmmContractRegistry", + }, + { + deployment: "oracle", + contractName: "MocOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterMoc", + }, + { + deployment: "oracle", + contractName: "SovOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterSov", + }, + { + deployment: "oracle", + contractName: "EthOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterEth", + }, + { + deployment: "oracle", + contractName: "BnbOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterBnb", + }, + { + deployment: "oracle", + contractName: "XusdOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterXusd", + }, + { + deployment: "oracle", + contractName: "FishOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterFish", + }, + { + deployment: "oracle", + contractName: "RifOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterRif", + }, + ]; + + for (const target of deploymentTargets) { + let contractAddress; + if (target.deployment === "oracle") { + contractAddress = await getAmmOracleAddress( + target.sourceContractNameToValidate, + target.sourceContractTypeToValidate + ); + } else { + contractAddress = target.deployment.address; + } + + const contract = await ethers.getContractAt(ownershipABI, contractAddress); + const pendingOwner = await contract.newOwner(); + expect( + pendingOwner.toLowerCase(), + `${target.contractName}: newOwner should be multisig after transferOwnership` + ).to.equal(multisigAddress.toLowerCase()); + } + }); + }); +}); diff --git a/tests-onchain/sipRevertAmmOwnershipPart2.test.js b/tests-onchain/sipRevertAmmOwnershipPart2.test.js new file mode 100644 index 000000000..d6ad98839 --- /dev/null +++ b/tests-onchain/sipRevertAmmOwnershipPart2.test.js @@ -0,0 +1,206 @@ +// first run a local forked mainnet node in a separate terminal window: +// npx hardhat node --fork https://mainnet-dev.sovryn.app/rpc --no-deploy +// now run the test: +// npx hardhat test tests-onchain/sipRevertAmmOwnershipPart2.test.js --network rskForkedMainnet + +const { + impersonateAccount, + mine, + time, + setBalance, +} = require("@nomicfoundation/hardhat-network-helpers"); +const hre = require("hardhat"); +const { getAmmOracleAddress } = require("../hardhat/helpers"); + +const { + ethers, + deployments: { createFixture, get }, +} = hre; + +const MAX_DURATION = ethers.BigNumber.from(24 * 60 * 60).mul(1092); + +const ONE_RBTC = ethers.utils.parseEther("1.0"); + +const getImpersonatedSigner = async (addressToImpersonate) => { + await impersonateAccount(addressToImpersonate); + return await ethers.getSigner(addressToImpersonate); +}; + +describe("Revert AMM Ownership Part 2 (GovernorAdmin) - Inverse of SIP-0046 Part 2", () => { + const getImpersonatedSignerFromJsonRpcProvider = async (addressToImpersonate) => { + const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545"); + await provider.send("hardhat_impersonateAccount", [addressToImpersonate]); + return provider.getSigner(addressToImpersonate); + }; + + const setupTest = createFixture(async ({ deployments }) => { + const deployer = (await ethers.getSigners())[0].address; + const deployerSigner = await ethers.getSigner(deployer); + + const multisigAddress = (await get("MultiSigWallet")).address; + const multisigSigner = await getImpersonatedSignerFromJsonRpcProvider(multisigAddress); + + await setBalance(deployer, ONE_RBTC.mul(10)); + await deployments.fixture(["ProtocolModules"], { + keepExistingDeployments: true, + }); + + const staking = await ethers.getContract("Staking", deployerSigner); + + const god = await deployments.get("GovernorAdmin"); + const governorAdmin = await ethers.getContractAt( + "GovernorAlpha", + god.address, + deployerSigner + ); + const governorAdminSigner = await getImpersonatedSigner(god.address); + + await setBalance(governorAdminSigner.address, ONE_RBTC); + const timelockOwner = await ethers.getContract("TimelockOwner", governorAdminSigner); + + const timelockOwnerSigner = await getImpersonatedSignerFromJsonRpcProvider( + timelockOwner.address + ); + await setBalance(timelockOwnerSigner._address, ONE_RBTC); + + return { + deployer, + deployerSigner, + staking, + governorAdmin, + governorAdminSigner, + timelockOwner, + timelockOwnerSigner, + multisigAddress, + multisigSigner, + }; + }); + + describe("SIP Revert AMM Ownership Part 2 - Test creation and execution", () => { + it("SIP Revert AMM Ownership Part 2 is executable and valid", async () => { + expect(hre.network.tags["forked"], "ERROR: Must run on a forked net").equal(true); + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: "https://mainnet-dev.sovryn.app/rpc", + }, + }, + ], + }); + + const { + deployer, + deployerSigner, + staking, + governorAdmin, + timelockOwnerSigner, + multisigAddress, + multisigSigner, + } = await setupTest(); + + // CREATE PROPOSAL + const sov = await ethers.getContract("SOV", timelockOwnerSigner); + const whaleAmount = (await sov.totalSupply()).mul(ethers.BigNumber.from(5)); + await sov.mint(deployer, whaleAmount); + + await sov.connect(deployerSigner).approve(staking.address, whaleAmount); + + if (await staking.paused()) await staking.connect(multisigSigner).pauseUnpause(false); + const currentTS = ethers.BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + await staking.stake(whaleAmount, currentTS.add(MAX_DURATION), deployer, deployer); + await mine(); + + // CREATE PROPOSAL AND VERIFY + const proposalIdBeforeSIP = await governorAdmin.latestProposalIds(deployer); + await hre.run("sips:create", { argsFunc: "getArgsSipRevertAmmOwnershipPart2" }); + const proposalId = await governorAdmin.latestProposalIds(deployer); + expect( + proposalId, + "Proposal was not created. Check the SIP creation is not commented out." + ).is.gt(proposalIdBeforeSIP); + + // VOTE FOR PROPOSAL + await mine(); + await governorAdmin.connect(deployerSigner).castVote(proposalId, true); + + // QUEUE PROPOSAL + let proposal = await governorAdmin.proposals(proposalId); + await mine(proposal.endBlock); + await governorAdmin.queue(proposalId); + + // EXECUTE PROPOSAL + proposal = await governorAdmin.proposals(proposalId); + await time.increaseTo(proposal.eta); + await expect(governorAdmin.execute(proposalId)) + .to.emit(governorAdmin, "ProposalExecuted") + .withArgs(proposalId); + + // VERIFY execution + expect((await governorAdmin.proposals(proposalId)).executed).to.be.true; + + // Verify transferOwnership was called: newOwner() should be multisig for each contract + const ownershipABI = [ + "function owner() view returns(address)", + "function newOwner() view returns(address)", + ]; + + const deploymentTargets = [ + { + deployment: "oracle", + contractName: "MyntOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterMynt", + }, + { + deployment: "oracle", + contractName: "DllrOracle", + sourceContractTypeToValidate: "ConverterV1", + sourceContractNameToValidate: "AmmConverterDllr", + }, + { + deployment: await get("AmmConversionPathFinder"), + contractName: "ConversionPathFinder", + }, + { + deployment: await get("AmmConverterUpgrader"), + contractName: "ConverterUpgrader", + }, + { + deployment: await get("AmmConverterRegistryData"), + contractName: "ConverterRegistryData", + }, + { + deployment: await get("AmmOracleWhitelist"), + contractName: "OracleWhitelist", + }, + { + deployment: await get("AmmRbtcWrapperProxy"), + contractName: "RbtcWrapperProxy", + }, + ]; + + for (const target of deploymentTargets) { + let contractAddress; + if (target.deployment === "oracle") { + contractAddress = await getAmmOracleAddress( + target.sourceContractNameToValidate, + target.sourceContractTypeToValidate + ); + } else { + contractAddress = target.deployment.address; + } + + const contract = await ethers.getContractAt(ownershipABI, contractAddress); + const pendingOwner = await contract.newOwner(); + expect( + pendingOwner.toLowerCase(), + `${target.contractName}: newOwner should be multisig after transferOwnership` + ).to.equal(multisigAddress.toLowerCase()); + } + }); + }); +}); diff --git a/tests-onchain/sipRevertAmmOwnershipPart3.test.js b/tests-onchain/sipRevertAmmOwnershipPart3.test.js new file mode 100644 index 000000000..391500eac --- /dev/null +++ b/tests-onchain/sipRevertAmmOwnershipPart3.test.js @@ -0,0 +1,174 @@ +// first run a local forked mainnet node in a separate terminal window: +// npx hardhat node --fork https://mainnet-dev.sovryn.app/rpc --no-deploy +// now run the test: +// npx hardhat test tests-onchain/sipRevertAmmOwnershipPart3.test.js --network rskForkedMainnet + +const { + impersonateAccount, + mine, + time, + setBalance, +} = require("@nomicfoundation/hardhat-network-helpers"); +const hre = require("hardhat"); + +const { + ethers, + deployments: { createFixture, get }, +} = hre; + +const MAX_DURATION = ethers.BigNumber.from(24 * 60 * 60).mul(1092); + +const ONE_RBTC = ethers.utils.parseEther("1.0"); + +const getImpersonatedSigner = async (addressToImpersonate) => { + await impersonateAccount(addressToImpersonate); + return await ethers.getSigner(addressToImpersonate); +}; + +describe("Revert AMM Ownership Part 3 (GovernorOwner) - Inverse of SIP-0046 Part 3", () => { + const getImpersonatedSignerFromJsonRpcProvider = async (addressToImpersonate) => { + const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545"); + await provider.send("hardhat_impersonateAccount", [addressToImpersonate]); + return provider.getSigner(addressToImpersonate); + }; + + const setupTest = createFixture(async ({ deployments }) => { + const deployer = (await ethers.getSigners())[0].address; + const deployerSigner = await ethers.getSigner(deployer); + + const multisigAddress = (await get("MultiSigWallet")).address; + const multisigSigner = await getImpersonatedSignerFromJsonRpcProvider(multisigAddress); + + await setBalance(deployer, ONE_RBTC.mul(10)); + await deployments.fixture(["ProtocolModules"], { + keepExistingDeployments: true, + }); + + const staking = await ethers.getContract("Staking", deployerSigner); + + const god = await deployments.get("GovernorOwner"); + const governorOwner = await ethers.getContractAt( + "GovernorAlpha", + god.address, + deployerSigner + ); + const governorOwnerSigner = await getImpersonatedSigner(god.address); + + await setBalance(governorOwnerSigner.address, ONE_RBTC); + const timelockOwner = await ethers.getContract("TimelockOwner", governorOwnerSigner); + + const timelockOwnerSigner = await getImpersonatedSignerFromJsonRpcProvider( + timelockOwner.address + ); + await setBalance(timelockOwnerSigner._address, ONE_RBTC); + + return { + deployer, + deployerSigner, + staking, + governorOwner, + governorOwnerSigner, + timelockOwner, + timelockOwnerSigner, + multisigAddress, + multisigSigner, + }; + }); + + describe("SIP Revert AMM Ownership Part 3 - Test creation and execution", () => { + it("SIP Revert AMM Ownership Part 3 is executable and valid", async () => { + expect(hre.network.tags["forked"], "ERROR: Must run on a forked net").equal(true); + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: "https://mainnet-dev.sovryn.app/rpc", + }, + }, + ], + }); + + const { + deployer, + deployerSigner, + staking, + governorOwner, + timelockOwnerSigner, + multisigAddress, + multisigSigner, + } = await setupTest(); + + // CREATE PROPOSAL + const sov = await ethers.getContract("SOV", timelockOwnerSigner); + const whaleAmount = (await sov.totalSupply()).mul(ethers.BigNumber.from(5)); + await sov.mint(deployer, whaleAmount); + + await sov.connect(deployerSigner).approve(staking.address, whaleAmount); + + if (await staking.paused()) await staking.connect(multisigSigner).pauseUnpause(false); + const currentTS = ethers.BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + await staking.stake(whaleAmount, currentTS.add(MAX_DURATION), deployer, deployer); + await mine(); + + // CREATE PROPOSAL AND VERIFY + const proposalIdBeforeSIP = await governorOwner.latestProposalIds(deployer); + await hre.run("sips:create", { argsFunc: "getArgsSipRevertAmmOwnershipPart3" }); + const proposalId = await governorOwner.latestProposalIds(deployer); + expect( + proposalId, + "Proposal was not created. Check the SIP creation is not commented out." + ).is.gt(proposalIdBeforeSIP); + + // VOTE FOR PROPOSAL + await mine(); + await governorOwner.connect(deployerSigner).castVote(proposalId, true); + + // QUEUE PROPOSAL + let proposal = await governorOwner.proposals(proposalId); + await mine(proposal.endBlock); + await governorOwner.queue(proposalId); + + // EXECUTE PROPOSAL + proposal = await governorOwner.proposals(proposalId); + await time.increaseTo(proposal.eta); + await expect(governorOwner.execute(proposalId)) + .to.emit(governorOwner, "ProposalExecuted") + .withArgs(proposalId); + + // VERIFY execution + expect((await governorOwner.proposals(proposalId)).executed).to.be.true; + + // Verify transferOwnership was called: newOwner() should be multisig for each converter + const ownershipABI = [ + "function owner() view returns(address)", + "function newOwner() view returns(address)", + ]; + + const converterDeploymentNames = [ + "AmmConverterDoc", + "AmmConverterUsdt", + "AmmConverterBpro", + "AmmConverterBnb", + "AmmConverterMoc", + "AmmConverterXusd", + "AmmConverterSov", + "AmmConverterEth", + "AmmConverterFish", + "AmmConverterMynt", + ]; + + for (const name of converterDeploymentNames) { + const deployment = await get(name); + const contract = await ethers.getContractAt(ownershipABI, deployment.address); + const pendingOwner = await contract.newOwner(); + expect( + pendingOwner.toLowerCase(), + `${name}: newOwner should be multisig after transferOwnership` + ).to.equal(multisigAddress.toLowerCase()); + } + }); + }); +}); diff --git a/tests-onchain/sipRevertAmmOwnershipPart4.test.js b/tests-onchain/sipRevertAmmOwnershipPart4.test.js new file mode 100644 index 000000000..240e39489 --- /dev/null +++ b/tests-onchain/sipRevertAmmOwnershipPart4.test.js @@ -0,0 +1,168 @@ +// first run a local forked mainnet node in a separate terminal window: +// npx hardhat node --fork https://mainnet-dev.sovryn.app/rpc --no-deploy +// now run the test: +// npx hardhat test tests-onchain/sipRevertAmmOwnershipPart4.test.js --network rskForkedMainnet + +const { + impersonateAccount, + mine, + time, + setBalance, +} = require("@nomicfoundation/hardhat-network-helpers"); +const hre = require("hardhat"); + +const { + ethers, + deployments: { createFixture, get }, +} = hre; + +const MAX_DURATION = ethers.BigNumber.from(24 * 60 * 60).mul(1092); + +const ONE_RBTC = ethers.utils.parseEther("1.0"); + +const getImpersonatedSigner = async (addressToImpersonate) => { + await impersonateAccount(addressToImpersonate); + return await ethers.getSigner(addressToImpersonate); +}; + +describe("Revert AMM Ownership Part 4 (GovernorOwner) - Inverse of SIP-0046 Part 4", () => { + const getImpersonatedSignerFromJsonRpcProvider = async (addressToImpersonate) => { + const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545"); + await provider.send("hardhat_impersonateAccount", [addressToImpersonate]); + return provider.getSigner(addressToImpersonate); + }; + + const setupTest = createFixture(async ({ deployments }) => { + const deployer = (await ethers.getSigners())[0].address; + const deployerSigner = await ethers.getSigner(deployer); + + const multisigAddress = (await get("MultiSigWallet")).address; + const multisigSigner = await getImpersonatedSignerFromJsonRpcProvider(multisigAddress); + + await setBalance(deployer, ONE_RBTC.mul(10)); + await deployments.fixture(["ProtocolModules"], { + keepExistingDeployments: true, + }); + + const staking = await ethers.getContract("Staking", deployerSigner); + + const god = await deployments.get("GovernorOwner"); + const governorOwner = await ethers.getContractAt( + "GovernorAlpha", + god.address, + deployerSigner + ); + const governorOwnerSigner = await getImpersonatedSigner(god.address); + + await setBalance(governorOwnerSigner.address, ONE_RBTC); + const timelockOwner = await ethers.getContract("TimelockOwner", governorOwnerSigner); + + const timelockOwnerSigner = await getImpersonatedSignerFromJsonRpcProvider( + timelockOwner.address + ); + await setBalance(timelockOwnerSigner._address, ONE_RBTC); + + return { + deployer, + deployerSigner, + staking, + governorOwner, + governorOwnerSigner, + timelockOwner, + timelockOwnerSigner, + multisigAddress, + multisigSigner, + }; + }); + + describe("SIP Revert AMM Ownership Part 4 - Test creation and execution", () => { + it("SIP Revert AMM Ownership Part 4 is executable and valid", async () => { + expect(hre.network.tags["forked"], "ERROR: Must run on a forked net").equal(true); + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: "https://mainnet-dev.sovryn.app/rpc", + }, + }, + ], + }); + + const { + deployer, + deployerSigner, + staking, + governorOwner, + timelockOwnerSigner, + multisigAddress, + multisigSigner, + } = await setupTest(); + + // CREATE PROPOSAL + const sov = await ethers.getContract("SOV", timelockOwnerSigner); + const whaleAmount = (await sov.totalSupply()).mul(ethers.BigNumber.from(5)); + await sov.mint(deployer, whaleAmount); + + await sov.connect(deployerSigner).approve(staking.address, whaleAmount); + + if (await staking.paused()) await staking.connect(multisigSigner).pauseUnpause(false); + const currentTS = ethers.BigNumber.from( + (await ethers.provider.getBlock("latest")).timestamp + ); + await staking.stake(whaleAmount, currentTS.add(MAX_DURATION), deployer, deployer); + await mine(); + + // CREATE PROPOSAL AND VERIFY + const proposalIdBeforeSIP = await governorOwner.latestProposalIds(deployer); + await hre.run("sips:create", { argsFunc: "getArgsSipRevertAmmOwnershipPart4" }); + const proposalId = await governorOwner.latestProposalIds(deployer); + expect( + proposalId, + "Proposal was not created. Check the SIP creation is not commented out." + ).is.gt(proposalIdBeforeSIP); + + // VOTE FOR PROPOSAL + await mine(); + await governorOwner.connect(deployerSigner).castVote(proposalId, true); + + // QUEUE PROPOSAL + let proposal = await governorOwner.proposals(proposalId); + await mine(proposal.endBlock); + await governorOwner.queue(proposalId); + + // EXECUTE PROPOSAL + proposal = await governorOwner.proposals(proposalId); + await time.increaseTo(proposal.eta); + await expect(governorOwner.execute(proposalId)) + .to.emit(governorOwner, "ProposalExecuted") + .withArgs(proposalId); + + // VERIFY execution + expect((await governorOwner.proposals(proposalId)).executed).to.be.true; + + // Verify transferOwnership was called: newOwner() should be multisig for each contract + const ownershipABI = [ + "function owner() view returns(address)", + "function newOwner() view returns(address)", + ]; + + const converterDeploymentNames = [ + "AmmConverterRif", + "AmmConverterDllr", + "AmmContractRegistry", + "AmmConverterFactory", + ]; + + for (const name of converterDeploymentNames) { + const deployment = await get(name); + const contract = await ethers.getContractAt(ownershipABI, deployment.address); + const pendingOwner = await contract.newOwner(); + expect( + pendingOwner.toLowerCase(), + `${name}: newOwner should be multisig after transferOwnership` + ).to.equal(multisigAddress.toLowerCase()); + } + }); + }); +});