consolidate MixinVaultCore and ZrxVault

This commit is contained in:
Michael Zhu
2019-09-23 18:22:02 -07:00
parent 1c42d0ab3c
commit dc06497cae
24 changed files with 573 additions and 959 deletions

View File

@@ -25,7 +25,7 @@ import {
toBaseUnitAmount,
} from '../utils/number_utils';
blockchainTests.resets('delegator unit rewards', env => {
blockchainTests.resets('Delegator rewards unit tests', env => {
let testContract: TestDelegatorRewardsContract;
before(async () => {

View File

@@ -24,7 +24,7 @@ import {
} from '../../src';
import { assertIntegerRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils';
blockchainTests.resets('finalizer unit tests', env => {
blockchainTests.resets('Finalizer unit tests', env => {
const { ZERO_AMOUNT } = constants;
const INITIAL_EPOCH = 0;
const INITIAL_BALANCE = toBaseUnitAmount(32);

View File

@@ -0,0 +1,207 @@
import { blockchainTests, Numberish } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { artifacts, TestCobbDouglasContract } from '../../src/';
import { assertRoughlyEquals, getRandomInteger, getRandomPortion, toDecimal } from '../utils/number_utils';
// tslint:disable: no-unnecessary-type-assertion
blockchainTests('LibCobbDouglas unit tests', env => {
const FUZZ_COUNT = 1024;
const PRECISION = 15;
let testContract: TestCobbDouglasContract;
let ownerAddress: string;
let notOwnerAddress: string;
before(async () => {
[ownerAddress, notOwnerAddress] = await env.getAccountAddressesAsync();
testContract = await TestCobbDouglasContract.deployFrom0xArtifactAsync(
artifacts.TestCobbDouglas,
env.provider,
env.txDefaults,
artifacts,
);
});
describe('cobbDouglas()', () => {
interface CobbDouglasParams {
totalRewards: Numberish;
ownerFees: Numberish;
totalFees: Numberish;
ownerStake: Numberish;
totalStake: Numberish;
alphaNumerator: Numberish;
alphaDenominator: Numberish;
gas?: number;
}
const MAX_COBB_DOUGLAS_GAS = 11e3;
const TX_GAS_FEE = 21e3;
const DEFAULT_COBB_DOUGLAS_PARAMS: CobbDouglasParams = {
totalRewards: 100e18,
ownerFees: 10e18,
totalFees: 500e18,
ownerStake: 1.1e21,
totalStake: 3e27,
alphaNumerator: 1,
alphaDenominator: 3,
gas: MAX_COBB_DOUGLAS_GAS,
};
async function callCobbDouglasAsync(params?: Partial<CobbDouglasParams>): Promise<BigNumber> {
const _params = {
...DEFAULT_COBB_DOUGLAS_PARAMS,
...params,
};
return testContract.cobbDouglas.callAsync(
new BigNumber(_params.totalRewards),
new BigNumber(_params.ownerFees),
new BigNumber(_params.totalFees),
new BigNumber(_params.ownerStake),
new BigNumber(_params.totalStake),
new BigNumber(_params.alphaNumerator),
new BigNumber(_params.alphaDenominator),
{
gas: TX_GAS_FEE + (_params.gas === undefined ? MAX_COBB_DOUGLAS_GAS : _params.gas),
},
);
}
function cobbDouglas(params?: Partial<CobbDouglasParams>): BigNumber {
const { totalRewards, ownerFees, totalFees, ownerStake, totalStake, alphaNumerator, alphaDenominator } = {
...DEFAULT_COBB_DOUGLAS_PARAMS,
...params,
};
const feeRatio = toDecimal(ownerFees).dividedBy(toDecimal(totalFees));
const stakeRatio = toDecimal(ownerStake).dividedBy(toDecimal(totalStake));
const alpha = toDecimal(alphaNumerator).dividedBy(toDecimal(alphaDenominator));
// totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)
return new BigNumber(
feeRatio
.pow(alpha)
.times(stakeRatio.pow(toDecimal(1).minus(alpha)))
.times(toDecimal(totalRewards))
.toFixed(0, BigNumber.ROUND_FLOOR),
);
}
function getRandomParams(overrides?: Partial<CobbDouglasParams>): CobbDouglasParams {
const totalRewards = _.get(overrides, 'totalRewards', getRandomInteger(0, 1e27)) as Numberish;
const totalFees = _.get(overrides, 'totalFees', getRandomInteger(1, 1e27)) as Numberish;
const ownerFees = _.get(overrides, 'ownerFees', getRandomPortion(totalFees)) as Numberish;
const totalStake = _.get(overrides, 'totalStake', getRandomInteger(1, 1e27)) as Numberish;
const ownerStake = _.get(overrides, 'ownerStake', getRandomPortion(totalStake)) as Numberish;
const alphaDenominator = _.get(overrides, 'alphaDenominator', getRandomInteger(1, 1e6)) as Numberish;
const alphaNumerator = _.get(overrides, 'alphaNumerator', getRandomPortion(alphaDenominator)) as Numberish;
return {
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaNumerator,
alphaDenominator,
};
}
it('computes the correct reward', async () => {
const expected = cobbDouglas();
const r = await callCobbDouglasAsync();
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with zero stake ratio', async () => {
const ownerStake = 0;
const expected = cobbDouglas({ ownerStake });
const r = await callCobbDouglasAsync({ ownerStake });
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with full stake ratio', async () => {
const ownerStake = DEFAULT_COBB_DOUGLAS_PARAMS.totalStake;
const expected = cobbDouglas({ ownerStake });
const r = await callCobbDouglasAsync({ ownerStake });
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with a very low stake ratio', async () => {
const ownerStake = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalStake).times(1e-18);
const expected = cobbDouglas({ ownerStake });
const r = await callCobbDouglasAsync({ ownerStake });
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with a very high stake ratio', async () => {
const ownerStake = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalStake).times(1 - 1e-18);
const expected = cobbDouglas({ ownerStake });
const r = await callCobbDouglasAsync({ ownerStake });
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with zero fee ratio', async () => {
const ownerFees = 0;
const expected = cobbDouglas({ ownerFees });
const r = await callCobbDouglasAsync({ ownerFees });
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with full fee ratio', async () => {
const ownerFees = DEFAULT_COBB_DOUGLAS_PARAMS.totalFees;
const expected = cobbDouglas({ ownerFees });
const r = await callCobbDouglasAsync({ ownerFees });
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with a very low fee ratio', async () => {
const ownerFees = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalFees).times(1e-18);
const expected = cobbDouglas({ ownerFees });
const r = await callCobbDouglasAsync({ ownerFees });
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with a very high fee ratio', async () => {
const ownerFees = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalFees).times(1 - 1e-18);
const expected = cobbDouglas({ ownerFees });
const r = await callCobbDouglasAsync({ ownerFees });
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with equal fee and stake ratios', async () => {
const ownerFees = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalFees).times(0.5);
const ownerStake = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalStake).times(0.5);
const expected = cobbDouglas({ ownerFees, ownerStake });
const r = await callCobbDouglasAsync({ ownerFees, ownerStake });
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with full fee and stake ratios', async () => {
const ownerFees = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalFees);
const ownerStake = new BigNumber(DEFAULT_COBB_DOUGLAS_PARAMS.totalStake);
const expected = cobbDouglas({ ownerFees, ownerStake });
const r = await callCobbDouglasAsync({ ownerFees, ownerStake });
assertRoughlyEquals(r, expected, PRECISION);
});
it('computes the correct reward with zero fee and stake ratios', async () => {
const ownerFees = 0;
const ownerStake = 0;
const expected = cobbDouglas({ ownerFees, ownerStake });
const r = await callCobbDouglasAsync({ ownerFees, ownerStake });
assertRoughlyEquals(r, expected, PRECISION);
});
blockchainTests.optional('fuzzing', () => {
const inputs = _.times(FUZZ_COUNT, () => getRandomParams());
for (const params of inputs) {
it(`cobbDouglas(${JSON.stringify(params)})`, async () => {
const expected = cobbDouglas(params);
const r = await callCobbDouglasAsync(params);
assertRoughlyEquals(r, expected, PRECISION);
});
}
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@@ -7,7 +7,7 @@ import { artifacts, TestLibFixedMathContract } from '../../src';
import { assertRoughlyEquals, fromFixed, toDecimal, toFixed } from '../utils/number_utils';
blockchainTests('LibFixedMath', env => {
blockchainTests('LibFixedMath unit tests', env => {
let testContract: TestLibFixedMathContract;
before(async () => {

View File

@@ -4,7 +4,7 @@ import { cartesianProduct } from 'js-combinatorics';
import { artifacts, TestLibProxyContract, TestLibProxyReceiverContract } from '../../src';
blockchainTests.resets('LibProxy', env => {
blockchainTests.resets('LibProxy unit tests', env => {
let proxy: TestLibProxyContract;
let receiver: TestLibProxyReceiverContract;

View File

@@ -3,7 +3,7 @@ import { BigNumber, SafeMathRevertErrors } from '@0x/utils';
import { artifacts, TestLibSafeDowncastContract } from '../../src/';
blockchainTests('LibSafeDowncast', env => {
blockchainTests('LibSafeDowncast unit tests', env => {
let testContract: TestLibSafeDowncastContract;
before(async () => {

View File

@@ -1,99 +0,0 @@
import { blockchainTests, expect, filterLogsToArguments } from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { AuthorizableRevertErrors } from '@0x/utils';
import { constants } from '../utils/constants';
import {
artifacts,
TestMixinVaultCoreContract,
TestMixinVaultCoreInCatastrophicFailureModeEventArgs,
TestMixinVaultCoreStakingProxySetEventArgs,
} from '../../src';
blockchainTests.resets('MixinVaultCore', env => {
let owner: string;
let nonOwnerAddresses: string[];
let testContract: TestMixinVaultCoreContract;
before(async () => {
[owner, ...nonOwnerAddresses] = await env.getAccountAddressesAsync();
testContract = await TestMixinVaultCoreContract.deployFrom0xArtifactAsync(
artifacts.TestMixinVaultCore,
env.provider,
env.txDefaults,
artifacts,
);
});
describe('Set staking proxy', () => {
async function testAssertStakingProxyAsync(callerAddress: string): Promise<void> {
const tx = testContract.assertStakingProxy.callAsync({ from: callerAddress });
const expectedError = new StakingRevertErrors.OnlyCallableByStakingContractError(callerAddress);
expect(tx).to.revertWith(expectedError);
}
it('Owner can set staking proxy', async () => {
const newAddress = nonOwnerAddresses[0];
const receipt = await testContract.setStakingProxy.awaitTransactionSuccessAsync(newAddress, {
from: owner,
});
const eventArgs = filterLogsToArguments<TestMixinVaultCoreStakingProxySetEventArgs>(
receipt.logs,
'StakingProxySet',
);
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].stakingProxyAddress).to.equal(newAddress);
expect(await testContract.stakingProxyAddress.callAsync()).to.equal(newAddress);
// The new staking proxy address should be able to pass the modifier check
await testContract.assertStakingProxy.callAsync({ from: newAddress });
return testAssertStakingProxyAsync(owner);
});
it('Non-authorized address cannot set staking proxy', async () => {
const notAuthorized = nonOwnerAddresses[0];
const newAddress = nonOwnerAddresses[1];
const tx = testContract.setStakingProxy.awaitTransactionSuccessAsync(newAddress, {
from: notAuthorized,
});
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
expect(tx).to.revertWith(expectedError);
expect(await testContract.stakingProxyAddress.callAsync()).to.equal(constants.NIL_ADDRESS);
return testAssertStakingProxyAsync(newAddress);
});
});
describe('Catastrophic failure mode', () => {
async function testCatastrophicFailureModeAsync(isInCatastrophicFailure: boolean): Promise<void> {
const [expectToSucceed, expectToRevert] = isInCatastrophicFailure
? [testContract.assertInCatastrophicFailure, testContract.assertNotInCatastrophicFailure]
: [testContract.assertNotInCatastrophicFailure, testContract.assertInCatastrophicFailure];
const expectedError = isInCatastrophicFailure
? new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError()
: new StakingRevertErrors.OnlyCallableIfInCatastrophicFailureError();
await expectToSucceed.callAsync();
expect(expectToRevert.callAsync()).to.revertWith(expectedError);
expect(await testContract.isInCatastrophicFailure.callAsync()).to.equal(isInCatastrophicFailure);
}
it('Owner can turn on catastrophic failure mode', async () => {
await testCatastrophicFailureModeAsync(false);
const receipt = await testContract.enterCatastrophicFailure.awaitTransactionSuccessAsync({ from: owner });
const eventArgs = filterLogsToArguments<TestMixinVaultCoreInCatastrophicFailureModeEventArgs>(
receipt.logs,
'InCatastrophicFailureMode',
);
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].sender).to.equal(owner);
return testCatastrophicFailureModeAsync(true);
});
it('Non-authorized address cannot turn on catastrophic failure mode', async () => {
await testCatastrophicFailureModeAsync(false);
const tx = testContract.enterCatastrophicFailure.awaitTransactionSuccessAsync({
from: nonOwnerAddresses[0],
});
expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(nonOwnerAddresses[0]));
return testCatastrophicFailureModeAsync(false);
});
});
});

View File

@@ -0,0 +1,76 @@
import { blockchainTests, expect, filterLogsToArguments } from '@0x/contracts-test-utils';
import { AuthorizableRevertErrors, BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { artifacts, IStakingEventsParamsSetEventArgs, MixinParamsContract } from '../../src/';
import { constants as stakingConstants } from '../utils/constants';
import { StakingParams } from '../utils/types';
blockchainTests('Configurable Parameters unit tests', env => {
let testContract: MixinParamsContract;
let authorizedAddress: string;
let notAuthorizedAddress: string;
before(async () => {
[authorizedAddress, notAuthorizedAddress] = await env.getAccountAddressesAsync();
testContract = await MixinParamsContract.deployFrom0xArtifactAsync(
artifacts.MixinParams,
env.provider,
env.txDefaults,
artifacts,
);
});
blockchainTests.resets('setParams()', () => {
async function setParamsAndAssertAsync(
params: Partial<StakingParams>,
from?: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const _params = {
...stakingConstants.DEFAULT_PARAMS,
...params,
};
const receipt = await testContract.setParams.awaitTransactionSuccessAsync(
new BigNumber(_params.epochDurationInSeconds),
new BigNumber(_params.rewardDelegatedStakeWeight),
new BigNumber(_params.minimumPoolStake),
new BigNumber(_params.maximumMakersInPool),
new BigNumber(_params.cobbDouglasAlphaNumerator),
new BigNumber(_params.cobbDouglasAlphaDenominator),
{ from },
);
// Assert event.
const events = filterLogsToArguments<IStakingEventsParamsSetEventArgs>(receipt.logs, 'ParamsSet');
expect(events.length).to.eq(1);
const event = events[0];
expect(event.epochDurationInSeconds).to.bignumber.eq(_params.epochDurationInSeconds);
expect(event.rewardDelegatedStakeWeight).to.bignumber.eq(_params.rewardDelegatedStakeWeight);
expect(event.minimumPoolStake).to.bignumber.eq(_params.minimumPoolStake);
expect(event.maximumMakersInPool).to.bignumber.eq(_params.maximumMakersInPool);
expect(event.cobbDouglasAlphaNumerator).to.bignumber.eq(_params.cobbDouglasAlphaNumerator);
expect(event.cobbDouglasAlphaDenominator).to.bignumber.eq(_params.cobbDouglasAlphaDenominator);
// Assert `getParams()`.
const actual = await testContract.getParams.callAsync();
expect(actual[0]).to.bignumber.eq(_params.epochDurationInSeconds);
expect(actual[1]).to.bignumber.eq(_params.rewardDelegatedStakeWeight);
expect(actual[2]).to.bignumber.eq(_params.minimumPoolStake);
expect(actual[3]).to.bignumber.eq(_params.maximumMakersInPool);
expect(actual[4]).to.bignumber.eq(_params.cobbDouglasAlphaNumerator);
expect(actual[5]).to.bignumber.eq(_params.cobbDouglasAlphaDenominator);
return receipt;
}
it('throws if not called by an authorized address', async () => {
const tx = setParamsAndAssertAsync({}, notAuthorizedAddress);
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorizedAddress);
return expect(tx).to.revertWith(expectedError);
});
it('works if called by owner', async () => {
return setParamsAndAssertAsync({});
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@@ -0,0 +1,545 @@
import {
blockchainTests,
constants,
expect,
filterLogsToArguments,
hexRandom,
Numberish,
randomAddress,
} from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { LogEntry } from 'ethereum-types';
import * as _ from 'lodash';
import {
artifacts,
IStakingEventsEvents,
IStakingEventsStakingPoolActivatedEventArgs,
TestProtocolFeesContract,
TestProtocolFeesERC20ProxyTransferFromEventArgs,
TestProtocolFeesEvents,
} from '../../src';
import { getRandomInteger } from '../utils/number_utils';
blockchainTests('Protocol Fees unit tests', env => {
let ownerAddress: string;
let exchangeAddress: string;
let notExchangeAddress: string;
let testContract: TestProtocolFeesContract;
let wethAssetData: string;
let minimumStake: BigNumber;
before(async () => {
[ownerAddress, exchangeAddress, notExchangeAddress] = await env.web3Wrapper.getAvailableAddressesAsync();
// Deploy the protocol fees contract.
testContract = await TestProtocolFeesContract.deployFrom0xArtifactAsync(
artifacts.TestProtocolFees,
env.provider,
{
...env.txDefaults,
from: ownerAddress,
},
artifacts,
exchangeAddress,
);
wethAssetData = await testContract.getWethAssetData.callAsync();
minimumStake = (await testContract.getParams.callAsync())[2];
});
interface CreateTestPoolOpts {
poolId: string;
operatorStake: Numberish;
membersStake: Numberish;
makers: string[];
}
async function createTestPoolAsync(opts?: Partial<CreateTestPoolOpts>): Promise<CreateTestPoolOpts> {
const _opts = {
poolId: hexRandom(),
operatorStake: getRandomInteger(minimumStake, '100e18'),
membersStake: getRandomInteger(minimumStake, '100e18'),
makers: _.times(2, () => randomAddress()),
...opts,
};
await testContract.createTestPool.awaitTransactionSuccessAsync(
_opts.poolId,
new BigNumber(_opts.operatorStake),
new BigNumber(_opts.membersStake),
_opts.makers,
);
return _opts;
}
blockchainTests.resets('payProtocolFee()', () => {
const DEFAULT_PROTOCOL_FEE_PAID = new BigNumber(150e3).times(1e9);
const { ZERO_AMOUNT } = constants;
const makerAddress = randomAddress();
const payerAddress = randomAddress();
describe('forbidden actions', () => {
it('should revert if called by a non-exchange', async () => {
const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: notExchangeAddress },
);
const expectedError = new StakingRevertErrors.OnlyCallableByExchangeError(notExchangeAddress);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `protocolFeePaid` is zero with zero value sent', async () => {
const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
ZERO_AMOUNT,
{ from: exchangeAddress, value: ZERO_AMOUNT },
);
const expectedError = new StakingRevertErrors.InvalidProtocolFeePaymentError(
StakingRevertErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid,
ZERO_AMOUNT,
ZERO_AMOUNT,
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `protocolFeePaid` is zero with non-zero value sent', async () => {
const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
ZERO_AMOUNT,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
const expectedError = new StakingRevertErrors.InvalidProtocolFeePaymentError(
StakingRevertErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid,
ZERO_AMOUNT,
DEFAULT_PROTOCOL_FEE_PAID,
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `protocolFeePaid` is < than the provided message value', async () => {
const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID.minus(1) },
);
const expectedError = new StakingRevertErrors.InvalidProtocolFeePaymentError(
StakingRevertErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment,
DEFAULT_PROTOCOL_FEE_PAID,
DEFAULT_PROTOCOL_FEE_PAID.minus(1),
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `protocolFeePaid` is > than the provided message value', async () => {
const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID.plus(1) },
);
const expectedError = new StakingRevertErrors.InvalidProtocolFeePaymentError(
StakingRevertErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment,
DEFAULT_PROTOCOL_FEE_PAID,
DEFAULT_PROTOCOL_FEE_PAID.plus(1),
);
return expect(tx).to.revertWith(expectedError);
});
});
async function getProtocolFeesAsync(poolId: string): Promise<BigNumber> {
return (await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId)).feesCollected;
}
describe('ETH fees', () => {
function assertNoWETHTransferLogs(logs: LogEntry[]): void {
const logsArgs = filterLogsToArguments<TestProtocolFeesERC20ProxyTransferFromEventArgs>(
logs,
TestProtocolFeesEvents.ERC20ProxyTransferFrom,
);
expect(logsArgs).to.deep.eq([]);
}
it('should not transfer WETH if value is sent', async () => {
await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
assertNoWETHTransferLogs(receipt.logs);
});
it('should credit pool if the maker is in a pool', async () => {
const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
assertNoWETHTransferLogs(receipt.logs);
const poolFees = await getProtocolFeesAsync(poolId);
expect(poolFees).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID);
});
it('should not credit the pool if maker is not in a pool', async () => {
const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
assertNoWETHTransferLogs(receipt.logs);
const poolFees = await getProtocolFeesAsync(poolId);
expect(poolFees).to.bignumber.eq(ZERO_AMOUNT);
});
it('fees paid to the same maker should go to the same pool', async () => {
const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const payAsync = async () => {
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
assertNoWETHTransferLogs(receipt.logs);
};
await payAsync();
await payAsync();
const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2);
const poolFees = await getProtocolFeesAsync(poolId);
expect(poolFees).to.bignumber.eq(expectedTotalFees);
});
});
describe('WETH fees', () => {
function assertWETHTransferLogs(logs: LogEntry[], fromAddress: string, amount: BigNumber): void {
const logsArgs = filterLogsToArguments<TestProtocolFeesERC20ProxyTransferFromEventArgs>(
logs,
TestProtocolFeesEvents.ERC20ProxyTransferFrom,
);
expect(logsArgs.length).to.eq(1);
for (const args of logsArgs) {
expect(args.assetData).to.eq(wethAssetData);
expect(args.from).to.eq(fromAddress);
expect(args.to).to.eq(testContract.address);
expect(args.amount).to.bignumber.eq(amount);
}
}
it('should transfer WETH if no value is sent and the maker is not in a pool', async () => {
await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: ZERO_AMOUNT },
);
assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID);
});
it('should update `protocolFeesThisEpochByPool` if the maker is in a pool', async () => {
const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: ZERO_AMOUNT },
);
assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID);
const poolFees = await getProtocolFeesAsync(poolId);
expect(poolFees).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID);
});
it('should not update `protocolFeesThisEpochByPool` if maker is not in a pool', async () => {
const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: ZERO_AMOUNT },
);
assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID);
const poolFees = await getProtocolFeesAsync(poolId);
expect(poolFees).to.bignumber.eq(ZERO_AMOUNT);
});
it('fees paid to the same maker should go to the same pool', async () => {
const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const payAsync = async () => {
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: ZERO_AMOUNT },
);
assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID);
};
await payAsync();
await payAsync();
const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2);
const poolFees = await getProtocolFeesAsync(poolId);
expect(poolFees).to.bignumber.eq(expectedTotalFees);
});
it('fees paid to the same maker in WETH then ETH should go to the same pool', async () => {
const { poolId } = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const payAsync = async (inWETH: boolean) => {
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{
from: exchangeAddress,
value: inWETH ? ZERO_AMOUNT : DEFAULT_PROTOCOL_FEE_PAID,
},
);
};
await payAsync(true);
await payAsync(false);
const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2);
const poolFees = await getProtocolFeesAsync(poolId);
expect(poolFees).to.bignumber.eq(expectedTotalFees);
});
});
describe('Dust stake', () => {
it('credits pools with stake > minimum', async () => {
const { poolId } = await createTestPoolAsync({
operatorStake: minimumStake.plus(1),
membersStake: 0,
makers: [makerAddress],
});
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
constants.NULL_ADDRESS,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
const feesCredited = await getProtocolFeesAsync(poolId);
expect(feesCredited).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID);
});
it('credits pools with stake == minimum', async () => {
const { poolId } = await createTestPoolAsync({
operatorStake: minimumStake,
membersStake: 0,
makers: [makerAddress],
});
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
constants.NULL_ADDRESS,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
const feesCredited = await getProtocolFeesAsync(poolId);
expect(feesCredited).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID);
});
it('does not credit pools with stake < minimum', async () => {
const { poolId } = await createTestPoolAsync({
operatorStake: minimumStake.minus(1),
membersStake: 0,
makers: [makerAddress],
});
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
constants.NULL_ADDRESS,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
const feesCredited = await getProtocolFeesAsync(poolId);
expect(feesCredited).to.bignumber.eq(0);
});
});
blockchainTests.resets('Finalization', () => {
let membersStakeWeight: number;
before(async () => {
membersStakeWeight = (await testContract.getParams.callAsync())[1];
});
interface FinalizationState {
numActivePools: BigNumber;
totalFeesCollected: BigNumber;
totalWeightedStake: BigNumber;
}
async function getFinalizationStateAsync(): Promise<FinalizationState> {
return {
numActivePools: await testContract.numActivePoolsThisEpoch.callAsync(),
totalFeesCollected: await testContract.totalFeesCollectedThisEpoch.callAsync(),
totalWeightedStake: await testContract.totalWeightedStakeThisEpoch.callAsync(),
};
}
interface PayToMakerResult {
poolActivatedEvents: IStakingEventsStakingPoolActivatedEventArgs[];
fee: BigNumber;
}
async function payToMakerAsync(poolMaker: string, fee?: Numberish): Promise<PayToMakerResult> {
const _fee = fee === undefined ? getRandomInteger(1, '1e18') : fee;
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
poolMaker,
payerAddress,
new BigNumber(_fee),
{ from: exchangeAddress, value: _fee },
);
const events = filterLogsToArguments<IStakingEventsStakingPoolActivatedEventArgs>(
receipt.logs,
IStakingEventsEvents.StakingPoolActivated,
);
return {
fee: new BigNumber(_fee),
poolActivatedEvents: events,
};
}
function toWeightedStake(operatorStake: Numberish, membersStake: Numberish): BigNumber {
return new BigNumber(membersStake)
.times(membersStakeWeight)
.dividedToIntegerBy(constants.PPM_DENOMINATOR)
.plus(operatorStake);
}
it('no active pools to start', async () => {
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(0);
expect(state.totalFeesCollected).to.bignumber.eq(0);
expect(state.totalWeightedStake).to.bignumber.eq(0);
});
it('pool is not registered to start', async () => {
const { poolId } = await createTestPoolAsync();
const pool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
expect(pool.feesCollected).to.bignumber.eq(0);
expect(pool.membersStake).to.bignumber.eq(0);
expect(pool.weightedStake).to.bignumber.eq(0);
});
it('activates a active pool the first time it earns a fee', async () => {
const pool = await createTestPoolAsync();
const {
poolId,
makers: [poolMaker],
} = pool;
const { fee, poolActivatedEvents } = await payToMakerAsync(poolMaker);
expect(poolActivatedEvents.length).to.eq(1);
expect(poolActivatedEvents[0].poolId).to.eq(poolId);
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake);
expect(actualPool.feesCollected).to.bignumber.eq(fee);
expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake);
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(1);
expect(state.totalFeesCollected).to.bignumber.eq(fee);
expect(state.totalWeightedStake).to.bignumber.eq(expectedWeightedStake);
});
it('only adds to the already activated pool in the same epoch', async () => {
const pool = await createTestPoolAsync();
const {
poolId,
makers: [poolMaker],
} = pool;
const { fee: fee1 } = await payToMakerAsync(poolMaker);
const { fee: fee2, poolActivatedEvents } = await payToMakerAsync(poolMaker);
expect(poolActivatedEvents).to.deep.eq([]);
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake);
const fees = BigNumber.sum(fee1, fee2);
expect(actualPool.feesCollected).to.bignumber.eq(fees);
expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake);
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(1);
expect(state.totalFeesCollected).to.bignumber.eq(fees);
expect(state.totalWeightedStake).to.bignumber.eq(expectedWeightedStake);
});
it('can activate multiple pools in the same epoch', async () => {
const pools = await Promise.all(_.times(3, async () => createTestPoolAsync()));
let totalFees = new BigNumber(0);
let totalWeightedStake = new BigNumber(0);
for (const pool of pools) {
const {
poolId,
makers: [poolMaker],
} = pool;
const { fee, poolActivatedEvents } = await payToMakerAsync(poolMaker);
expect(poolActivatedEvents.length).to.eq(1);
expect(poolActivatedEvents[0].poolId).to.eq(poolId);
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake);
expect(actualPool.feesCollected).to.bignumber.eq(fee);
expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake);
totalFees = totalFees.plus(fee);
totalWeightedStake = totalWeightedStake.plus(expectedWeightedStake);
}
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(pools.length);
expect(state.totalFeesCollected).to.bignumber.eq(totalFees);
expect(state.totalWeightedStake).to.bignumber.eq(totalWeightedStake);
});
it('resets the pool after the epoch advances', async () => {
const pool = await createTestPoolAsync();
const {
poolId,
makers: [poolMaker],
} = pool;
await payToMakerAsync(poolMaker);
await testContract.advanceEpoch.awaitTransactionSuccessAsync();
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
expect(actualPool.feesCollected).to.bignumber.eq(0);
expect(actualPool.membersStake).to.bignumber.eq(0);
expect(actualPool.weightedStake).to.bignumber.eq(0);
});
describe('Multiple makers', () => {
it('fees paid to different makers in the same pool go to that pool', async () => {
const { poolId, makers } = await createTestPoolAsync();
const { fee: fee1 } = await payToMakerAsync(makers[0]);
const { fee: fee2 } = await payToMakerAsync(makers[1]);
const expectedTotalFees = BigNumber.sum(fee1, fee2);
const poolFees = await getProtocolFeesAsync(poolId);
expect(poolFees).to.bignumber.eq(expectedTotalFees);
});
it('fees paid to makers in different pools go to their respective pools', async () => {
const {
poolId: poolId1,
makers: [maker1],
} = await createTestPoolAsync();
const {
poolId: poolId2,
makers: [maker2],
} = await createTestPoolAsync();
const { fee: fee1 } = await payToMakerAsync(maker1);
const { fee: fee2 } = await payToMakerAsync(maker2);
const [poolFees, otherPoolFees] = await Promise.all([
getProtocolFeesAsync(poolId1),
getProtocolFeesAsync(poolId2),
]);
expect(poolFees).to.bignumber.eq(fee1);
expect(otherPoolFees).to.bignumber.eq(fee2);
});
});
});
});
});
// tslint:disable: max-file-line-count

View File

@@ -0,0 +1,438 @@
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import {
blockchainTests,
constants,
expect,
expectTransactionFailedAsync,
filterLogsToArguments,
} from '@0x/contracts-test-utils';
import { assetDataUtils, StakingRevertErrors } from '@0x/order-utils';
import { RevertReason } from '@0x/types';
import { AuthorizableRevertErrors, BigNumber, SafeMathRevertErrors } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { constants as stakingConstants } from '../utils/constants';
import {
artifacts,
ZrxVaultContract,
ZrxVaultDepositEventArgs,
ZrxVaultInCatastrophicFailureModeEventArgs,
ZrxVaultStakingProxySetEventArgs,
ZrxVaultWithdrawEventArgs,
ZrxVaultZrxProxySetEventArgs,
} from '../../src';
blockchainTests.resets('ZrxVault unit tests', env => {
let accounts: string[];
let owner: string;
let nonOwnerAddresses: string[];
let erc20Wrapper: ERC20Wrapper;
let zrxVault: ZrxVaultContract;
let zrxAssetData: string;
let zrxProxyAddress: string;
before(async () => {
// create accounts
accounts = await env.getAccountAddressesAsync();
[owner, ...nonOwnerAddresses] = accounts;
// set up ERC20Wrapper
erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner);
// deploy erc20 proxy
const erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
zrxProxyAddress = erc20ProxyContract.address;
// deploy zrx token
const [zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, constants.DUMMY_TOKEN_DECIMALS);
zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxTokenContract.address);
await erc20Wrapper.setBalancesAndAllowancesAsync();
zrxVault = await ZrxVaultContract.deployFrom0xArtifactAsync(
artifacts.ZrxVault,
env.provider,
env.txDefaults,
artifacts,
zrxProxyAddress,
zrxTokenContract.address,
);
// configure erc20 proxy to accept calls from zrx vault
await erc20ProxyContract.addAuthorizedAddress.awaitTransactionSuccessAsync(zrxVault.address);
});
enum ZrxTransfer {
Deposit,
Withdrawal,
}
async function verifyTransferPostconditionsAsync(
transferType: ZrxTransfer,
staker: string,
amount: BigNumber,
initialVaultBalance: BigNumber,
initialTokenBalance: BigNumber,
receipt: TransactionReceiptWithDecodedLogs,
): Promise<void> {
const eventArgs =
transferType === ZrxTransfer.Deposit
? filterLogsToArguments<ZrxVaultDepositEventArgs>(receipt.logs, 'Deposit')
: filterLogsToArguments<ZrxVaultWithdrawEventArgs>(receipt.logs, 'Withdraw');
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].staker).to.equal(staker);
expect(eventArgs[0].amount).to.bignumber.equal(amount);
const newVaultBalance = await zrxVault.balanceOf.callAsync(staker);
const newTokenBalance = await erc20Wrapper.getBalanceAsync(staker, zrxAssetData);
const [expectedVaultBalance, expectedTokenBalance] =
transferType === ZrxTransfer.Deposit
? [initialVaultBalance.plus(amount), initialTokenBalance.minus(amount)]
: [initialVaultBalance.minus(amount), initialTokenBalance.plus(amount)];
expect(newVaultBalance).to.bignumber.equal(expectedVaultBalance);
expect(newTokenBalance).to.bignumber.equal(expectedTokenBalance);
}
describe('Normal operation', () => {
describe('Setting proxies', () => {
async function verifyStakingProxySetAsync(
receipt: TransactionReceiptWithDecodedLogs,
newProxy: string,
): Promise<void> {
const eventArgs = filterLogsToArguments<ZrxVaultStakingProxySetEventArgs>(
receipt.logs,
'StakingProxySet',
);
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].stakingProxyAddress).to.equal(newProxy);
const actualAddress = await zrxVault.stakingProxyAddress.callAsync();
expect(actualAddress).to.equal(newProxy);
}
it('Owner can set the ZRX proxy', async () => {
const newProxy = nonOwnerAddresses[0];
const receipt = await zrxVault.setZrxProxy.awaitTransactionSuccessAsync(newProxy, {
from: owner,
});
const eventArgs = filterLogsToArguments<ZrxVaultZrxProxySetEventArgs>(receipt.logs, 'ZrxProxySet');
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].zrxProxyAddress).to.equal(newProxy);
});
it('Authorized address can set the ZRX proxy', async () => {
const [authorized, newProxy] = nonOwnerAddresses;
await zrxVault.addAuthorizedAddress.awaitTransactionSuccessAsync(authorized, { from: owner });
const receipt = await zrxVault.setZrxProxy.awaitTransactionSuccessAsync(newProxy, {
from: authorized,
});
const eventArgs = filterLogsToArguments<ZrxVaultZrxProxySetEventArgs>(receipt.logs, 'ZrxProxySet');
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].zrxProxyAddress).to.equal(newProxy);
});
it('Non-authorized address cannot set the ZRX proxy', async () => {
const [notAuthorized, newProxy] = nonOwnerAddresses;
const tx = zrxVault.setZrxProxy.awaitTransactionSuccessAsync(newProxy, {
from: notAuthorized,
});
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
expect(tx).to.revertWith(expectedError);
});
it('Owner can set the staking proxy', async () => {
const newProxy = nonOwnerAddresses[0];
const receipt = await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(newProxy, {
from: owner,
});
await verifyStakingProxySetAsync(receipt, newProxy);
});
it('Authorized address can set the staking proxy', async () => {
const [authorized, newProxy] = nonOwnerAddresses;
await zrxVault.addAuthorizedAddress.awaitTransactionSuccessAsync(authorized, { from: owner });
const receipt = await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(newProxy, {
from: authorized,
});
await verifyStakingProxySetAsync(receipt, newProxy);
});
it('Non-authorized address cannot set the staking proxy', async () => {
const [notAuthorized, newProxy] = nonOwnerAddresses;
const tx = zrxVault.setStakingProxy.awaitTransactionSuccessAsync(newProxy, {
from: notAuthorized,
});
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
expect(tx).to.revertWith(expectedError);
const actualAddress = await zrxVault.stakingProxyAddress.callAsync();
expect(actualAddress).to.equal(stakingConstants.NIL_ADDRESS);
});
});
describe('ZRX management', () => {
let staker: string;
let stakingProxy: string;
let initialVaultBalance: BigNumber;
let initialTokenBalance: BigNumber;
before(async () => {
[staker, stakingProxy] = nonOwnerAddresses;
await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(stakingProxy, { from: owner });
await zrxVault.depositFrom.awaitTransactionSuccessAsync(staker, new BigNumber(10), {
from: stakingProxy,
});
});
beforeEach(async () => {
initialVaultBalance = await zrxVault.balanceOf.callAsync(staker);
initialTokenBalance = await erc20Wrapper.getBalanceAsync(staker, zrxAssetData);
});
describe('Deposit', () => {
it('Staking proxy can deposit zero amount on behalf of staker', async () => {
const receipt = await zrxVault.depositFrom.awaitTransactionSuccessAsync(
staker,
constants.ZERO_AMOUNT,
{
from: stakingProxy,
},
);
await verifyTransferPostconditionsAsync(
ZrxTransfer.Deposit,
staker,
constants.ZERO_AMOUNT,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Staking proxy can deposit nonzero amount on behalf of staker', async () => {
const receipt = await zrxVault.depositFrom.awaitTransactionSuccessAsync(staker, new BigNumber(1), {
from: stakingProxy,
});
await verifyTransferPostconditionsAsync(
ZrxTransfer.Deposit,
staker,
new BigNumber(1),
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Staking proxy can deposit entire ZRX balance on behalf of staker', async () => {
const receipt = await zrxVault.depositFrom.awaitTransactionSuccessAsync(
staker,
initialTokenBalance,
{
from: stakingProxy,
},
);
await verifyTransferPostconditionsAsync(
ZrxTransfer.Deposit,
staker,
initialTokenBalance,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it("Reverts if attempting to deposit more than staker's ZRX balance", async () => {
const tx = zrxVault.depositFrom.sendTransactionAsync(staker, initialTokenBalance.plus(1), {
from: stakingProxy,
});
expectTransactionFailedAsync(tx, RevertReason.TransferFailed);
});
});
describe('Withdrawal', () => {
it('Staking proxy can withdraw zero amount on behalf of staker', async () => {
const receipt = await zrxVault.withdrawFrom.awaitTransactionSuccessAsync(
staker,
constants.ZERO_AMOUNT,
{
from: stakingProxy,
},
);
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
constants.ZERO_AMOUNT,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Staking proxy can withdraw nonzero amount on behalf of staker', async () => {
const receipt = await zrxVault.withdrawFrom.awaitTransactionSuccessAsync(staker, new BigNumber(1), {
from: stakingProxy,
});
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
new BigNumber(1),
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Staking proxy can withdraw entire vault balance on behalf of staker', async () => {
const receipt = await zrxVault.withdrawFrom.awaitTransactionSuccessAsync(
staker,
initialVaultBalance,
{
from: stakingProxy,
},
);
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
initialVaultBalance,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it("Reverts if attempting to withdraw more than staker's vault balance", async () => {
const tx = zrxVault.withdrawFrom.awaitTransactionSuccessAsync(staker, initialVaultBalance.plus(1), {
from: stakingProxy,
});
const expectedError = new SafeMathRevertErrors.Uint256BinOpError(
SafeMathRevertErrors.BinOpErrorCodes.SubtractionUnderflow,
initialVaultBalance,
initialVaultBalance.plus(1),
);
expect(tx).to.revertWith(expectedError);
});
});
});
});
describe('Catastrophic Failure Mode', () => {
describe('Authorization', () => {
async function verifyCatastrophicFailureModeAsync(
sender: string,
receipt: TransactionReceiptWithDecodedLogs,
): Promise<void> {
const eventArgs = filterLogsToArguments<ZrxVaultInCatastrophicFailureModeEventArgs>(
receipt.logs,
'InCatastrophicFailureMode',
);
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].sender).to.equal(sender);
expect(await zrxVault.isInCatastrophicFailure.callAsync()).to.be.true();
}
it('Owner can turn on Catastrophic Failure Mode', async () => {
const receipt = await zrxVault.enterCatastrophicFailure.awaitTransactionSuccessAsync({ from: owner });
await verifyCatastrophicFailureModeAsync(owner, receipt);
});
it('Authorized address can turn on Catastrophic Failure Mode', async () => {
const authorized = nonOwnerAddresses[0];
await zrxVault.addAuthorizedAddress.awaitTransactionSuccessAsync(authorized, { from: owner });
const receipt = await zrxVault.enterCatastrophicFailure.awaitTransactionSuccessAsync({
from: authorized,
});
await verifyCatastrophicFailureModeAsync(authorized, receipt);
});
it('Non-authorized address cannot turn on Catastrophic Failure Mode', async () => {
const notAuthorized = nonOwnerAddresses[0];
const tx = zrxVault.enterCatastrophicFailure.awaitTransactionSuccessAsync({
from: notAuthorized,
});
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
expect(tx).to.revertWith(expectedError);
expect(await zrxVault.isInCatastrophicFailure.callAsync()).to.be.false();
});
});
describe('Affected functionality', () => {
let staker: string;
let stakingProxy: string;
let initialVaultBalance: BigNumber;
let initialTokenBalance: BigNumber;
before(async () => {
[staker, stakingProxy, ...nonOwnerAddresses] = nonOwnerAddresses;
await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(stakingProxy, { from: owner });
await zrxVault.depositFrom.awaitTransactionSuccessAsync(staker, new BigNumber(10), {
from: stakingProxy,
});
await zrxVault.enterCatastrophicFailure.awaitTransactionSuccessAsync({ from: owner });
});
beforeEach(async () => {
initialVaultBalance = await zrxVault.balanceOf.callAsync(staker);
initialTokenBalance = await erc20Wrapper.getBalanceAsync(staker, zrxAssetData);
});
it('Owner cannot set the ZRX proxy', async () => {
const newProxy = nonOwnerAddresses[0];
const tx = zrxVault.setZrxProxy.awaitTransactionSuccessAsync(newProxy, {
from: owner,
});
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
expect(tx).to.revertWith(expectedError);
const actualAddress = await zrxVault.zrxAssetProxy.callAsync();
expect(actualAddress).to.equal(zrxProxyAddress);
});
it('Authorized address cannot set the ZRX proxy', async () => {
const [authorized, newProxy] = nonOwnerAddresses;
await zrxVault.addAuthorizedAddress.awaitTransactionSuccessAsync(authorized, { from: owner });
const tx = zrxVault.setZrxProxy.awaitTransactionSuccessAsync(newProxy, {
from: authorized,
});
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
expect(tx).to.revertWith(expectedError);
const actualAddress = await zrxVault.zrxAssetProxy.callAsync();
expect(actualAddress).to.equal(zrxProxyAddress);
});
it('Staking proxy cannot deposit ZRX', async () => {
const tx = zrxVault.depositFrom.awaitTransactionSuccessAsync(staker, new BigNumber(1), {
from: stakingProxy,
});
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
expect(tx).to.revertWith(expectedError);
});
describe('Withdrawal', () => {
it('Staking proxy cannot call `withdrawFrom`', async () => {
const tx = zrxVault.withdrawFrom.awaitTransactionSuccessAsync(staker, new BigNumber(1), {
from: stakingProxy,
});
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
expect(tx).to.revertWith(expectedError);
});
it('Staker can withdraw all their ZRX', async () => {
const receipt = await zrxVault.withdrawAllFrom.awaitTransactionSuccessAsync(staker, {
from: staker,
});
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
initialVaultBalance,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Owner can withdraw ZRX on behalf of a staker', async () => {
const receipt = await zrxVault.withdrawAllFrom.awaitTransactionSuccessAsync(staker, {
from: owner,
});
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
initialVaultBalance,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Non-owner address can withdraw ZRX on behalf of a staker', async () => {
const receipt = await zrxVault.withdrawAllFrom.awaitTransactionSuccessAsync(staker, {
from: nonOwnerAddresses[0],
});
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
initialVaultBalance,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
});
});
});
});