consolidate MixinVaultCore and ZrxVault
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
207
contracts/staking/test/unit_tests/lib_cobb_douglas_test.ts
Normal file
207
contracts/staking/test/unit_tests/lib_cobb_douglas_test.ts
Normal 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
|
||||
@@ -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 () => {
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
76
contracts/staking/test/unit_tests/params_test.ts
Normal file
76
contracts/staking/test/unit_tests/params_test.ts
Normal 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
|
||||
545
contracts/staking/test/unit_tests/protocol_fees_test.ts
Normal file
545
contracts/staking/test/unit_tests/protocol_fees_test.ts
Normal 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
|
||||
438
contracts/staking/test/unit_tests/zrx_vault_test.ts
Normal file
438
contracts/staking/test/unit_tests/zrx_vault_test.ts
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user