import { artifacts as assetProxyArtifacts, ERC20ProxyContract } from '@0x/contracts-asset-proxy'; import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20'; import { artifacts as stakingArtifacts, constants as stakingConstants, StakeInfo, StakeStatus, StakingProxyContract, TestStakingContract, ZrxVaultContract, } from '@0x/contracts-staking'; import { blockchainTests, constants, expect, getRandomInteger, randomAddress, verifyEventsFromLogs, } from '@0x/contracts-test-utils'; import { TreasuryVote } from '@0x/protocol-utils'; import { BigNumber, hexUtils } from '@0x/utils'; import * as ethUtil from 'ethereumjs-util'; import { artifacts } from './artifacts'; import { DefaultPoolOperatorContract, ZrxTreasuryContract, ZrxTreasuryEvents } from './wrappers'; blockchainTests.resets('Treasury governance', env => { const TREASURY_PARAMS = { votingPeriod: new BigNumber(3).times(stakingConstants.ONE_DAY_IN_SECONDS), proposalThreshold: new BigNumber(100), quorumThreshold: new BigNumber(1000), defaultPoolId: stakingConstants.INITIAL_POOL_ID, }; const PROPOSAL_DESCRIPTION = 'A very compelling proposal!'; const TREASURY_BALANCE = constants.INITIAL_ERC20_BALANCE; const INVALID_PROPOSAL_ID = new BigNumber(999); const GRANT_PROPOSALS = [ { recipient: randomAddress(), amount: getRandomInteger(1, TREASURY_BALANCE.dividedToIntegerBy(2)) }, { recipient: randomAddress(), amount: getRandomInteger(1, TREASURY_BALANCE.dividedToIntegerBy(2)) }, ]; interface ProposedAction { target: string; data: string; value: BigNumber; } let zrx: DummyERC20TokenContract; let weth: DummyERC20TokenContract; let erc20ProxyContract: ERC20ProxyContract; let staking: TestStakingContract; let treasury: ZrxTreasuryContract; let defaultPoolId: string; let defaultPoolOperator: DefaultPoolOperatorContract; let admin: string; let nonDefaultPoolId: string; let poolOperator: string; let delegator: string; let relayer: string; let delegatorPrivateKey: string; let actions: ProposedAction[]; async function deployStakingAsync(): Promise { erc20ProxyContract = await ERC20ProxyContract.deployFrom0xArtifactAsync( assetProxyArtifacts.ERC20Proxy, env.provider, env.txDefaults, assetProxyArtifacts, ); const zrxVaultContract = await ZrxVaultContract.deployFrom0xArtifactAsync( stakingArtifacts.ZrxVault, env.provider, env.txDefaults, stakingArtifacts, erc20ProxyContract.address, zrx.address, ); await erc20ProxyContract.addAuthorizedAddress(zrxVaultContract.address).awaitTransactionSuccessAsync(); await zrxVaultContract.addAuthorizedAddress(admin).awaitTransactionSuccessAsync(); const stakingLogic = await TestStakingContract.deployFrom0xArtifactAsync( stakingArtifacts.TestStaking, env.provider, env.txDefaults, artifacts, weth.address, zrxVaultContract.address, ); const stakingProxyContract = await StakingProxyContract.deployFrom0xArtifactAsync( stakingArtifacts.StakingProxy, env.provider, env.txDefaults, artifacts, stakingLogic.address, ); await stakingProxyContract.addAuthorizedAddress(admin).awaitTransactionSuccessAsync(); await zrxVaultContract.setStakingProxy(stakingProxyContract.address).awaitTransactionSuccessAsync(); staking = new TestStakingContract(stakingProxyContract.address, env.provider, env.txDefaults); } async function fastForwardToNextEpochAsync(): Promise { const epochEndTime = await staking.getCurrentEpochEarliestEndTimeInSeconds().callAsync(); const lastBlockTime = await env.web3Wrapper.getBlockTimestampAsync('latest'); const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber()); await env.web3Wrapper.increaseTimeAsync(dt); // mine next block await env.web3Wrapper.mineBlockAsync(); await staking.endEpoch().awaitTransactionSuccessAsync(); } before(async () => { const accounts = await env.getAccountAddressesAsync(); [admin, poolOperator, delegator, relayer] = accounts; delegatorPrivateKey = hexUtils.toHex(constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(delegator)]); zrx = await DummyERC20TokenContract.deployFrom0xArtifactAsync( erc20Artifacts.DummyERC20Token, env.provider, env.txDefaults, erc20Artifacts, constants.DUMMY_TOKEN_NAME, constants.DUMMY_TOKEN_SYMBOL, constants.DUMMY_TOKEN_DECIMALS, constants.DUMMY_TOKEN_TOTAL_SUPPLY, ); weth = await DummyERC20TokenContract.deployFrom0xArtifactAsync( erc20Artifacts.DummyERC20Token, env.provider, env.txDefaults, erc20Artifacts, constants.DUMMY_TOKEN_NAME, constants.DUMMY_TOKEN_SYMBOL, constants.DUMMY_TOKEN_DECIMALS, constants.DUMMY_TOKEN_TOTAL_SUPPLY, ); await deployStakingAsync(); await zrx.mint(constants.INITIAL_ERC20_BALANCE).awaitTransactionSuccessAsync({ from: poolOperator }); await zrx.mint(constants.INITIAL_ERC20_BALANCE).awaitTransactionSuccessAsync({ from: delegator }); await zrx .approve(erc20ProxyContract.address, constants.INITIAL_ERC20_ALLOWANCE) .awaitTransactionSuccessAsync({ from: poolOperator }); await zrx .approve(erc20ProxyContract.address, constants.INITIAL_ERC20_ALLOWANCE) .awaitTransactionSuccessAsync({ from: delegator }); defaultPoolOperator = await DefaultPoolOperatorContract.deployFrom0xArtifactAsync( artifacts.DefaultPoolOperator, env.provider, env.txDefaults, { ...artifacts, ...erc20Artifacts }, staking.address, weth.address, ); defaultPoolId = stakingConstants.INITIAL_POOL_ID; const createStakingPoolTx = staking.createStakingPool(stakingConstants.PPM, false); nonDefaultPoolId = await createStakingPoolTx.callAsync({ from: poolOperator }); await createStakingPoolTx.awaitTransactionSuccessAsync({ from: poolOperator }); treasury = await ZrxTreasuryContract.deployFrom0xArtifactAsync( artifacts.ZrxTreasury, env.provider, env.txDefaults, { ...artifacts, ...erc20Artifacts }, staking.address, TREASURY_PARAMS, ); await zrx.mint(TREASURY_BALANCE).awaitTransactionSuccessAsync(); await zrx.transfer(treasury.address, TREASURY_BALANCE).awaitTransactionSuccessAsync(); actions = [ { target: zrx.address, data: zrx .transfer(GRANT_PROPOSALS[0].recipient, GRANT_PROPOSALS[0].amount) .getABIEncodedTransactionData(), value: constants.ZERO_AMOUNT, }, { target: zrx.address, data: zrx .transfer(GRANT_PROPOSALS[1].recipient, GRANT_PROPOSALS[1].amount) .getABIEncodedTransactionData(), value: constants.ZERO_AMOUNT, }, ]; }); describe('getVotingPower()', () => { it('Unstaked ZRX has no voting power', async () => { const votingPower = await treasury.getVotingPower(delegator, []).callAsync(); expect(votingPower).to.bignumber.equal(0); }); it('Staked but undelegated ZRX has no voting power', async () => { await staking.stake(constants.INITIAL_ERC20_BALANCE).awaitTransactionSuccessAsync({ from: delegator }); const votingPower = await treasury.getVotingPower(delegator, []).callAsync(); expect(votingPower).to.bignumber.equal(0); }); it('ZRX delegated during epoch N has no voting power during Epoch N', async () => { await staking.stake(TREASURY_PARAMS.proposalThreshold).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), TREASURY_PARAMS.proposalThreshold, ) .awaitTransactionSuccessAsync({ from: delegator }); const votingPower = await treasury.getVotingPower(delegator, []).callAsync(); expect(votingPower).to.bignumber.equal(0); await fastForwardToNextEpochAsync(); }); it('ZRX delegated to the default pool retains full voting power', async () => { await staking.stake(TREASURY_PARAMS.proposalThreshold).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), TREASURY_PARAMS.proposalThreshold, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const votingPower = await treasury.getVotingPower(delegator, []).callAsync(); expect(votingPower).to.bignumber.equal(TREASURY_PARAMS.proposalThreshold); }); it('ZRX delegated to a non-default pool splits voting power between delegator and pool operator', async () => { await staking.stake(TREASURY_PARAMS.proposalThreshold).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, nonDefaultPoolId), TREASURY_PARAMS.proposalThreshold, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const delegatorVotingPower = await treasury.getVotingPower(delegator, []).callAsync(); expect(delegatorVotingPower).to.bignumber.equal(TREASURY_PARAMS.proposalThreshold.dividedBy(2)); const operatorVotingPower = await treasury.getVotingPower(poolOperator, [nonDefaultPoolId]).callAsync(); expect(operatorVotingPower).to.bignumber.equal(TREASURY_PARAMS.proposalThreshold.dividedBy(2)); }); it('Reverts if given duplicate pool IDs', async () => { await staking.stake(TREASURY_PARAMS.proposalThreshold).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, nonDefaultPoolId), TREASURY_PARAMS.proposalThreshold, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const tx = treasury.getVotingPower(poolOperator, [nonDefaultPoolId, nonDefaultPoolId]).callAsync(); return expect(tx).to.revertWith('getVotingPower/DUPLICATE_POOL_ID'); }); it('Correctly sums voting power delegated to multiple pools', async () => { await staking .stake(TREASURY_PARAMS.proposalThreshold.times(2)) .awaitTransactionSuccessAsync({ from: delegator }); // Delegate half of total stake to the default pool. await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), TREASURY_PARAMS.proposalThreshold, ) .awaitTransactionSuccessAsync({ from: delegator }); // Delegate the other half to a non-default pool. await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, nonDefaultPoolId), TREASURY_PARAMS.proposalThreshold, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const delegatorVotingPower = await treasury.getVotingPower(delegator, []).callAsync(); expect(delegatorVotingPower).to.bignumber.equal(TREASURY_PARAMS.proposalThreshold.times(1.5)); }); it('Correctly sums voting power for operator with multiple pools', async () => { const createStakingPoolTx = staking.createStakingPool(stakingConstants.PPM, false); const firstPool = nonDefaultPoolId; const secondPool = await createStakingPoolTx.callAsync({ from: poolOperator }); await createStakingPoolTx.awaitTransactionSuccessAsync({ from: poolOperator }); const amountDelegatedToDefaultPool = new BigNumber(1337); const amountSelfDelegatedToFirstPool = new BigNumber(420); const amountExternallyDelegatedToSecondPool = new BigNumber(2020); await staking .stake(amountDelegatedToDefaultPool.plus(amountSelfDelegatedToFirstPool)) .awaitTransactionSuccessAsync({ from: poolOperator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), amountDelegatedToDefaultPool, ) .awaitTransactionSuccessAsync({ from: poolOperator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, firstPool), amountSelfDelegatedToFirstPool, ) .awaitTransactionSuccessAsync({ from: poolOperator }); await staking .stake(amountExternallyDelegatedToSecondPool) .awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, secondPool), amountExternallyDelegatedToSecondPool, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const votingPower = await treasury.getVotingPower(poolOperator, [firstPool, secondPool]).callAsync(); expect(votingPower).to.bignumber.equal( amountDelegatedToDefaultPool .plus(amountSelfDelegatedToFirstPool) .plus(amountExternallyDelegatedToSecondPool.dividedToIntegerBy(2)), ); }); }); describe('propose()', () => { it('Cannot create proposal without sufficient voting power', async () => { const votingPower = TREASURY_PARAMS.proposalThreshold.minus(1); await staking.stake(votingPower).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), votingPower, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const currentEpoch = await staking.currentEpoch().callAsync(); const tx = treasury .propose(actions, currentEpoch.plus(2), PROPOSAL_DESCRIPTION, []) .awaitTransactionSuccessAsync({ from: delegator }); return expect(tx).to.revertWith('propose/INSUFFICIENT_VOTING_POWER'); }); it('Cannot create proposal with no actions', async () => { const votingPower = TREASURY_PARAMS.proposalThreshold; await staking.stake(votingPower).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), votingPower, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const currentEpoch = await staking.currentEpoch().callAsync(); const tx = treasury .propose([], currentEpoch.plus(2), PROPOSAL_DESCRIPTION, []) .awaitTransactionSuccessAsync({ from: delegator }); return expect(tx).to.revertWith('propose/NO_ACTIONS_PROPOSED'); }); it('Cannot create proposal with an invalid execution epoch', async () => { const votingPower = TREASURY_PARAMS.proposalThreshold; await staking.stake(votingPower).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), votingPower, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const currentEpoch = await staking.currentEpoch().callAsync(); const tx = treasury .propose(actions, currentEpoch.plus(1), PROPOSAL_DESCRIPTION, []) .awaitTransactionSuccessAsync({ from: delegator }); return expect(tx).to.revertWith('propose/INVALID_EXECUTION_EPOCH'); }); it('Can create a valid proposal', async () => { const votingPower = TREASURY_PARAMS.proposalThreshold; await staking.stake(votingPower).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), votingPower, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const currentEpoch = await staking.currentEpoch().callAsync(); const executionEpoch = currentEpoch.plus(2); const tx = await treasury .propose(actions, executionEpoch, PROPOSAL_DESCRIPTION, []) .awaitTransactionSuccessAsync({ from: delegator }); const proposalId = new BigNumber(0); verifyEventsFromLogs( tx.logs, [ { proposer: delegator, operatedPoolIds: [], proposalId, actions, executionEpoch, description: PROPOSAL_DESCRIPTION, }, ], ZrxTreasuryEvents.ProposalCreated, ); expect(await treasury.proposalCount().callAsync()).to.bignumber.equal(1); }); }); describe('castVote() and castVoteBySignature()', () => { const VOTE_PROPOSAL_ID = new BigNumber(0); const DELEGATOR_VOTING_POWER = new BigNumber(420); before(async () => { await staking.stake(DELEGATOR_VOTING_POWER).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), DELEGATOR_VOTING_POWER, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const currentEpoch = await staking.currentEpoch().callAsync(); await treasury .propose(actions, currentEpoch.plus(2), PROPOSAL_DESCRIPTION, []) .awaitTransactionSuccessAsync({ from: delegator }); }); // castVote() it('Cannot vote on invalid proposalId', async () => { await fastForwardToNextEpochAsync(); await fastForwardToNextEpochAsync(); const tx = treasury .castVote(INVALID_PROPOSAL_ID, true, []) .awaitTransactionSuccessAsync({ from: delegator }); return expect(tx).to.revertWith('_castVote/INVALID_PROPOSAL_ID'); }); it('Cannot vote before voting period starts', async () => { const tx = treasury.castVote(VOTE_PROPOSAL_ID, true, []).awaitTransactionSuccessAsync({ from: delegator }); return expect(tx).to.revertWith('_castVote/VOTING_IS_CLOSED'); }); it('Cannot vote after voting period ends', async () => { await fastForwardToNextEpochAsync(); await fastForwardToNextEpochAsync(); await env.web3Wrapper.increaseTimeAsync(TREASURY_PARAMS.votingPeriod.plus(1).toNumber()); await env.web3Wrapper.mineBlockAsync(); const tx = treasury.castVote(VOTE_PROPOSAL_ID, true, []).awaitTransactionSuccessAsync({ from: delegator }); return expect(tx).to.revertWith('_castVote/VOTING_IS_CLOSED'); }); it('Cannot vote twice on same proposal', async () => { await fastForwardToNextEpochAsync(); await fastForwardToNextEpochAsync(); await treasury.castVote(VOTE_PROPOSAL_ID, true, []).awaitTransactionSuccessAsync({ from: delegator }); const tx = treasury.castVote(VOTE_PROPOSAL_ID, false, []).awaitTransactionSuccessAsync({ from: delegator }); return expect(tx).to.revertWith('_castVote/ALREADY_VOTED'); }); it('Can cast a valid vote', async () => { await fastForwardToNextEpochAsync(); await fastForwardToNextEpochAsync(); const tx = await treasury .castVote(VOTE_PROPOSAL_ID, true, []) .awaitTransactionSuccessAsync({ from: delegator }); verifyEventsFromLogs( tx.logs, [ { voter: delegator, operatedPoolIds: [], proposalId: VOTE_PROPOSAL_ID, support: true, votingPower: DELEGATOR_VOTING_POWER, }, ], ZrxTreasuryEvents.VoteCast, ); }); // castVoteBySignature() it('Cannot vote by signature on invalid proposalId', async () => { await fastForwardToNextEpochAsync(); await fastForwardToNextEpochAsync(); const vote = new TreasuryVote({ proposalId: INVALID_PROPOSAL_ID, verifyingContract: admin, }); const signature = vote.getSignatureWithKey(delegatorPrivateKey); const tx = treasury .castVoteBySignature(INVALID_PROPOSAL_ID, true, [], signature.v, signature.r, signature.s) .awaitTransactionSuccessAsync({ from: relayer }); return expect(tx).to.revertWith('_castVote/INVALID_PROPOSAL_ID'); }); it('Cannot vote by signature before voting period starts', async () => { const vote = new TreasuryVote({ proposalId: VOTE_PROPOSAL_ID, verifyingContract: admin, }); const signature = vote.getSignatureWithKey(delegatorPrivateKey); const tx = treasury .castVoteBySignature(VOTE_PROPOSAL_ID, true, [], signature.v, signature.r, signature.s) .awaitTransactionSuccessAsync({ from: relayer }); return expect(tx).to.revertWith('_castVote/VOTING_IS_CLOSED'); }); it('Cannot vote by signature after voting period ends', async () => { await fastForwardToNextEpochAsync(); await fastForwardToNextEpochAsync(); await env.web3Wrapper.increaseTimeAsync(TREASURY_PARAMS.votingPeriod.plus(1).toNumber()); await env.web3Wrapper.mineBlockAsync(); const vote = new TreasuryVote({ proposalId: VOTE_PROPOSAL_ID, verifyingContract: admin, }); const signature = vote.getSignatureWithKey(delegatorPrivateKey); const tx = treasury .castVoteBySignature(VOTE_PROPOSAL_ID, true, [], signature.v, signature.r, signature.s) .awaitTransactionSuccessAsync({ from: relayer }); return expect(tx).to.revertWith('_castVote/VOTING_IS_CLOSED'); }); it('Can recover the address from signature correctly', async () => { const vote = new TreasuryVote({ proposalId: VOTE_PROPOSAL_ID, verifyingContract: admin, }); const signature = vote.getSignatureWithKey(delegatorPrivateKey); const publicKey = ethUtil.ecrecover( ethUtil.toBuffer(vote.getEIP712Hash()), signature.v, ethUtil.toBuffer(signature.r), ethUtil.toBuffer(signature.s), ); const address = ethUtil.publicToAddress(publicKey); expect(ethUtil.bufferToHex(address)).to.be.equal(delegator); }); it('Can cast a valid vote by signature', async () => { await fastForwardToNextEpochAsync(); await fastForwardToNextEpochAsync(); const vote = new TreasuryVote({ proposalId: VOTE_PROPOSAL_ID, verifyingContract: treasury.address, chainId: 1337, support: false, }); const signature = vote.getSignatureWithKey(delegatorPrivateKey); const tx = await treasury .castVoteBySignature(VOTE_PROPOSAL_ID, false, [], signature.v, signature.r, signature.s) .awaitTransactionSuccessAsync({ from: relayer }); verifyEventsFromLogs( tx.logs, [ { voter: delegator, operatedPoolIds: [], proposalId: VOTE_PROPOSAL_ID, support: vote.support, votingPower: DELEGATOR_VOTING_POWER, }, ], ZrxTreasuryEvents.VoteCast, ); }); it('Cannot vote by signature twice on same proposal', async () => { await fastForwardToNextEpochAsync(); await fastForwardToNextEpochAsync(); await treasury.castVote(VOTE_PROPOSAL_ID, true, []).awaitTransactionSuccessAsync({ from: delegator }); const secondVote = new TreasuryVote({ proposalId: VOTE_PROPOSAL_ID, verifyingContract: treasury.address, chainId: 1337, support: false, }); const signature = secondVote.getSignatureWithKey(delegatorPrivateKey); const secondVoteTx = treasury .castVoteBySignature(VOTE_PROPOSAL_ID, false, [], signature.v, signature.r, signature.s) .awaitTransactionSuccessAsync({ from: relayer }); return expect(secondVoteTx).to.revertWith('_castVote/ALREADY_VOTED'); }); }); describe('execute()', () => { let passedProposalId: BigNumber; let failedProposalId: BigNumber; let defeatedProposalId: BigNumber; let ongoingVoteProposalId: BigNumber; before(async () => { // Operator has enough ZRX to create and pass a proposal await staking.stake(TREASURY_PARAMS.quorumThreshold).awaitTransactionSuccessAsync({ from: poolOperator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), TREASURY_PARAMS.quorumThreshold, ) .awaitTransactionSuccessAsync({ from: poolOperator }); // Delegator only has enough ZRX to create a proposal await staking.stake(TREASURY_PARAMS.proposalThreshold).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), TREASURY_PARAMS.proposalThreshold, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const currentEpoch = await staking.currentEpoch().callAsync(); // Proposal 0 let tx = treasury.propose(actions, currentEpoch.plus(4), PROPOSAL_DESCRIPTION, []); passedProposalId = await tx.callAsync({ from: delegator }); await tx.awaitTransactionSuccessAsync({ from: delegator }); // Proposal 1 tx = treasury.propose(actions, currentEpoch.plus(3), PROPOSAL_DESCRIPTION, []); failedProposalId = await tx.callAsync({ from: delegator }); await tx.awaitTransactionSuccessAsync({ from: delegator }); // Proposal 2 tx = treasury.propose(actions, currentEpoch.plus(3), PROPOSAL_DESCRIPTION, []); defeatedProposalId = await tx.callAsync({ from: delegator }); await tx.awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); // Proposal 3 tx = treasury.propose(actions, currentEpoch.plus(3), PROPOSAL_DESCRIPTION, []); ongoingVoteProposalId = await tx.callAsync({ from: delegator }); await tx.awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); /********** Start Vote Epoch for Proposals 0, 1, 2 **********/ // Proposal 0 passes await treasury.castVote(passedProposalId, true, []).awaitTransactionSuccessAsync({ from: poolOperator }); // Proposal 1 fails to reach quorum await treasury.castVote(failedProposalId, true, []).awaitTransactionSuccessAsync({ from: delegator }); // Proposal 2 is voted down await treasury.castVote(defeatedProposalId, true, []).awaitTransactionSuccessAsync({ from: delegator }); await treasury.castVote(defeatedProposalId, false, []).awaitTransactionSuccessAsync({ from: poolOperator }); /********** End Vote Epoch for Proposals 0, 1, 2 **********/ await fastForwardToNextEpochAsync(); /********** Start Execution Epoch for Proposals 1, 2, 3 **********/ /********** Start Vote Epoch for Proposal 3 **********************/ // Proposal 3 has enough votes to pass, but the vote is ongoing await treasury .castVote(ongoingVoteProposalId, true, []) .awaitTransactionSuccessAsync({ from: poolOperator }); }); it('Cannot execute an invalid proposalId', async () => { const tx = treasury.execute(INVALID_PROPOSAL_ID, actions).awaitTransactionSuccessAsync(); return expect(tx).to.revertWith('execute/INVALID_PROPOSAL_ID'); }); it('Cannot execute a proposal whose vote is ongoing', async () => { const tx = treasury.execute(ongoingVoteProposalId, actions).awaitTransactionSuccessAsync(); return expect(tx).to.revertWith('_assertProposalExecutable/PROPOSAL_HAS_NOT_PASSED'); }); it('Cannot execute a proposal that failed to reach quorum', async () => { const tx = treasury.execute(failedProposalId, actions).awaitTransactionSuccessAsync(); return expect(tx).to.revertWith('_assertProposalExecutable/PROPOSAL_HAS_NOT_PASSED'); }); it('Cannot execute a proposal that was defeated in its vote', async () => { const tx = treasury.execute(defeatedProposalId, actions).awaitTransactionSuccessAsync(); return expect(tx).to.revertWith('_assertProposalExecutable/PROPOSAL_HAS_NOT_PASSED'); }); it('Cannot execute before or after the execution epoch', async () => { const tooEarly = treasury.execute(passedProposalId, actions).awaitTransactionSuccessAsync(); await expect(tooEarly).to.revertWith('_assertProposalExecutable/CANNOT_EXECUTE_THIS_EPOCH'); await fastForwardToNextEpochAsync(); // Proposal 0 is executable here await fastForwardToNextEpochAsync(); const tooLate = treasury.execute(passedProposalId, actions).awaitTransactionSuccessAsync(); return expect(tooLate).to.revertWith('_assertProposalExecutable/CANNOT_EXECUTE_THIS_EPOCH'); }); it('Cannot execute the same proposal twice', async () => { await fastForwardToNextEpochAsync(); await treasury.execute(passedProposalId, actions).awaitTransactionSuccessAsync(); const tx = treasury.execute(passedProposalId, actions).awaitTransactionSuccessAsync(); return expect(tx).to.revertWith('_assertProposalExecutable/PROPOSAL_ALREADY_EXECUTED'); }); it('Cannot execute actions that do not match the proposal `actionsHash`', async () => { await fastForwardToNextEpochAsync(); const tx = treasury .execute(passedProposalId, [ { target: zrx.address, data: zrx.transfer(randomAddress(), GRANT_PROPOSALS[0].amount).getABIEncodedTransactionData(), value: constants.ZERO_AMOUNT, }, ]) .awaitTransactionSuccessAsync(); return expect(tx).to.revertWith('_assertProposalExecutable/INVALID_ACTIONS'); }); it('Can execute a valid proposal', async () => { await fastForwardToNextEpochAsync(); const tx = await treasury.execute(passedProposalId, actions).awaitTransactionSuccessAsync(); verifyEventsFromLogs(tx.logs, [{ proposalId: passedProposalId }], ZrxTreasuryEvents.ProposalExecuted); expect(await zrx.balanceOf(GRANT_PROPOSALS[0].recipient).callAsync()).to.bignumber.equal( GRANT_PROPOSALS[0].amount, ); expect(await zrx.balanceOf(GRANT_PROPOSALS[1].recipient).callAsync()).to.bignumber.equal( GRANT_PROPOSALS[1].amount, ); }); }); describe('Default pool operator contract', () => { it('Returns WETH to the staking proxy', async () => { const wethAmount = new BigNumber(1337); await weth.mint(wethAmount).awaitTransactionSuccessAsync(); // Some amount of WETH ends up in the default pool operator // contract, e.g. from errant staking rewards. await weth.transfer(defaultPoolOperator.address, wethAmount).awaitTransactionSuccessAsync(); // This function should send all the WETH to the staking proxy. await defaultPoolOperator.returnStakingRewards().awaitTransactionSuccessAsync(); expect(await weth.balanceOf(defaultPoolOperator.address).callAsync()).to.bignumber.equal(0); expect(await weth.balanceOf(staking.address).callAsync()).to.bignumber.equal(wethAmount); }); }); describe('Can update thresholds via proposal', () => { it('Updates proposal and quorum thresholds', async () => { // Delegator has enough ZRX to create and pass a proposal await staking.stake(TREASURY_PARAMS.quorumThreshold).awaitTransactionSuccessAsync({ from: delegator }); await staking .moveStake( new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Delegated, defaultPoolId), TREASURY_PARAMS.quorumThreshold, ) .awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); const currentEpoch = await staking.currentEpoch().callAsync(); const newProposalThreshold = new BigNumber(420); const newQuorumThreshold = new BigNumber(1337); const updateThresholdsAction = { target: treasury.address, data: treasury .updateThresholds(newProposalThreshold, newQuorumThreshold) .getABIEncodedTransactionData(), value: constants.ZERO_AMOUNT, }; const tx = treasury.propose( [updateThresholdsAction], currentEpoch.plus(3), `Updates proposal threshold to ${newProposalThreshold} and quorum threshold to ${newQuorumThreshold}`, [], ); const proposalId = await tx.callAsync({ from: delegator }); await tx.awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); await fastForwardToNextEpochAsync(); await treasury.castVote(proposalId, true, []).awaitTransactionSuccessAsync({ from: delegator }); await fastForwardToNextEpochAsync(); await treasury .execute(proposalId, [updateThresholdsAction]) .awaitTransactionSuccessAsync({ from: delegator }); const proposalThreshold = await treasury.proposalThreshold().callAsync(); const quorumThreshold = await treasury.quorumThreshold().callAsync(); expect(proposalThreshold).to.bignumber.equal(newProposalThreshold); expect(quorumThreshold).to.bignumber.equal(newQuorumThreshold); }); }); });