diff --git a/evm/governance.js b/evm/governance.js index 83953b06..3b440614 100644 --- a/evm/governance.js +++ b/evm/governance.js @@ -5,7 +5,7 @@ require('dotenv').config(); const { ethers } = require('hardhat'); const { getDefaultProvider, - utils: { defaultAbiCoder, keccak256, Interface, parseEther }, + utils: { defaultAbiCoder, keccak256, Interface, parseEther, arrayify }, Contract, BigNumber, constants: { AddressZero }, @@ -31,6 +31,7 @@ const { const { getWallet } = require('./sign-utils.js'); const IGovernance = require('@axelar-network/axelar-gmp-sdk-solidity/interfaces/IAxelarServiceGovernance.json'); const IGateway = require('@axelar-network/axelar-gmp-sdk-solidity/interfaces/IAxelarGateway.json'); +const IUpgradable = require('@axelar-network/axelar-gmp-sdk-solidity/interfaces/IUpgradable.json'); let proposals = []; @@ -63,6 +64,31 @@ async function getGatewaySetupParams(governance, gateway, contracts, options) { return setupParams; } +async function getSetupParams(options) { + const toUpgrade = options.contractToUpgrade; + let setupParams; + + switch (toUpgrade) { + case 'InterchainTokenService': { + const newOperator = options.newOperator; + + if (newOperator) { + if (!isValidAddress(newOperator)) { + throw new Error(`Invalid new operator address: ${newOperator}`); + } + + setupParams = arrayify(newOperator); + } else { + setupParams = '0x'; + } + + return setupParams; + } + } + + throw new Error(`${toUpgrade} is not supported.`); +} + async function processCommand(_, chain, options) { const { env, contractName, address, action, date, privateKey, yes } = options; @@ -417,7 +443,7 @@ async function processCommand(_, chain, options) { return; } - case 'executeUpgrade': { + case 'executeGatewayUpgrade': { target = contracts.AxelarGateway?.address; const gateway = new Contract(target, IGateway.abi, wallet); const implementation = options.implementation || chain.contracts.AxelarGateway?.implementation; @@ -489,6 +515,172 @@ async function processCommand(_, chain, options) { break; } + case 'upgrade': { + const isTimeLock = options.isTimeLock; + + if (!isTimeLock && contractName === 'InterchainGovernance') { + throw new Error(`Invalid governance action for InterchainGovernance: ${action}`); + } + + let eta = 0; + + if (isTimeLock) { + eta = dateToEta(date); + + const currTime = getCurrentTimeInSeconds(); + printInfo('Current time', etaToDate(currTime)); + + const minEta = currTime + contractConfig?.minimumTimeDelay; + printInfo('Minimum eta', etaToDate(minEta)); + + if (eta < minEta) { + printWarn(`${date} is less than the minimum eta.`); + } + + printInfo('Time difference between current time and eta', etaToDate(eta - currTime)); + } + + const toUpgrade = options.contractToUpgrade; + + const implementation = options.implementation || chain.contracts[toUpgrade]?.implementation; + + if (!isValidAddress(implementation)) { + throw new Error(`Invalid new ${toUpgrade} implementation address: ${implementation}`); + } + + const toUpgradeContract = new Contract(target, IUpgradable.abi, wallet); + + printInfo(`Current ${toUpgrade} implementation`, await toUpgradeContract.implementation()); + printInfo(`New ${toUpgrade} implementation`, implementation); + + const newImplementationCodeHash = await getBytecodeHash(implementation, chain.name, provider); + printInfo(`New ${toUpgrade} implementation code hash`, newImplementationCodeHash); + + const setupParams = await getSetupParams(options); + + printInfo(`Setup Params for upgrading ${toUpgrade}`, setupParams); + + calldata = toUpgradeContract.interface.encodeFunctionData('upgrade', [implementation, newImplementationCodeHash, setupParams]); + + const commandType = 0; + const types = ['uint256', 'address', 'bytes', 'uint256', 'uint256']; + const values = [commandType, target, calldata, nativeValue, eta]; + + gmpPayload = defaultAbiCoder.encode(types, values); + const proposalEta = await governance.getProposalEta(target, calldata, nativeValue); + + if (!BigNumber.from(proposalEta).eq(0)) { + printWarn('The proposal already exixts', etaToDate(proposalEta)); + } + + title = `Chain ${chain.name} ${toUpgrade} upgrade proposal`; + description = `This proposal upgrades the ${toUpgrade} contract ${toUpgradeContract.address} on chain ${chain.name} to a new implementation contract ${implementation}`; + + break; + } + + case 'executeUpgrade': { + const isTimeLock = options.isTimeLock; + + if (!isTimeLock && contractName === 'InterchainGovernance') { + throw new Error(`Invalid governance action for InterchainGovernance: ${action}`); + } + + const toUpgrade = options.contractToUpgrade; + + target = contracts[toUpgrade]?.address; + const toUpgradeContract = new Contract(target, IUpgradable.abi, wallet); + const implementation = options.implementation || chain.contracts[toUpgrade]?.implementation; + const implementationCodehash = chain.contracts[toUpgrade]?.implementationCodehash; + printInfo(`New ${toUpgrade} implementation code hash`, implementationCodehash); + + if (!isValidAddress(implementation)) { + throw new Error(`Invalid new ${toUpgrade} implementation address: ${implementation}`); + } + + const setupParams = await getSetupParams(options); + + printInfo(`Setup Params for upgrading ${toUpgrade}`, setupParams); + + calldata = toUpgradeContract.interface.encodeFunctionData('upgrade', [implementation, implementationCodehash, setupParams]); + + const proposalHash = keccak256(defaultAbiCoder.encode(['address', 'bytes', 'uint256'], [target, calldata, nativeValue])); + const eta = await governance.getTimeLock(proposalHash); + + if (!calldata) { + throw new Error(`Calldata required for this governance action: ${action}`); + } + + let tx; + + if (isTimeLock) { + if (eta.eq(0)) { + printError('TimeLock proposal does not exist.'); + return; + } + + printInfo('Proposal ETA', etaToDate(eta)); + + const currTime = getCurrentTimeInSeconds(); + printInfo('Current time', etaToDate(currTime)); + + if (currTime < eta) { + throw new Error(`Upgrade timeLock proposal is not yet eligible for execution.`); + } + + if (prompt('Proceed with executing this proposal?', yes)) { + throw new Error('Proposal execution cancelled.'); + } + + tx = await governance.executeProposal(target, calldata, nativeValue, gasOptions); + } else { + const isApproved = await governance.multisigApprovals(proposalHash); + + if (!isApproved) { + throw new Error('Upgrade multisig proposal has not been approved.'); + } + + const isSigner = await governance.isSigner(wallet.address); + + if (!isSigner) { + throw new Error(`Caller is not a valid signer address: ${wallet.address}`); + } + + const executeInterface = new Interface(governance.interface.fragments); + const executeCalldata = executeInterface.encodeFunctionData('executeMultisigProposal', [target, calldata, nativeValue]); + const topic = keccak256(executeCalldata); + + const hasSignerVoted = await governance.hasSignerVoted(wallet.address, topic); + + if (hasSignerVoted) { + throw new Error(`Signer has already voted: ${wallet.address}`); + } + + const signerVoteCount = await governance.getSignerVotesCount(topic); + printInfo(`${signerVoteCount} signers have already voted.`); + + if (prompt('Proceed with executing this proposal?', yes)) { + throw new Error('Proposal execution cancelled.'); + } + + tx = await governance.executeMultisigProposal(target, calldata, nativeValue, gasOptions); + } + + printInfo('Proposal execution tx', tx.hash); + + const receipt = await tx.wait(chain.confirmations); + + const eventEmitted = wasEventEmitted(receipt, governance, 'ProposalExecuted'); + + if (!eventEmitted) { + throw new Error('Proposal execution failed.'); + } + + printInfo('Proposal executed.'); + + break; + } + case 'withdraw': { if (!isValidTimeFormat(date)) { throw new Error(`Invalid ETA: ${date}. Please pass the eta in the format YYYY-MM-DDTHH:mm:ss`); @@ -618,6 +810,11 @@ program.addOption( .choices(['InterchainGovernance', 'AxelarServiceGovernance']) .default('InterchainGovernance'), ); +program.addOption( + new Option('-u, --contractToUpgrade ', 'contracted to be upgraded') + .choices(['InterchainTokenService']) + .default('InterchainTokenService'), +); program.addOption(new Option('-a, --address
', 'override address')); program.addOption(new Option('-p, --privateKey ', 'private key').makeOptionMandatory(true).env('PRIVATE_KEY')); program.addOption(new Option('-y, --yes', 'skip deployment prompt confirmation').env('YES')); @@ -632,6 +829,8 @@ program.addOption( 'executeProposal', 'executeMultisigProposal', 'gatewayUpgrade', + 'executeGatewayUpgrade', + 'upgrade', 'submitUpgrade', 'executeUpgrade', 'cancelUpgrade', @@ -641,6 +840,8 @@ program.addOption( ); program.addOption(new Option('--newGovernance ', 'governance address').env('GOVERNANCE')); program.addOption(new Option('--newMintLimiter ', 'mint limiter address').env('MINT_LIMITER')); +program.addOption(new Option('--newOperator ', 'new operator address')); +program.addOption(new Option('--isTimeLock ', 'ITS upgrade proposal type').default(false)); program.addOption(new Option('--commandId ', 'command id')); program.addOption(new Option('--target ', 'governance execution target')); program.addOption(new Option('--calldata ', 'calldata'));