* update abi-gen with new method interfaces * wip: get all packages to build * wip: get all packages to build * Fix two contract wrapper calls * Export necessary types part of the contract wrapper public interfaces * Revive and fix wrapper_unit_tests * Remove duplicate type * Fix lib_exchange_rich_error_decoder tests * Fix remaining test failures in contracts-* packages * Prettier fixes * remove transactionHelper * lint and update changelogs * Fix prettier * Revert changes to reference docs * Add back changelog already published and add revert changelog entry * Add missing CHANGELOG entries * Add missing comma * Update mesh-rpc-client dep * Update Mesh RPC logic in @0x/orderbook to v6.0.1-beta * Align package versions
394 lines
17 KiB
TypeScript
394 lines
17 KiB
TypeScript
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
|
|
import { blockchainTests, describe } from '@0x/contracts-test-utils';
|
|
import { StakingRevertErrors } from '@0x/order-utils';
|
|
import { BigNumber } from '@0x/utils';
|
|
import * as _ from 'lodash';
|
|
|
|
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
|