* 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
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 { 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
|