const { expect } = require('chai') const { ethers } = require('hardhat') const { BigNumber } = require('@ethersproject/bignumber') const config = require('../../config') const { takeSnapshot, revertSnapshot } = require('../utils') describe('Gov Exploit Patch Upgrade Tests', () => { const zero = BigNumber.from(0) const ProposalState = { Pending: 0, Active: 1, Defeated: 2, Timelocked: 3, AwaitingExecution: 4, Executed: 5, Expired: 6, } let periods = { EXECUTION_DELAY: zero, EXECUTION_EXPIRATION: zero, QUORUM_VOTES: zero, PROPOSAL_THRESHOLD: zero, VOTING_DELAY: zero, VOTING_PERIOD: zero, CLOSING_PERIOD: zero, VOTE_EXTEND_TIME: zero, } async function setPeriods(_periods, govc) { _periods.EXECUTION_DELAY = await govc.EXECUTION_DELAY() _periods.EXECUTION_EXPIRATION = await govc.EXECUTION_EXPIRATION() _periods.QUORUM_VOTES = await govc.QUORUM_VOTES() _periods.PROPOSAL_THRESHOLD = await govc.PROPOSAL_THRESHOLD() _periods.VOTING_DELAY = await govc.VOTING_DELAY() _periods.VOTING_PERIOD = await govc.VOTING_PERIOD() _periods.CLOSING_PERIOD = await govc.CLOSING_PERIOD() _periods.VOTE_EXTEND_TIME = await govc.VOTE_EXTEND_TIME() return _periods } let tornwhale let proposer let initialProposalDeployer let maliciousProposalDeployer let initialProposalImpl let maliciousProposalImpl let proposalContractsDeployer let proposalDeployer let registryDeployer let stakingDeployer let proposerBalanceInitial let torn let governance let metamorphicFactory let registryImplementation let staking let exploit = { hacker: undefined, salt: '00000000006578706c6f6974', // hex "exploit", hacker addrs must be prepended address: '0x0000000000000000000000000000000000000000', // Has to be filled } // From other tests let getToken = async (tokenAddress) => { return await ethers.getContractAt('@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', tokenAddress) } let minewait = async (time) => { await ethers.provider.send('evm_increaseTime', [time]) await ethers.provider.send('evm_mine', []) } let pE = (x) => { return ethers.utils.parseEther(`${x}`) } let snapshotId before(async function () { // Pick our signer proposer = (await ethers.getSigners())[2] // Prepare hacker signer and salt exploit.hacker = (await ethers.getSigners())[3] exploit.salt = exploit.hacker.address + exploit.salt // Ok get current gov governance = (await ethers.getContractAt('GovernanceStakingUpgrade', config.governance)).connect(proposer) // Impersonate await ethers.provider.send('hardhat_impersonateAccount', [config.governance]) // Pick whale tornwhale = ethers.provider.getSigner(config.governance) // Connect to above torn = (await getToken(config.TORN)).connect(tornwhale) // Set balance of governance contract await ethers.provider.send('hardhat_setBalance', [proposer.address, pE(10).toHexString()]) // Take gov balance const govbal = await torn.balanceOf(governance.address) // Transfer await torn.transfer(proposer.address, govbal.div(2)) // Note bal proposerBalanceInitial = await torn.balanceOf(proposer.address) // Check bal was allocated expect(await torn.balanceOf(proposer.address)).to.equal(govbal.div(2)) // Connect torn = torn.connect(proposer) // Allow torn to be locked await torn.approve(governance.address, proposerBalanceInitial) // Lock it await governance.connect(proposer).lockWithApproval(proposerBalanceInitial) // Get the proposal periods for say executing, voting, and so on periods = await setPeriods(periods, governance) // Contracts factories initialProposalDeployer = await ethers.getContractFactory('InitialProposal') maliciousProposalDeployer = await ethers.getContractFactory('MaliciousProposal') stakingDeployer = await ethers.getContractFactory('TornadoStakingRewards') registryDeployer = await ethers.getContractFactory('RelayerRegistry') proposalDeployer = await ethers.getContractFactory('PatchProposal') // Metamorphic & Exploit metamorphicFactory = ( await ethers.getContractAt('MetamorphicContractFactory', '0x00000000e82eb0431756271F0d00CFB143685e7B') ).connect(exploit.hacker) initialProposalImpl = await initialProposalDeployer.deploy() maliciousProposalImpl = await maliciousProposalDeployer.deploy() staking = await stakingDeployer.deploy(config.governance, config.TORN, config.registry) registryImplementation = await registryDeployer.deploy( config.TORN, config.governance, config.ens, config.staking, config.feeManager, ) exploit.address = await metamorphicFactory.findMetamorphicContractAddress(exploit.salt) // Snapshot snapshotId = await takeSnapshot() }) describe('Integrative: Patched Governance', () => { after(async () => { await revertSnapshot(snapshotId) snapshotId = await takeSnapshot() }) it('Should be able to execute the proposal', async () => { // Load these storage variables for comparison const oldVaultAddr = await governance.userVault() const oldGasCompAddr = await governance.gasCompensationVault() const oldStaking = await governance.Staking() // Deploy the proposal const proposal = await proposalDeployer.deploy(staking.address, registryImplementation.address) // Propose await governance.propose(proposal.address, 'PATCH') // Get the proposal id const proposalId = await governance.latestProposalIds(proposer.address) // Get proposal data let proposalData = await governance.proposals(proposalId) // Mine up until we can start voting await minewait(periods.VOTING_DELAY.add(1).toNumber()) await governance.castVote(proposalId, true) await ethers.provider.send('evm_setNextBlockTimestamp', [ proposalData.endTime.add(periods.EXECUTION_DELAY).add(BigNumber.from(1000)).toNumber(), ]) await ethers.provider.send('evm_mine', []) await governance.execute(proposalId) const newVaultAddr = await governance.userVault() const newGasCompAddr = await governance.gasCompensationVault() const newStaking = await governance.Staking() expect(oldGasCompAddr).to.equal(newGasCompAddr) expect(newVaultAddr).to.equal(oldVaultAddr) expect(newStaking) .to.not.equal(oldStaking) .and.to.not.equal('0x0000000000000000000000000000000000000000') }) it('Should not be susceptible to the contract metamorphosis exploit', async () => { // First deploy @ metamorphic the valid contract let response = await metamorphicFactory.deployMetamorphicContractFromExistingImplementation( exploit.salt, initialProposalImpl.address, [], ) const initialProposalAddress = (await response.wait()).events[0].args[0] // Must equal expect(initialProposalAddress).to.equal(exploit.address) // Load the contract const initialProposal = await ethers.getContractAt('InitialProposal', initialProposalAddress) // Propose the valid one await governance.propose(initialProposal.address, 'VALID') // Get the proposal id const proposalId = await governance.latestProposalIds(proposer.address) // Get proposal data let proposalData = await governance.proposals(proposalId) // Mine up until we can start voting await minewait(periods.VOTING_DELAY.add(1).toNumber()) // Vote for this await governance.castVote(proposalId, true) // Prepare time so we can execute await ethers.provider.send('evm_setNextBlockTimestamp', [ proposalData.endTime.add(periods.EXECUTION_DELAY).add(BigNumber.from(1000)).toNumber(), ]) await ethers.provider.send('evm_mine', []) // Since the proposal has now passed, terminate the original contract await initialProposal.emergencyStop() // Run metamorphic deployment again response = await metamorphicFactory.deployMetamorphicContractFromExistingImplementation( exploit.salt, maliciousProposalImpl.address, [], ) const maliciousProposalAddress = (await response.wait()).events[0].args[0] // Confirm again expect(maliciousProposalAddress).to.equal(exploit.address) // Load the contract const maliciousProposal = await ethers.getContractAt('MaliciousProposal', maliciousProposalAddress) // Now execute await expect(governance.execute(proposalId)).to.be.revertedWith( 'Governance::propose: metamorphic contracts not allowed', ) // Terminate the contract for the next test await maliciousProposal.emergencyStop() }) }) describe('Integrative: Unpatched Governance', () => { after(async () => { await revertSnapshot(snapshotId) snapshotId = await takeSnapshot() }) it('The standard contract should be susceptible to the metamorphosis exploit', async () => { // First deploy @ metamorphic the valid contract let response = await metamorphicFactory.deployMetamorphicContractFromExistingImplementation( exploit.salt, initialProposalImpl.address, [], ) const initialProposalAddress = (await response.wait()).events[0].args[0] // Must equal expect(initialProposalAddress).to.equal(exploit.address) // Load the contract const initialProposal = await ethers.getContractAt('InitialProposal', initialProposalAddress) // Propose the valid one await governance.propose(initialProposal.address, 'VALID') // Get the proposal id const proposalId = await governance.latestProposalIds(proposer.address) // Get proposal data let proposalData = await governance.proposals(proposalId) // Mine up until we can start voting await minewait(periods.VOTING_DELAY.add(1).toNumber()) // Vote for this await governance.castVote(proposalId, true) // Prepare time so we can execute await ethers.provider.send('evm_setNextBlockTimestamp', [ proposalData.endTime.add(periods.EXECUTION_DELAY).add(BigNumber.from(1000)).toNumber(), ]) await ethers.provider.send('evm_mine', []) // Since the proposal has now passed, terminate the original contract await initialProposal.emergencyStop() // Run metamorphic deployment again response = await metamorphicFactory.deployMetamorphicContractFromExistingImplementation( exploit.salt, maliciousProposalImpl.address, [], ) const maliciousProposalAddress = (await response.wait()).events[0].args[0] // Confirm again expect(maliciousProposalAddress).to.equal(exploit.address) // Load the contract const maliciousProposal = await ethers.getContractAt('MaliciousProposal', maliciousProposalAddress) // Check that the malicious proposer is the deployer const deployer = await maliciousProposal.deployer() // Get bal before const deployerBalanceBefore = await torn.balanceOf(deployer) const governanceBalanceBefore = await torn.balanceOf(governance.address) expect(governanceBalanceBefore).to.be.gt(zero) // Now execute await governance.execute(proposalId) // Check bal after const deployerBalanceAfter = await torn.balanceOf(deployer) const governanceBalanceAfter = await torn.balanceOf(governance.address) // Protected expect(deployerBalanceAfter).to.be.equal(deployerBalanceBefore.add(governanceBalanceBefore)) expect(governanceBalanceAfter).to.equal(zero) }) }) })