diff --git a/contracts/staking/contracts/src/Staking.sol b/contracts/staking/contracts/src/Staking.sol index af4e8942db..80f3f58e0d 100644 --- a/contracts/staking/contracts/src/Staking.sol +++ b/contracts/staking/contracts/src/Staking.sol @@ -85,6 +85,12 @@ contract Staking is _withdraw(msg.sender, amount); } + function forceTimelockSync(address owner) + external + { + _forceTimelockSync(owner); + } + ///// STAKE BALANCES ///// function getTotalStake(address owner) @@ -111,6 +117,14 @@ contract Staking is return _getDeactivatedStake(owner); } + function getActivatableStake(address owner) + external + view + returns (uint256) + { + return _getActivatableStake(owner); + } + function getWithdrawableStake(address owner) external view diff --git a/contracts/staking/contracts/src/core/MixinConstants.sol b/contracts/staking/contracts/src/core/MixinConstants.sol index 6978bbf30e..559534c9e3 100644 --- a/contracts/staking/contracts/src/core/MixinConstants.sol +++ b/contracts/staking/contracts/src/core/MixinConstants.sol @@ -35,7 +35,7 @@ contract MixinConstants { uint64 constant public INITIAL_TIMELOCK_PERIOD = INITIAL_EPOCH; - uint64 constant public EPOCH_PERIOD_IN_SECONDS = 1; // @TODO SET FOR DEPLOYMENT + uint64 constant public EPOCH_PERIOD_IN_SECONDS = 1000; // @TODO SET FOR DEPLOYMENT - uint64 constant public TIMELOCK_PERIOD_IN_EPOCHS = 2; // @TODO SET FOR DEPLOYMENT + uint64 constant public TIMELOCK_PERIOD_IN_EPOCHS = 3; // @TODO SET FOR DEPLOYMENT } diff --git a/contracts/staking/contracts/src/core/MixinEpoch.sol b/contracts/staking/contracts/src/core/MixinEpoch.sol index 1edabea961..5ccde2a457 100644 --- a/contracts/staking/contracts/src/core/MixinEpoch.sol +++ b/contracts/staking/contracts/src/core/MixinEpoch.sol @@ -50,7 +50,7 @@ contract MixinEpoch is currentEpochStartTimeInSeconds = currentBlockTimestamp; // increment timelock period, if needed - if (_getCurrentTimelockPeriodEndEpoch() == nextEpoch) { + if (_getCurrentTimelockPeriodEndEpoch() <= nextEpoch) { currentTimelockPeriod += 1; currentTimelockPeriodStartEpoch = currentEpoch; } diff --git a/contracts/staking/contracts/src/core/MixinStake.sol b/contracts/staking/contracts/src/core/MixinStake.sol index 223d8850a6..58609e7817 100644 --- a/contracts/staking/contracts/src/core/MixinStake.sol +++ b/contracts/staking/contracts/src/core/MixinStake.sol @@ -89,6 +89,7 @@ contract MixinStake is "INSUFFICIENT_BALANCE" ); activeStakeByOwner[owner] = _safeSub(activeStakeByOwner[owner], amount); + _timelockStake(owner, amount); } function _deactivateAndTimelockDelegatedStake(address owner, bytes32 poolId, uint256 amount) @@ -109,6 +110,12 @@ contract MixinStake is _burnStake(owner, amount); } + function _forceTimelockSync(address owner) + internal + { + _syncTimelockedStake(owner); + } + ///// PRIVATE HELPERS ///// function _mintStake(address owner, uint256 amount) diff --git a/contracts/staking/contracts/src/core/MixinStakeBalances.sol b/contracts/staking/contracts/src/core/MixinStakeBalances.sol index 718ba8a97e..067db1e7da 100644 --- a/contracts/staking/contracts/src/core/MixinStakeBalances.sol +++ b/contracts/staking/contracts/src/core/MixinStakeBalances.sol @@ -55,22 +55,20 @@ contract MixinStakeBalances is return _safeSub(_getTotalStake(owner), _getActivatedStake(owner)); } - /* - function _getStakeAvailableForActivation() - internal + function _getActivatableStake(address owner) + internal view returns (uint256) { - + return _safeSub(_getDeactivatedStake(owner), _getTimelockedStake(owner)); } - */ function _getWithdrawableStake(address owner) internal view returns (uint256) { - return _getDeactivatedStake(owner); + return _getActivatableStake(owner); } function _getTimelockedStake(address owner) diff --git a/contracts/staking/test/core_test.ts b/contracts/staking/test/core_test.ts index 8a64b2ce28..23d1142828 100644 --- a/contracts/staking/test/core_test.ts +++ b/contracts/staking/test/core_test.ts @@ -68,11 +68,11 @@ describe('Staking Core', () => { await blockchainLifecycle.revertAsync(); }); describe('end-to-end tests', () => { - it('epochs & timelock periods (timelock period = 2 epochs)', async () => { + it.only('epochs & timelock periods', async () => { ///// 0/3 Validate Assumptions ///// expect(await stakingWrapper.getEpochPeriodInSecondsAsync()).to.be.bignumber.equal(stakingConstants.EPOCH_PERIOD_IN_SECONDS); expect(await stakingWrapper.getTimelockPeriodInEpochsAsync()).to.be.bignumber.equal(stakingConstants.TIMELOCK_PERIOD_IN_EPOCHS); - expect(stakingConstants.TIMELOCK_PERIOD_IN_EPOCHS).to.be.bignumber.equal(2); + ///// 1/3 Validate Initial Epoch & Timelock Period ///// { // epoch @@ -93,11 +93,8 @@ describe('Staking Core', () => { expect(currentTimelockPeriod).to.be.bignumber.equal(stakingConstants.INITIAL_TIMELOCK_PERIOD); } ///// 3/3 Increment Epoch (Timelock Should Increment) ///// - await stakingWrapper.skipToNextEpochAsync(); + await stakingWrapper.skipToNextTimelockPeriodAsync(); { - // epoch - const currentEpoch = await stakingWrapper.getCurrentEpochAsync(); - expect(currentEpoch).to.be.bignumber.equal(stakingConstants.INITIAL_EPOCH.plus(2)); // timelock period const currentTimelockPeriod = await stakingWrapper.getCurrentTimelockPeriodAsync(); expect(currentTimelockPeriod).to.be.bignumber.equal(stakingConstants.INITIAL_TIMELOCK_PERIOD.plus(1)); @@ -106,7 +103,7 @@ describe('Staking Core', () => { it.skip('staking/unstaking', async () => { ///// 1/3 SETUP TEST PARAMETERS ///// const amountToStake = stakingWrapper.toBaseUnitAmount(10); - const amountToUnstake = stakingWrapper.toBaseUnitAmount(5); + const amountToUnstake = stakingWrapper.toBaseUnitAmount(4); const owner = stakers[0]; // check zrx token balances before minting stake const zrxTokenBalanceOfVaultBeforeStaking = await stakingWrapper.getZrxTokenBalanceOfZrxVault(); @@ -116,8 +113,7 @@ describe('Staking Core', () => { ///// 2/3 STAKE ZRX ///// { // mint stake - const stakeMinted = await stakingWrapper.depositAndStakeAsync(owner, amountToStake); - expect(stakeMinted).to.be.bignumber.equal(amountToStake); + await stakingWrapper.depositAndStakeAsync(owner, amountToStake); // check stake balance after minting const stakeBalance = await stakingWrapper.getTotalStakeAsync(owner); expect(stakeBalance).to.be.bignumber.equal(amountToStake); @@ -130,23 +126,80 @@ describe('Staking Core', () => { const zrxTokenBalanceOfStakerAfterStaking = await stakingWrapper.getZrxTokenBalance(owner); expect(zrxTokenBalanceOfStakerAfterStaking).to.be.bignumber.equal(zrxTokenBalanceOfStakerBeforeStaking.minus(amountToStake)); } - ///// 3/3 UNSTAKE ZRX ///// + ///// 3/3 DEACTIVATE AND TIMELOCK STAKE ///// + { + // unstake + await stakingWrapper.deactivateAndTimelockStakeAsync(owner, amountToUnstake); + // check total stake balance didn't change + const totalStake = await stakingWrapper.getTotalStakeAsync(owner); + expect(totalStake).to.be.bignumber.equal(amountToStake); + // check timelocked stake is no longer activated + const activatedStakeBalance = await stakingWrapper.getActivatedStakeAsync(owner); + expect(activatedStakeBalance).to.be.bignumber.equal(amountToStake.minus(amountToUnstake)); + // check that timelocked stake is deactivated + const deactivatedStakeBalance = await stakingWrapper.getDeactivatedStakeAsync(owner); + expect(deactivatedStakeBalance).to.be.bignumber.equal(amountToUnstake); + // check amount that is timelocked + const timelockedStakeBalance = await stakingWrapper.getTimelockedStakeAsync(owner); + expect(timelockedStakeBalance).to.be.bignumber.equal(amountToUnstake); + // check that timelocked stake cannot be withdrawn + const withdrawableStakeBalance = await stakingWrapper.getWithdrawableStakeAsync(owner); + expect(withdrawableStakeBalance).to.be.bignumber.equal(0); + // check that timelocked stake cannot be reactivated + const activatableStakeBalance = await stakingWrapper.getActivatableStakeAsync(owner); + expect(activatableStakeBalance).to.be.bignumber.equal(0); + } + ///// 4 SKIP TO NEXT EPOCH - NOTHING SHOULD HAVE CHANGED ///// + await stakingWrapper.skipToNextEpochAsync(); { // unstake - const stakeBurned = await stakingWrapper.deactivateAndTimelockStakeAsync(owner, amountToUnstake); - expect(stakeBurned).to.be.bignumber.equal(amountToUnstake); - // check stake balance after burning - const stakeBalance = await stakingWrapper.getActivatedStakeAsync(owner); - expect(stakeBalance).to.be.bignumber.equal(amountToStake.minus(amountToUnstake)); + await stakingWrapper.deactivateAndTimelockStakeAsync(owner, amountToUnstake); + // check total stake balance didn't change + const totalStake = await stakingWrapper.getTotalStakeAsync(owner); + expect(totalStake).to.be.bignumber.equal(amountToStake); + // check timelocked stake is no longer activated + const activatedStakeBalance = await stakingWrapper.getActivatedStakeAsync(owner); + expect(activatedStakeBalance).to.be.bignumber.equal(amountToStake.minus(amountToUnstake)); + // check that timelocked stake is deactivated + const deactivatedStakeBalance = await stakingWrapper.getDeactivatedStakeAsync(owner); + expect(deactivatedStakeBalance).to.be.bignumber.equal(amountToUnstake); + // check amount that is timelocked + const timelockedStakeBalance = await stakingWrapper.getTimelockedStakeAsync(owner); + expect(timelockedStakeBalance).to.be.bignumber.equal(amountToUnstake); + // check that timelocked stake cannot be withdrawn + const withdrawableStakeBalance = await stakingWrapper.getWithdrawableStakeAsync(owner); + expect(withdrawableStakeBalance).to.be.bignumber.equal(0); + // check that timelocked stake cannot be reactivated + const activatableStakeBalance = await stakingWrapper.getActivatableStakeAsync(owner); + expect(activatableStakeBalance).to.be.bignumber.equal(0); + } + + ///// Stake a 0 amount to force an update ////// + + + ///// 5 Reactivate Stake ///// + + ///// 6 Deactivate and Timelock Stake ///// + + ///// 6 Fastforward Blockchain ///// + + ///// Withdraw Stake ///// + + { // check zrx vault balance + /* const vaultBalance = await stakingWrapper.getZrxVaultBalance(owner); expect(vaultBalance).to.be.bignumber.equal(amountToStake.minus(amountToUnstake)); + // check zrx token balances const zrxTokenBalanceOfVaultAfterStaking = await stakingWrapper.getZrxTokenBalanceOfZrxVault(); expect(zrxTokenBalanceOfVaultAfterStaking).to.be.bignumber.equal(amountToStake.minus(amountToUnstake)); const zrxTokenBalanceOfStakerAfterStaking = await stakingWrapper.getZrxTokenBalance(owner); expect(zrxTokenBalanceOfStakerAfterStaking).to.be.bignumber.equal(zrxTokenBalanceOfStakerBeforeStaking.minus(amountToStake).plus(amountToUnstake)); + */ } + + }); it('nth root', async () => { diff --git a/contracts/staking/test/utils/constants.ts b/contracts/staking/test/utils/constants.ts index 4fac5b05ed..41777aed31 100644 --- a/contracts/staking/test/utils/constants.ts +++ b/contracts/staking/test/utils/constants.ts @@ -8,6 +8,6 @@ export const constants = { NIL_ADDRESS: "0x0000000000000000000000000000000000000000", INITIAL_EPOCH: (new BigNumber(0)), INITIAL_TIMELOCK_PERIOD: new BigNumber(0), - EPOCH_PERIOD_IN_SECONDS: new BigNumber(1), // @TODO SET FOR DEPLOYMENT*/ - TIMELOCK_PERIOD_IN_EPOCHS: new BigNumber(2), // @TODO SET FOR DEPLOYMENT + EPOCH_PERIOD_IN_SECONDS: new BigNumber(1000), // @TODO SET FOR DEPLOYMENT*/ + TIMELOCK_PERIOD_IN_EPOCHS: new BigNumber(3), // @TODO SET FOR DEPLOYMENT }; \ No newline at end of file diff --git a/contracts/staking/test/utils/staking_wrapper.ts b/contracts/staking/test/utils/staking_wrapper.ts index f66b59bfd7..d9468371eb 100644 --- a/contracts/staking/test/utils/staking_wrapper.ts +++ b/contracts/staking/test/utils/staking_wrapper.ts @@ -156,6 +156,11 @@ export class StakingWrapper { const txReceipt = await this._executeTransactionAsync(calldata, owner); return txReceipt; } + public async forceTimelockSyncAsync(owner: string): Promise { + const calldata = this.getStakingContract().forceTimelockSync.getABIEncodedTransactionData(owner); + const txReceipt = await this._executeTransactionAsync(calldata, this._ownerAddres); + return txReceipt; + } ///// STAKE BALANCES ///// public async getTotalStakeAsync(owner: string): Promise { const calldata = this.getStakingContract().getTotalStake.getABIEncodedTransactionData(owner); @@ -172,6 +177,11 @@ export class StakingWrapper { const deactivatedStake = await this._callAsync(calldata); return deactivatedStake; } + public async getActivatableStakeAsync(owner: string): Promise { + const calldata = this.getStakingContract().getActivatableStake.getABIEncodedTransactionData(owner); + const activatableStake = await this._callAsync(calldata); + return activatableStake; + } public async getWithdrawableStakeAsync(owner: string): Promise { const calldata = this.getStakingContract().getWithdrawableStake.getABIEncodedTransactionData(owner); const withdrawableStake = await this._callAsync(calldata); @@ -183,7 +193,7 @@ export class StakingWrapper { return timelockedStake; } public async getStakeDelegatedByOwnerAsync(owner: string): Promise { - const calldata = this.getStakingContract().getTimelockedStake.getABIEncodedTransactionData(owner); + const calldata = this.getStakingContract().getStakeDelegatedByOwner.getABIEncodedTransactionData(owner); const stakeDelegatedByOwner = await this._callAsync(calldata); return stakeDelegatedByOwner; } @@ -249,6 +259,15 @@ export class StakingWrapper { await this._web3Wrapper.mineBlockAsync(); return txReceipt; } + public async skipToNextTimelockPeriodAsync(): Promise { + const timelockEndEpoch = await this.getCurrentTimelockPeriodEndEpochAsync(); + const currentEpoch = await this.getCurrentEpochAsync(); + const nEpochsToJump = timelockEndEpoch.minus(currentEpoch); + const nEpochsToJumpAsNumber = nEpochsToJump.toNumber(); + for (let i = 0; i < nEpochsToJumpAsNumber; ++i) { + await this.skipToNextEpochAsync(); + } + } public async getEpochPeriodInSecondsAsync(): Promise { const calldata = this.getStakingContract().getEpochPeriodInSeconds.getABIEncodedTransactionData(); const returnData = await this._callAsync(calldata);