647 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			647 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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 { BigNumber } from '@0x/utils';
 | 
						||
import * as _ from 'lodash';
 | 
						||
 | 
						||
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 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 () => {
 | 
						||
        [admin, poolOperator, delegator] = await env.getAccountAddressesAsync();
 | 
						||
        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()', () => {
 | 
						||
        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 });
 | 
						||
        });
 | 
						||
        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,
 | 
						||
            );
 | 
						||
        });
 | 
						||
    });
 | 
						||
    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();
 | 
						||
            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);
 | 
						||
        });
 | 
						||
    });
 | 
						||
});
 |