protocol/contracts/staking/test/stake_test.ts
F. Eugene Aumson f11d8a5bd8
@0x/order-utils refactors for v3: orderParsingUtils, signatureUtils, orderHashUtils, RevertErrors, transactionHashUtils (#2321)
* move orderParsingUtils from order-utils to connect

* Remove many functions from signatureUtils

Removed from the exported object, that is.  All of them are used in
other existing code, so they were all moved to be as local to their
usage as possible.

* remove orderHashUtils.isValidOrderHash()

* Move all *RevertErrors from order-utils...

...into their respective @0x/contracts- packages.

* Refactor @0x/order-utils' orderHashUtils away

- Move existing routines into @0x/contracts-test-utils

- Migrate non-contract-test callers to a newly-exposed getOrderHash()
method in DevUtils.

* Move all *RevertErrors from @0x/utils...

...into their respective @0x/contracts- packages.

* rm transactionHashUtils.isValidTransactionHash()

* DevUtils.sol: Fail yarn test if too big to deploy

* Refactor @0x/order-utils transactionHashUtils away

- Move existing routines into @0x/contracts-test-utils

- Migrate non-contract-test callers to a newly-exposed
getTransactionHash() method in DevUtils.

* Consolidate `Removed export...` CHANGELOG entries

* Rm EthBalanceChecker from devutils wrapper exports

* Stop importing from '.' or '.../src'

* fix builds

* fix prettier; dangling promise

* increase max bundle size
2019-11-14 17:14:24 -05:00

394 lines
17 KiB
TypeScript

import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { blockchainTests, describe } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import StakingRevertErrors = require('../src/staking_revert_errors');
import { StakeInfo, StakeStatus } from '../src/types';
import { StakerActor } from './actors/staker_actor';
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
import { toBaseUnitAmount } from './utils/number_utils';
// tslint:disable:no-unnecessary-type-assertion
blockchainTests.resets('Stake Statuses', env => {
// constants
const ZERO = new BigNumber(0);
// tokens & addresses
let accounts: string[];
let owner: string;
let actors: string[];
// wrappers
let stakingApiWrapper: StakingApiWrapper;
let erc20Wrapper: ERC20Wrapper;
// stake actor
let staker: StakerActor;
let poolIds: string[];
let unusedPoolId: string;
let poolOperator: string;
// tests
before(async () => {
// create accounts
accounts = await env.getAccountAddressesAsync();
owner = accounts[0];
actors = accounts.slice(2, 5);
// set up ERC20Wrapper
erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner);
// deploy staking contracts
stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper);
// setup new staker
staker = new StakerActor(actors[0], stakingApiWrapper);
// setup pools
poolOperator = actors[1];
poolIds = await Promise.all([
await stakingApiWrapper.utils.createStakingPoolAsync(poolOperator, 4, false),
await stakingApiWrapper.utils.createStakingPoolAsync(poolOperator, 5, false),
]);
const lastPoolId = await stakingApiWrapper.stakingContract.lastPoolId().callAsync();
unusedPoolId = `0x${new BigNumber(lastPoolId)
.plus(1)
.toString(16)
.padStart(64, '0')}`;
});
describe('Stake', () => {
it('should successfully stake zero ZRX', async () => {
const amount = toBaseUnitAmount(0);
await staker.stakeAsync(amount);
});
it('should successfully stake non-zero ZRX', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
});
it('should retain stake balance across 1 epoch', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.goToNextEpochAsync();
});
it('should retain stake balance across 2 epochs', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.goToNextEpochAsync();
await staker.goToNextEpochAsync();
});
});
describe('Move Stake', () => {
it("should be able to rebalance next epoch's stake", async () => {
// epoch 2
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
// still epoch 2 ~ should be able to move stake again
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Undelegated),
amount,
);
});
it("should be able to reassign next epoch's stake", async () => {
// epoch 2
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
// still epoch 2 ~ should be able to move stake again
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Delegated, poolIds[1]),
amount,
);
});
it('should fail to move the same stake more than once', async () => {
// epoch 2
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
// stake is now undelegated, should not be able to move it out of active status again
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[1]),
amount,
new StakingRevertErrors.InsufficientBalanceError(amount, ZERO),
);
});
});
describe('Stake and Move', () => {
it("should be able to rebalance next epoch's stake", async () => {
// epoch 2
const amount = toBaseUnitAmount(10);
await staker.stakeAndMoveAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
// still epoch 2 ~ should be able to move stake again
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Undelegated),
amount,
);
});
it('should fail to move the same stake more than once', async () => {
// epoch 2
const amount = toBaseUnitAmount(10);
await staker.stakeAndMoveAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
// stake is now undelegated, should not be able to move it out of active status again
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[1]),
amount,
new StakingRevertErrors.InsufficientBalanceError(amount, ZERO),
);
});
});
describe('Move Zero Stake', () => {
it('undelegated -> undelegated', async () => {
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Undelegated),
ZERO,
);
});
it('undelegated -> delegated', async () => {
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
ZERO,
);
});
it('delegated -> undelegated', async () => {
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Undelegated),
ZERO,
);
});
it('delegated -> delegated (same pool)', async () => {
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
ZERO,
);
});
it('delegated -> delegated (other pool)', async () => {
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Delegated, poolIds[1]),
ZERO,
);
});
});
describe('Move Non-Zero Stake', () => {
const testMovePartialStake = async (from: StakeInfo, to: StakeInfo) => {
// setup
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
if (from.status !== StakeStatus.Undelegated) {
await staker.moveStakeAsync(new StakeInfo(StakeStatus.Undelegated), from, amount);
}
// run test, checking balances in epochs [n .. n + 2]
// in epoch `n` - `next` is set
// in epoch `n+1` - `current` is set
await staker.moveStakeAsync(from, to, amount.div(2));
await staker.goToNextEpochAsync();
};
it('undelegated -> undelegated', async () => {
await testMovePartialStake(new StakeInfo(StakeStatus.Undelegated), new StakeInfo(StakeStatus.Undelegated));
});
it('undelegated -> delegated', async () => {
await testMovePartialStake(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
);
});
it('delegated -> undelegated', async () => {
await testMovePartialStake(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Undelegated),
);
});
it('delegated -> delegated (same pool)', async () => {
await testMovePartialStake(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
);
});
it('delegated -> delegated (other pool)', async () => {
await testMovePartialStake(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Delegated, poolIds[1]),
);
});
it('undelegated -> delegated (non-existent pool)', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, unusedPoolId),
amount,
new StakingRevertErrors.PoolExistenceError(unusedPoolId, false),
);
});
it('delegated -> delegated (non-existent pool)', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Delegated, unusedPoolId),
amount,
new StakingRevertErrors.PoolExistenceError(unusedPoolId, false),
);
});
});
describe('Unstake', () => {
it('should successfully unstake zero ZRX', async () => {
const amount = toBaseUnitAmount(0);
await staker.unstakeAsync(amount);
});
it('should successfully unstake after becoming undelegated', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Undelegated),
amount,
);
await staker.goToNextEpochAsync(); // stake is now undelegated
await staker.unstakeAsync(amount);
});
it('should fail to unstake in the same epoch as stake was undelegated', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
await staker.goToNextEpochAsync(); // stake is now delegated
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
new StakeInfo(StakeStatus.Undelegated),
amount,
);
await staker.unstakeAsync(amount, new StakingRevertErrors.InsufficientBalanceError(amount, ZERO));
});
it('should fail to unstake in same epoch that undelegated stake has been delegated', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
await staker.unstakeAsync(amount, new StakingRevertErrors.InsufficientBalanceError(amount, ZERO));
});
it('should fail to unstake one epoch after undelegated stake has been delegated', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
await staker.goToNextEpochAsync(); // stake is now undelegated
await staker.unstakeAsync(amount, new StakingRevertErrors.InsufficientBalanceError(amount, ZERO));
});
it('should fail to unstake >1 epoch after undelegated stake has been delegated', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolIds[0]),
amount,
);
await staker.goToNextEpochAsync(); // stake is now undelegated
await staker.goToNextEpochAsync(); // stake is now withdrawable
await staker.unstakeAsync(amount, new StakingRevertErrors.InsufficientBalanceError(amount, ZERO));
});
it('should successfuly unstake freshly deposited stake', async () => {
const amount = toBaseUnitAmount(10);
await staker.stakeAsync(amount);
await staker.unstakeAsync(amount);
});
});
describe('Simulations', () => {
// it('Simulation from Staking Spec', async () => {
// // Epoch 1: Stake some ZRX
// await staker.stakeAsync(toBaseUnitAmount(4));
// // Later in Epoch 1: User delegates and deactivates some stake
// await staker.moveStakeAsync(
// new StakeInfo(StakeStatus.Active),
// new StakeInfo(StakeStatus.Undelegated),
// toBaseUnitAmount(1),
// );
// await staker.moveStakeAsync(
// new StakeInfo(StakeStatus.Active),
// new StakeInfo(StakeStatus.Delegated, poolIds[0]),
// toBaseUnitAmount(2),
// );
// // Epoch 2: Status updates (no user intervention required)
// await staker.goToNextEpochAsync();
// // Epoch 3: Stake that has been undelegated for an epoch can be withdrawn (no user intervention required)
// await staker.goToNextEpochAsync();
// // Later in Epoch 3: User reactivates half of their undelegated stake; this becomes Active next epoch
// await staker.moveStakeAsync(
// new StakeInfo(StakeStatus.Undelegated),
// new StakeInfo(StakeStatus.Active),
// toBaseUnitAmount(0.5),
// );
// // Later in Epoch 3: User re-delegates half of their stake from Pool 1 to Pool 2
// await staker.moveStakeAsync(
// new StakeInfo(StakeStatus.Delegated, poolIds[0]),
// new StakeInfo(StakeStatus.Delegated, poolIds[1]),
// toBaseUnitAmount(1),
// );
// // Epoch 4: Status updates (no user intervention required)
// await staker.goToNextEpochAsync();
// // Later in Epoch 4: User deactivates all active stake
// await staker.moveStakeAsync(
// new StakeInfo(StakeStatus.Active),
// new StakeInfo(StakeStatus.Undelegated),
// toBaseUnitAmount(1.5),
// );
// // Later in Epoch 4: User withdraws all available undelegated stake
// await staker.unstakeAsync(toBaseUnitAmount(0.5));
// // Epoch 5: Status updates (no user intervention required)
// await staker.goToNextEpochAsync();
// // Later in Epoch 5: User reactivates a portion of their stake
// await staker.moveStakeAsync(
// new StakeInfo(StakeStatus.Undelegated),
// new StakeInfo(StakeStatus.Active),
// toBaseUnitAmount(1),
// );
// // Epoch 6: Status updates (no user intervention required)
// await staker.goToNextEpochAsync();
// });
});
});
// tslint:enable:no-unnecessary-type-assertion