diff --git a/contracts/staking/.prettierrc b/contracts/staking/.prettierrc new file mode 100644 index 0000000000..c568ca733a --- /dev/null +++ b/contracts/staking/.prettierrc @@ -0,0 +1,10 @@ +{ + "overrides": [ + { + "files": "./test/**.ts", + "options": { + "printWidth": 80 + } + } + ] +} diff --git a/contracts/staking/compiler.json b/contracts/staking/compiler.json index f432301d36..59b9936053 100644 --- a/contracts/staking/compiler.json +++ b/contracts/staking/compiler.json @@ -1,25 +1,30 @@ { - "artifactsDir": "./generated-artifacts", - "contractsDir": "./contracts", - "useDockerisedSolc": false, - "isOfflineMode": false, - "compilerSettings": { - "evmVersion": "constantinople", - "optimizer": { - "enabled": true, - "runs": 1000000, - "details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true } - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode.object", - "evm.bytecode.sourceMap", - "evm.deployedBytecode.object", - "evm.deployedBytecode.sourceMap" - ] - } - } + "artifactsDir": "./generated-artifacts", + "contractsDir": "./contracts", + "useDockerisedSolc": false, + "isOfflineMode": false, + "compilerSettings": { + "evmVersion": "constantinople", + "optimizer": { + "enabled": true, + "runs": 1000000, + "details": { + "yul": true, + "deduplicate": true, + "cse": true, + "constantOptimizer": true + } + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } } + } } diff --git a/contracts/staking/contracts/src/Staking.sol b/contracts/staking/contracts/src/Staking.sol index 6e329705eb..18f0469eca 100644 --- a/contracts/staking/contracts/src/Staking.sol +++ b/contracts/staking/contracts/src/Staking.sol @@ -21,6 +21,7 @@ pragma experimental ABIEncoderV2; import "./interfaces/IStaking.sol"; import "./sys/MixinParams.sol"; +import "./sys/MixinFinalizer.sol"; import "./stake/MixinStake.sol"; import "./staking_pools/MixinStakingPool.sol"; import "./fees/MixinExchangeFees.sol"; @@ -31,7 +32,7 @@ contract Staking is MixinParams, MixinStakingPool, MixinStake, - MixinExchangeFees + MixinExchangeFees, { // this contract can receive ETH // solhint-disable no-empty-blocks diff --git a/contracts/staking/contracts/src/sys/MixinFinalizer.sol b/contracts/staking/contracts/src/sys/MixinFinalizer.sol index 50d7382f81..a226ed1633 100644 --- a/contracts/staking/contracts/src/sys/MixinFinalizer.sol +++ b/contracts/staking/contracts/src/sys/MixinFinalizer.sol @@ -335,6 +335,15 @@ contract MixinFinalizer is rewards.membersStake = pool.delegatedStake; } + /// @dev Converts the entire WETH balance of the contract into ETH. + function _unwrapWETH() internal { + uint256 wethBalance = IEtherToken(WETH_ADDRESS) + .balanceOf(address(this)); + if (wethBalance != 0) { + IEtherToken(WETH_ADDRESS).withdraw(wethBalance); + } + } + /// @dev Computes the reward owed to a pool during finalization and /// credits it to that pool for the CURRENT epoch. /// @param poolId The pool's ID. @@ -377,13 +386,4 @@ contract MixinFinalizer is ); } } - - /// @dev Converts the entire WETH balance of the contract into ETH. - function _unwrapWETH() private { - uint256 wethBalance = IEtherToken(WETH_ADDRESS) - .balanceOf(address(this)); - if (wethBalance != 0) { - IEtherToken(WETH_ADDRESS).withdraw(wethBalance); - } - } } diff --git a/contracts/staking/contracts/test/TestDelegatorRewards.sol b/contracts/staking/contracts/test/TestDelegatorRewards.sol new file mode 100644 index 0000000000..5184b2a341 --- /dev/null +++ b/contracts/staking/contracts/test/TestDelegatorRewards.sol @@ -0,0 +1,29 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "../src/Staking.sol"; + + +contract TestDelegatorRewards is + Staking +{ + // TODO +} diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index 43b78853f4..2919f41aae 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -3,7 +3,7 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ -import { ContractArtifact } from 'ethereum-types'; +import { ContractArtifact } from "ethereum-types"; import * as EthVault from '../generated-artifacts/EthVault.json'; import * as IEthVault from '../generated-artifacts/IEthVault.json'; diff --git a/contracts/staking/test/actors/finalizer_actor.ts b/contracts/staking/test/actors/finalizer_actor.ts index 11c370d7d1..d424861915 100644 --- a/contracts/staking/test/actors/finalizer_actor.ts +++ b/contracts/staking/test/actors/finalizer_actor.ts @@ -62,7 +62,7 @@ export class FinalizerActor extends BaseActor { memberRewardByPoolId, ); // finalize - await this._stakingApiWrapper.utils.skipToNextEpochAsync(); + await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); // assert reward vault changes const finalRewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds); expect(finalRewardVaultBalanceByPoolId, 'final pool balances in reward vault').to.be.deep.equal( diff --git a/contracts/staking/test/actors/staker_actor.ts b/contracts/staking/test/actors/staker_actor.ts index 1bd47b1932..e2b130a017 100644 --- a/contracts/staking/test/actors/staker_actor.ts +++ b/contracts/staking/test/actors/staker_actor.ts @@ -151,7 +151,7 @@ export class StakerActor extends BaseActor { const initZrxBalanceOfVault = await this._stakingApiWrapper.utils.getZrxTokenBalanceOfZrxVaultAsync(); const initBalances = await this._getBalancesAsync(); // go to next epoch - await this._stakingApiWrapper.utils.skipToNextEpochAsync(); + await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); // check balances const expectedBalances = this._getNextEpochBalances(initBalances); await this._assertBalancesAsync(expectedBalances); diff --git a/contracts/staking/test/epoch_test.ts b/contracts/staking/test/epoch_test.ts index e32df331fb..3d7e9c5b0b 100644 --- a/contracts/staking/test/epoch_test.ts +++ b/contracts/staking/test/epoch_test.ts @@ -38,7 +38,7 @@ blockchainTests('Epochs', env => { expect(currentEpoch).to.be.bignumber.equal(stakingConstants.INITIAL_EPOCH); } ///// 3/3 Increment Epoch (TimeLock Should Not Increment) ///// - await stakingApiWrapper.utils.skipToNextEpochAsync(); + await stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync(); { // epoch const currentEpoch = await stakingApiWrapper.stakingContract.currentEpoch.callAsync(); diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index 5347f02f59..faad8c8b78 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -701,8 +701,7 @@ blockchainTests.resets('Testing Rewards', env => { const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const totalStakeAmount = BigNumber.sum(...stakeAmounts); // stake and delegate both - const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as - Array<[StakerActor, BigNumber]>; + const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; for (const [staker, stakeAmount] of stakersAndStake) { await staker.stakeAsync(stakeAmount); await staker.moveStakeAsync( @@ -724,8 +723,7 @@ blockchainTests.resets('Testing Rewards', env => { toBaseUnitAmount(0), ); } - const expectedStakerRewards = stakeAmounts - .map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); + const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); await validateEndBalances({ stakerRewardVaultBalance_1: toBaseUnitAmount(0), stakerRewardVaultBalance_2: toBaseUnitAmount(0), @@ -739,8 +737,7 @@ blockchainTests.resets('Testing Rewards', env => { const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)]; const totalStakeAmount = BigNumber.sum(...stakeAmounts); // stake and delegate both - const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as - Array<[StakerActor, BigNumber]>; + const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; for (const [staker, stakeAmount] of stakersAndStake) { await staker.stakeAsync(stakeAmount); await staker.moveStakeAsync( @@ -754,8 +751,7 @@ blockchainTests.resets('Testing Rewards', env => { // finalize const reward = toBaseUnitAmount(10); await payProtocolFeeAndFinalize(reward); - const expectedStakerRewards = stakeAmounts - .map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); + const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount)); await validateEndBalances({ stakerRewardVaultBalance_1: expectedStakerRewards[0], stakerRewardVaultBalance_2: expectedStakerRewards[1], @@ -776,19 +772,21 @@ blockchainTests.resets('Testing Rewards', env => { const sneakyStakerExpectedEthVaultBalance = expectedStakerRewards[0]; await undelegateZeroAsync(sneakyStaker); // Should have been credited the correct amount of rewards. - let sneakyStakerEthVaultBalance = await stakingApiWrapper - .ethVaultContract.balanceOf - .callAsync(sneakyStaker.getOwner()); - expect(sneakyStakerEthVaultBalance, 'EthVault balance after first undelegate') - .to.bignumber.eq(sneakyStakerExpectedEthVaultBalance); + let sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync( + sneakyStaker.getOwner(), + ); + expect(sneakyStakerEthVaultBalance, 'EthVault balance after first undelegate').to.bignumber.eq( + sneakyStakerExpectedEthVaultBalance, + ); // Now he'll try to do it again to see if he gets credited twice. await undelegateZeroAsync(sneakyStaker); /// The total amount credited should remain the same. - sneakyStakerEthVaultBalance = await stakingApiWrapper - .ethVaultContract.balanceOf - .callAsync(sneakyStaker.getOwner()); - expect(sneakyStakerEthVaultBalance, 'EthVault balance after second undelegate') - .to.bignumber.eq(sneakyStakerExpectedEthVaultBalance); + sneakyStakerEthVaultBalance = await stakingApiWrapper.ethVaultContract.balanceOf.callAsync( + sneakyStaker.getOwner(), + ); + expect(sneakyStakerEthVaultBalance, 'EthVault balance after second undelegate').to.bignumber.eq( + sneakyStakerExpectedEthVaultBalance, + ); }); }); }); diff --git a/contracts/staking/test/unit_tests/delegator_reward_balance.ts b/contracts/staking/test/unit_tests/delegator_reward_balance.ts new file mode 100644 index 0000000000..1fb12adac8 --- /dev/null +++ b/contracts/staking/test/unit_tests/delegator_reward_balance.ts @@ -0,0 +1,22 @@ +import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils'; + +import { artifacts, TestDelegatorRewardsContract } from '../../src'; + +blockchainTests('delegator rewards', env => { + let testContract: TestDelegatorRewardsContract; + + before(async () => { + testContract = await TestDelegatorRewardsContract.deployFrom0xArtifactAsync( + artifacts.TestLibFixedMath, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + describe('computeRewardBalanceOfDelegator()', () => { + it('does stuff', () => { + // TODO + }); + }); +}); diff --git a/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts b/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts index 129d72ef09..89326d93fb 100644 --- a/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts +++ b/contracts/staking/test/unit_tests/lib_proxy_unit_test.ts @@ -8,6 +8,7 @@ import { testCombinatoriallyWithReferenceFunc, } from '@0x/contracts-test-utils'; import { StakingRevertErrors } from '@0x/order-utils'; +import { cartesianProduct } from 'js-combinatorics'; import { artifacts, TestLibProxyContract, TestLibProxyReceiverContract } from '../../src'; @@ -196,79 +197,88 @@ blockchainTests.resets('LibProxy', env => { describe('Combinatorial Tests', () => { // Combinatorial Scenarios for `proxyCall()`. - const revertRuleScenarios: RevertRule[] = [ - RevertRule.RevertOnError, - RevertRule.AlwaysRevert, - RevertRule.NeverRevert, - ]; - const ignoreIngressScenarios: boolean[] = [false, true]; - const customEgressScenarios: string[] = [ - constants.NULL_BYTES4, - constructRandomFailureCalldata(), // Random failure calldata is used because it is nonzero and won't collide. - ]; - const calldataScenarios: string[] = [constructRandomFailureCalldata(), constructRandomSuccessCalldata()]; - - // A reference function that returns the expected success and returndata values of a given call to `proxyCall()`. - async function referenceFuncAsync( - revertRule: RevertRule, - customEgressSelector: string, - shouldIgnoreIngressSelector: boolean, - calldata: string, - ): Promise<[boolean, string]> { - // Determine whether or not the call should succeed. - let shouldSucceed = true; - if ( - ((shouldIgnoreIngressSelector && customEgressSelector !== constants.NULL_BYTES4) || - (!shouldIgnoreIngressSelector && customEgressSelector === constants.NULL_BYTES4)) && - calldata.length === 10 // This corresponds to a hex length of 4 - ) { - shouldSucceed = false; - } - - // Override the above success value if the RevertRule defines the success. - if (revertRule === RevertRule.AlwaysRevert) { - shouldSucceed = false; - } - if (revertRule === RevertRule.NeverRevert) { - shouldSucceed = true; - } - - // Construct the data that should be returned. - let returnData = calldata; - if (shouldIgnoreIngressSelector) { - returnData = hexSlice(returnData, 4); - } - if (customEgressSelector !== constants.NULL_BYTES4) { - returnData = hexConcat(customEgressSelector, returnData); - } - - // Return the success and return data values. - return [shouldSucceed, returnData]; + function getCombinatorialTestDescription(params: [RevertRule, boolean, string, string]): string { + const REVERT_RULE_NAMES = [ + 'RevertOnError', + 'AlwaysRevert', + 'NeverRevert', + ]; + return [ + `revertRule: ${REVERT_RULE_NAMES[params[0]]}`, + `ignoreIngressSelector: ${params[1]}`, + `customEgressSelector: ${params[2]}`, + `calldata: ${ + params[3].length / 2 - 2 > 4 + ? // tslint:disable-next-line + hexSlice(params[3], 0, 4) + '...' + : params[3] + }`, + ].join(', '); } - // A wrapper for `publicProxyCall()` that allow us to combinatorially test `proxyCall()` for the - // scenarios defined above. - async function testFuncAsync( - revertRule: RevertRule, - customEgressSelector: string, - shouldIgnoreIngressSelector: boolean, - calldata: string, - ): Promise<[boolean, string]> { - return publicProxyCallAsync({ - calldata, - customEgressSelector, - ignoreIngressSelector: shouldIgnoreIngressSelector, - revertRule, + const scenarios = [ + // revertRule + [ + RevertRule.RevertOnError, + RevertRule.AlwaysRevert, + RevertRule.NeverRevert, + ], + // ignoreIngressSelector + [false, true], + // customEgressSelector + [ + constants.NULL_BYTES4, + // Random failure calldata is used because it is nonzero and + // won't collide. + constructRandomFailureCalldata(), + ], + // calldata + [ + constructRandomFailureCalldata(), + constructRandomSuccessCalldata(), + ], + ] as [RevertRule[], boolean[], string[], string[]]; + + for (const params of cartesianProduct(...scenarios).toArray()) { + const [revertRule, shouldIgnoreIngressSelector, customEgressSelector, calldata] = params; + it(getCombinatorialTestDescription(params), async () => { + // Determine whether or not the call should succeed. + let shouldSucceed = true; + if ( + ((shouldIgnoreIngressSelector && customEgressSelector !== constants.NULL_BYTES4) || + (!shouldIgnoreIngressSelector && customEgressSelector === constants.NULL_BYTES4)) && + calldata.length === 10 // This corresponds to a hex length of 4 + ) { + shouldSucceed = false; + } + + // Override the above success value if the RevertRule defines the success. + if (revertRule === RevertRule.AlwaysRevert) { + shouldSucceed = false; + } + if (revertRule === RevertRule.NeverRevert) { + shouldSucceed = true; + } + + // Construct the data that should be returned. + let returnData = calldata; + if (shouldIgnoreIngressSelector) { + returnData = hexSlice(returnData, 4); + } + if (customEgressSelector !== constants.NULL_BYTES4) { + returnData = hexConcat(customEgressSelector, returnData); + } + + const [didSucceed, actualReturnData] = await publicProxyCallAsync({ + calldata, + customEgressSelector, + ignoreIngressSelector: shouldIgnoreIngressSelector, + revertRule, + }); + expect(didSucceed).to.be.eq(shouldSucceed); + expect(actualReturnData).to.be.eq(returnData); }); } - - // Combinatorially test proxy call. - testCombinatoriallyWithReferenceFunc('proxyCall', referenceFuncAsync, testFuncAsync, [ - revertRuleScenarios, - customEgressScenarios, - ignoreIngressScenarios, - calldataScenarios, - ]); }); }); }); diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index 6979649ba7..c9806da681 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -1,28 +1,34 @@ import { ERC20Wrapper } from '@0x/contracts-asset-proxy'; import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20'; -import { BlockchainTestsEnvironment, constants } from '@0x/contracts-test-utils'; +import { BlockchainTestsEnvironment, constants, filterLogsToArguments, txDefaults } from '@0x/contracts-test-utils'; import { BigNumber, logUtils } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; -import { ContractArtifact, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import { BlockParamLiteral, ContractArtifact, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; import { artifacts, EthVaultContract, + IStakingEventsEpochEndedEventArgs, + IStakingEventsStakingPoolActivatedEventArgs, ReadOnlyProxyContract, StakingContract, + StakingEvents, StakingPoolRewardVaultContract, StakingProxyContract, ZrxVaultContract, } from '../../src'; import { constants as stakingConstants } from './constants'; -import { StakingParams } from './types'; +import { EndOfEpochInfo, StakingParams } from './types'; export class StakingApiWrapper { - public stakingContractAddress: string; // The address of the real Staking.sol contract - public stakingContract: StakingContract; // The StakingProxy.sol contract wrapped as a StakingContract to borrow API - public stakingProxyContract: StakingProxyContract; // The StakingProxy.sol contract as a StakingProxyContract + // The address of the real Staking.sol contract + public stakingContractAddress: string; + // The StakingProxy.sol contract wrapped as a StakingContract to borrow API + public stakingContract: StakingContract; + // The StakingProxy.sol contract as a StakingProxyContract + public stakingProxyContract: StakingProxyContract; public zrxVaultContract: ZrxVaultContract; public ethVaultContract: EthVaultContract; public rewardVaultContract: StakingPoolRewardVaultContract; @@ -30,21 +36,53 @@ export class StakingApiWrapper { public utils = { // Epoch Utils fastForwardToNextEpochAsync: async (): Promise => { - // increase timestamp of next block - const { epochDurationInSeconds } = await this.utils.getParamsAsync(); - await this._web3Wrapper.increaseTimeAsync(epochDurationInSeconds.toNumber()); + // increase timestamp of next block by how many seconds we need to + // get to the next epoch. + const epochEndTime = await this.stakingContract.getCurrentEpochEarliestEndTimeInSeconds.callAsync(); + const lastBlockTime = await this._web3Wrapper.getBlockTimestampAsync('latest'); + const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber()); + await this._web3Wrapper.increaseTimeAsync(dt); // mine next block await this._web3Wrapper.mineBlockAsync(); }, - skipToNextEpochAsync: async (): Promise => { + skipToNextEpochAndFinalizeAsync: async (): Promise => { await this.utils.fastForwardToNextEpochAsync(); - // increment epoch in contracts - const txReceipt = await this.stakingContract.finalizeFees.awaitTransactionSuccessAsync(); - logUtils.log(`Finalization costed ${txReceipt.gasUsed} gas`); - // mine next block - await this._web3Wrapper.mineBlockAsync(); - return txReceipt; + const endOfEpochInfo = await this.utils.endEpochAsync(); + const receipt = await this.stakingContract.finalizePools.awaitTransactionSuccessAsync( + endOfEpochInfo.activePoolIds, + ); + logUtils.log(`Finalization cost ${receipt.gasUsed} gas`); + return receipt; + }, + + endEpochAsync: async (): Promise => { + const activePoolIds = await this.utils.findActivePoolIdsAsync(); + const receipt = await this.stakingContract.endEpoch.awaitTransactionSuccessAsync(); + const [epochEndedEvent] = filterLogsToArguments( + receipt.logs, + StakingEvents.EpochEnded, + ); + return { + closingEpoch: epochEndedEvent.epoch, + activePoolIds, + rewardsAvailable: epochEndedEvent.rewardsAvailable, + totalFeesCollected: epochEndedEvent.totalFeesCollected, + totalWeightedStake: epochEndedEvent.totalWeightedStake, + }; + }, + + findActivePoolIdsAsync: async (epoch?: number): Promise => { + const _epoch = epoch !== undefined ? epoch : await this.stakingContract.getCurrentEpoch.callAsync(); + const events = filterLogsToArguments( + await this.stakingContract.getLogsAsync( + StakingEvents.StakingPoolActivated, + { fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest }, + { epoch: _epoch }, + ), + StakingEvents.StakingPoolActivated, + ); + return events.map(e => e.poolId); }, // Other Utils diff --git a/contracts/staking/test/utils/types.ts b/contracts/staking/test/utils/types.ts index f9933da21e..9aa685b05d 100644 --- a/contracts/staking/test/utils/types.ts +++ b/contracts/staking/test/utils/types.ts @@ -53,6 +53,14 @@ export interface SimulationParams { withdrawByUndelegating: boolean; } +export interface EndOfEpochInfo { + closingEpoch: BigNumber; + activePoolIds: string[]; + rewardsAvailable: BigNumber; + totalFeesCollected: BigNumber; + totalWeightedStake: BigNumber; +} + export interface StakeBalance { currentEpochBalance: BigNumber; nextEpochBalance: BigNumber;