protocol/contracts/treasury/test/treasury_test.ts
Lawrence Forman 23f6e9e53c
fix mixed @0x deps (#328)
Co-authored-by: Lawrence Forman <me@merklejerk.com>
2021-09-15 00:01:15 -04:00

757 lines
38 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<void> {
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<void> {
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);
});
});
});