Merge pull request #1910 from 0xProject/feature/contracts-staking/first-implementation

[WIP] First Implementation of Staking Contracts (ZEIP 31)
This commit is contained in:
Greg Hysz
2019-08-21 16:48:04 -07:00
committed by GitHub
75 changed files with 7780 additions and 37 deletions

View File

@@ -13,6 +13,10 @@
{
"note": "Compile and export all contracts, artifacts, and wrappers by default",
"pr": 2055
},
{
"note": "Remove unused dependency on IAuthorizable in IAssetProxy",
"pr": 1910
}
]
},

View File

@@ -18,8 +18,6 @@
pragma solidity ^0.5.9;
import "./IAuthorizable.sol";
contract IAssetProxy {

View File

@@ -5,6 +5,10 @@
{
"note": "Created package",
"pr": 1821
},
{
"note": "First implementation",
"pr": 1910
}
]
}

View File

@@ -1,6 +1,73 @@
## ERC1155 Tokens
## Staking Contracts
This package implements the stake-based liquidity incentives defined by [ZEIP-31](https://github.com/0xProject/ZEIPs/issues/31). Addresses of the deployed contracts can be found in the 0x [wiki](https://0xproject.com/wiki#Deployed-Addresses) or the [DEPLOYS](./DEPLOYS.json) file within this package.
This package implements the stake-based liquidity incentives defined by [ZEIP-31](https://github.com/0xProject/ZEIPs/issues/31).
Functionality:
1. Stake your ZRX tokens to unlock their utility within the 0x ecosystem.
- Earn rebates on market making on the 0x protocol
- Participate in governance over the 0x protocol
2. Create staking pools to leverage the weight of other stakers.
- Increase your market making rebates.
- Increase your voting power.
3. Delegate your Zrx to staking pools to
- Earn a portion of the pool's market making rebates.
- Support a pool's mission sharing your voting power.
## Architecture
This system is composed of four deployed contracts:
1. Staking Contract, which is an upgradeable/stateless contract that implements staking logic.
2. Staking Contract Proxy, which stores staking state and delegates to the Staking Contract.
3. Zrx Vault, which securely holds staked Zrx Tokens.
4. Staking Pool Reward Vault, which securely holds rewards earned by staking pools.
These contracts connect to each other and the broader 0x ecosystem like this:
![](images/architecture.png)
## Architecture (Kill Switch)
If a vulnerability is discovered in the staking contract, operations may be halted to conduct forensics:
1. The 0x Exchange contract stops charging protocol fees
2. The staking contract is set to read-only mode
3. Vaults may be detached from the staking contract, depending on the vulnerability.
![](images/architecture_kill_switch.png)
## Architecture (Catastrophic Failures)
In this worst-case scenario, state has been irreperably corrupted and the staking contracts must be re-deployed. Users withdraw their funds from the vaults and re-stake under the new system, at will.
4. Vaults enter "Catostrophic Failure Mode" allowing users to withdraw their ZRX and Rewards.
5. A Balance Oracle is deployed for determining the Reward balance of each user. (\*)
![](images/architecture_failure_mode.png)
(\*) A Balance Oracle is implemented retroactively, and depends on how state has been corrupted. For example, if state used to compute rewards is not corrupted, then it would be used by the oracle. Conversely, if this state is corrupted, we may need to reconstruct balances from previous state. (No balance oracle is required for ZRX.)
## Contracts Directory Structure
The contracts can be found in `contracts/src`.
```
* Staking.sol | This is a stateless contract that encapsulates all the staking logic.
* StakingProxy.sol | This is a stateful contract that proxies into the Staking contract.
* fees/ | This contains mixins that implement the logic for 0x Protocol fees & rebates.
* immutable/ | This contains mixins that should not be changed.
* interfaces/ | This contains interfaces used throughout the entire staking system.
* libs/ | This contains libraries used by the staking contract; for example, math and signature validation.
* stake/ | This contains mixins that implement the core staking logic.
* staking_pools/ | This contains mixins that implement logic for creating and managing staking pools.
* sys/ | This contains mixins that implement low-level functionality, like scheduling.
* vaults/ | This contains the vaults (like the Zrx Token Vault).
```
## Testing Architecture
These contracts use an actor/simulation pattern. A simulation runs with a specified set of actors, initial state and expected output. Actors have a specific role and validate each call they make to the staking system; for example, there is a Staking Actor who stakes/unstakes their Zrx tokens and validates balances/events. Similarly, there could exist an actor who tries to steal funds.
## Installation

View File

@@ -18,9 +18,44 @@
pragma solidity ^0.5.9;
import "./IStaking.sol";
import "./interfaces/IStaking.sol";
import "./fees/MixinExchangeManager.sol";
import "./stake/MixinZrxVault.sol";
import "./staking_pools/MixinStakingPoolRewardVault.sol";
import "./sys/MixinScheduler.sol";
import "./stake/MixinStakeBalances.sol";
import "./stake/MixinTimeLockedStake.sol";
import "./stake/MixinStake.sol";
import "./stake/MixinDelegatedStake.sol";
import "./staking_pools/MixinStakingPool.sol";
import "./fees/MixinExchangeFees.sol";
import "./staking_pools/MixinStakingPoolRewards.sol";
contract Staking is IStaking {
contract Staking is
IStaking,
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinOwnable,
MixinExchangeManager,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinZrxVault,
MixinStakingPool,
MixinTimeLockedStake,
MixinStakeBalances,
MixinStake,
MixinStakingPoolRewards,
MixinExchangeFees,
MixinDelegatedStake
{
// this contract can receive ETH
// solhint-disable no-empty-blocks
function ()
external
payable
{}
}

View File

@@ -0,0 +1,109 @@
/*
Copyright 2018 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;
import "./immutable/MixinStorage.sol";
import "./interfaces/IStakingProxy.sol";
import "./sys/MixinOwnable.sol";
contract StakingProxy is
IStakingProxy,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinOwnable
{
/// @dev Constructor.
/// @param _stakingContract Staking contract to delegate calls to.
constructor(address _stakingContract)
public
{
owner = msg.sender;
stakingContract = _stakingContract;
}
/// @dev Delegates calls to the staking contract, if it is set.
// solhint-disable no-complex-fallback
function ()
external
payable
{
address _stakingContract = stakingContract;
if (_stakingContract == NIL_ADDRESS) {
return;
}
assembly {
// copy calldata to memory
calldatacopy(
0x0,
0x0,
calldatasize()
)
// delegate call into staking contract
let success := delegatecall(
gas, // forward all gas
_stakingContract, // calling staking contract
0x0, // start of input (calldata)
calldatasize(), // length of input (calldata)
0x0, // write output over input
0 // length of output is unknown
)
// copy return data to memory
returndatacopy(
0x0,
0x0,
returndatasize()
)
// rethrow any exceptions
if iszero(success) {
revert(0, returndatasize())
}
// return call results
return(0, returndatasize())
}
}
/// @dev Attach a staking contract; future calls will be delegated to the staking contract.
/// Note that this is callable only by this contract's owner.
/// @param _stakingContract Address of staking contract.
function attachStakingContract(address _stakingContract)
external
onlyOwner
{
stakingContract = _stakingContract;
emit StakingContractAttachedToProxy(_stakingContract);
}
/// @dev Detach the current staking contract.
/// Note that this is callable only by this contract's owner.
function detachStakingContract()
external
onlyOwner
{
stakingContract = NIL_ADDRESS;
emit StakingContractDetachedFromProxy();
}
}

View File

@@ -0,0 +1,244 @@
/*
Copyright 2018 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;
import "../libs/LibSafeMath.sol";
import "../libs/LibFeeMath.sol";
import "../immutable/MixinStorage.sol";
import "../immutable/MixinConstants.sol";
import "../interfaces/IStakingEvents.sol";
import "../interfaces/IStructs.sol";
import "../stake/MixinStakeBalances.sol";
import "../sys/MixinScheduler.sol";
import "../staking_pools/MixinStakingPool.sol";
import "../staking_pools/MixinStakingPoolRewardVault.sol";
import "./MixinExchangeManager.sol";
/// @dev This mixin contains the logic for 0x protocol fees.
/// Protocol fees are sent by 0x exchanges every time there is a trade.
/// If the maker has associated their address with a pool (see MixinStakingPool.sol), then
/// the fee will be attributed to their pool. At the end of an epoch the maker and
/// their pool will receive a rebate that is proportional to (i) the fee volume attributed
/// to their pool over the epoch, and (ii) the amount of stake provided by the maker and
/// their delegators. Note that delegated stake (see MixinStake) is weighted less than
/// stake provided by directly by the maker; this is a disincentive for market makers to
/// monopolize a single pool that they all delegate to.
contract MixinExchangeFees is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinOwnable,
MixinExchangeManager,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinStakingPool,
MixinTimeLockedStake,
MixinStakeBalances
{
using LibSafeMath for uint256;
/// @dev Pays a protocol fee in ETH.
/// Only a known 0x exchange can call this method. See (MixinExchangeManager).
/// @param makerAddress The address of the order's maker.
function payProtocolFee(address makerAddress)
external
payable
onlyExchange
{
uint256 amount = msg.value;
bytes32 poolId = getStakingPoolIdOfMaker(makerAddress);
uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId];
protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch._add(amount);
if (_feesCollectedThisEpoch == 0) {
activePoolsThisEpoch.push(poolId);
}
}
/// @dev Pays the rebates for to market making pool that was active this epoch,
/// then updates the epoch and other time-based periods via the scheduler (see MixinScheduler).
/// This is intentionally permissionless, and may be called by anyone.
function finalizeFees()
external
{
// distribute fees to market maker pools as a reward
(uint256 totalActivePools,
uint256 totalFeesCollected,
uint256 totalWeightedStake,
uint256 totalRewardsPaid,
uint256 initialContractBalance,
uint256 finalContractBalance) = _distributeFeesAmongMakerPools();
emit RewardsPaid(
totalActivePools,
totalFeesCollected,
totalWeightedStake,
totalRewardsPaid,
initialContractBalance,
finalContractBalance
);
_goToNextEpoch();
}
/// @dev Returns the total amount of fees collected thus far, in the current epoch.
/// @return Amount of fees.
function getTotalProtocolFeesThisEpoch()
public
view
returns (uint256)
{
return address(this).balance;
}
/// @dev Returns the amount of fees attributed to the input pool.
/// @param poolId Pool Id to query.
/// @return Amount of fees.
function getProtocolFeesThisEpochByPool(bytes32 poolId)
public
view
returns (uint256)
{
return protocolFeesThisEpochByPool[poolId];
}
/// @dev Pays rewards to market making pools that were active this epoch.
/// Each pool receives a portion of the fees generated this epoch (see LibFeeMath) that is
/// proportional to (i) the fee volume attributed to their pool over the epoch, and
/// (ii) the amount of stake provided by the maker and their delegators. Rebates are paid
/// into the Reward Vault (see MixinStakingPoolRewardVault) where they can be withdraw by makers and
/// the members of their pool. There will be a small amount of ETH leftover in this contract
/// after paying out the rebates; at present, this rolls over into the next epoch. Eventually,
/// we plan to deposit this leftover into a DAO managed by the 0x community.
/// @return totalActivePools Total active pools this epoch.
/// @return totalFeesCollected Total fees collected this epoch, across all active pools.
/// @return totalWeightedStake Total weighted stake attributed to each pool. Delegated stake is weighted less.
/// @return totalRewardsPaid Total rewards paid out across all active pools.
/// @return initialContractBalance Balance of this contract before paying rewards.
/// @return finalContractBalance Balance of this contract after paying rewards.
function _distributeFeesAmongMakerPools()
private
returns (
uint256 totalActivePools,
uint256 totalFeesCollected,
uint256 totalWeightedStake,
uint256 totalRewardsPaid,
uint256 initialContractBalance,
uint256 finalContractBalance
)
{
// initialize return values
totalActivePools = activePoolsThisEpoch.length;
totalFeesCollected = 0;
totalWeightedStake = 0;
totalRewardsPaid = 0;
initialContractBalance = address(this).balance;
finalContractBalance = initialContractBalance;
// sanity check - is there a balance to payout and were there any active pools?
if (initialContractBalance == 0 || totalActivePools == 0) {
return (
totalActivePools,
totalFeesCollected,
totalWeightedStake,
totalRewardsPaid,
initialContractBalance,
finalContractBalance
);
}
// step 1/3 - compute stats for active maker pools
IStructs.ActivePool[] memory activePools = new IStructs.ActivePool[](totalActivePools);
for (uint256 i = 0; i != totalActivePools; i++) {
bytes32 poolId = activePoolsThisEpoch[i];
// compute weighted stake
uint256 totalStakeDelegatedToPool = getTotalStakeDelegatedToPool(poolId);
uint256 stakeHeldByPoolOperator = getActivatedAndUndelegatedStake(getStakingPoolOperator(poolId));
uint256 weightedStake = stakeHeldByPoolOperator._add(
totalStakeDelegatedToPool
._mul(REWARD_PAYOUT_DELEGATED_STAKE_PERCENT_VALUE)
._div(100)
);
// store pool stats
activePools[i].poolId = poolId;
activePools[i].feesCollected = protocolFeesThisEpochByPool[poolId];
activePools[i].weightedStake = weightedStake;
// update cumulative amounts
totalFeesCollected = totalFeesCollected._add(activePools[i].feesCollected);
totalWeightedStake = totalWeightedStake._add(activePools[i].weightedStake);
}
// sanity check - this is a gas optimization that can be used because we assume a non-zero
// split between stake and fees generated in the cobb-douglas computation (see below).
if (totalFeesCollected == 0 || totalWeightedStake == 0) {
return (
totalActivePools,
totalFeesCollected,
totalWeightedStake,
totalRewardsPaid,
initialContractBalance,
finalContractBalance
);
}
// step 2/3 - record reward for each pool
for (uint256 i = 0; i != totalActivePools; i++) {
// compute reward using cobb-douglas formula
uint256 reward = LibFeeMath._cobbDouglasSuperSimplified(
initialContractBalance,
activePools[i].feesCollected,
totalFeesCollected,
activePools[i].weightedStake,
totalWeightedStake
);
// record reward in vault
_recordDepositInStakingPoolRewardVault(activePools[i].poolId, reward);
totalRewardsPaid = totalRewardsPaid._add(reward);
// clear state for gas refunds
protocolFeesThisEpochByPool[activePools[i].poolId] = 0;
activePoolsThisEpoch[i] = 0;
}
activePoolsThisEpoch.length = 0;
// step 3/3 send total payout to vault
require(
totalRewardsPaid <= initialContractBalance,
"MISCALCULATED_REWARDS"
);
if (totalRewardsPaid > 0) {
_depositIntoStakingPoolRewardVault(totalRewardsPaid);
}
finalContractBalance = address(this).balance;
return (
totalActivePools,
totalFeesCollected,
totalWeightedStake,
totalRewardsPaid,
initialContractBalance,
finalContractBalance
);
}
}

View File

@@ -0,0 +1,86 @@
/*
Copyright 2018 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;
import "../interfaces/IStakingEvents.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../sys/MixinOwnable.sol";
/// @dev This mixin contains logic for managing exchanges.
/// Any exchange contract that connects to the staking contract
/// must be added here. When an exchange contract is deprecated
/// then it should be removed.
contract MixinExchangeManager is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinOwnable
{
/// @dev Asserts that the call is coming from a valid exchange.
modifier onlyExchange() {
require(
isValidExchangeAddress(msg.sender),
"ONLY_CALLABLE_BY_EXCHANGE"
);
_;
}
/// @dev Adds a new exchange address
/// @param addr Address of exchange contract to add
function addExchangeAddress(address addr)
external
onlyOwner
{
require(
!validExchanges[addr],
"EXCHANGE_ADDRESS_ALREADY_REGISTERED"
);
validExchanges[addr] = true;
emit ExchangeAdded(addr);
}
/// @dev Removes an existing exchange address
/// @param addr Address of exchange contract to remove
function removeExchangeAddress(address addr)
external
onlyOwner
{
require(
validExchanges[addr],
"EXCHANGE_ADDRESS_NOT_REGISTERED"
);
validExchanges[addr] = false;
emit ExchangeRemoved(addr);
}
/// @dev Returns true iff the address is a valid exchange.
/// @param addr Address of exchange contract.
/// @return True iff input address is a valid exchange.
function isValidExchangeAddress(address addr)
public
view
returns (bool)
{
return validExchanges[addr];
}
}

View File

@@ -0,0 +1,47 @@
/*
Copyright 2018 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;
import "./MixinDeploymentConstants.sol";
contract MixinConstants is
MixinDeploymentConstants
{
uint64 constant internal MAX_UINT_64 = 0xFFFFFFFFFFFFFFFF;
uint256 constant internal TOKEN_MULTIPLIER = 1000000000000000000; // 10**18
// The upper 16 bytes represent the pool id, so this would be pool id 1. See MixinStakinPool for more information.
bytes32 constant internal INITIAL_POOL_ID = 0x0000000000000000000000000000000100000000000000000000000000000000;
// The upper 16 bytes represent the pool id, so this would be an increment of 1. See MixinStakinPool for more information.
uint256 constant internal POOL_ID_INCREMENT_AMOUNT = 0x0000000000000000000000000000000100000000000000000000000000000000;
bytes32 constant internal NIL_MAKER_ID = 0x0000000000000000000000000000000000000000000000000000000000000000;
address constant internal NIL_ADDRESS = 0x0000000000000000000000000000000000000000;
bytes32 constant internal UNKNOWN_STAKING_POOL_ID = 0x0000000000000000000000000000000000000000000000000000000000000000;
uint64 constant internal INITIAL_EPOCH = 0;
uint64 constant internal INITIAL_TIMELOCK_PERIOD = INITIAL_EPOCH;
}

View File

@@ -0,0 +1,35 @@
/*
Copyright 2018 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;
contract MixinDeploymentConstants {
// @TODO SET THESE VALUES FOR DEPLOYMENT
uint64 constant internal EPOCH_DURATION_IN_SECONDS = 1000;
uint64 constant internal TIMELOCK_DURATION_IN_EPOCHS = 3;
uint256 constant internal COBB_DOUGLAS_ALPHA_DENOMINATOR = 6;
uint256 constant internal REWARD_PAYOUT_DELEGATED_STAKE_PERCENT_VALUE = 90;
uint256 constant internal CHAIN_ID = 1;
}

View File

@@ -0,0 +1,105 @@
/*
Copyright 2018 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;
import "../interfaces/IZrxVault.sol";
import "../interfaces/IStakingPoolRewardVault.sol";
import "./MixinConstants.sol";
import "../interfaces/IStructs.sol";
// solhint-disable max-states-count
contract MixinStorage is
MixinDeploymentConstants,
MixinConstants
{
// address of owner
address internal owner;
// address of staking contract
address internal stakingContract;
// mapping from Owner to Amount Staked
mapping (address => uint256) internal stakeByOwner;
// mapping from Owner to Amount of Instactive Stake
mapping (address => uint256) internal activatedStakeByOwner;
// mapping from Owner to Amount TimeLocked
mapping (address => IStructs.TimeLock) internal timeLockedStakeByOwner;
// mapping from Owner to Amount Delegated
mapping (address => uint256) internal delegatedStakeByOwner;
// mapping from Owner to Pool Id to Amount Delegated
mapping (address => mapping (bytes32 => uint256)) internal delegatedStakeToPoolByOwner;
// mapping from Pool Id to Amount Delegated
mapping (bytes32 => uint256) internal delegatedStakeByPoolId;
// total activated stake in the system
uint256 internal totalActivatedStake;
// tracking Pool Id
bytes32 internal nextPoolId = INITIAL_POOL_ID;
// mapping from Pool Id to Pool
mapping (bytes32 => IStructs.Pool) internal poolById;
// mapping from Maker Address to Pool Id
// A Maker can only hold a single token
mapping (address => bytes32) internal poolIdByMakerAddress;
// mapping from Pool Id to Addresses
mapping (bytes32 => address[]) internal makerAddressesByPoolId;
// current epoch
uint64 internal currentEpoch = INITIAL_EPOCH;
// current epoch start time
uint64 internal currentEpochStartTimeInSeconds;
// current withdrawal period
uint64 internal currentTimeLockPeriod = INITIAL_TIMELOCK_PERIOD;
// current epoch start time
uint64 internal currentTimeLockPeriodStartEpoch = INITIAL_EPOCH;
// fees collected this epoch
mapping (bytes32 => uint256) internal protocolFeesThisEpochByPool;
// pools that were active in the current epoch
bytes32[] internal activePoolsThisEpoch;
// mapping from POol Id to Shadow Rewards
mapping (bytes32 => uint256) internal shadowRewardsByPoolId;
// shadow balances by
mapping (address => mapping (bytes32 => uint256)) internal shadowRewardsInPoolByOwner;
// registrered 0x exchanges
mapping (address => bool) internal validExchanges;
// ZRX vault
IZrxVault internal zrxVault;
// Rebate Vault
IStakingPoolRewardVault internal rewardVault;
}

View File

@@ -1,6 +1,6 @@
/*
Copyright 2019 ZeroEx Intl.
Copyright 2018 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,6 +19,9 @@
pragma solidity ^0.5.9;
// solhint-disable no-empty-blocks
interface IStaking {
/// THIS INTERFACE IS LEFT INTENTIONALLY BLANK ///
/// @TODO Generate this file before deploying.
}

View File

@@ -0,0 +1,107 @@
pragma solidity ^0.5.9;
interface IStakingEvents {
/// @dev Emitted by MixinStake when new Stake is minted.
/// @param owner of Stake.
/// @param amount of Stake minted.
event StakeMinted(
address owner,
uint256 amount
);
/// @dev Emitted by MixinStake when Stake is burned.
/// @param owner of Stake.
/// @param amount of Stake burned.
event StakeBurned(
address owner,
uint256 amount
);
/// @dev Emitted by MixinExchangeManager when an exchange is added.
/// @param exchangeAddress Address of new exchange.
event ExchangeAdded(
address exchangeAddress
);
/// @dev Emitted by MixinExchangeManager when an exchange is removed.
/// @param exchangeAddress Address of removed exchange.
event ExchangeRemoved(
address exchangeAddress
);
/// @dev Emitted by MixinScheduler when the epoch is changed.
/// @param epoch The epoch we changed to.
/// @param startTimeInSeconds The start time of the epoch.
/// @param earliestEndTimeInSeconds The earliest this epoch can end.
event EpochChanged(
uint64 epoch,
uint64 startTimeInSeconds,
uint64 earliestEndTimeInSeconds
);
/// @dev Emitted by MixinScheduler when the timeLock period is changed.
/// @param timeLockPeriod The timeLock period we changed to.
/// @param startEpoch The epoch this period started.
/// @param endEpoch The epoch this period ends.
event TimeLockPeriodChanged(
uint64 timeLockPeriod,
uint64 startEpoch,
uint64 endEpoch
);
/// @dev Emitted by MixinExchangeFees when rewards are paid out.
/// @param totalActivePools Total active pools this epoch.
/// @param totalFeesCollected Total fees collected this epoch, across all active pools.
/// @param totalWeightedStake Total weighted stake attributed to each pool. Delegated stake is weighted less.
/// @param totalRewardsPaid Total rewards paid out across all active pools.
/// @param initialContractBalance Balance of this contract before paying rewards.
/// @param finalContractBalance Balance of this contract after paying rewards.
event RewardsPaid(
uint256 totalActivePools,
uint256 totalFeesCollected,
uint256 totalWeightedStake,
uint256 totalRewardsPaid,
uint256 initialContractBalance,
uint256 finalContractBalance
);
/// @dev Emitted by MixinOwnable when the contract's ownership changes
/// @param newOwner New owner of the contract
event OwnershipTransferred(
address newOwner
);
/// @dev Emitted by MixinStakingPool when a new pool is created.
/// @param poolId Unique id generated for pool.
/// @param operatorAddress Address of creator/operator of pool.
/// @param operatorShare The share of rewards given to the operator.
event StakingPoolCreated(
bytes32 poolId,
address operatorAddress,
uint8 operatorShare
);
/// @dev Emitted by MixinStakingPool when a new maker is added to a pool.
/// @param poolId Unique id of pool.
/// @param makerAddress Adress of maker added to pool.
event MakerAddedToStakingPool(
bytes32 poolId,
address makerAddress
);
/// @dev Emitted by MixinStakingPool when a maker is removed from a pool.
/// @param poolId Unique id of pool.
/// @param makerAddress Adress of maker added to pool.
event MakerRemovedFromStakingPool(
bytes32 poolId,
address makerAddress
);
/// @dev Emitted by MixinStakingPoolRewardVault when the vault's address is changed.
/// @param rewardVaultAddress Address of new reward vault.
event StakingPoolRewardVaultChanged(
address rewardVaultAddress
);
}

View File

@@ -0,0 +1,149 @@
/*
Copyright 2018 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;
/// @dev This vault manages staking pool rewards.
/// Rewards can be deposited and withdraw by the staking contract.
/// There is a "Catastrophic Failure Mode" that, when invoked, only
/// allows withdrawals to be made. Once this vault is in catostrophic
/// failure mode, it cannot be returned to normal mode; this prevents
/// corruption of related state in the staking contract.
interface IStakingPoolRewardVault {
/// @dev Holds the balance for a staking pool.
/// @param initialzed True iff the balance struct is initialized.
/// @param operatorShare Percentage of the total balance owned by the operator.
/// @param operatorBalance Balance in ETH of the operator.
/// @param membersBalance Balance in ETH co-owned by the pool members.
struct Balance {
bool initialized;
uint8 operatorShare;
uint96 operatorBalance;
uint96 membersBalance;
}
/// @dev Emitted when reward is deposited.
/// @param poolId The pool the reward was deposited for.
/// Note that a poolId of "0" means "unknown" at time of deposit.
/// In this case, the reward would be deposited later in the transaction.
/// This is an optimization for the staking contract, which may make many deposits
/// in the same transaction.
/// @param amount The amount in ETH deposited.
event RewardDeposited(
bytes32 poolId,
uint256 amount
);
/// @dev Emitted when a reward is withdrawn for an operator.
/// @param amount The amount in ETH withdrawn.
/// @param poolId The pool the reward was deposited for.
event RewardWithdrawnForOperator(
bytes32 poolId,
uint256 amount
);
/// @dev Emitted when a reward is withdrawn for a pool member.
/// @param amount The amount in ETH withdrawn.
/// @param poolId The pool the reward was deposited for.
event RewardWithdrawnForMember(
bytes32 poolId,
uint256 amount
);
/// @dev Emitted when a staking pool is registered.
/// @param poolId Unique Id of pool that was registered.
/// @param operatorShare Share of rewards owned by operator.
event StakingPoolRegistered(
bytes32 poolId,
uint8 operatorShare
);
/// @dev Default constructor.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
function ()
external
payable;
/// @dev Deposit a reward in ETH for a specific pool.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
function depositFor(bytes32 poolId)
external
payable;
/// @dev Record a deposit for a pool. This deposit should be in the same transaction,
/// which is enforced by the staking contract. We do not enforce it here to save (a lot of) gas.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function recordDepositFor(bytes32 poolId, uint256 amount)
external;
/// @dev Withdraw some amount in ETH of an operator's reward.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function withdrawForOperator(bytes32 poolId, uint256 amount)
external;
/// @dev Withdraw some amount in ETH of a pool member.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function withdrawForMember(bytes32 poolId, uint256 amount)
external;
/// @dev Register a new staking pool.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param poolOperatorShare Percentage of rewards given to the pool operator.
function registerStakingPool(bytes32 poolId, uint8 poolOperatorShare)
external;
/// @dev Returns the total balance of a pool.
/// @param poolId Unique Id of pool.
/// @return Balance in ETH.
function balanceOf(bytes32 poolId)
external
view
returns (uint256);
/// @dev Returns the balance of a pool operator.
/// @param poolId Unique Id of pool.
/// @return Balance in ETH.
function balanceOfOperator(bytes32 poolId)
external
view
returns (uint256);
/// @dev Returns the balance co-owned by members of a pool.
/// @param poolId Unique Id of pool.
/// @return Balance in ETH.
function balanceOfMembers(bytes32 poolId)
external
view
returns (uint256);
}

View File

@@ -0,0 +1,50 @@
/*
Copyright 2018 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;
interface IStakingProxy /* is IStaking */
{
/// @dev Emitted by StakingProxy when a staking contract is attached.
/// @param newStakingContractAddress Address of newly attached staking contract.
event StakingContractAttachedToProxy(
address newStakingContractAddress
);
/// @dev Emitted by StakingProxy when a staking contract is detached.
event StakingContractDetachedFromProxy();
/// @dev Delegates calls to the staking contract, if it is set.
// solhint-disable no-complex-fallback
function ()
external
payable;
/// @dev Attach a staking contract; future calls will be delegated to the staking contract.
/// Note that this is callable only by this contract's owner.
/// @param _stakingContract Address of staking contract.
function attachStakingContract(address _stakingContract)
external;
/// @dev Detach the current staking contract.
/// Note that this is callable only by this contract's owner.
function detachStakingContract()
external;
}

View File

@@ -0,0 +1,70 @@
/*
Copyright 2018 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;
interface IStructs {
/// @dev Allowed signature types.
enum SignatureType {
Illegal, // 0x00, default value
Invalid, // 0x01
EIP712, // 0x02
EthSign, // 0x03
Wallet, // 0x04
NSignatureTypes // 0x05, number of signature types. Always leave at end.
}
/// @dev Required fields for a maker to approve a staking pool.
/// @param poolId Unique Id of staking pool.
/// @param makerAddress Address of maker who has approved the pool.
struct StakingPoolApproval {
bytes32 poolId;
address makerAddress;
}
/// @dev State for Staking Pools (see MixinStakingPool).
/// @param operatorAddress Address of pool operator.
/// @param operatorShare Portion of pool rewards owned by operator.
struct Pool {
address payable operatorAddress;
uint8 operatorShare;
}
/// @dev State for a pool that actively traded during the current epoch.
/// (see MixinExchangeFees).
/// @param poolId Unique Id of staking pool.
/// @param feesCollected Fees collected in ETH by this pool in the current epoch.
/// @param weightedStake Amount of weighted stake currently held by the pool.
struct ActivePool {
bytes32 poolId;
uint256 feesCollected;
uint256 weightedStake;
}
/// @dev Tracks timeLocked stake (see MixinTimeLockedStake).
/// @param lockedAt The TimeLock Period that stake was most recently locked at.
/// @param total Amount of stake that is timeLocked.
/// @param pending Stake pending to be un-TimeLocked next TimeLock Period.
struct TimeLock {
uint64 lockedAt;
uint96 total;
uint96 pending;
}
}

View File

@@ -0,0 +1,58 @@
/*
Copyright 2018 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;
/// @dev This mixin contains core logic for vaults.
/// This includes management of the staking contract
/// and setting the vault to "Catastrophic Failure Mode".
/// It's up to the vault how they handle this failure mode; however,
/// all vaults should disable all functionality aside from withdrawals.
/// Vaults should only be set to Catastrophic Failure Mode iff there is
/// non-recoverable corruption of the staking contracts. If there is a
/// recoverable flaw/bug/vulnerability, simply detach the staking contract
/// by setting its address to `address(0)`. Once in Catostrophic Failure Mode,
/// a vault cannot be reset to normal mode; this prevents corruption of related
/// state in the staking contract.
interface IVaultCore {
/// @dev Emitted when the Staking contract is changed.
/// @param stakingContractAddress Address of the new Staking contract.
event StakingContractChanged(
address stakingContractAddress
);
/// @dev Emitted when the Staking contract is put into Catostrophic Failure Mode
/// @param sender Address of sender (`msg.sender`)
event InCatostrophicFailureMode(
address sender
);
/// @dev Sets the address of the Staking Contract.
/// Note that only the contract owner can call this function.
/// @param _stakingContractAddress Address of Staking contract.
function setStakingContract(address payable _stakingContractAddress)
external;
/// @dev Vault enters into Catostrophic Failure Mode.
/// *** WARNING - ONCE IN CATOSTROPHIC FAILURE MODE, YOU CAN NEVER GO BACK! ***
/// Note that only the contract owner can call this function.
function enterCatostrophicFailure()
external;
}

View File

@@ -0,0 +1,32 @@
/*
Copyright 2018 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;
interface IWallet /* is EIP-1271 */ {
/// @dev Should return whether the signature provided is valid for the provided data
/// @param data Arbitrary length data signed on the behalf of address(this)
/// @param signature Signature byte array associated with _data
///
/// MUST return the bytes4 magic value 0x20c13b0b when function passes.
/// MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5)
/// MUST allow external calls
function isValidSignature(
bytes calldata data,
bytes calldata signature)
external
view
returns (bytes4 magicValue);
}

View File

@@ -0,0 +1,106 @@
/*
Copyright 2018 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;
/// @dev This vault manages Zrx Tokens.
/// When a user mints stake, their Zrx Tokens are deposited into this vault.
/// Similarly, when they burn stake, their Zrx Tokens are withdrawn from this vault.
/// There is a "Catastrophic Failure Mode" that, when invoked, only
/// allows withdrawals to be made. Once this vault is in catostrophic
/// failure mode, it cannot be returned to normal mode; this prevents
/// corruption of related state in the staking contract.
interface IZrxVault {
/// @dev Emitted when Zrx Tokens are deposited into the vault.
/// @param sender Address of sender (`msg.sender`).
/// @param owner of Zrx Tokens.
/// @param amount of Zrx Tokens deposited.
event ZrxDepositedIntoVault(
address indexed sender,
address indexed owner,
uint256 amount
);
/// @dev Emitted when Zrx Tokens are withdrawn from the vault.
/// @param sender Address of sender (`msg.sender`).
/// @param owner of Zrx Tokens.
/// @param amount of Zrx Tokens withdrawn.
event ZrxWithdrawnFromVault(
address indexed sender,
address indexed owner,
uint256 amount
);
/// @dev Emitted when the ERC20 Proxy is changed.
/// @param erc20ProxyAddress Address of the new ERC20 proxy.
event Erc20ProxyChanged(
address erc20ProxyAddress
);
/// @dev Emitted when the Zrx Asset Data is changed.
/// @param zrxAssetData New Zrx Asset Data.
event ZrxAssetDataChanged(
bytes zrxAssetData
);
/// @dev Sets the ERC20 proxy.
/// Note that only the contract owner can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param erc20ProxyAddress Address of the 0x ERC20 Proxy.
function setErc20Proxy(address erc20ProxyAddress)
external;
/// @dev Sets the Zrx Asset Data.
/// Note that only the contract owner can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param _zrxAssetData Zrx asset data for the ERC20 Proxy.
function setZrxAssetData(bytes calldata _zrxAssetData)
external;
/// @dev Deposit an `amount` of Zrx Tokens from `owner` into the vault.
/// Note that only the Staking contract can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param owner of Zrx Tokens.
/// @param amount of Zrx Tokens to deposit.
function depositFrom(address owner, uint256 amount)
external;
/// @dev Withdraw an `amount` of Zrx Tokens to `owner` from the vault.
/// Note that only the Staking contract can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param owner of Zrx Tokens.
/// @param amount of Zrx Tokens to withdraw.
function withdrawFrom(address owner, uint256 amount)
external;
/// @dev Withdraw ALL Zrx Tokens to `owner` from the vault.
/// Note that this can only be called when *in* Catostrophic Failure mode.
/// @param owner of Zrx Tokens.
function withdrawAllFrom(address owner)
external
returns (uint256);
/// @dev Returns the balance in Zrx Tokens of the `owner`
/// @return Balance in Zrx.
function balanceOf(address owner)
external
view
returns (uint256);
}

View File

@@ -0,0 +1,100 @@
/*
Copyright 2018 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 "@0x/contracts-utils/contracts/src/LibEIP712.sol";
import "../interfaces/IStructs.sol";
library LibEIP712Hash {
// EIP712 Domain Name value for the Staking contract
string constant internal EIP712_STAKING_DOMAIN_NAME = "0x Protocol Staking";
// EIP712 Domain Version value for the Staking contract
string constant internal EIP712_STAKING_DOMAIN_VERSION = "1.0.0";
// Hash for the EIP712 StakingPool approval message
// keccak256(abi.encodePacked(
// "StakingPoolApproval(",
// "bytes32 poolId,",
// "address makerAddress",
// ")"
// ));
bytes32 constant internal EIP712_STAKING_POOL_APPROVAL_SCHEMA_HASH = 0x9b699f12ef1c0f7b43076182dcccc0c548c9a784cfcf27114f98d684e06826b6;
/// @dev Calculated the EIP712 hash of the StakingPool approval mesasage using the domain separator of this contract.
/// @param approval StakingPool approval message containing the transaction hash, transaction signature, and expiration of the approval.
/// @return EIP712 hash of the StakingPool approval message with the domain separator of this contract.
function _hashStakingPoolApprovalMessage(
IStructs.StakingPoolApproval memory approval,
uint256 chainId,
address verifierAddress
)
internal
pure
returns (bytes32 approvalHash)
{
approvalHash = _hashEIP712StakingMessage(
_hashStakingPoolApproval(approval),
chainId,
verifierAddress
);
return approvalHash;
}
/// @dev Calculates EIP712 encoding for a hash struct in the EIP712 domain
/// of this contract.
/// @param hashStruct The EIP712 hash struct.
/// @return EIP712 hash applied to this EIP712 Domain.
function _hashEIP712StakingMessage(
bytes32 hashStruct,
uint256 chainId,
address verifierAddress
)
internal
pure
returns (bytes32 result)
{
bytes32 eip712StakingDomainHash = LibEIP712.hashEIP712Domain(
EIP712_STAKING_DOMAIN_NAME,
EIP712_STAKING_DOMAIN_VERSION,
chainId,
verifierAddress
);
return LibEIP712.hashEIP712Message(eip712StakingDomainHash, hashStruct);
}
/// @dev Calculated the EIP712 hash of the StakingPool approval mesasage with no domain separator.
/// @param approval StakingPool approval message containing the transaction hash, transaction signature, and expiration of the approval.
/// @return EIP712 hash of the StakingPool approval message with no domain separator.
function _hashStakingPoolApproval(IStructs.StakingPoolApproval memory approval)
internal
pure
returns (bytes32 result)
{
result = keccak256(abi.encode(
EIP712_STAKING_POOL_APPROVAL_SCHEMA_HASH,
approval.poolId,
approval.makerAddress
));
return result;
}
}

View File

@@ -0,0 +1,315 @@
/*
Copyright 2018 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;
/// @dev This library implements math helpers for fee computation.
/// *** READ MixinExchangeFees BEFORE CONTINUING ***
/// To do - Optimization / Precision / SafeMath.
/// To do - Once better nth root - choose a value that is not a divisor of 18, like 7.
/// To do - Update these values for deployment.
/// There may be better, more efficient ways of implementing these functions.
/// This works well enough to test the end-to-system, but it would be really
/// good to get some math experts in here to check out this code. We should also
/// look at existing projects, in case similar functionality exists and has been
/// audited by a third-party.
library LibFeeMath {
// Denominator of alpha in cobb-douglas function
uint256 constant internal COBB_DOUGLAS_ALPHA_DENOMINATOR = 6;
// Reflects number of decimal places in a token amount
uint256 constant internal TOKEN_MULTIPLIER = 1000000000000000000;
// The divisor for finding the nth root of token amounts.
uint256 constant internal NTH_ROOT_OF_TOKEN_MULTIPLIER = 1000;
/// @dev Computes the nth root of a number.
/// @param base to compute the root.
/// @param n nth root.
/// @return nth root of base.
function _nthRoot(uint256 base, uint256 n)
internal
pure
returns (uint256 root)
{
assembly {
///// Implements Newton's Approximation, derived from Newton's nth Root Algorithm /////
///// See https://en.wikipedia.org/wiki/Nth_root#nth_root_algorithm
// 1. Find greatest power-of-2 <= `value`
let nearestPowerOf2 := 0x100000000000000000000000000000000
let m := 128
for {let p := 64}
gt(p, 0)
{ p := div(p, 2) }
{
switch gt(nearestPowerOf2, base)
case 1 {
nearestPowerOf2 := shr(p, nearestPowerOf2)
m := sub(m, p)
}
case 0 {
switch lt(nearestPowerOf2, base)
case 1 {
nearestPowerOf2 := shl(p, nearestPowerOf2)
m := add(m, p)
}
case 0 {
p := 0
}
}
}
if gt(nearestPowerOf2, base) {
nearestPowerOf2 := shr(1, nearestPowerOf2)
m := sub(m, 1)
}
// 2. Find greatest power-of-2 that, when raised to the power of `n`,
// is <= `value`
let x := exp(2, div(m, n))
// 3. Find y such that `x` + `y` = `base`
let y := sub(base, exp2(x, n))
// 4. Run Newton's Approximation to approximate the root
let denominator := mul(n, exp2(x, sub(n, 1)))
root := add(x, div(y, denominator))
// 5. Run Newton's nth Root Algorithm
let delta := 1 // run at least once
// solhint-disable no-empty-blocks
for {}
gt(delta, 0)
{}
{
// compute lhs
let lhsDenominator := exp2(root, sub(n, 1))
let lhs := div(base, lhsDenominator)
// check for overlow
switch lt(lhs, root)
case 0 {
// underestimate
delta := div(sub(lhs, root), n)
root := add(root, delta)
}
case 1 {
// overestimate
delta := div(sub(root, lhs), n)
root := sub(root, delta)
}
}
// @HACK(hysz) - ganache core workaround (issue #430)
function exp2(b, p) -> z {
z := b
for {p := sub(p, 1)}
gt(p, 0)
{p := sub(p, 1)}
{
z := mul(z, b)
}
}
}
}
/// @dev Computes the nth root of a fixed point value.
/// @param base to compute the root.
/// @param n nth root.
/// @return nth root of base.
function _nthRootFixedPoint(
uint256 base,
uint256 n
)
internal
pure
returns (uint256 root)
{
uint256 scalar = 10**18;
uint256 numerator = _nthRoot(base, n);
uint256 denominator = _nthRoot(scalar, n);
root = (scalar * numerator) / denominator;
}
/// @dev Computes the nth root of a fixed point value, where the
/// number of decimal places is known before hand (hardcoded above).
/// @param base to compute the root.
/// @return nth root of base.
function _nthRootFixedPointFixedN(
uint256 base
)
internal
pure
returns (uint256 root)
{
uint256 numerator = _nthRoot(base, COBB_DOUGLAS_ALPHA_DENOMINATOR);
root = (TOKEN_MULTIPLIER * numerator) / NTH_ROOT_OF_TOKEN_MULTIPLIER;
return root;
}
/// @dev Computes an exponent of a value in the form (ab)/c, minimizing loss of precision.
/// @param numerator of fraction
/// @param scalar to be multiplied by the numerator
/// @param denominator of fraction
/// @param power to raise value to
/// @return Exponent of input value.
function _exp(uint256 numerator, uint256 scalar, uint256 denominator, uint256 power)
internal
pure
returns (uint256 result)
{
result = (numerator * scalar) / denominator;
for (power = power - 1; power > 0; power -= 1) {
result = (result * numerator) / denominator;
}
return result;
}
/// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch.
/// Note that in this function there is no limitation on alpha; we tend to get better rounding
/// on the simplified versions below.
/// @param totalRewards collected over an epoch.
/// @param ownerFees Fees attributed to the owner of the staking pool.
/// @param totalFees collected across all active staking pools in the epoch.
/// @param ownerStake Stake attributed to the owner of the staking pool.
/// @param totalStake collected across all active staking pools in the epoch.
/// @param alphaNumerator Numerator of `alpha` in the cobb-dougles function.
/// @param alphaDenominator Denominator of `alpha` in the cobb-douglas function.
/// @return Result of computing the cobb-douglas function.
function _cobbDouglas(
uint256 totalRewards,
uint256 ownerFees,
uint256 totalFees,
uint256 ownerStake,
uint256 totalStake,
uint8 alphaNumerator,
uint8 alphaDenominator
)
internal
pure
returns (uint256)
{
return _exp(_nthRootFixedPoint(ownerFees * totalStake, alphaDenominator),
((totalRewards * ownerStake) / totalStake),
_nthRootFixedPoint(totalFees * ownerStake, alphaDenominator),
alphaNumerator
);
}
/// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch.
/// Note - we assume that alpha = 1/x
/// @param totalRewards collected over an epoch.
/// @param ownerFees Fees attributed to the owner of the staking pool.
/// @param totalFees collected across all active staking pools in the epoch.
/// @param ownerStake Stake attributed to the owner of the staking pool.
/// @param totalStake collected across all active staking pools in the epoch.
/// @param alphaDenominator Denominator of `alpha` in the cobb-douglas function.
/// @return Result of computing the cobb-douglas function.
function _cobbDouglasSimplified(
uint256 totalRewards,
uint256 ownerFees,
uint256 totalFees,
uint256 ownerStake,
uint256 totalStake,
uint8 alphaDenominator
)
internal
pure
returns (uint256)
{
return (_nthRootFixedPoint(ownerFees * totalStake, alphaDenominator) * totalRewards * ownerStake) /
(_nthRootFixedPoint(totalFees * ownerStake, alphaDenominator) * totalStake);
}
/// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch.
/// Note - we assume that (1 - alpha) = 1/x
/// @param totalRewards collected over an epoch.
/// @param ownerFees Fees attributed to the owner of the staking pool.
/// @param totalFees collected across all active staking pools in the epoch.
/// @param ownerStake Stake attributed to the owner of the staking pool.
/// @param totalStake collected across all active staking pools in the epoch.
/// @param alphaDenominator Denominator of `alpha` in the cobb-douglas function.
/// @return Result of computing the cobb-douglas function.
function _cobbDouglasSimplifiedInverse(
uint256 totalRewards,
uint256 ownerFees,
uint256 totalFees,
uint256 ownerStake,
uint256 totalStake,
uint8 alphaDenominator
)
internal
pure
returns (uint256)
{
return (_nthRootFixedPoint(ownerStake * totalFees, alphaDenominator) * totalRewards * ownerFees) /
(_nthRootFixedPoint(totalStake * ownerFees, alphaDenominator) * totalFees);
}
/// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch.
/// Note - we assume that alpha = 1/x, where x is defined by `COBB_DOUGLAS_ALPHA_DENOMINATOR`
/// @param totalRewards collected over an epoch.
/// @param ownerFees Fees attributed to the owner of the staking pool.
/// @param totalFees collected across all active staking pools in the epoch.
/// @param ownerStake Stake attributed to the owner of the staking pool.
/// @param totalStake collected across all active staking pools in the epoch.
/// @return Result of computing the cobb-douglas function.
function _cobbDouglasSuperSimplified(
uint256 totalRewards,
uint256 ownerFees,
uint256 totalFees,
uint256 ownerStake,
uint256 totalStake
)
internal
pure
returns (uint256)
{
return (_nthRootFixedPointFixedN(ownerFees * totalStake) * totalRewards * ownerStake) /
(_nthRootFixedPointFixedN(totalFees * ownerStake) * totalStake);
}
/// @dev The cobb-douglas function used to compute fee-based rewards for staking pools in a given epoch.
/// Note - we assume that (1 - alpha) = 1/x, where x is defined by `COBB_DOUGLAS_ALPHA_DENOMINATOR`
/// @param totalRewards collected over an epoch.
/// @param ownerFees Fees attributed to the owner of the staking pool.
/// @param totalFees collected across all active staking pools in the epoch.
/// @param ownerStake Stake attributed to the owner of the staking pool.
/// @param totalStake collected across all active staking pools in the epoch.
/// @return Result of computing the cobb-douglas function.
function _cobbDouglasSuperSimplifiedInverse(
uint256 totalRewards,
uint256 ownerFees,
uint256 totalFees,
uint256 ownerStake,
uint256 totalStake
)
internal
pure
returns (uint256)
{
return (_nthRootFixedPointFixedN(ownerStake * totalFees) * totalRewards * ownerFees) /
(_nthRootFixedPointFixedN(totalStake * ownerFees) * totalFees);
}
}

View File

@@ -0,0 +1,127 @@
/*
Copyright 2018 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;
import "./LibSafeMath.sol";
/// @dev This library contains logic for computing the reward balances of staking pool members.
/// *** READ MixinStakingPoolRewards BEFORE CONTINUING ***
library LibRewardMath {
using LibSafeMath for uint256;
/// @dev Computes a member's payout denominated in the real asset (ETH).
/// Use this function when a member is liquidating their position in the pool (undelegating all their stake);
/// their shadow balance must be reset to zero so there is no need to compute it here.
/// @param amountDelegatedByOwner Amount of Stake delegated by the member to the staking pool.
/// @param totalAmountDelegated Total amount of Stake delegated by all members of the staking pool.
/// @param amountOfShadowAssetHeldByOwner The shadow balance of the member.
/// @param totalAmountOfShadowAsset The sum total of shadow balances across all members of the pool.
/// @param totalAmountOfRealAsset The total amount of ETH shared by members of the pool.
function _computePayoutDenominatedInRealAsset(
uint256 amountDelegatedByOwner,
uint256 totalAmountDelegated,
uint256 amountOfShadowAssetHeldByOwner,
uint256 totalAmountOfShadowAsset,
uint256 totalAmountOfRealAsset
)
internal
pure
returns (uint256)
{
uint256 combinedPayout = amountDelegatedByOwner
._mul(totalAmountOfShadowAsset._add(totalAmountOfRealAsset))
._div(totalAmountDelegated);
// we round up the amount of shadow assets when computing buy-ins.
// the result is that sometimes the amount of actual assets in the pool
// is less than the shadow eth. in this case, we'll end up with a floating imbalance.
uint256 payoutInRealAsset = combinedPayout < amountOfShadowAssetHeldByOwner ?
0 :
combinedPayout - amountOfShadowAssetHeldByOwner;
return payoutInRealAsset;
}
/// @dev Computes a member's payout denominated in the real asset (ETH).
/// Use this function when a member is undelegating a portion (but not all) of their stake.
/// @param partialAmountDelegatedByOwner Amount of Stake being undelegated by the member to the staking pool.
/// @param amountDelegatedByOwner Amount of Stake delegated by the member to the staking pool.
/// This includes `partialAmountDelegatedByOwner`.
/// @param totalAmountDelegated Total amount of Stake delegated by all members of the staking pool.
/// @param amountOfShadowAssetHeldByOwner The shadow balance of the member.
/// @param totalAmountOfShadowAsset The sum total of shadow balances across all members of the pool.
/// @param totalAmountOfRealAsset The total amount of ETH shared by members of the pool.
function _computePartialPayout(
uint256 partialAmountDelegatedByOwner,
uint256 amountDelegatedByOwner,
uint256 totalAmountDelegated,
uint256 amountOfShadowAssetHeldByOwner,
uint256 totalAmountOfShadowAsset,
uint256 totalAmountOfRealAsset
)
internal
pure
returns (
uint256 payoutInRealAsset,
uint256 payoutInShadowAsset
)
{
payoutInShadowAsset = amountOfShadowAssetHeldByOwner
._mul(partialAmountDelegatedByOwner)
._div(amountDelegatedByOwner);
payoutInRealAsset = _computePayoutDenominatedInRealAsset(
partialAmountDelegatedByOwner,
totalAmountDelegated,
payoutInShadowAsset,
totalAmountOfShadowAsset,
totalAmountOfRealAsset
);
return (payoutInRealAsset, payoutInShadowAsset);
}
/// @dev Computes how much shadow asset to mint a member who wants to
/// join (or delegate more stake to) a staking pool.
/// See MixinStakingPoolRewards for more information on shadow assets.
/// @param amountToDelegateByOwner Amount of Stake the new member would delegate.
/// @param totalAmountDelegated Total amount currently delegated to the pool.
/// This does *not* include `amountToDelegateByOwner`.
/// @param totalAmountOfShadowAsset The sum total of shadow balances across all members of the pool.
/// @param totalAmountOfRealAsset The total amount of ETH shared by members of the pool.
function _computeBuyInDenominatedInShadowAsset(
uint256 amountToDelegateByOwner,
uint256 totalAmountDelegated,
uint256 totalAmountOfShadowAsset,
uint256 totalAmountOfRealAsset
)
internal
pure
returns (uint256)
{
if (totalAmountDelegated == 0) {
return 0;
}
return amountToDelegateByOwner
._mul(totalAmountOfShadowAsset._add(totalAmountOfRealAsset))
._add(totalAmountDelegated._sub(1)) // we round up when computing shadow asset
._div(totalAmountDelegated);
}
}

View File

@@ -0,0 +1,96 @@
/*
Copyright 2018 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;
library LibSafeMath {
uint256 constant internal MAX_UINT_96 = 79228162514264337593543950335; // 2**96-1
uint256 constant internal MAX_UINT_64 = 18446744073709551615; // 2**64-1
/// @dev Returns the addition of two unsigned integers, reverting on overflow.
/// Note that this reverts on overflow.
function _add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "OVERFLOW");
return c;
}
/// @dev Returns the subtraction of two unsigned integers.
/// Note that this reverts on underflow.
function _sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "UNDEROVERFLOW");
uint256 c = a - b;
return c;
}
/// @dev Returns the multiplication of two unsigned integers, reverting on overflow.
/// Note that this reverts on overflow.
function _mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
/// @dev Returns the integer division of two unsigned integers.
/// Note that this reverts on division by zero. The result is rounded towards zero.
function _div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0, "DIVISION_BY_ZERO");
uint256 c = a / b;
return c;
}
/// @dev Safely downcasts to a uint96
/// Note that this reverts if the input value is too large.
function _downcastToUint96(uint256 a)
internal
pure
returns (uint96)
{
require(
a <= MAX_UINT_96,
"VALUE_TOO_LARGE_TO_DOWNCAST_TO_UINT96"
);
return uint96(a);
}
/// @dev Safely downcasts to a uint64
/// Note that this reverts if the input value is too large.
function _downcastToUint64(uint256 a)
internal
pure
returns (uint64)
{
require(
a <= MAX_UINT_64,
"VALUE_TOO_LARGE_TO_DOWNCAST_TO_UINT64"
);
return uint64(a);
}
}

View File

@@ -0,0 +1,64 @@
/*
Copyright 2018 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;
library LibSafeMath64 {
/// @dev Returns the addition of two unsigned integers, reverting on overflow.
/// Note that this reverts on overflow.
function _add(uint64 a, uint64 b) internal pure returns (uint64) {
uint64 c = a + b;
require(c >= a, "OVERFLOW");
return c;
}
/// @dev Returns the subtraction of two unsigned integers.
/// Note that this reverts on underflow.
function _sub(uint64 a, uint64 b) internal pure returns (uint64) {
require(b <= a, "UNDEROVERFLOW");
uint64 c = a - b;
return c;
}
/// @dev Returns the multiplication of two unsigned integers, reverting on overflow.
/// Note that this reverts on overflow.
function _mul(uint64 a, uint64 b) internal pure returns (uint64) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint64 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
/// @dev Returns the integer division of two unsigned integers.
/// Note that this reverts on division by zero. The result is rounded towards zero.
function _div(uint64 a, uint64 b) internal pure returns (uint64) {
require(b > 0, "DIVISION_BY_ZERO");
uint64 c = a / b;
return c;
}
}

View File

@@ -0,0 +1,82 @@
/*
Copyright 2018 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;
library LibSafeMath96 {
/// @dev Returns the addition of two unsigned integers, reverting on overflow.
/// Note that this reverts on overflow.
function _add(uint96 a, uint96 b) internal pure returns (uint96) {
uint96 c = a + b;
require(c >= a, "OVERFLOW");
return c;
}
/// @dev Returns the subtraction of two unsigned integers.
/// Note that this reverts on underflow.
function _sub(uint96 a, uint96 b) internal pure returns (uint96) {
require(b <= a, "UNDEROVERFLOW");
uint96 c = a - b;
return c;
}
/// @dev Returns the multiplication of two unsigned integers, reverting on overflow.
/// Note that this reverts on overflow.
function _mul(uint96 a, uint96 b) internal pure returns (uint96) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint96 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
/// @dev Returns the integer division of two unsigned integers.
/// Note that this reverts on division by zero. The result is rounded towards zero.
function _div(uint96 a, uint96 b) internal pure returns (uint96) {
require(b > 0, "DIVISION_BY_ZERO");
uint96 c = a / b;
return c;
}
/// @dev Computes the percentage `numerator` is of `value`.
/// @param value The whole amount.
/// @param slice A slice of `value`.
/// @return The percentage `slice` is of `value`
function _computePercentageCeil(uint96 value, uint8 slice)
internal
pure
returns (uint96)
{
uint96 scaledNumerator = _mul(value, slice);
uint96 ceilScalar = uint96(99);
uint96 denominator = uint96(100);
return _div(
_add(scaledNumerator, ceilScalar),
denominator
);
}
}

View File

@@ -0,0 +1,181 @@
/*
Copyright 2018 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;
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "../interfaces/IStructs.sol";
import "../interfaces/IWallet.sol";
library LibSignatureValidator {
using LibBytes for bytes;
// bytes4(keccak256("isValidSignature(bytes,bytes)")
bytes4 constant internal EIP1271_MAGIC_VALUE = 0x20c13b0b;
/// @dev Verifies that a hash has been signed by the given signer.
/// @param hash Any 32 byte hash.
/// @param signerAddress Address that should have signed the given hash.
/// @param signature Proof that the hash has been signed by signer.
/// @return True if the address recovered from the provided signature matches the input signer address.
function _isValidSignature(
bytes32 hash,
address signerAddress,
bytes memory signature
)
internal
view
returns (bool isValid)
{
require(
signature.length > 0,
"LENGTH_GREATER_THAN_0_REQUIRED"
);
// Pop last byte off of signature byte array.
uint8 signatureTypeRaw = uint8(signature.popLastByte());
// Ensure signature is supported
require(
signatureTypeRaw < uint8(IStructs.SignatureType.NSignatureTypes),
"SIGNATURE_UNSUPPORTED"
);
IStructs.SignatureType signatureType = IStructs.SignatureType(signatureTypeRaw);
// Variables are not scoped in Solidity.
uint8 v;
bytes32 r;
bytes32 s;
address recovered;
// Always illegal signature.
// This is always an implicit option since a signer can create a
// signature array with invalid type or length. We may as well make
// it an explicit option. This aids testing and analysis. It is
// also the initialization value for the enum type.
if (signatureType == IStructs.SignatureType.Illegal) {
revert("SIGNATURE_ILLEGAL");
// Always invalid signature.
// Like Illegal, this is always implicitly available and therefore
// offered explicitly. It can be implicitly created by providing
// a correctly formatted but incorrect signature.
} else if (signatureType == IStructs.SignatureType.Invalid) {
require(
signature.length == 0,
"LENGTH_0_REQUIRED"
);
isValid = false;
return isValid;
// Signature using EIP712
} else if (signatureType == IStructs.SignatureType.EIP712) {
require(
signature.length == 65,
"LENGTH_65_REQUIRED"
);
v = uint8(signature[0]);
r = signature.readBytes32(1);
s = signature.readBytes32(33);
recovered = ecrecover(
hash,
v,
r,
s
);
isValid = signerAddress == recovered;
return isValid;
// Signed using web3.eth_sign
} else if (signatureType == IStructs.SignatureType.EthSign) {
require(
signature.length == 65,
"LENGTH_65_REQUIRED"
);
v = uint8(signature[0]);
r = signature.readBytes32(1);
s = signature.readBytes32(33);
recovered = ecrecover(
keccak256(abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
hash
)),
v,
r,
s
);
isValid = signerAddress == recovered;
return isValid;
// Signature verified by wallet contract.
// If used with an order, the maker of the order is the wallet contract.
} else if (signatureType == IStructs.SignatureType.Wallet) {
isValid = _isValidWalletSignature(
hash,
signerAddress,
signature
);
return isValid;
}
// Anything else is illegal (We do not return false because
// the signature may actually be valid, just not in a format
// that we currently support. In this case returning false
// may lead the caller to incorrectly believe that the
// signature was invalid.)
revert("SIGNATURE_UNSUPPORTED");
}
/// @dev Verifies signature using logic defined by Wallet contract.
/// @param hash Any 32 byte hash.
/// @param walletAddress Address that should have signed the given hash
/// and defines its own signature verification method.
/// @param signature Proof that the hash has been signed by signer.
/// @return True if signature is valid for given wallet..
function _isValidWalletSignature(
bytes32 hash,
address walletAddress,
bytes memory signature
)
internal
view
returns (bool isValid)
{
// contruct hash as bytes, so that it is a valid EIP-1271 payload
bytes memory hashAsBytes = new bytes(32);
assembly {
mstore(add(hashAsBytes, 32), hash)
}
// Static call `isValidSignature` in the destination wallet
bytes memory callData = abi.encodeWithSelector(
IWallet(walletAddress).isValidSignature.selector,
hash,
signature
);
(bool success, bytes memory result) = walletAddress.staticcall(callData);
// Sanity check call and extract the magic value
require(
success,
"WALLET_ERROR"
);
bytes4 magicValue = result.readBytes4(0);
isValid = (magicValue == EIP1271_MAGIC_VALUE);
return isValid;
}
}

View File

@@ -0,0 +1,168 @@
/*
Copyright 2018 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;
import "../libs/LibSafeMath.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../interfaces/IStakingEvents.sol";
import "./MixinZrxVault.sol";
import "../staking_pools/MixinStakingPoolRewardVault.sol";
import "../sys/MixinScheduler.sol";
import "./MixinStakeBalances.sol";
import "./MixinTimeLockedStake.sol";
import "./MixinStake.sol";
import "../staking_pools/MixinStakingPoolRewards.sol";
/// @dev This mixin contains logic for managing delegated stake.
/// **** Read MixinStake before continuing ****
/// Stake can be delegated to staking pools in order to trustlessly
/// leverage the weight of several stakers. The meaning of this
/// leverage depends on the context in which stake the is being utilized.
/// For example, the amount of fee-based rewards a market maker receives
/// is correlated to how much stake has been delegated to their pool (see MixinExchangeFees).
contract MixinDelegatedStake is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinOwnable,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinZrxVault,
MixinStakingPool,
MixinTimeLockedStake,
MixinStakeBalances,
MixinStake,
MixinStakingPoolRewards
{
using LibSafeMath for uint256;
/// @dev Deposit Zrx and mint stake in the "Activated & Delegated" state.
/// Note that the sender must be payable, as they may receive rewards in ETH from their staking pool.
/// @param poolId Unique Id of staking pool to delegate stake to.
/// @param amount of Zrx to deposit / Stake to mint.
function depositZrxAndDelegateToStakingPool(bytes32 poolId, uint256 amount)
external
{
address payable owner = msg.sender;
_mintStake(owner, amount);
activateStake(amount);
_delegateStake(owner, poolId, amount);
}
/// @dev Activates stake that is presently in the Deactivated & Withdrawable state.
/// Note that the sender must be payable, as they may receive rewards in ETH from their staking pool.
/// The newly activated stake is then delegated to a staking pool.
/// @param poolId Unique Id of staking pool to delegate stake to.
/// @param amount of Stake to activate & delegate.
function activateAndDelegateStake(
bytes32 poolId,
uint256 amount
)
public
{
activateStake(amount);
address payable owner = msg.sender;
_delegateStake(owner, poolId, amount);
}
/// @dev Deactivate & TimeLock stake that is currently in the Activated & Delegated state.
/// Note that the sender must be payable, as they may receive rewards in ETH from their staking pool.
/// @param poolId Unique Id of staking pool that the Stake is currently delegated to.
/// @param amount of Stake to deactivate and timeLock.
function deactivateAndTimeLockDelegatedStake(bytes32 poolId, uint256 amount)
public
{
deactivateAndTimeLockStake(amount);
address payable owner = msg.sender;
_undelegateStake(owner, poolId, amount);
}
/// @dev Delegates stake from `owner` to the staking pool with id `poolId`
/// @param owner of Stake
/// @param poolId Unique Id of staking pool to delegate stake to.
/// @param amount of Stake to delegate.
function _delegateStake(
address payable owner,
bytes32 poolId,
uint256 amount
)
private
{
// take snapshot of parameters before any computation
uint256 _delegatedStakeByOwner = delegatedStakeByOwner[owner];
uint256 _delegatedStakeToPoolByOwner = delegatedStakeToPoolByOwner[owner][poolId];
uint256 _delegatedStakeByPoolId = delegatedStakeByPoolId[poolId];
// join staking pool
_joinStakingPool(
poolId,
owner,
amount,
_delegatedStakeByPoolId
);
// increment how much stake the owner has delegated
delegatedStakeByOwner[owner] = _delegatedStakeByOwner._add(amount);
// increment how much stake the owner has delegated to the input pool
delegatedStakeToPoolByOwner[owner][poolId] = _delegatedStakeToPoolByOwner._add(amount);
// increment how much stake has been delegated to pool
delegatedStakeByPoolId[poolId] = _delegatedStakeByPoolId._add(amount);
}
/// @dev Undelegates stake of `owner` from the staking pool with id `poolId`
/// @param owner of Stake
/// @param poolId Unique Id of staking pool to undelegate stake from.
/// @param amount of Stake to undelegate.
function _undelegateStake(
address payable owner,
bytes32 poolId,
uint256 amount
)
private
{
// take snapshot of parameters before any computation
uint256 _delegatedStakeByOwner = delegatedStakeByOwner[owner];
uint256 _delegatedStakeToPoolByOwner = delegatedStakeToPoolByOwner[owner][poolId];
uint256 _delegatedStakeByPoolId = delegatedStakeByPoolId[poolId];
// leave the staking pool
_leaveStakingPool(
poolId,
owner,
amount,
_delegatedStakeToPoolByOwner,
_delegatedStakeByPoolId
);
// decrement how much stake the owner has delegated
delegatedStakeByOwner[owner] = _delegatedStakeByOwner._sub(amount);
// decrement how much stake the owner has delegated to the input pool
delegatedStakeToPoolByOwner[owner][poolId] = _delegatedStakeToPoolByOwner._sub(amount);
// decrement how much stake has been delegated to pool
delegatedStakeByPoolId[poolId] = _delegatedStakeByPoolId._sub(amount);
}
}

View File

@@ -0,0 +1,195 @@
/*
Copyright 2018 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;
import "../libs/LibSafeMath.sol";
import "../libs/LibRewardMath.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../interfaces/IStakingEvents.sol";
import "./MixinZrxVault.sol";
import "../staking_pools/MixinStakingPoolRewardVault.sol";
import "../sys/MixinScheduler.sol";
import "./MixinStakeBalances.sol";
import "./MixinTimeLockedStake.sol";
/// @dev This mixin contains logic for managing ZRX tokens and Stake.
/// Stake is minted when ZRX is deposited and burned when ZRX is withdrawn.
/// Stake can exist in one of many states:
/// 1. Activated
/// 2. Activated & Delegated
/// 3. Deactivated & TimeLocked
/// 4. Deactivated & Withdrawable
///
/// -- State Definitions --
/// Activated Stake
/// Stake in this state can be used as a utility within the 0x ecosystem.
/// For example, it carries weight when computing fee-based rewards (see MixinExchangeFees).
/// In the future, it may be used to participate in the 0x governance system.
///
/// Activated & Delegated Stake
/// Stake in this state also serves as a utility that is shared between the delegator and delegate.
/// For example, if delegated to a staking pool then it carries weight when computing fee-based rewards for
/// the staking pool; however, in this case, delegated stake carries less weight that regular stake (see MixinStakingPool).
///
/// Deactivated & TimeLocked Stake
/// Stake in this state cannot be used as a utility within the 0x ecosystem.
/// Stake is timeLocked when it moves out of activated states (Activated / Activated & Delagated).
/// By limiting the portability of stake, we mitigate undesirable behavior such as switching staking pools
/// in the middle of an epoch.
///
/// Deactivated & Withdrawable
/// Stake in this state cannot be used as a utility with in the 0x ecosystem.
/// This stake can, however, be burned and withdrawn as Zrx tokens.
/// ----------------------------
///
/// -- Valid State Transtions --
/// Activated -> Deactivated & TimeLocked
///
/// Activated & Delegated -> Deactivated & TimeLocked
///
/// Deactivated & TimeLocked -> Deactivated & Withdrawable
///
/// Deactivated & Withdrawable -> Activated
/// Deactivated & Withdrawable -> Activated & Delegated
/// Deactivated & Withdrawable -> Deactivated & Withdrawable
/// ----------------------------
///
/// Freshly minted stake is in the "Deactvated & Withdrawable" State, so it can
/// either be activated, delegated or withdrawn.
/// See MixinDelegatedStake and MixinTimeLockedStake for more on respective state transitions.
contract MixinStake is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinOwnable,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinZrxVault,
MixinTimeLockedStake,
MixinStakeBalances
{
using LibSafeMath for uint256;
/// @dev Deposit Zrx. This mints stake for the sender that is in the "Deactivated & Withdrawable" state.
/// @param amount of Zrx to deposit / Stake to mint.
function depositZrxAndMintDeactivatedStake(uint256 amount)
external
{
_mintStake(msg.sender, amount);
}
/// @dev Deposit Zrx and mint stake in the activated stake.
/// This is a convenience function, and can be used in-place of
/// calling `depositZrxAndMintDeactivatedStake` and `activateStake`.
/// This mints stake for the sender that is in the "Activated" state.
/// @param amount of Zrx to deposit / Stake to mint.
function depositZrxAndMintActivatedStake(uint256 amount)
external
{
_mintStake(msg.sender, amount);
activateStake(amount);
}
/// @dev Burns deactivated stake and withdraws the corresponding amount of Zrx.
/// @param amount of Stake to burn / Zrx to withdraw
function burnDeactivatedStakeAndWithdrawZrx(uint256 amount)
external
{
address owner = msg.sender;
_syncTimeLockedStake(owner);
require(
amount <= getDeactivatedStake(owner),
"INSUFFICIENT_BALANCE"
);
_burnStake(owner, amount);
}
/// @dev Activates stake that is presently in the Deactivated & Withdrawable state.
/// @param amount of Stake to activate.
function activateStake(uint256 amount)
public
{
address owner = msg.sender;
_syncTimeLockedStake(owner);
require(
amount <= getActivatableStake(owner),
"INSUFFICIENT_BALANCE"
);
activatedStakeByOwner[owner] = activatedStakeByOwner[owner]._add(amount);
totalActivatedStake = totalActivatedStake._add(amount);
}
/// @dev Deactivate & TimeLock stake that is currently in the Activated state.
/// @param amount of Stake to deactivate and timeLock.
function deactivateAndTimeLockStake(uint256 amount)
public
{
address owner = msg.sender;
_syncTimeLockedStake(owner);
require(
amount <= getActivatedStake(owner),
"INSUFFICIENT_BALANCE"
);
activatedStakeByOwner[owner] = activatedStakeByOwner[owner]._sub(amount);
totalActivatedStake = totalActivatedStake._sub(amount);
_timeLockStake(owner, amount);
}
/// @dev Mints Stake in the Deactivated & Withdrawable state.
/// @param owner to mint Stake for.
/// @param amount of Stake to mint.
function _mintStake(address owner, uint256 amount)
internal
{
// deposit equivalent amount of ZRX into vault
_depositFromOwnerIntoZrxVault(owner, amount);
// mint stake
stakeByOwner[owner] = stakeByOwner[owner]._add(amount);
// emit stake event
emit StakeMinted(
owner,
amount
);
}
/// @dev Burns Stake in the Deactivated & Withdrawable state.
/// @param owner to mint Stake for.
/// @param amount of Stake to mint.
function _burnStake(address owner, uint256 amount)
internal
{
// burn stake
stakeByOwner[owner] = stakeByOwner[owner]._sub(amount);
// withdraw equivalent amount of ZRX from vault
_withdrawToOwnerFromZrxVault(owner, amount);
// emit stake event
emit StakeBurned(
owner,
amount
);
}
}

View File

@@ -0,0 +1,189 @@
/*
Copyright 2018 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 "../libs/LibSafeMath.sol";
import "../interfaces/IStructs.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../sys/MixinScheduler.sol";
import "./MixinTimeLockedStake.sol";
/// @dev This mixin contains logic for querying stake balances.
/// **** Read MixinStake before continuing ****
contract MixinStakeBalances is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinScheduler,
MixinTimeLockedStake
{
using LibSafeMath for uint256;
/// @dev Returns the total activated stake across all owners.
/// This stake is in the "Activated" OR "Activated & Delegated" states.
/// @return Total active stake.
function getActivatedStakeAcrossAllOwners()
public
view
returns (uint256)
{
return totalActivatedStake;
}
/// @dev Returns the total stake for a given owner.
/// This stake can be in any state.
/// @param owner to query.
/// @return Total active stake for owner.
function getTotalStake(address owner)
public
view
returns (uint256)
{
return stakeByOwner[owner];
}
/// @dev Returns the activated stake for a given owner.
/// This stake is in the "Activated" OR "Activated & Delegated" states.
/// @param owner to query.
/// @return Activated stake for owner.
function getActivatedStake(address owner)
public
view
returns (uint256)
{
return activatedStakeByOwner[owner];
}
/// @dev Returns the deactivated stake for a given owner.
/// This stake is in the "Deactivated & TimeLocked" OR "Deactivated & Withdrawable" states.
/// @param owner to query.
/// @return Deactivated stake for owner.
function getDeactivatedStake(address owner)
public
view
returns (uint256)
{
return getTotalStake(owner)._sub(getActivatedStake(owner));
}
/// @dev Returns the activated & undelegated stake for a given owner.
/// This stake is in the "Activated" state.
/// @param owner to query.
/// @return Activated stake for owner.
function getActivatedAndUndelegatedStake(address owner)
public
view
returns (uint256)
{
return activatedStakeByOwner[owner]._sub(getStakeDelegatedByOwner(owner));
}
/// @dev Returns the stake that can be activated for a given owner.
/// This stake is in the "Deactivated & Withdrawable" state.
/// @param owner to query.
/// @return Activatable stake for owner.
function getActivatableStake(address owner)
public
view
returns (uint256)
{
return getDeactivatedStake(owner)._sub(getTimeLockedStake(owner));
}
/// @dev Returns the stake that can be withdrawn for a given owner.
/// This stake is in the "Deactivated & Withdrawable" state.
/// @param owner to query.
/// @return Withdrawable stake for owner.
function getWithdrawableStake(address owner)
public
view
returns (uint256)
{
return getActivatableStake(owner);
}
/// @dev Returns the stake delegated by a given owner.
/// This stake is in the "Activated & Delegated" state.
/// @param owner to query.
/// @return Delegated stake for owner.
function getStakeDelegatedByOwner(address owner)
public
view
returns (uint256)
{
return delegatedStakeByOwner[owner];
}
/// @dev Returns the stake delegated to a specific staking pool, by a given owner.
/// This stake is in the "Activated & Delegated" state.
/// @param owner to query.
/// @param poolId Unique Id of pool.
/// @return Stake delegaated to pool by owner.
function getStakeDelegatedToPoolByOwner(address owner, bytes32 poolId)
public
view
returns (uint256)
{
return delegatedStakeToPoolByOwner[owner][poolId];
}
/// @dev Returns the total stake delegated to a specific staking pool, across all members.
/// This stake is in the "Activated & Delegated" state.
/// @param poolId Unique Id of pool.
/// @return Total stake delegaated to pool.
function getTotalStakeDelegatedToPool(bytes32 poolId)
public
view
returns (uint256)
{
return delegatedStakeByPoolId[poolId];
}
/// @dev Returns the timeLocked stake for a given owner.
/// This stake is in the "Deactivated & TimeLocked" state.
/// @param owner to query.
/// @return TimeLocked stake for owner.
function getTimeLockedStake(address owner)
public
view
returns (uint256)
{
(IStructs.TimeLock memory timeLock,) = _getSynchronizedTimeLock(owner);
return timeLock.total;
}
/// @dev Returns the starting TimeLock Period of timeLocked state for a given owner.
/// This stake is in the "Deactivated & TimeLocked" state.
/// See MixinScheduling and MixinTimeLock.
/// @param owner to query.
/// @return Start of timeLock for owner's timeLocked stake.
function getTimeLockStart(address owner)
public
view
returns (uint256)
{
(IStructs.TimeLock memory timeLock,) = _getSynchronizedTimeLock(owner);
return timeLock.lockedAt;
}
}

View File

@@ -0,0 +1,148 @@
/*
Copyright 2018 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;
import "../libs/LibSafeMath.sol";
import "../libs/LibRewardMath.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../interfaces/IStakingEvents.sol";
import "../sys/MixinScheduler.sol";
/// @dev This mixin contains logic for timeLocking stake.
/// **** Read MixinStake before continuing ****
/// When stake moves from an Activated state it must first go to
/// the Deactivated & TimeLocked state. The stake will be timeLocked
/// for a period of time, called a TimeLock Period, which is measured in epochs.
/// (see MixinScheduler).
/// Stake remains timeLocked for at least one full TimeLock Period; so,
/// if your stake is locked sometime during TimeLock Period #1 then it will
/// be un-TimeLocked at TimeLock Period #3.
/// Note that no action is required by the user to un-TimeLock their stake, and
/// when stake is un-TimeLocked it is moved to the state Deactivated & Withdrawable.
/// (see MixinStake).
///
/// -- The TimeLocking Data Structure --
/// Three fields are used to represent a timeLock:
/// 1. Total timeLocked stake (called `total`)
/// 2. TimeLocked stake pending removal of timeLock, on next TimeLock Period (called `pending`)
/// 3. The most recent TimeLock Period in which stake was timeLocked. (called `lockedAt`)
///
/// Each user has exactly one instance of this timeLock struct, which manages all of
/// their timeLocked stake. This data structure is defined in `IStructs.TimeLock`.
/// This data structure was designed to fit into one word of storage, as a gas optimization.
/// Its fields are updated only when a user interacts with their stake.
/// ------------------------------------
///
/// -- TimeLocking Example --
/// In the example below, the user executes a series of actions on their stake (`Action`) during `TimeLock Period` N.
/// The fields of the user's timeLocked struct (`lockedAt`, `total`, `pending`) are illustrated exactly as
/// they would be represented in storage.
/// The field `un-TimeLocked` is the amount of un-TimeLocked stake, as represented *in storage*; however, because
/// state is only updated when the user interacts with their stake, this field may lag.
/// The field `un-TimeLocked (virtual)` is the true amount of un-TimeLocked stake, as represented in the system;
/// the value in this field represents stake that has moved from the state
/// "Deactivated & TimeLocke" to "Deactivated & Withdrawable" (see MixinStake).
///
/// | Action | TimeLock Period | lockedAt | total | pending | un-TimeLocked | un-TimeLocked (virtual) |
/// | | 0 | 0 | 0 | 0 | 0 | 0 |
/// | lock(5) | 1 | 1 | 5 | 0 | 0 | 0 |
/// | | 2 | 1 | 5 | 0 | 0 | 0 |
/// | lock(10) | 2 | 2 | 15 | 5 | 0 | 0 |
/// | | 3 | 2 | 15 | 5 | 0 | 5 |
/// | lock(15) | 3 | 3 | 30 | 15 | 5 | 5 |
/// | | 4 | 3 | 30 | 15 | 5 | 15 |
/// | | 5 | 3 | 30 | 15 | 5 | 30 |
/// | lock(0) | 5 | 5 | 30 | 30 | 30 | 30 |
/// | lock(20) | 6 | 6 | 50 | 30 | 30 | 30 |
/// | unlock(30) | 6 | 6 | 20 | 0 | 0 | 0 |
/// | | 7 | 6 | 20 | 0 | 0 | 0 |
/// | | 8 | 6 | 20 | 0 | 0 | 20 |
/// -------------------------------------------------------------------------------------------------------------
contract MixinTimeLockedStake is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinScheduler
{
using LibSafeMath for uint256;
/// @dev Forces the timeLock data structure to sync to state.
/// This is not necessary but may optimize some subsequent calls.
/// @param owner of Stake.
function forceTimeLockSync(address owner)
external
{
_syncTimeLockedStake(owner);
}
/// @dev TimeLocks Stake
/// This moves state into the Deactivated & TimeLocked state.
/// @param owner of Stake.
/// @param amount of Stake to timeLock.
function _timeLockStake(address owner, uint256 amount)
internal
{
(IStructs.TimeLock memory ownerTimeLock,) = _getSynchronizedTimeLock(owner);
uint96 downcastAmount = amount._downcastToUint96();
ownerTimeLock.total += downcastAmount;
timeLockedStakeByOwner[owner] = ownerTimeLock;
}
/// @dev Updates storage to reflect the most up-to-date timeLock data structure for a given owner.
/// @param owner of Stake.
function _syncTimeLockedStake(address owner)
internal
{
(IStructs.TimeLock memory ownerTimeLock, bool isOutOfSync) = _getSynchronizedTimeLock(owner);
if (!isOutOfSync) {
return;
}
timeLockedStakeByOwner[owner] = ownerTimeLock;
}
/// @dev Returns the most up-to-date timeLock data structure for a given owner.
/// @param owner of Stake.
function _getSynchronizedTimeLock(address owner)
internal
view
returns (
IStructs.TimeLock memory ownerTimeLock,
bool isOutOfSync
)
{
uint64 currentTimeLockPeriod = getCurrentTimeLockPeriod();
ownerTimeLock = timeLockedStakeByOwner[owner];
isOutOfSync = false;
if (currentTimeLockPeriod == ownerTimeLock.lockedAt._add(1)) {
// shift n periods
ownerTimeLock.pending = ownerTimeLock.total;
isOutOfSync = true;
} else if (currentTimeLockPeriod > ownerTimeLock.lockedAt) {
// TimeLock has expired - zero out
ownerTimeLock.lockedAt = 0;
ownerTimeLock.total = 0;
ownerTimeLock.pending = 0;
}
return (ownerTimeLock, isOutOfSync);
}
}

View File

@@ -0,0 +1,72 @@
/*
Copyright 2018 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;
import "../interfaces/IZrxVault.sol";
import "../immutable/MixinStorage.sol";
import "../sys/MixinOwnable.sol";
/// @dev This mixin contains logic for managing and interfacing with the Zrx Vault.
/// (see vaults/ZrxVault.sol).
contract MixinZrxVault is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinOwnable
{
/// @dev Set the Zrx Vault.
/// @param zrxVaultAddress Address of the Zrx Vault.
function setZrxVault(address zrxVaultAddress)
external
onlyOwner
{
zrxVault = IZrxVault(zrxVaultAddress);
}
/// @dev Return the current Zrx Vault
/// @return Zrx Vault
function getZrxVault()
public
view
returns (address)
{
return address(zrxVault);
}
/// @dev Deposits Zrx Tokens from the `owner` into the vault.
/// @param owner of Zrx Tokens
/// @param amount of tokens to deposit.
function _depositFromOwnerIntoZrxVault(address owner, uint256 amount)
internal
{
zrxVault.depositFrom(owner, amount);
}
/// @dev Withdraws Zrx Tokens from to `owner` from the vault.
/// @param owner of deposited Zrx Tokens
/// @param amount of tokens to withdraw.
function _withdrawToOwnerFromZrxVault(address owner, uint256 amount)
internal
{
zrxVault.withdrawFrom(owner, amount);
}
}

View File

@@ -0,0 +1,312 @@
/*
Copyright 2018 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 "../libs/LibSafeMath.sol";
import "../libs/LibSignatureValidator.sol";
import "../libs/LibEIP712Hash.sol";
import "../interfaces/IStructs.sol";
import "../interfaces/IStakingEvents.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "./MixinStakingPoolRewardVault.sol";
/// @dev This mixin contains logic for staking pools.
/// A pool has a single operator and any number of delegators (members).
/// Any staker can create a pool, although at present it is only beneficial
/// for market makers to create staking pools. A market maker *must* create a
/// pool in order to receive fee-based rewards at the end of each epoch (see MixinExchangeFees).
/// Moreover, creating a staking pool leverages the delegated stake within the pool,
/// which is counted towards a maker's total stake when computing rewards. A market maker
/// can register any number of makerAddresses with their pool, and can incentivize delegators
/// to join their pool by specifying a fixed percentage of their fee-based rewards to be split amonst
/// the members of their pool. Any rewards set aside for members of the pool is divided based on
/// how much stake each member delegated.
///
/// Terminology:
/// "Pool Id" - A unique id generated by this contract and assigned to each pool when it is created.
/// "Pool Operator" - The creator and operator of the pool.
/// "Pool Members" - Members of the pool who opted-in by delegating to the pool.
/// "Market Makers" - Market makers on the 0x protocol.
///
/// How-To for Market Makers:
/// 1. Create a pool, specifying what percentage of rewards kept for yourself.
/// The remaining is divided among members of your pool.
/// 2. Add the addresses that you use to market make on 0x.
/// 3. Leverage the staking power of others by convincing them to delegate to your pool.
contract MixinStakingPool is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinOwnable,
MixinStakingPoolRewardVault
{
using LibSafeMath for uint256;
/// @dev Asserts that the sender is the operator of the input pool.
/// @param poolId Pool sender must be operator of.
modifier onlyStakingPoolOperator(bytes32 poolId) {
require(
msg.sender == getStakingPoolOperator(poolId),
"ONLY_CALLABLE_BY_POOL_OPERATOR"
);
_;
}
/// @dev Asserts that the sender is the operator of the input pool or the input maker.
/// @param poolId Pool sender must be operator of.
/// @param makerAddress Address of a maker in the pool.
modifier onlyStakingPoolOperatorOrMaker(bytes32 poolId, address makerAddress) {
require(
msg.sender == getStakingPoolOperator(poolId) || msg.sender == makerAddress,
"ONLY_CALLABLE_BY_POOL_OPERATOR_OR_MAKER"
);
_;
}
/// @dev Create a new staking pool. The sender will be the operator of this pool.
/// Note that an operator must be payable.
/// @param operatorShare The percentage of any rewards owned by the operator.
/// @return poolId The unique pool id generated for this pool.
function createStakingPool(uint8 operatorShare)
external
returns (bytes32 poolId)
{
// note that an operator must be payable
address payable operatorAddress = msg.sender;
// assign pool id and generate next id
poolId = nextPoolId;
nextPoolId = _computeNextStakingPoolId(poolId);
// store metadata about this pool
IStructs.Pool memory pool = IStructs.Pool({
operatorAddress: operatorAddress,
operatorShare: operatorShare
});
poolById[poolId] = pool;
// register pool in reward vault
_registerStakingPoolInRewardVault(poolId, operatorShare);
// notify
emit StakingPoolCreated(poolId, operatorAddress, operatorShare);
return poolId;
}
/// @dev Adds a maker to a staking pool. Note that this is only callable by the pool operator.
/// @param poolId Unique id of pool.
/// @param makerAddress Address of maker.
/// @param makerSignature Signature proving that maker has agreed to join the pool.
function addMakerToStakingPool(
bytes32 poolId,
address makerAddress,
bytes calldata makerSignature
)
external
onlyStakingPoolOperator(poolId)
{
// sanity check - did maker agree to join this pool?
require(
isValidMakerSignature(poolId, makerAddress, makerSignature),
"INVALID_MAKER_SIGNATURE"
);
require(
!isMakerAssignedToStakingPool(makerAddress),
"MAKER_ADDRESS_ALREADY_REGISTERED"
);
poolIdByMakerAddress[makerAddress] = poolId;
makerAddressesByPoolId[poolId].push(makerAddress);
// notify
emit MakerAddedToStakingPool(
poolId,
makerAddress
);
}
/// @dev Adds a maker to a staking pool. Note that this is only callable by the pool operator or maker.
/// Note also that the maker does not have to *agree* to leave the pool; this action is
/// at the sole discretion of the pool operator.
/// @param poolId Unique id of pool.
/// @param makerAddress Address of maker.
function removeMakerFromStakingPool(
bytes32 poolId,
address makerAddress
)
external
onlyStakingPoolOperatorOrMaker(poolId, makerAddress)
{
require(
getStakingPoolIdOfMaker(makerAddress) == poolId,
"MAKER_ADDRESS_NOT_REGISTERED"
);
// load list of makers for the input pool.
address[] storage makerAddressesByPoolIdPtr = makerAddressesByPoolId[poolId];
uint256 makerAddressesByPoolIdLength = makerAddressesByPoolIdPtr.length;
// find index of maker to remove.
uint indexOfMakerAddress = 0;
for (; indexOfMakerAddress < makerAddressesByPoolIdLength; ++indexOfMakerAddress) {
if (makerAddressesByPoolIdPtr[indexOfMakerAddress] == makerAddress) {
break;
}
}
// remove the maker from the list of makers for this pool.
// (i) move maker at end of list to the slot occupied by the maker to remove, then
// (ii) zero out the slot at the end of the list and decrement the length.
uint256 indexOfLastMakerAddress = makerAddressesByPoolIdLength - 1;
if (indexOfMakerAddress != indexOfLastMakerAddress) {
makerAddressesByPoolIdPtr[indexOfMakerAddress] = makerAddressesByPoolIdPtr[indexOfLastMakerAddress];
}
makerAddressesByPoolIdPtr[indexOfLastMakerAddress] = NIL_ADDRESS;
makerAddressesByPoolIdPtr.length -= 1;
// reset the pool id assigned to the maker.
poolIdByMakerAddress[makerAddress] = NIL_MAKER_ID;
// notify
emit MakerRemovedFromStakingPool(
poolId,
makerAddress
);
}
/// @dev Returns true iff the input signature is valid; meaning that the maker agrees to
/// be added to the pool.
/// @param poolId Unique id of pool the maker wishes to join.
/// @param makerAddress Address of maker.
/// @param makerSignature Signature of the maker.
/// @return isValid True iff the maker agrees to be added to the pool.
function isValidMakerSignature(bytes32 poolId, address makerAddress, bytes memory makerSignature)
public
view
returns (bool isValid)
{
bytes32 approvalHash = getStakingPoolApprovalMessageHash(poolId, makerAddress);
isValid = LibSignatureValidator._isValidSignature(approvalHash, makerAddress, makerSignature);
return isValid;
}
/// @dev Returns the approval message hash - this is what a maker must sign in order to
/// be added to a pool.
/// @param poolId Unique id of pool the maker wishes to join.
/// @param makerAddress Address of maker.
/// @return approvalHash Hash of message the maker must sign.
function getStakingPoolApprovalMessageHash(bytes32 poolId, address makerAddress)
public
view
returns (bytes32 approvalHash)
{
IStructs.StakingPoolApproval memory approval = IStructs.StakingPoolApproval({
poolId: poolId,
makerAddress: makerAddress
});
// hash approval message and check signer address
address verifierAddress = address(this);
approvalHash = LibEIP712Hash._hashStakingPoolApprovalMessage(approval, CHAIN_ID, verifierAddress);
return approvalHash;
}
/// @dev Returns the pool id of an input maker.
function getStakingPoolIdOfMaker(address makerAddress)
public
view
returns (bytes32)
{
return poolIdByMakerAddress[makerAddress];
}
/// @dev Returns true iff the maker is assigned to a staking pool.
/// @param makerAddress Address of maker
/// @return True iff assigned.
function isMakerAssignedToStakingPool(address makerAddress)
public
view
returns (bool)
{
return getStakingPoolIdOfMaker(makerAddress) != NIL_MAKER_ID;
}
/// @dev Returns the makers for a given pool.
/// @param poolId Unique id of pool.
/// @return _makerAddressesByPoolId Makers for pool.
function getMakersForStakingPool(bytes32 poolId)
public
view
returns (address[] memory _makerAddressesByPoolId)
{
return makerAddressesByPoolId[poolId];
}
/// @dev Returns the unique id that will be assigned to the next pool that is created.
/// @return Pool id.
function getNextStakingPoolId()
public
view
returns (bytes32)
{
return nextPoolId;
}
/// @dev Returns the pool operator
/// @param poolId Unique id of pool
/// @return operatorAddress Operator of the pool
function getStakingPoolOperator(bytes32 poolId)
public
view
returns (address operatorAddress)
{
operatorAddress = poolById[poolId].operatorAddress;
return operatorAddress;
}
/// @dev Convenience function for loading information on a pool.
/// @param poolId Unique id of pool.
/// @return pool Pool info.
function _getStakingPool(bytes32 poolId)
internal
view
returns (IStructs.Pool memory pool)
{
pool = poolById[poolId];
return pool;
}
/// @dev Computes the unique id that comes after the input pool id.
/// @param poolId Unique id of pool.
/// @return Next pool id after input pool.
function _computeNextStakingPoolId(bytes32 poolId)
internal
pure
returns (bytes32)
{
return bytes32(uint256(poolId)._add(POOL_ID_INCREMENT_AMOUNT));
}
}

View File

@@ -0,0 +1,138 @@
/*
Copyright 2018 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;
import "../interfaces/IStakingEvents.sol";
import "../interfaces/IStakingPoolRewardVault.sol";
import "../immutable/MixinStorage.sol";
import "../sys/MixinOwnable.sol";
/// @dev This mixin contains logic for interfacing with the Staking Pool Reward Vault (vaults/StakingPoolRewardVault.sol)
/// Note that setters are callable only by the owner of this contract, and withdraw functionality is accessible only
/// from within this contract.
contract MixinStakingPoolRewardVault is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinOwnable
{
/// @dev Sets the address of the reward vault.
/// This can only be called by the owner of this contract.
function setStakingPoolRewardVault(address payable rewardVaultAddress)
external
onlyOwner
{
rewardVault = IStakingPoolRewardVault(rewardVaultAddress);
emit StakingPoolRewardVaultChanged(rewardVaultAddress);
}
/// @dev Returns the staking pool reward vault
/// @return Address of reward vault.
function getStakingPoolRewardVault()
public
view
returns (address)
{
return address(rewardVault);
}
/// @dev Returns the total balance in ETH of a staking pool, as recorded in the vault.
/// @param poolId Unique id of pool.
/// @return Balance.
function getTotalBalanceInStakingPoolRewardVault(bytes32 poolId)
public
view
returns (uint256)
{
return rewardVault.balanceOf(poolId);
}
/// @dev Returns the balance in ETH of the staking pool operator, as recorded in the vault.
/// @param poolId Unique id of pool.
/// @return Balance.
function getBalanceOfOperatorInStakingPoolRewardVault(bytes32 poolId)
public
view
returns (uint256)
{
return rewardVault.balanceOfOperator(poolId);
}
/// @dev Returns the balance in ETH co-owned by the members of a pool, as recorded in the vault.
/// @param poolId Unique id of pool.
/// @return Balance.
function getBalanceOfMembersInStakingPoolRewardVault(bytes32 poolId)
public
view
returns (uint256)
{
return rewardVault.balanceOfMembers(poolId);
}
/// @dev Registers a staking pool in the reward vault.
/// @param poolId Unique id of pool.
/// @param operatorShare The percentage of the rewards owned by the operator.
function _registerStakingPoolInRewardVault(bytes32 poolId, uint8 operatorShare)
internal
{
rewardVault.registerStakingPool(
poolId,
operatorShare
);
}
/// @dev Withdraws an amount in ETH of the reward for a pool operator.
/// @param poolId Unique id of pool.
/// @param amount The amount to withdraw.
function _withdrawFromOperatorInStakingPoolRewardVault(bytes32 poolId, uint256 amount)
internal
{
rewardVault.withdrawForOperator(poolId, amount);
}
/// @dev Withdraws an amount in ETH of the reward for a pool member.
/// @param poolId Unique id of pool.
/// @param amount The amount to withdraw.
function _withdrawFromMemberInStakingPoolRewardVault(bytes32 poolId, uint256 amount)
internal
{
rewardVault.withdrawForMember(poolId, amount);
}
/// @dev Deposits an amount in ETH into the reward vault.
/// @param amount The amount in ETH to deposit.
function _depositIntoStakingPoolRewardVault(uint256 amount)
internal
{
address payable rewardVaultAddress = address(uint160(address(rewardVault)));
rewardVaultAddress.transfer(amount);
}
/// @dev Records an amount deposited into the reward vault for a specific pool.
/// @param poolId Unique id of pool.
/// @param amount The amount in ETH to record.
function _recordDepositInStakingPoolRewardVault(bytes32 poolId, uint256 amount)
internal
{
rewardVault.recordDepositFor(poolId, amount);
}
}

View File

@@ -0,0 +1,310 @@
/*
Copyright 2018 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;
import "../libs/LibSafeMath.sol";
import "../libs/LibRewardMath.sol";
import "../immutable/MixinStorage.sol";
import "../immutable/MixinConstants.sol";
import "../stake/MixinStakeBalances.sol";
import "./MixinStakingPoolRewardVault.sol";
import "./MixinStakingPool.sol";
/// @dev This mixin contains logic for staking pool rewards.
/// Rewards for a pool are generated by their market makers trading on the 0x protocol (MixinStakingPool).
/// The operator of a pool receives a fixed percentage of all rewards; generally, the operator is the
/// sole market maker of a pool. The remaining rewards are divided among the members of a pool; each member
/// gets an amount proportional to how much stake they have delegated to the pool.
///
/// Note that members can freely join or leave a staking pool at any time, by delegating/undelegating their stake.
/// Moreover, there is no limit to how many members a pool can have. To limit the state-updates needed to track member balances,
/// we store only a single balance shared by all members. This state is updated every time a reward is paid to the pool - which
/// is currently at the end of each epoch. Additionally, each member has an associated "Shadow Balance" which is updated only
/// when a member delegates/undelegates stake to the pool, along with a "Total Shadow Balance" that represents the cumulative
/// Shadow Balances of all members in a pool.
///
/// -- Member Balances --
/// Terminology:
/// Real Balance - The reward balance in ETH of a member.
/// Total Real Balance - The sum total of reward balances in ETH across all members of a pool.
/// Shadow Balance - The realized reward balance of a member.
/// Total Shadow Balance - The sum total of realized reward balances across all members of a pool.
/// How it works:
/// 1. When a member delegates, their ownership of the pool increases; however, this new ownership applies
/// only to future rewards and must not change the rewards currently owned by other members. Thus, when a
/// member delegates stake, we *increase* their Shadow Balance and the Total Shadow Balance of the pool.
///
/// 2. When a member withdraws a portion of their reward, their realized balance increases but their ownership
/// within the pool remains unchanged. Thus, we simultaneously *decrease* their Real Balance and
/// *increase* their Shadow Balance by the amount withdrawn. The cumulative balance decrease and increase, respectively.
///
/// 3. When a member undelegates, the portion of their reward that corresponds to that stake is also withdrawn. Thus,
/// their realized balance *increases* while their ownership of the pool *decreases*. To reflect this, we
/// decrease their Shadow Balance, the Total Shadow Balance, their Real Balance, and the Total Real Balance.
contract MixinStakingPoolRewards is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage,
MixinOwnable,
MixinScheduler,
MixinStakingPoolRewardVault,
MixinStakingPool,
MixinTimeLockedStake,
MixinStakeBalances
{
using LibSafeMath for uint256;
/// @dev Withdraws an amount in ETH of the reward for the pool operator.
/// @param poolId Unique id of pool.
/// @param amount The amount to withdraw.
function withdrawRewardForStakingPoolOperator(bytes32 poolId, uint256 amount)
external
onlyStakingPoolOperator(poolId)
{
_withdrawFromOperatorInStakingPoolRewardVault(poolId, amount);
poolById[poolId].operatorAddress.transfer(amount);
}
/// @dev Withdraws the total balance in ETH of the reward for the pool operator.
/// @param poolId Unique id of pool.
/// @return The amount withdrawn.
function withdrawTotalRewardForStakingPoolOperator(bytes32 poolId)
external
onlyStakingPoolOperator(poolId)
returns (uint256)
{
uint256 amount = getBalanceOfOperatorInStakingPoolRewardVault(poolId);
_withdrawFromOperatorInStakingPoolRewardVault(poolId, amount);
poolById[poolId].operatorAddress.transfer(amount);
return amount;
}
/// @dev Withdraws an amount in ETH of the reward for a pool member.
/// @param poolId Unique id of pool.
/// @param amount The amount to withdraw.
function withdrawRewardForStakingPoolMember(bytes32 poolId, uint256 amount)
external
{
// sanity checks
address payable member = msg.sender;
uint256 memberBalance = computeRewardBalanceOfStakingPoolMember(poolId, member);
require(
amount <= memberBalance,
"INVALID_AMOUNT"
);
// update shadow rewards
shadowRewardsInPoolByOwner[member][poolId] = shadowRewardsInPoolByOwner[member][poolId]._add(amount);
shadowRewardsByPoolId[poolId] = shadowRewardsByPoolId[poolId]._add(amount);
// perform withdrawal
_withdrawFromMemberInStakingPoolRewardVault(poolId, amount);
member.transfer(amount);
}
/// @dev Withdraws the total balance in ETH of the reward for a pool member.
/// @param poolId Unique id of pool.
/// @return The amount withdrawn.
function withdrawTotalRewardForStakingPoolMember(bytes32 poolId)
external
returns (uint256)
{
// sanity checks
address payable member = msg.sender;
uint256 amount = computeRewardBalanceOfStakingPoolMember(poolId, member);
// update shadow rewards
shadowRewardsInPoolByOwner[member][poolId] = shadowRewardsInPoolByOwner[member][poolId]._add(amount);
shadowRewardsByPoolId[poolId] = shadowRewardsByPoolId[poolId]._add(amount);
// perform withdrawal and return amount withdrawn
_withdrawFromMemberInStakingPoolRewardVault(poolId, amount);
member.transfer(amount);
return amount;
}
/// @dev Returns the sum total reward balance in ETH of a staking pool, across all members and the pool operator.
/// @param poolId Unique id of pool.
/// @return Balance.
function getTotalRewardBalanceOfStakingPool(bytes32 poolId)
external
view
returns (uint256)
{
return getTotalBalanceInStakingPoolRewardVault(poolId);
}
/// @dev Returns the reward balance in ETH of the pool operator.
/// @param poolId Unique id of pool.
/// @return Balance.
function getRewardBalanceOfStakingPoolOperator(bytes32 poolId)
external
view
returns (uint256)
{
return getBalanceOfOperatorInStakingPoolRewardVault(poolId);
}
/// @dev Returns the reward balance in ETH co-owned by the members of a pool.
/// @param poolId Unique id of pool.
/// @return Balance.
function getRewardBalanceOfStakingPoolMembers(bytes32 poolId)
external
view
returns (uint256)
{
return getBalanceOfMembersInStakingPoolRewardVault(poolId);
}
/// @dev Returns the shadow balance of a specific member of a staking pool.
/// @param poolId Unique id of pool.
/// @param member The member of the pool.
/// @return Balance.
function getShadowBalanceOfStakingPoolMember(bytes32 poolId, address member)
public
view
returns (uint256)
{
return shadowRewardsInPoolByOwner[member][poolId];
}
/// @dev Returns the total shadow balance of a staking pool.
/// @param poolId Unique id of pool.
/// @return Balance.
function getTotalShadowBalanceOfStakingPool(bytes32 poolId)
public
view
returns (uint256)
{
return shadowRewardsByPoolId[poolId];
}
/// @dev Computes the reward balance in ETH of a specific member of a pool.
/// @param poolId Unique id of pool.
/// @param member The member of the pool.
/// @return Balance.
function computeRewardBalanceOfStakingPoolMember(bytes32 poolId, address member)
public
view
returns (uint256)
{
uint256 poolBalance = getBalanceOfMembersInStakingPoolRewardVault(poolId);
return LibRewardMath._computePayoutDenominatedInRealAsset(
delegatedStakeToPoolByOwner[member][poolId],
delegatedStakeByPoolId[poolId],
shadowRewardsInPoolByOwner[member][poolId],
shadowRewardsByPoolId[poolId],
poolBalance
);
}
/// @dev A member joins a staking pool.
/// This function increments the shadow balance of the member, along
/// with the total shadow balance of the pool. This ensures that
/// any rewards belonging to existing members will not be diluted.
/// @param poolId Unique Id of pool to join.
/// @param member The member to join.
/// @param amountOfStakeToDelegate The stake to be delegated by `member` upon joining.
/// @param totalStakeDelegatedToPool The amount of stake currently delegated to the pool.
/// This does not include `amountOfStakeToDelegate`.
function _joinStakingPool(
bytes32 poolId,
address payable member,
uint256 amountOfStakeToDelegate,
uint256 totalStakeDelegatedToPool
)
internal
{
// update delegator's share of reward pool
uint256 poolBalance = getBalanceOfMembersInStakingPoolRewardVault(poolId);
uint256 buyIn = LibRewardMath._computeBuyInDenominatedInShadowAsset(
amountOfStakeToDelegate,
totalStakeDelegatedToPool,
shadowRewardsByPoolId[poolId],
poolBalance
);
// the buy-in will be > 0 iff there exists a non-zero reward.
if (buyIn > 0) {
shadowRewardsInPoolByOwner[member][poolId] = shadowRewardsInPoolByOwner[member][poolId]._add(buyIn);
shadowRewardsByPoolId[poolId] = shadowRewardsByPoolId[poolId]._add(buyIn);
}
}
/// @dev A member leaves a staking pool.
/// This function decrements the shadow balance of the member, along
/// with the total shadow balance of the pool. This ensures that
/// any rewards belonging to co-members will not be inflated.
/// @param poolId Unique Id of pool to leave.
/// @param member The member to leave.
/// @param amountOfStakeToUndelegate The stake to be undelegated by `member` upon leaving.
/// @param totalStakeDelegatedToPoolByMember The amount of stake currently delegated to the pool by the member.
/// This includes `amountOfStakeToUndelegate`.
/// @param totalStakeDelegatedToPool The total amount of stake currently delegated to the pool, across all members.
/// This includes `amountOfStakeToUndelegate`.
function _leaveStakingPool(
bytes32 poolId,
address payable member,
uint256 amountOfStakeToUndelegate,
uint256 totalStakeDelegatedToPoolByMember,
uint256 totalStakeDelegatedToPool
)
internal
{
// get payout
uint256 poolBalance = getBalanceOfMembersInStakingPoolRewardVault(poolId);
uint256 payoutInRealAsset = 0;
uint256 payoutInShadowAsset = 0;
if (totalStakeDelegatedToPoolByMember == amountOfStakeToUndelegate) {
// full payout; this is computed separately to avoid extra computation and rounding.
payoutInShadowAsset = shadowRewardsInPoolByOwner[member][poolId];
payoutInRealAsset = LibRewardMath._computePayoutDenominatedInRealAsset(
amountOfStakeToUndelegate,
totalStakeDelegatedToPool,
payoutInShadowAsset,
shadowRewardsByPoolId[poolId],
poolBalance
);
} else {
// partial payout
(payoutInRealAsset, payoutInShadowAsset) = LibRewardMath._computePartialPayout(
amountOfStakeToUndelegate,
totalStakeDelegatedToPoolByMember,
totalStakeDelegatedToPool,
shadowRewardsInPoolByOwner[member][poolId],
shadowRewardsByPoolId[poolId],
poolBalance
);
}
// update shadow rewards
shadowRewardsInPoolByOwner[member][poolId] = shadowRewardsInPoolByOwner[member][poolId]._sub(payoutInShadowAsset);
shadowRewardsByPoolId[poolId] = shadowRewardsByPoolId[poolId]._sub(payoutInShadowAsset);
// withdraw payout for member
if (payoutInRealAsset > 0) {
_withdrawFromMemberInStakingPoolRewardVault(poolId, payoutInRealAsset);
member.transfer(payoutInRealAsset);
}
}
}

View File

@@ -0,0 +1,25 @@
pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/Ownable.sol";
import "../interfaces/IStakingEvents.sol";
import "../immutable/MixinStorage.sol";
/// @dev This mixin contains logic for ownable contracts.
/// Note that unlike the standardized `ownable` contract,
/// there is no state declared here. It is instead located
/// in `immutable/MixinStorage.sol` and its value is set
/// by the delegating proxy (StakingProxy.sol)
contract MixinOwnable is
Ownable,
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage
{
// solhint-disable no-empty-blocks
constructor()
public
{}
}

View File

@@ -0,0 +1,175 @@
/*
Copyright 2018 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;
import "../libs/LibSafeMath.sol";
import "../libs/LibSafeMath64.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../interfaces/IStructs.sol";
import "../interfaces/IStakingEvents.sol";
/// @dev This mixin contains logic for time-based scheduling.
/// All processes in the system are segmented into time intervals, called epochs.
/// Epochs have a fixed minimum time period that is configured when this contract is deployed.
/// The current epoch only changes by calling this contract, which can be invoked by anyone.
/// Epochs serve as the basis for all other time intervals, which provides a more stable
/// and consistent scheduling metric than time. TimeLocks, for example, are measured in epochs.
contract MixinScheduler is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage
{
using LibSafeMath for uint256;
using LibSafeMath64 for uint64;
/// @dev Returns the current epoch.
/// @return Epoch.
function getCurrentEpoch()
public
view
returns (uint64)
{
return currentEpoch;
}
/// @dev Returns the current epoch period, measured in seconds.
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
/// @return Time in seconds.
function getEpochDurationInSeconds()
public
pure
returns (uint64)
{
return EPOCH_DURATION_IN_SECONDS;
}
/// @dev Returns the start time in seconds of the current epoch.
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
/// @return Time in seconds.
function getCurrentEpochStartTimeInSeconds()
public
view
returns (uint64)
{
return currentEpochStartTimeInSeconds;
}
/// @dev Returns the earliest end time in seconds of this epoch.
/// The next epoch can begin once this time is reached.
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
/// @return Time in seconds.
function getCurrentEpochEarliestEndTimeInSeconds()
public
view
returns (uint64)
{
return getCurrentEpochStartTimeInSeconds()._add(getEpochDurationInSeconds());
}
/// @dev Returns the current timeLock period.
/// @return TimeLock period.
function getCurrentTimeLockPeriod()
public
view
returns (uint64)
{
return currentTimeLockPeriod;
}
/// @dev Returns the length of a timeLock period, measured in epochs.
/// TimeLock period = [startEpoch..endEpoch)
/// @return TimeLock period end.
function getTimeLockDurationInEpochs()
public
pure
returns (uint64)
{
return TIMELOCK_DURATION_IN_EPOCHS;
}
/// @dev Returns the epoch that the current timeLock period started at.
/// TimeLock period = [startEpoch..endEpoch)
/// @return TimeLock period start.
function getCurrentTimeLockPeriodStartEpoch()
public
view
returns (uint64)
{
return currentTimeLockPeriodStartEpoch;
}
/// @dev Returns the epoch that the current timeLock period will end.
/// TimeLock period = [startEpoch..endEpoch)
/// @return TimeLock period.
function getCurrentTimeLockPeriodEndEpoch()
public
view
returns (uint64)
{
return getCurrentTimeLockPeriodStartEpoch()._add(getTimeLockDurationInEpochs());
}
/// @dev Moves to the next epoch, given the current epoch period has ended.
/// Time intervals that are measured in epochs (like timeLocks) are also incremented, given
/// their periods have ended.
function _goToNextEpoch()
internal
{
// get current timestamp
// solhint-disable-next-line not-rely-on-time
uint64 currentBlockTimestamp = block.timestamp._downcastToUint64();
// validate that we can increment the current epoch
require(
getCurrentEpochEarliestEndTimeInSeconds() <= currentBlockTimestamp,
"BLOCK_TIMESTAMP_TOO_LOW"
);
// incremment epoch
uint64 nextEpoch = currentEpoch._add(1);
currentEpoch = nextEpoch;
currentEpochStartTimeInSeconds = currentBlockTimestamp;
uint64 earliestEndTimeInSeconds = currentEpochStartTimeInSeconds._add(getEpochDurationInSeconds());
// notify of epoch change
emit EpochChanged(
currentEpoch,
currentEpochStartTimeInSeconds,
earliestEndTimeInSeconds
);
// increment timeLock period, if needed
if (getCurrentTimeLockPeriodEndEpoch() <= nextEpoch) {
currentTimeLockPeriod = currentTimeLockPeriod._add(1);
currentTimeLockPeriodStartEpoch = currentEpoch;
uint64 endEpoch = currentEpoch._add(getTimeLockDurationInEpochs());
// notify
emit TimeLockPeriodChanged(
currentTimeLockPeriod,
currentTimeLockPeriodStartEpoch,
endEpoch
);
}
}
}

View File

@@ -0,0 +1,101 @@
/*
Copyright 2018 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;
import "@0x/contracts-utils/contracts/src/Authorizable.sol";
import "../interfaces/IVaultCore.sol";
/// @dev This mixin contains core logic for vaults.
/// This includes management of the staking contract
/// and setting the vault to "Catastrophic Failure Mode".
/// It's up to the vault how they handle this failure mode; however,
/// all vaults should disable all functionality aside from withdrawals.
/// Vaults should only be set to Catastrophic Failure Mode iff there is
/// non-recoverable corruption of the staking contracts. If there is a
/// recoverable flaw/bug/vulnerability, simply detach the staking contract
/// by setting its address to `address(0)`. Once in Catostrophic Failure Mode,
/// a vault cannot be reset to normal mode; this prevents corruption of related
/// state in the staking contract.
contract MixinVaultCore is
Authorizable,
IVaultCore
{
// Address of staking contract
address payable internal stakingContractAddress;
// True iff vault has been set to Catostrophic Failure Mode
bool internal isInCatostrophicFailure;
/// @dev Constructor.
constructor() public {
stakingContractAddress = 0x0000000000000000000000000000000000000000;
isInCatostrophicFailure = false;
}
/// @dev Asserts that the sender (`msg.sender`) is the staking contract.
modifier onlyStakingContract {
require(
msg.sender == stakingContractAddress,
"ONLY_CALLABLE_BY_STAKING_CONTRACT"
);
_;
}
/// @dev Asserts that this contract *is in* Catostrophic Failure Mode.
modifier onlyInCatostrophicFailure {
require(
isInCatostrophicFailure,
"ONLY_CALLABLE_IN_CATOSTROPHIC_FAILURE"
);
_;
}
/// @dev Asserts that this contract *is not in* Catostrophic Failure Mode.
modifier onlyNotInCatostrophicFailure {
require(
!isInCatostrophicFailure,
"ONLY_CALLABLE_NOT_IN_CATOSTROPHIC_FAILURE"
);
_;
}
/// @dev Sets the address of the Staking Contract.
/// Note that only the contract owner can call this function.
/// @param _stakingContractAddress Address of Staking contract.
function setStakingContract(address payable _stakingContractAddress)
external
onlyOwner
{
stakingContractAddress = _stakingContractAddress;
emit StakingContractChanged(stakingContractAddress);
}
/// @dev Vault enters into Catostrophic Failure Mode.
/// *** WARNING - ONCE IN CATOSTROPHIC FAILURE MODE, YOU CAN NEVER GO BACK! ***
/// Note that only the contract owner can call this function.
function enterCatostrophicFailure()
external
onlyOwner
{
isInCatostrophicFailure = true;
emit InCatostrophicFailureMode(msg.sender);
}
}

View File

@@ -0,0 +1,229 @@
/*
Copyright 2018 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;
import "../libs/LibSafeMath.sol";
import "../libs/LibSafeMath96.sol";
import "./MixinVaultCore.sol";
import "../interfaces/IStakingPoolRewardVault.sol";
import "../immutable/MixinConstants.sol";
/// @dev This vault manages staking pool rewards.
/// Rewards can be deposited and withdraw by the staking contract.
/// There is a "Catastrophic Failure Mode" that, when invoked, only
/// allows withdrawals to be made. Once this vault is in catostrophic
/// failure mode, it cannot be returned to normal mode; this prevents
/// corruption of related state in the staking contract.
///
/// When in Catastrophic Failure Mode, the Staking contract can still
/// perform withdrawals on behalf of its users.
contract StakingPoolRewardVault is
Authorizable,
IStakingPoolRewardVault,
MixinDeploymentConstants,
MixinConstants,
MixinVaultCore
{
using LibSafeMath for uint256;
using LibSafeMath96 for uint96;
// mapping from Pool to Reward Balance in ETH
mapping (bytes32 => Balance) internal balanceByPoolId;
/// @dev Fallback function. This contract is payable, but only by the staking contract.
function ()
external
payable
onlyStakingContract
onlyNotInCatostrophicFailure
{
emit RewardDeposited(UNKNOWN_STAKING_POOL_ID, msg.value);
}
/// @dev Deposit a reward in ETH for a specific pool.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
function depositFor(bytes32 poolId)
external
payable
onlyStakingContract
onlyNotInCatostrophicFailure
{
// update balance of pool
uint256 amount = msg.value;
Balance memory balance = balanceByPoolId[poolId];
_incrementBalanceStruct(balance, amount);
balanceByPoolId[poolId] = balance;
// notify
emit RewardDeposited(poolId, amount);
}
/// @dev Record a deposit for a pool. This deposit should be in the same transaction,
/// which is enforced by the staking contract. We do not enforce it here to save (a lot of) gas.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function recordDepositFor(bytes32 poolId, uint256 amount)
external
onlyStakingContract
onlyNotInCatostrophicFailure
{
// update balance of pool
Balance memory balance = balanceByPoolId[poolId];
_incrementBalanceStruct(balance, amount);
balanceByPoolId[poolId] = balance;
}
/// @dev Withdraw some amount in ETH of an operator's reward.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function withdrawForOperator(bytes32 poolId, uint256 amount)
external
onlyStakingContract
{
// sanity check - sufficient balance?
require(
amount <= balanceByPoolId[poolId].operatorBalance,
"AMOUNT_EXCEEDS_BALANCE_OF_POOL"
);
// update balance and transfer `amount` in ETH to staking contract
balanceByPoolId[poolId].operatorBalance -= amount._downcastToUint96();
stakingContractAddress.transfer(amount);
// notify
emit RewardWithdrawnForOperator(poolId, amount);
}
/// @dev Withdraw some amount in ETH of a pool member.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param amount Amount in ETH to record.
function withdrawForMember(bytes32 poolId, uint256 amount)
external
onlyStakingContract
{
// sanity check - sufficient balance?
require(
amount <= balanceByPoolId[poolId].membersBalance,
"AMOUNT_EXCEEDS_BALANCE_OF_POOL"
);
// update balance and transfer `amount` in ETH to staking contract
balanceByPoolId[poolId].membersBalance -= amount._downcastToUint96();
stakingContractAddress.transfer(amount);
// notify
emit RewardWithdrawnForMember(poolId, amount);
}
/// @dev Register a new staking pool.
/// Note that this is only callable by the staking contract, and when
/// not in catastrophic failure mode.
/// @param poolId Unique Id of pool.
/// @param poolOperatorShare Percentage of rewards given to the pool operator.
function registerStakingPool(bytes32 poolId, uint8 poolOperatorShare)
external
onlyStakingContract
onlyNotInCatostrophicFailure
{
// operator share must be a valid percentage
require(
poolOperatorShare <= 100,
"OPERATOR_SHARE_MUST_BE_BETWEEN_0_AND_100"
);
// pool must not exist
Balance memory balance = balanceByPoolId[poolId];
require(
!balance.initialized,
"POOL_ALREADY_EXISTS"
);
// set initial balance
balance.initialized = true;
balance.operatorShare = poolOperatorShare;
balanceByPoolId[poolId] = balance;
// notify
emit StakingPoolRegistered(poolId, poolOperatorShare);
}
/// @dev Returns the total balance of a pool.
/// @param poolId Unique Id of pool.
/// @return Balance in ETH.
function balanceOf(bytes32 poolId)
external
view
returns (uint256)
{
Balance memory balance = balanceByPoolId[poolId];
return balance.operatorBalance + balance.membersBalance;
}
/// @dev Returns the balance of a pool operator.
/// @param poolId Unique Id of pool.
/// @return Balance in ETH.
function balanceOfOperator(bytes32 poolId)
external
view
returns (uint256)
{
return balanceByPoolId[poolId].operatorBalance;
}
/// @dev Returns the balance co-owned by members of a pool.
/// @param poolId Unique Id of pool.
/// @return Balance in ETH.
function balanceOfMembers(bytes32 poolId)
external
view
returns (uint256)
{
return balanceByPoolId[poolId].membersBalance;
}
/// @dev Increments a balance struct, splitting the input amount between the
/// pool operator and members of the pool based on the pool operator's share.
/// @param balance Balance struct to increment.
/// @param amount256Bit Amount to add to balance.
function _incrementBalanceStruct(Balance memory balance, uint256 amount256Bit)
private
pure
{
// balances are stored as uint96; safely downscale.
uint96 amount = amount256Bit._downcastToUint96();
// compute portions. One of the two must round down: the operator always receives the leftover from rounding.
uint96 operatorPortion = amount._computePercentageCeil(balance.operatorShare);
uint96 poolPortion = amount._sub(operatorPortion);
// update balances
balance.operatorBalance = balance.operatorBalance._add(operatorPortion);
balance.membersBalance = balance.membersBalance._add(poolPortion);
}
}

View File

@@ -0,0 +1,181 @@
/*
Copyright 2018 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;
import "../libs/LibSafeMath.sol";
import "../interfaces/IZrxVault.sol";
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol";
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "./MixinVaultCore.sol";
/// @dev This vault manages Zrx Tokens.
/// When a user mints stake, their Zrx Tokens are deposited into this vault.
/// Similarly, when they burn stake, their Zrx Tokens are withdrawn from this vault.
/// There is a "Catastrophic Failure Mode" that, when invoked, only
/// allows withdrawals to be made. Once this vault is in catostrophic
/// failure mode, it cannot be returned to normal mode; this prevents
/// corruption of related state in the staking contract.
contract ZrxVault is
Authorizable,
IZrxVault,
MixinVaultCore
{
using LibSafeMath for uint256;
// mapping from Owner to ZRX balance
mapping (address => uint256) internal balances;
// 0x ERC20 Proxy
IAssetProxy internal erc20Proxy;
// Zrx Token
IERC20Token internal zrxToken;
// Asset data for the ERC20 Proxy
bytes internal zrxAssetData;
/// @dev Constructor.
/// @param erc20ProxyAddress Address of the 0x ERC20 Proxy.
/// @param zrxTokenAddress Address of the Zrx Token.
/// @param _zrxAssetData Zrx asset data for the ERC20 Proxy.
constructor(
address erc20ProxyAddress,
address zrxTokenAddress,
bytes memory _zrxAssetData
)
public
{
erc20Proxy = IAssetProxy(erc20ProxyAddress);
zrxToken = IERC20Token(zrxTokenAddress);
zrxAssetData = _zrxAssetData;
}
/// @dev Sets the ERC20 proxy.
/// Note that only the contract owner can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param erc20ProxyAddress Address of the 0x ERC20 Proxy.
function setErc20Proxy(address erc20ProxyAddress)
external
onlyOwner
onlyNotInCatostrophicFailure
{
erc20Proxy = IAssetProxy(erc20ProxyAddress);
emit Erc20ProxyChanged(erc20ProxyAddress);
}
/// @dev Sets the Zrx Asset Data.
/// Note that only the contract owner can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param _zrxAssetData Zrx asset data for the ERC20 Proxy.
function setZrxAssetData(bytes calldata _zrxAssetData)
external
onlyOwner
onlyNotInCatostrophicFailure
{
zrxAssetData = _zrxAssetData;
emit ZrxAssetDataChanged(_zrxAssetData);
}
/// @dev Deposit an `amount` of Zrx Tokens from `owner` into the vault.
/// Note that only the Staking contract can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param owner of Zrx Tokens.
/// @param amount of Zrx Tokens to deposit.
function depositFrom(address owner, uint256 amount)
external
onlyStakingContract
onlyNotInCatostrophicFailure
{
// update balance
balances[owner] = balances[owner]._add(amount);
// notify
emit ZrxDepositedIntoVault(msg.sender, owner, amount);
// deposit ZRX from owner
erc20Proxy.transferFrom(
zrxAssetData,
owner,
address(this),
amount
);
}
/// @dev Withdraw an `amount` of Zrx Tokens to `owner` from the vault.
/// Note that only the Staking contract can call this.
/// Note that this can only be called when *not* in Catostrophic Failure mode.
/// @param owner of Zrx Tokens.
/// @param amount of Zrx Tokens to withdraw.
function withdrawFrom(address owner, uint256 amount)
external
onlyStakingContract
onlyNotInCatostrophicFailure
{
_withdrawFrom(owner, amount);
}
/// @dev Withdraw ALL Zrx Tokens to `owner` from the vault.
/// Note that this can only be called when *in* Catostrophic Failure mode.
/// @param owner of Zrx Tokens.
function withdrawAllFrom(address owner)
external
onlyInCatostrophicFailure
returns (uint256)
{
// get total balance
uint256 totalBalance = balances[owner];
// withdraw ZRX to owner
_withdrawFrom(owner, totalBalance);
return totalBalance;
}
/// @dev Returns the balance in Zrx Tokens of the `owner`
/// @return Balance in Zrx.
function balanceOf(address owner)
external
view
returns (uint256)
{
return balances[owner];
}
/// @dev Withdraw an `amount` of Zrx Tokens to `owner` from the vault.
/// @param owner of Zrx Tokens.
/// @param amount of Zrx Tokens to withdraw.
function _withdrawFrom(address owner, uint256 amount)
internal
{
// update balance
// note that this call will revert if trying to withdraw more
// than the current balance
balances[owner] = balances[owner]._sub(amount);
// notify
emit ZrxWithdrawnFromVault(msg.sender, owner, amount);
// withdraw ZRX to owner
zrxToken.transfer(
owner,
amount
);
}
}

View File

@@ -0,0 +1,112 @@
/*
Copyright 2018 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.5;
import "../src/libs/LibFeeMath.sol";
contract LibFeeMathTest {
function nthRoot(uint256 base, uint256 n) public pure returns (uint256 root) {
return LibFeeMath._nthRoot(base, n);
}
function nthRootFixedPoint(
uint256 base,
uint256 n
)
public
pure
returns (uint256 root)
{
return LibFeeMath._nthRootFixedPoint(base, n);
}
function cobbDouglas(
uint256 totalRewards,
uint256 ownerFees,
uint256 totalFees,
uint256 ownerStake,
uint256 totalStake,
uint8 alphaNumerator,
uint8 alphaDenominator
)
public
pure
returns (uint256)
{
return LibFeeMath._cobbDouglas(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaNumerator,
alphaDenominator
);
}
function cobbDouglasSimplified(
uint256 totalRewards,
uint256 ownerFees,
uint256 totalFees,
uint256 ownerStake,
uint256 totalStake,
uint8 alphaDenominator
)
public
pure
returns (uint256)
{
return LibFeeMath._cobbDouglasSimplified(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaDenominator
);
}
function cobbDouglasSimplifiedInverse(
uint256 totalRewards,
uint256 ownerFees,
uint256 totalFees,
uint256 ownerStake,
uint256 totalStake,
uint8 alphaDenominator
)
public
// pure
returns (uint256)
{
return LibFeeMath._cobbDouglasSimplifiedInverse(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaDenominator
);
}
}

BIN
contracts/staking/images/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -10,6 +10,7 @@
"test": "test"
},
"scripts": {
"build2": "tsc -b",
"build": "yarn pre_build && tsc -b",
"build:ci": "yarn build",
"pre_build": "run-s compile contracts:gen generate_contract_wrappers",
@@ -35,7 +36,7 @@
"compile:truffle": "truffle compile"
},
"config": {
"abis": "./generated-artifacts/@(IStaking|Staking).json",
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStructs|IVaultCore|IWallet|IZrxVault|LibEIP712Hash|LibFeeMath|LibFeeMathTest|LibRewardMath|LibSafeMath|LibSafeMath64|LibSafeMath96|LibSignatureValidator|MixinConstants|MixinDelegatedStake|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinOwnable|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinTimeLockedStake|MixinVaultCore|MixinZrxVault|Staking|StakingPoolRewardVault|StakingProxy|ZrxVault).json",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
},
"repository": {
@@ -48,14 +49,15 @@
},
"homepage": "https://github.com/0xProject/0x-monorepo/contracts/tokens/README.md",
"devDependencies": {
"@0x/abi-gen": "^2.0.9",
"@0x/contracts-gen": "^1.0.8",
"@0x/abi-gen": "^4.1.0",
"@0x/contracts-gen": "^1.0.13",
"@0x/contracts-test-utils": "^3.1.2",
"@0x/dev-utils": "^2.2.1",
"@0x/sol-compiler": "^3.1.6",
"@0x/tslint-config": "^3.0.1",
"@types/lodash": "4.14.104",
"@types/node": "*",
"@0x/utils": "^4.3.1",
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"chai-bignumber": "^3.0.0",
@@ -70,12 +72,15 @@
"typescript": "3.0.1"
},
"dependencies": {
"@0x/base-contract": "^5.0.5",
"@0x/contracts-utils": "^3.1.1",
"@0x/base-contract": "^5.1.0",
"@0x/contracts-utils": "^3.2.1",
"@0x/contracts-asset-proxy": "^2.2.5",
"@0x/contracts-erc20": "^2.2.0",
"@0x/order-utils": "^8.1.0",
"@0x/types": "^2.2.2",
"@0x/typescript-typings": "^4.2.2",
"@0x/utils": "^4.3.1",
"@0x/web3-wrapper": "^6.0.5",
"@0x/web3-wrapper": "^6.0.6",
"ethereum-types": "^2.1.2",
"ethereumjs-util": "^5.1.1",
"lodash": "^4.17.11"

View File

@@ -6,5 +6,76 @@
import { ContractArtifact } from 'ethereum-types';
import * as IStaking from '../generated-artifacts/IStaking.json';
import * as IStakingEvents from '../generated-artifacts/IStakingEvents.json';
import * as IStakingPoolRewardVault from '../generated-artifacts/IStakingPoolRewardVault.json';
import * as IStakingProxy from '../generated-artifacts/IStakingProxy.json';
import * as IStructs from '../generated-artifacts/IStructs.json';
import * as IVaultCore from '../generated-artifacts/IVaultCore.json';
import * as IWallet from '../generated-artifacts/IWallet.json';
import * as IZrxVault from '../generated-artifacts/IZrxVault.json';
import * as LibEIP712Hash from '../generated-artifacts/LibEIP712Hash.json';
import * as LibFeeMath from '../generated-artifacts/LibFeeMath.json';
import * as LibFeeMathTest from '../generated-artifacts/LibFeeMathTest.json';
import * as LibRewardMath from '../generated-artifacts/LibRewardMath.json';
import * as LibSafeMath from '../generated-artifacts/LibSafeMath.json';
import * as LibSafeMath64 from '../generated-artifacts/LibSafeMath64.json';
import * as LibSafeMath96 from '../generated-artifacts/LibSafeMath96.json';
import * as LibSignatureValidator from '../generated-artifacts/LibSignatureValidator.json';
import * as MixinConstants from '../generated-artifacts/MixinConstants.json';
import * as MixinDelegatedStake from '../generated-artifacts/MixinDelegatedStake.json';
import * as MixinDeploymentConstants from '../generated-artifacts/MixinDeploymentConstants.json';
import * as MixinExchangeFees from '../generated-artifacts/MixinExchangeFees.json';
import * as MixinExchangeManager from '../generated-artifacts/MixinExchangeManager.json';
import * as MixinOwnable from '../generated-artifacts/MixinOwnable.json';
import * as MixinScheduler from '../generated-artifacts/MixinScheduler.json';
import * as MixinStake from '../generated-artifacts/MixinStake.json';
import * as MixinStakeBalances from '../generated-artifacts/MixinStakeBalances.json';
import * as MixinStakingPool from '../generated-artifacts/MixinStakingPool.json';
import * as MixinStakingPoolRewards from '../generated-artifacts/MixinStakingPoolRewards.json';
import * as MixinStakingPoolRewardVault from '../generated-artifacts/MixinStakingPoolRewardVault.json';
import * as MixinStorage from '../generated-artifacts/MixinStorage.json';
import * as MixinTimeLockedStake from '../generated-artifacts/MixinTimeLockedStake.json';
import * as MixinVaultCore from '../generated-artifacts/MixinVaultCore.json';
import * as MixinZrxVault from '../generated-artifacts/MixinZrxVault.json';
import * as Staking from '../generated-artifacts/Staking.json';
export const artifacts = { IStaking: IStaking as ContractArtifact, Staking: Staking as ContractArtifact };
import * as StakingPoolRewardVault from '../generated-artifacts/StakingPoolRewardVault.json';
import * as StakingProxy from '../generated-artifacts/StakingProxy.json';
import * as ZrxVault from '../generated-artifacts/ZrxVault.json';
export const artifacts = {
Staking: Staking as ContractArtifact,
StakingProxy: StakingProxy as ContractArtifact,
MixinExchangeFees: MixinExchangeFees as ContractArtifact,
MixinExchangeManager: MixinExchangeManager as ContractArtifact,
MixinConstants: MixinConstants as ContractArtifact,
MixinDeploymentConstants: MixinDeploymentConstants as ContractArtifact,
MixinStorage: MixinStorage as ContractArtifact,
IStaking: IStaking as ContractArtifact,
IStakingEvents: IStakingEvents as ContractArtifact,
IStakingPoolRewardVault: IStakingPoolRewardVault as ContractArtifact,
IStakingProxy: IStakingProxy as ContractArtifact,
IStructs: IStructs as ContractArtifact,
IVaultCore: IVaultCore as ContractArtifact,
IWallet: IWallet as ContractArtifact,
IZrxVault: IZrxVault as ContractArtifact,
LibEIP712Hash: LibEIP712Hash as ContractArtifact,
LibFeeMath: LibFeeMath as ContractArtifact,
LibRewardMath: LibRewardMath as ContractArtifact,
LibSafeMath: LibSafeMath as ContractArtifact,
LibSafeMath64: LibSafeMath64 as ContractArtifact,
LibSafeMath96: LibSafeMath96 as ContractArtifact,
LibSignatureValidator: LibSignatureValidator as ContractArtifact,
MixinDelegatedStake: MixinDelegatedStake as ContractArtifact,
MixinStake: MixinStake as ContractArtifact,
MixinStakeBalances: MixinStakeBalances as ContractArtifact,
MixinTimeLockedStake: MixinTimeLockedStake as ContractArtifact,
MixinZrxVault: MixinZrxVault as ContractArtifact,
MixinStakingPool: MixinStakingPool as ContractArtifact,
MixinStakingPoolRewardVault: MixinStakingPoolRewardVault as ContractArtifact,
MixinStakingPoolRewards: MixinStakingPoolRewards as ContractArtifact,
MixinOwnable: MixinOwnable as ContractArtifact,
MixinScheduler: MixinScheduler as ContractArtifact,
MixinVaultCore: MixinVaultCore as ContractArtifact,
StakingPoolRewardVault: StakingPoolRewardVault as ContractArtifact,
ZrxVault: ZrxVault as ContractArtifact,
LibFeeMathTest: LibFeeMathTest as ContractArtifact,
};

View File

@@ -4,4 +4,38 @@
* -----------------------------------------------------------------------------
*/
export * from '../generated-wrappers/i_staking';
export * from '../generated-wrappers/i_staking_events';
export * from '../generated-wrappers/i_staking_pool_reward_vault';
export * from '../generated-wrappers/i_staking_proxy';
export * from '../generated-wrappers/i_structs';
export * from '../generated-wrappers/i_vault_core';
export * from '../generated-wrappers/i_wallet';
export * from '../generated-wrappers/i_zrx_vault';
export * from '../generated-wrappers/lib_e_i_p712_hash';
export * from '../generated-wrappers/lib_fee_math';
export * from '../generated-wrappers/lib_fee_math_test';
export * from '../generated-wrappers/lib_reward_math';
export * from '../generated-wrappers/lib_safe_math';
export * from '../generated-wrappers/lib_safe_math64';
export * from '../generated-wrappers/lib_safe_math96';
export * from '../generated-wrappers/lib_signature_validator';
export * from '../generated-wrappers/mixin_constants';
export * from '../generated-wrappers/mixin_delegated_stake';
export * from '../generated-wrappers/mixin_deployment_constants';
export * from '../generated-wrappers/mixin_exchange_fees';
export * from '../generated-wrappers/mixin_exchange_manager';
export * from '../generated-wrappers/mixin_ownable';
export * from '../generated-wrappers/mixin_scheduler';
export * from '../generated-wrappers/mixin_stake';
export * from '../generated-wrappers/mixin_stake_balances';
export * from '../generated-wrappers/mixin_staking_pool';
export * from '../generated-wrappers/mixin_staking_pool_reward_vault';
export * from '../generated-wrappers/mixin_staking_pool_rewards';
export * from '../generated-wrappers/mixin_storage';
export * from '../generated-wrappers/mixin_time_locked_stake';
export * from '../generated-wrappers/mixin_vault_core';
export * from '../generated-wrappers/mixin_zrx_vault';
export * from '../generated-wrappers/staking';
export * from '../generated-wrappers/staking_pool_reward_vault';
export * from '../generated-wrappers/staking_proxy';
export * from '../generated-wrappers/zrx_vault';

View File

@@ -0,0 +1,17 @@
import { StakingWrapper } from '../utils/staking_wrapper';
export class BaseActor {
protected readonly _owner: string;
protected readonly _stakingWrapper: StakingWrapper;
constructor(owner: string, stakingWrapper: StakingWrapper) {
this._owner = owner;
this._stakingWrapper = stakingWrapper;
}
public getOwner(): string {
return this._owner;
}
public getStakingWrapper(): StakingWrapper {
return this._stakingWrapper;
}
}

View File

@@ -0,0 +1,162 @@
import { expectTransactionFailedAsync } from '@0x/contracts-test-utils';
import { RevertReason } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import { StakingWrapper } from '../utils/staking_wrapper';
import { DelegatorBalances, StakerBalances } from '../utils/types';
import { StakerActor } from './staker_actor';
const expect = chai.expect;
export class DelegatorActor extends StakerActor {
constructor(owner: string, stakingWrapper: StakingWrapper) {
super(owner, stakingWrapper);
}
public async depositZrxAndDelegateToStakingPoolAsync(
poolId: string,
amount: BigNumber,
revertReason?: RevertReason,
): Promise<void> {
// query init balances
const initZrxBalanceOfVault = await this._stakingWrapper.getZrxTokenBalanceOfZrxVaultAsync();
const initDelegatorBalances = await this.getBalancesAsync([poolId]);
// deposit stake
const txReceiptPromise = this._stakingWrapper.depositZrxAndDelegateToStakingPoolAsync(
this._owner,
poolId,
amount,
);
if (revertReason !== undefined) {
await expectTransactionFailedAsync(txReceiptPromise, revertReason);
return;
}
await txReceiptPromise;
// @TODO check receipt logs and return value via eth_call
// check balances
const expectedDelegatorBalances = initDelegatorBalances;
expectedDelegatorBalances.zrxBalance = initDelegatorBalances.zrxBalance.minus(amount);
expectedDelegatorBalances.stakeBalance = initDelegatorBalances.stakeBalance.plus(amount);
expectedDelegatorBalances.stakeBalanceInVault = initDelegatorBalances.stakeBalanceInVault.plus(amount);
expectedDelegatorBalances.activatedStakeBalance = initDelegatorBalances.activatedStakeBalance.plus(amount);
expectedDelegatorBalances.delegatedStakeBalance = initDelegatorBalances.delegatedStakeBalance.plus(amount);
expectedDelegatorBalances.stakeDelegatedToPoolByOwner[0] = initDelegatorBalances.stakeDelegatedToPoolByOwner[0].plus(
amount,
);
expectedDelegatorBalances.stakeDelegatedToPool[0] = initDelegatorBalances.stakeDelegatedToPool[0].plus(amount);
await this.assertBalancesAsync(expectedDelegatorBalances, [poolId]);
// check zrx balance of vault
const finalZrxBalanceOfVault = await this._stakingWrapper.getZrxTokenBalanceOfZrxVaultAsync();
expect(finalZrxBalanceOfVault).to.be.bignumber.equal(initZrxBalanceOfVault.plus(amount));
}
public async activateAndDelegateStakeAsync(
poolId: string,
amount: BigNumber,
revertReason?: RevertReason,
): Promise<void> {
// query init balances
const initDelegatorBalances = await this.getBalancesAsync([poolId]);
// activate and delegate
const txReceiptPromise = this._stakingWrapper.activateAndDelegateStakeAsync(this._owner, poolId, amount);
if (revertReason !== undefined) {
await expectTransactionFailedAsync(txReceiptPromise, revertReason);
return;
}
await txReceiptPromise;
// @TODO check receipt logs and return value via eth_call
// check balances
// check balances
const expectedDelegatorBalances = initDelegatorBalances;
expectedDelegatorBalances.activatedStakeBalance = initDelegatorBalances.activatedStakeBalance.plus(amount);
expectedDelegatorBalances.withdrawableStakeBalance = expectedDelegatorBalances.withdrawableStakeBalance.minus(
amount,
);
expectedDelegatorBalances.activatableStakeBalance = expectedDelegatorBalances.activatableStakeBalance.minus(
amount,
);
expectedDelegatorBalances.deactivatedStakeBalance = expectedDelegatorBalances.deactivatedStakeBalance.minus(
amount,
);
expectedDelegatorBalances.delegatedStakeBalance = initDelegatorBalances.delegatedStakeBalance.plus(amount);
expectedDelegatorBalances.stakeDelegatedToPoolByOwner[0] = initDelegatorBalances.stakeDelegatedToPoolByOwner[0].plus(
amount,
);
expectedDelegatorBalances.stakeDelegatedToPool[0] = initDelegatorBalances.stakeDelegatedToPool[0].plus(amount);
await this.assertBalancesAsync(expectedDelegatorBalances, [poolId]);
}
public async deactivateAndTimeLockDelegatedStakeAsync(
poolId: string,
amount: BigNumber,
revertReason?: RevertReason,
): Promise<void> {
// query init balances
const initDelegatorBalances = await this.getBalancesAsync([poolId]);
// deactivate and timeLock
const txReceiptPromise = this._stakingWrapper.deactivateAndTimeLockDelegatedStakeAsync(
this._owner,
poolId,
amount,
);
if (revertReason !== undefined) {
await expectTransactionFailedAsync(txReceiptPromise, revertReason);
return;
}
await txReceiptPromise;
// @TODO check receipt logs and return value via eth_call
// check balances
const expectedDelegatorBalances = initDelegatorBalances;
expectedDelegatorBalances.activatedStakeBalance = initDelegatorBalances.activatedStakeBalance.minus(amount);
expectedDelegatorBalances.timeLockedStakeBalance = expectedDelegatorBalances.timeLockedStakeBalance.plus(
amount,
);
expectedDelegatorBalances.deactivatedStakeBalance = expectedDelegatorBalances.deactivatedStakeBalance.plus(
amount,
);
expectedDelegatorBalances.delegatedStakeBalance = initDelegatorBalances.delegatedStakeBalance.minus(amount);
expectedDelegatorBalances.stakeDelegatedToPoolByOwner[0] = initDelegatorBalances.stakeDelegatedToPoolByOwner[0].minus(
amount,
);
expectedDelegatorBalances.stakeDelegatedToPool[0] = initDelegatorBalances.stakeDelegatedToPool[0].minus(amount);
await this.assertBalancesAsync(expectedDelegatorBalances, [poolId]);
}
public async getBalancesAsync(maybePoolIds?: string[]): Promise<DelegatorBalances> {
const stakerBalances = await super.getBalancesAsync();
const delegatorBalances = {
...stakerBalances,
delegatedStakeBalance: await this._stakingWrapper.getStakeDelegatedByOwnerAsync(this._owner),
stakeDelegatedToPoolByOwner: Array(),
stakeDelegatedToPool: Array(),
};
const poolIds = maybePoolIds !== undefined ? maybePoolIds : [];
for (const poolId of poolIds) {
const stakeDelegatedToPoolByOwner = await this._stakingWrapper.getStakeDelegatedToPoolByOwnerAsync(
poolId,
this._owner,
);
delegatorBalances.stakeDelegatedToPoolByOwner.push(stakeDelegatedToPoolByOwner);
const stakeDelegatedToPool = await this._stakingWrapper.getTotalStakeDelegatedToPoolAsync(poolId);
delegatorBalances.stakeDelegatedToPool.push(stakeDelegatedToPool);
}
return delegatorBalances;
}
public async assertBalancesAsync(expectedBalances: DelegatorBalances, maybePoolIds?: string[]): Promise<void> {
await super.assertBalancesAsync(expectedBalances);
const balances = await this.getBalancesAsync(maybePoolIds);
expect(balances.delegatedStakeBalance, 'delegated stake balance').to.be.bignumber.equal(
expectedBalances.delegatedStakeBalance,
);
const poolIds = maybePoolIds !== undefined ? maybePoolIds : [];
for (let i = 0; i < poolIds.length; i++) {
expect(
balances.stakeDelegatedToPoolByOwner[i],
`stake delegated to pool ${poolIds[i]} by owner`,
).to.be.bignumber.equal(expectedBalances.stakeDelegatedToPoolByOwner[i]);
expect(
balances.stakeDelegatedToPool[i],
`total stake delegated to pool ${poolIds[i]}`,
).to.be.bignumber.equal(expectedBalances.stakeDelegatedToPool[i]);
}
}
}

View File

@@ -0,0 +1,41 @@
import { SignatureType } from '@0x/types';
import * as _ from 'lodash';
import { StakingWrapper } from '../utils/staking_wrapper';
import { SignedStakingPoolApproval } from '../utils/types';
import { BaseActor } from './base_actor';
export class MakerActor extends BaseActor {
private readonly _ownerPrivateKeyIfExists?: Buffer;
private readonly _signatureVerifierIfExists?: string;
private readonly _chainIdIfExists?: number;
constructor(
owner: string,
stakingWrapper: StakingWrapper,
ownerPrivateKey?: Buffer,
signatureVerifier?: string,
chainId?: number,
) {
super(owner, stakingWrapper);
this._ownerPrivateKeyIfExists = ownerPrivateKey;
this._signatureVerifierIfExists = signatureVerifier;
this._chainIdIfExists = chainId;
}
public signApprovalForStakingPool(
poolId: string,
signatureType: SignatureType = SignatureType.EthSign,
): SignedStakingPoolApproval {
const approval = this._stakingWrapper.signApprovalForStakingPool(
poolId,
this._owner,
this._ownerPrivateKeyIfExists,
this._signatureVerifierIfExists,
this._chainIdIfExists,
signatureType,
);
return approval;
}
}

View File

@@ -0,0 +1,80 @@
import { expectTransactionFailedAsync } from '@0x/contracts-test-utils';
import { RevertReason } from '@0x/types';
import * as chai from 'chai';
import * as _ from 'lodash';
import { constants as stakingConstants } from '../utils/constants';
import { StakingWrapper } from '../utils/staking_wrapper';
import { BaseActor } from './base_actor';
const expect = chai.expect;
export class PoolOperatorActor extends BaseActor {
constructor(owner: string, stakingWrapper: StakingWrapper) {
super(owner, stakingWrapper);
}
public async createStakingPoolAsync(operatorShare: number, revertReason?: RevertReason): Promise<string> {
// query next pool id
const nextPoolId = await this._stakingWrapper.getNextStakingPoolIdAsync();
// create pool
const poolIdPromise = this._stakingWrapper.createStakingPoolAsync(this._owner, operatorShare);
if (revertReason !== undefined) {
await expectTransactionFailedAsync(poolIdPromise, revertReason);
return '';
}
const poolId = await poolIdPromise;
// validate pool id
expect(poolId, 'pool id').to.be.bignumber.equal(nextPoolId);
return poolId;
}
public async addMakerToStakingPoolAsync(
poolId: string,
makerAddress: string,
makerSignature: string,
revertReason?: RevertReason,
): Promise<void> {
// add maker
const txReceiptPromise = this._stakingWrapper.addMakerToStakingPoolAsync(
poolId,
makerAddress,
makerSignature,
this._owner,
);
if (revertReason !== undefined) {
await expectTransactionFailedAsync(txReceiptPromise, revertReason);
return;
}
await txReceiptPromise;
// check the pool id of the maker
const poolIdOfMaker = await this._stakingWrapper.getStakingPoolIdOfMakerAsync(makerAddress);
expect(poolIdOfMaker, 'pool id of maker').to.be.equal(poolId);
// check the list of makers for the pool
const makerAddressesForPool = await this._stakingWrapper.getMakersForStakingPoolAsync(poolId);
expect(makerAddressesForPool, 'maker addresses for pool').to.include(makerAddress);
}
public async removeMakerFromStakingPoolAsync(
poolId: string,
makerAddress: string,
revertReason?: RevertReason,
): Promise<void> {
// remove maker
const txReceiptPromise = this._stakingWrapper.removeMakerFromStakingPoolAsync(
poolId,
makerAddress,
this._owner,
);
if (revertReason !== undefined) {
await expectTransactionFailedAsync(txReceiptPromise, revertReason);
return;
}
await txReceiptPromise;
// check the pool id of the maker
const poolIdOfMakerAfterRemoving = await this._stakingWrapper.getStakingPoolIdOfMakerAsync(makerAddress);
expect(poolIdOfMakerAfterRemoving, 'pool id of maker').to.be.equal(stakingConstants.NIL_POOL_ID);
// check the list of makers for the pool
const makerAddressesForPoolAfterRemoving = await this._stakingWrapper.getMakersForStakingPoolAsync(poolId);
expect(makerAddressesForPoolAfterRemoving, 'maker addresses for pool').to.not.include(makerAddress);
}
}

View File

@@ -0,0 +1,169 @@
import { expectTransactionFailedAsync } from '@0x/contracts-test-utils';
import { RevertReason } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import { StakingWrapper } from '../utils/staking_wrapper';
import { StakerBalances } from '../utils/types';
import { BaseActor } from './base_actor';
const expect = chai.expect;
export class StakerActor extends BaseActor {
constructor(owner: string, stakingWrapper: StakingWrapper) {
super(owner, stakingWrapper);
}
public async depositZrxAndMintDeactivatedStakeAsync(amount: BigNumber, revertReason?: RevertReason): Promise<void> {
await this._stakingWrapper.depositZrxAndMintDeactivatedStakeAsync(this._owner, amount);
throw new Error('Checks Unimplemented');
}
public async depositZrxAndMintActivatedStakeAsync(amount: BigNumber, revertReason?: RevertReason): Promise<void> {
// query init balances
const initZrxBalanceOfVault = await this._stakingWrapper.getZrxTokenBalanceOfZrxVaultAsync();
const initStakerBalances = await this.getBalancesAsync();
// deposit stake
const txReceiptPromise = this._stakingWrapper.depositZrxAndMintActivatedStakeAsync(this._owner, amount);
if (revertReason !== undefined) {
await expectTransactionFailedAsync(txReceiptPromise, revertReason);
return;
}
await txReceiptPromise;
// @TODO check receipt logs and return value via eth_call
// check balances
const expectedStakerBalances = initStakerBalances;
expectedStakerBalances.zrxBalance = initStakerBalances.zrxBalance.minus(amount);
expectedStakerBalances.stakeBalance = initStakerBalances.stakeBalance.plus(amount);
expectedStakerBalances.stakeBalanceInVault = initStakerBalances.stakeBalanceInVault.plus(amount);
expectedStakerBalances.activatedStakeBalance = initStakerBalances.activatedStakeBalance.plus(amount);
await this.assertBalancesAsync(expectedStakerBalances);
// check zrx balance of vault
const finalZrxBalanceOfVault = await this._stakingWrapper.getZrxTokenBalanceOfZrxVaultAsync();
expect(finalZrxBalanceOfVault).to.be.bignumber.equal(initZrxBalanceOfVault.plus(amount));
}
public async activateStakeAsync(amount: BigNumber, revertReason?: RevertReason): Promise<void> {
// query init balances
const initStakerBalances = await this.getBalancesAsync();
// activate stake
const txReceiptPromise = this._stakingWrapper.activateStakeAsync(this._owner, amount);
if (revertReason !== undefined) {
await expectTransactionFailedAsync(txReceiptPromise, revertReason);
return;
}
await txReceiptPromise;
// @TODO check receipt logs and return value via eth_call
// check balances
const expectedStakerBalances = initStakerBalances;
expectedStakerBalances.withdrawableStakeBalance = initStakerBalances.withdrawableStakeBalance.minus(amount);
expectedStakerBalances.activatableStakeBalance = initStakerBalances.activatableStakeBalance.minus(amount);
expectedStakerBalances.activatedStakeBalance = initStakerBalances.activatedStakeBalance.plus(amount);
expectedStakerBalances.deactivatedStakeBalance = initStakerBalances.deactivatedStakeBalance.minus(amount);
await this.assertBalancesAsync(expectedStakerBalances);
}
public async deactivateAndTimeLockStakeAsync(amount: BigNumber, revertReason?: RevertReason): Promise<void> {
// query init balances
const initStakerBalances = await this.getBalancesAsync();
// deactivate and timeLock stake
const txReceiptPromise = this._stakingWrapper.deactivateAndTimeLockStakeAsync(this._owner, amount);
if (revertReason !== undefined) {
await expectTransactionFailedAsync(txReceiptPromise, revertReason);
return;
}
await txReceiptPromise;
// @TODO check receipt logs and return value via eth_call
// check balances
const expectedStakerBalances = initStakerBalances;
expectedStakerBalances.activatedStakeBalance = initStakerBalances.activatedStakeBalance.minus(amount);
expectedStakerBalances.timeLockedStakeBalance = initStakerBalances.timeLockedStakeBalance.plus(amount);
expectedStakerBalances.deactivatedStakeBalance = initStakerBalances.deactivatedStakeBalance.plus(amount);
await this.assertBalancesAsync(expectedStakerBalances);
}
public async burnDeactivatedStakeAndWithdrawZrxAsync(
amount: BigNumber,
revertReason?: RevertReason,
): Promise<void> {
// query init balances
const initZrxBalanceOfVault = await this._stakingWrapper.getZrxTokenBalanceOfZrxVaultAsync();
const initStakerBalances = await this.getBalancesAsync();
// withdraw stake
const txReceiptPromise = this._stakingWrapper.burnDeactivatedStakeAndWithdrawZrxAsync(this._owner, amount);
if (revertReason !== undefined) {
await expectTransactionFailedAsync(txReceiptPromise, revertReason);
return;
}
await txReceiptPromise;
// @TODO check receipt logs and return value via eth_call
// check balances
const expectedStakerBalances = initStakerBalances;
expectedStakerBalances.zrxBalance = initStakerBalances.zrxBalance.plus(amount);
expectedStakerBalances.stakeBalance = initStakerBalances.stakeBalance.minus(amount);
expectedStakerBalances.stakeBalanceInVault = initStakerBalances.stakeBalanceInVault.minus(amount);
expectedStakerBalances.withdrawableStakeBalance = initStakerBalances.withdrawableStakeBalance.minus(amount);
expectedStakerBalances.activatableStakeBalance = initStakerBalances.activatableStakeBalance.minus(amount);
expectedStakerBalances.deactivatedStakeBalance = initStakerBalances.deactivatedStakeBalance.minus(amount);
await this.assertBalancesAsync(expectedStakerBalances);
// check zrx balance of vault
const finalZrxBalanceOfVault = await this._stakingWrapper.getZrxTokenBalanceOfZrxVaultAsync();
expect(finalZrxBalanceOfVault).to.be.bignumber.equal(initZrxBalanceOfVault.minus(amount));
}
public async getBalancesAsync(): Promise<StakerBalances> {
const stakerBalances = {
zrxBalance: await this._stakingWrapper.getZrxTokenBalanceAsync(this._owner),
stakeBalance: await this._stakingWrapper.getTotalStakeAsync(this._owner),
stakeBalanceInVault: await this._stakingWrapper.getZrxVaultBalanceAsync(this._owner),
withdrawableStakeBalance: await this._stakingWrapper.getWithdrawableStakeAsync(this._owner),
activatableStakeBalance: await this._stakingWrapper.getActivatableStakeAsync(this._owner),
activatedStakeBalance: await this._stakingWrapper.getActivatedStakeAsync(this._owner),
timeLockedStakeBalance: await this._stakingWrapper.getTimeLockedStakeAsync(this._owner),
deactivatedStakeBalance: await this._stakingWrapper.getDeactivatedStakeAsync(this._owner),
};
return stakerBalances;
}
public async assertBalancesAsync(expectedBalances: StakerBalances): Promise<void> {
const balances = await this.getBalancesAsync();
expect(balances.zrxBalance, 'zrx balance').to.be.bignumber.equal(expectedBalances.zrxBalance);
expect(balances.stakeBalance, 'stake balance').to.be.bignumber.equal(expectedBalances.stakeBalance);
expect(balances.stakeBalanceInVault, 'stake balance, recorded in vault').to.be.bignumber.equal(
expectedBalances.stakeBalanceInVault,
);
expect(balances.withdrawableStakeBalance, 'withdrawable stake balance').to.be.bignumber.equal(
expectedBalances.withdrawableStakeBalance,
);
expect(balances.activatableStakeBalance, 'activatable stake balance').to.be.bignumber.equal(
expectedBalances.activatableStakeBalance,
);
expect(balances.activatedStakeBalance, 'activated stake balance').to.be.bignumber.equal(
expectedBalances.activatedStakeBalance,
);
expect(balances.timeLockedStakeBalance, 'timeLocked stake balance').to.be.bignumber.equal(
expectedBalances.timeLockedStakeBalance,
);
expect(balances.deactivatedStakeBalance, 'deactivated stake balance').to.be.bignumber.equal(
expectedBalances.deactivatedStakeBalance,
);
}
public async forceTimeLockSyncAsync(): Promise<void> {
const initBalances = await this.getBalancesAsync();
await this._stakingWrapper.forceTimeLockSyncAsync(this._owner);
await this.assertBalancesAsync(initBalances);
}
public async skipToNextTimeLockPeriodAsync(): Promise<void> {
// query some initial values
const initBalances = await this.getBalancesAsync();
const timeLockStart = await this._stakingWrapper.getTimeLockStartAsync(this._owner);
// skip to next period
await this._stakingWrapper.skipToNextTimeLockPeriodAsync();
// validate new balances
const expectedBalances = initBalances;
const currentTimeLockPeriod = await this._stakingWrapper.getCurrentTimeLockPeriodAsync();
if (currentTimeLockPeriod.minus(timeLockStart).isGreaterThan(1)) {
expectedBalances.activatableStakeBalance = initBalances.activatableStakeBalance.plus(
initBalances.timeLockedStakeBalance,
);
expectedBalances.withdrawableStakeBalance = expectedBalances.activatableStakeBalance;
expectedBalances.timeLockedStakeBalance = new BigNumber(0);
}
await this.assertBalancesAsync(expectedBalances);
}
}

View File

@@ -0,0 +1,93 @@
import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { chaiSetup, provider, web3Wrapper } from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import { constants as stakingConstants } from './utils/constants';
import { StakingWrapper } from './utils/staking_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
// tslint:disable:no-unnecessary-type-assertion
describe('Epochs', () => {
// constants
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
// tokens & addresses
let accounts: string[];
let owner: string;
let zrxTokenContract: DummyERC20TokenContract;
let erc20ProxyContract: ERC20ProxyContract;
// wrappers
let stakingWrapper: StakingWrapper;
let erc20Wrapper: ERC20Wrapper;
// tests
before(async () => {
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
before(async () => {
// create accounts
accounts = await web3Wrapper.getAvailableAddressesAsync();
owner = accounts[0];
// deploy erc20 proxy
erc20Wrapper = new ERC20Wrapper(provider, accounts, owner);
erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
// deploy zrx token
[zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, ZRX_TOKEN_DECIMALS);
await erc20Wrapper.setBalancesAndAllowancesAsync();
// deploy staking contracts
stakingWrapper = new StakingWrapper(provider, owner, erc20ProxyContract, zrxTokenContract, accounts);
await stakingWrapper.deployAndConfigureContractsAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('Epochs & TimeLocks', () => {
it('basic epochs & timeLock periods', async () => {
///// 0/3 Validate Assumptions /////
expect(await stakingWrapper.getEpochDurationInSecondsAsync()).to.be.bignumber.equal(
stakingConstants.EPOCH_DURATION_IN_SECONDS,
);
expect(await stakingWrapper.getTimeLockDurationInEpochsAsync()).to.be.bignumber.equal(
stakingConstants.TIMELOCK_DURATION_IN_EPOCHS,
);
///// 1/3 Validate Initial Epoch & TimeLock Period /////
{
// epoch
const currentEpoch = await stakingWrapper.getCurrentEpochAsync();
expect(currentEpoch).to.be.bignumber.equal(stakingConstants.INITIAL_EPOCH);
// timeLock period
const currentTimeLockPeriod = await stakingWrapper.getCurrentTimeLockPeriodAsync();
expect(currentTimeLockPeriod).to.be.bignumber.equal(stakingConstants.INITIAL_TIMELOCK_PERIOD);
}
///// 2/3 Increment Epoch (TimeLock Should Not Increment) /////
await stakingWrapper.skipToNextEpochAsync();
{
// epoch
const currentEpoch = await stakingWrapper.getCurrentEpochAsync();
expect(currentEpoch).to.be.bignumber.equal(stakingConstants.INITIAL_EPOCH.plus(1));
// timeLock period
const currentTimeLockPeriod = await stakingWrapper.getCurrentTimeLockPeriodAsync();
expect(currentTimeLockPeriod).to.be.bignumber.equal(stakingConstants.INITIAL_TIMELOCK_PERIOD);
}
///// 3/3 Increment Epoch (TimeLock Should Increment) /////
await stakingWrapper.skipToNextTimeLockPeriodAsync();
{
// timeLock period
const currentTimeLockPeriod = await stakingWrapper.getCurrentTimeLockPeriodAsync();
expect(currentTimeLockPeriod).to.be.bignumber.equal(stakingConstants.INITIAL_TIMELOCK_PERIOD.plus(1));
}
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@@ -0,0 +1,84 @@
import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { chaiSetup, expectTransactionFailedAsync, provider, web3Wrapper } from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { RevertReason } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import { StakingWrapper } from './utils/staking_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
// tslint:disable:no-unnecessary-type-assertion
describe('Exchange Integrations', () => {
// constants
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
// tokens & addresses
let accounts: string[];
let owner: string;
let exchange: string;
let zrxTokenContract: DummyERC20TokenContract;
let erc20ProxyContract: ERC20ProxyContract;
// wrappers
let stakingWrapper: StakingWrapper;
let erc20Wrapper: ERC20Wrapper;
// tests
before(async () => {
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
before(async () => {
// create accounts
accounts = await web3Wrapper.getAvailableAddressesAsync();
owner = accounts[0];
exchange = accounts[1];
// deploy erc20 proxy
erc20Wrapper = new ERC20Wrapper(provider, accounts, owner);
erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
// deploy zrx token
[zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, ZRX_TOKEN_DECIMALS);
await erc20Wrapper.setBalancesAndAllowancesAsync();
// deploy staking contracts
stakingWrapper = new StakingWrapper(provider, owner, erc20ProxyContract, zrxTokenContract, accounts);
await stakingWrapper.deployAndConfigureContractsAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('Exchange Tracking in Staking Contract', () => {
it('basic exchange tracking', async () => {
// 1 try querying an invalid addresses
const invalidAddress = '0x0000000000000000000000000000000000000001';
const isInvalidAddressValid = await stakingWrapper.isValidExchangeAddressAsync(invalidAddress);
expect(isInvalidAddressValid).to.be.false();
// 2 add valid address
await stakingWrapper.addExchangeAddressAsync(exchange);
const isValidAddressValid = await stakingWrapper.isValidExchangeAddressAsync(exchange);
expect(isValidAddressValid).to.be.true();
// 3 try adding valid address again
await expectTransactionFailedAsync(
stakingWrapper.addExchangeAddressAsync(exchange),
RevertReason.ExchangeAddressAlreadyRegistered,
);
// 4 remove valid address
await stakingWrapper.removeExchangeAddressAsync(exchange);
const isValidAddressStillValid = await stakingWrapper.isValidExchangeAddressAsync(exchange);
expect(isValidAddressStillValid).to.be.false();
// 5 try removing valid address again
await expectTransactionFailedAsync(
stakingWrapper.removeExchangeAddressAsync(exchange),
RevertReason.ExchangeAddressNotRegistered,
);
// @todo should not be able to add / remove an exchange if not contract owner.
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@@ -0,0 +1,184 @@
import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { chaiSetup, provider, web3Wrapper } from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import { StakingWrapper } from './utils/staking_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
// tslint:disable:no-unnecessary-type-assertion
describe('Math Libraries', () => {
// constants
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
// tokens & addresses
let accounts: string[];
let owner: string;
let zrxTokenContract: DummyERC20TokenContract;
let erc20ProxyContract: ERC20ProxyContract;
// wrappers
let stakingWrapper: StakingWrapper;
let erc20Wrapper: ERC20Wrapper;
// tests
before(async () => {
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
before(async () => {
// create accounts
accounts = await web3Wrapper.getAvailableAddressesAsync();
owner = accounts[0];
// deploy erc20 proxy
erc20Wrapper = new ERC20Wrapper(provider, accounts, owner);
erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
// deploy zrx token
[zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, ZRX_TOKEN_DECIMALS);
await erc20Wrapper.setBalancesAndAllowancesAsync();
// deploy staking contracts
stakingWrapper = new StakingWrapper(provider, owner, erc20ProxyContract, zrxTokenContract, accounts);
await stakingWrapper.deployAndConfigureContractsAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('LibFeesMath', () => {
it('nth root', async () => {
const base = new BigNumber(1419857);
const n = new BigNumber(5);
const root = await stakingWrapper.nthRootAsync(base, n);
expect(root).to.be.bignumber.equal(17);
});
it('nth root #2', async () => {
const base = new BigNumber(3375);
const n = new BigNumber(3);
const root = await stakingWrapper.nthRootAsync(base, n);
expect(root).to.be.bignumber.equal(15);
});
it('nth root #3 with fixed point', async () => {
const decimals = 18;
const base = StakingWrapper.toFixedPoint(4.234, decimals);
const n = new BigNumber(2);
const root = await stakingWrapper.nthRootFixedPointAsync(base, n);
const rootAsFloatingPoint = StakingWrapper.toFloatingPoint(root, decimals);
const expectedResult = new BigNumber(2.057668584);
expect(rootAsFloatingPoint).to.be.bignumber.equal(expectedResult);
});
it('nth root #3 with fixed point (integer nth root would fail here)', async () => {
const decimals = 18;
const base = StakingWrapper.toFixedPoint(5429503678976, decimals);
const n = new BigNumber(9);
const root = await stakingWrapper.nthRootFixedPointAsync(base, n);
const rootAsFloatingPoint = StakingWrapper.toFloatingPoint(root, decimals);
const expectedResult = new BigNumber(26);
expect(rootAsFloatingPoint).to.be.bignumber.equal(expectedResult);
});
it.skip('nth root #4 with fixed point (integer nth root would fail here) (max number of decimals - currently does not retain)', async () => {
// @TODO This is the gold standard for nth root. Retain all these decimals :)
const decimals = 18;
const base = StakingWrapper.toFixedPoint(new BigNumber('5429503678976.295036789761543678', 10), decimals);
const n = new BigNumber(9);
const root = await stakingWrapper.nthRootFixedPointAsync(base, n);
const rootAsFloatingPoint = StakingWrapper.toFloatingPoint(root, decimals);
const expectedResult = new BigNumber(26);
expect(rootAsFloatingPoint).to.be.bignumber.equal(expectedResult);
});
it('cobb douglas - approximate', async () => {
const totalRewards = StakingWrapper.toBaseUnitAmount(57.154398);
const ownerFees = StakingWrapper.toBaseUnitAmount(5.64375);
const totalFees = StakingWrapper.toBaseUnitAmount(29.00679);
const ownerStake = StakingWrapper.toBaseUnitAmount(56);
const totalStake = StakingWrapper.toBaseUnitAmount(10906);
const alphaNumerator = new BigNumber(3);
const alphaDenominator = new BigNumber(7);
// create expected output
// https://www.wolframalpha.com/input/?i=57.154398+*+(5.64375%2F29.00679)+%5E+(3%2F7)+*+(56+%2F+10906)+%5E+(1+-+3%2F7)
const expectedOwnerReward = new BigNumber(1.3934);
// run computation
const ownerReward = await stakingWrapper.cobbDouglasAsync(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaNumerator,
alphaDenominator,
);
const ownerRewardFloatingPoint = StakingWrapper.trimFloat(
StakingWrapper.toFloatingPoint(ownerReward, 18),
4,
);
// validation
expect(ownerRewardFloatingPoint).to.be.bignumber.equal(expectedOwnerReward);
});
it('cobb douglas - simplified (alpha = 1/x)', async () => {
// setup test parameters
const totalRewards = StakingWrapper.toBaseUnitAmount(57.154398);
const ownerFees = StakingWrapper.toBaseUnitAmount(5.64375);
const totalFees = StakingWrapper.toBaseUnitAmount(29.00679);
const ownerStake = StakingWrapper.toBaseUnitAmount(56);
const totalStake = StakingWrapper.toBaseUnitAmount(10906);
const alphaDenominator = new BigNumber(3);
// create expected output
// https://www.wolframalpha.com/input/?i=57.154398+*+(5.64375%2F29.00679)+%5E+(1%2F3)+*+(56+%2F+10906)+%5E+(1+-+1%2F3)
const expectedOwnerReward = new BigNumber(0.98572107681878);
// run computation
const ownerReward = await stakingWrapper.cobbDouglasSimplifiedAsync(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaDenominator,
);
const ownerRewardFloatingPoint = StakingWrapper.trimFloat(
StakingWrapper.toFloatingPoint(ownerReward, 18),
14,
);
// validation
expect(ownerRewardFloatingPoint).to.be.bignumber.equal(expectedOwnerReward);
});
it('cobb douglas - simplified inverse (1 - alpha = 1/x)', async () => {
const totalRewards = StakingWrapper.toBaseUnitAmount(57.154398);
const ownerFees = StakingWrapper.toBaseUnitAmount(5.64375);
const totalFees = StakingWrapper.toBaseUnitAmount(29.00679);
const ownerStake = StakingWrapper.toBaseUnitAmount(56);
const totalStake = StakingWrapper.toBaseUnitAmount(10906);
const inverseAlphaDenominator = new BigNumber(3);
// create expected output
// https://www.wolframalpha.com/input/?i=57.154398+*+(5.64375%2F29.00679)+%5E+(2%2F3)+*+(56+%2F+10906)+%5E+(1+-+2%2F3)
const expectedOwnerReward = new BigNumber(3.310822494188);
// run computation
const ownerReward = await stakingWrapper.cobbDouglasSimplifiedInverseAsync(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
inverseAlphaDenominator,
);
const ownerRewardFloatingPoint = StakingWrapper.trimFloat(
StakingWrapper.toFloatingPoint(ownerReward, 18),
12,
);
// validation
expect(ownerRewardFloatingPoint).to.be.bignumber.equal(expectedOwnerReward);
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@@ -0,0 +1,271 @@
import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { chaiSetup, expectTransactionFailedAsync, provider, web3Wrapper } from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { RevertReason } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as ethUtil from 'ethereumjs-util';
import * as _ from 'lodash';
import { MakerActor } from './actors/maker_actor';
import { PoolOperatorActor } from './actors/pool_operator_actor';
import { constants as stakingConstants } from './utils/constants';
import { StakingWrapper } from './utils/staking_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
// tslint:disable:no-unnecessary-type-assertion
describe('Staking Pool Management', () => {
// constants
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
// tokens & addresses
let accounts: string[];
let owner: string;
let users: string[];
let zrxTokenContract: DummyERC20TokenContract;
let erc20ProxyContract: ERC20ProxyContract;
// wrappers
let stakingWrapper: StakingWrapper;
let erc20Wrapper: ERC20Wrapper;
// tests
before(async () => {
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
before(async () => {
// create accounts
accounts = await web3Wrapper.getAvailableAddressesAsync();
owner = accounts[0];
users = accounts.slice(1);
// deploy erc20 proxy
erc20Wrapper = new ERC20Wrapper(provider, accounts, owner);
erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
// deploy zrx token
[zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, ZRX_TOKEN_DECIMALS);
await erc20Wrapper.setBalancesAndAllowancesAsync();
// deploy staking contracts
stakingWrapper = new StakingWrapper(provider, owner, erc20ProxyContract, zrxTokenContract, accounts);
await stakingWrapper.deployAndConfigureContractsAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('Staking Pool Management', () => {
it('Should successfully create a pool', async () => {
// test parameters
const operatorAddress = users[0];
const operatorShare = 39;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
// create pool
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
expect(poolId).to.be.equal(stakingConstants.INITIAL_POOL_ID);
// check that the next pool id was incremented
const expectedNextPoolId = '0x0000000000000000000000000000000200000000000000000000000000000000';
const nextPoolId = await stakingWrapper.getNextStakingPoolIdAsync();
expect(nextPoolId).to.be.equal(expectedNextPoolId);
});
it('Should successfully add/remove a maker to a pool', async () => {
// test parameters
const operatorAddress = users[0];
const operatorShare = 39;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
const makerAddress = users[1];
const maker = new MakerActor(makerAddress, stakingWrapper);
// create pool
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
expect(poolId).to.be.equal(stakingConstants.INITIAL_POOL_ID);
// add maker to pool
const makerApproval = maker.signApprovalForStakingPool(poolId);
await poolOperator.addMakerToStakingPoolAsync(poolId, makerAddress, makerApproval.signature);
// remove maker from pool
await poolOperator.removeMakerFromStakingPoolAsync(poolId, makerAddress);
});
it('Should successfully add/remove multipler makers to the same pool', async () => {
// test parameters
const operatorAddress = users[0];
const operatorShare = 39;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
const makerAddresses = users.slice(1, 4);
const makers = [
new MakerActor(makerAddresses[0], stakingWrapper),
new MakerActor(makerAddresses[1], stakingWrapper),
new MakerActor(makerAddresses[2], stakingWrapper),
];
// create pool
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
expect(poolId).to.be.equal(stakingConstants.INITIAL_POOL_ID);
// add makers to pool
const makerApprovals = [
makers[0].signApprovalForStakingPool(poolId),
makers[1].signApprovalForStakingPool(poolId),
makers[2].signApprovalForStakingPool(poolId),
];
await poolOperator.addMakerToStakingPoolAsync(poolId, makerAddresses[0], makerApprovals[0].signature);
await poolOperator.addMakerToStakingPoolAsync(poolId, makerAddresses[1], makerApprovals[1].signature);
await poolOperator.addMakerToStakingPoolAsync(poolId, makerAddresses[2], makerApprovals[2].signature);
// remove maker from pool
await poolOperator.removeMakerFromStakingPoolAsync(poolId, makerAddresses[0]);
await poolOperator.removeMakerFromStakingPoolAsync(poolId, makerAddresses[1]);
await poolOperator.removeMakerFromStakingPoolAsync(poolId, makerAddresses[2]);
});
it('Should fail to add the same maker twice', async () => {
// test parameters
const operatorAddress = users[0];
const operatorShare = 39;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
const makerAddress = users[1];
const maker = new MakerActor(makerAddress, stakingWrapper);
// create pool
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
expect(poolId).to.be.equal(stakingConstants.INITIAL_POOL_ID);
// add maker to pool
const makerApproval = maker.signApprovalForStakingPool(poolId);
await poolOperator.addMakerToStakingPoolAsync(poolId, makerAddress, makerApproval.signature);
// add same maker to pool again
await poolOperator.addMakerToStakingPoolAsync(
poolId,
makerAddress,
makerApproval.signature,
RevertReason.MakerAddressAlreadyRegistered,
);
});
it('Should fail to remove a maker that does not exist', async () => {
// test parameters
const operatorAddress = users[0];
const operatorShare = 39;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
const makerAddress = users[1];
// create pool
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
expect(poolId).to.be.equal(stakingConstants.INITIAL_POOL_ID);
// remove non-existent maker from pool
await poolOperator.removeMakerFromStakingPoolAsync(
poolId,
makerAddress,
RevertReason.MakerAddressNotRegistered,
);
});
it('Should fail to add a maker who signed with the wrong private key', async () => {
// test parameters
const operatorAddress = users[0];
const operatorShare = 39;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
const makerAddress = users[1];
const badMakerPrivateKey = ethUtil.toBuffer(
'0x0000000000000000000000000000000000000000000000000000000000000001',
);
const maker = new MakerActor(makerAddress, stakingWrapper, badMakerPrivateKey);
// create pool
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
expect(poolId).to.be.equal(stakingConstants.INITIAL_POOL_ID);
// add maker to pool
const makerApproval = maker.signApprovalForStakingPool(poolId);
await poolOperator.addMakerToStakingPoolAsync(
poolId,
makerAddress,
makerApproval.signature,
RevertReason.InvalidMakerSignature,
);
});
it('Should fail to add a maker who signed with the wrong staking contract address', async () => {
// test parameters
const operatorAddress = users[0];
const operatorShare = 39;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
const makerAddress = users[1];
const forceMakerKeyLookup = undefined;
const notStakingContractAddress = users[2];
const maker = new MakerActor(makerAddress, stakingWrapper, forceMakerKeyLookup, notStakingContractAddress);
// create pool
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
expect(poolId).to.be.equal(stakingConstants.INITIAL_POOL_ID);
// add maker to pool
const makerApproval = maker.signApprovalForStakingPool(poolId);
await poolOperator.addMakerToStakingPoolAsync(
poolId,
makerAddress,
makerApproval.signature,
RevertReason.InvalidMakerSignature,
);
});
it('Should fail to add a maker who signed with the wrong chain id', async () => {
// test parameters
const operatorAddress = users[0];
const operatorShare = 39;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
const makerAddress = users[1];
const forceMakerKeyLookup = undefined;
const forceStakingContractLookup = undefined;
const badChainId = 209348;
const maker = new MakerActor(
makerAddress,
stakingWrapper,
forceMakerKeyLookup,
forceStakingContractLookup,
badChainId,
);
// create pool
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
expect(poolId).to.be.equal(stakingConstants.INITIAL_POOL_ID);
// add maker to pool
const makerApproval = maker.signApprovalForStakingPool(poolId);
await poolOperator.addMakerToStakingPoolAsync(
poolId,
makerAddress,
makerApproval.signature,
RevertReason.InvalidMakerSignature,
);
});
it('Should fail to add a maker when called by someone other than the pool operator', async () => {
// test parameters
const operatorAddress = users[0];
const operatorShare = 39;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
const makerAddress = users[1];
const maker = new MakerActor(makerAddress, stakingWrapper);
const notOperatorAddress = users[2];
// create pool
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
expect(poolId).to.be.equal(stakingConstants.INITIAL_POOL_ID);
// add maker to pool
const makerApproval = maker.signApprovalForStakingPool(poolId);
await expectTransactionFailedAsync(
stakingWrapper.addMakerToStakingPoolAsync(
poolId,
makerAddress,
makerApproval.signature,
notOperatorAddress,
),
RevertReason.OnlyCallableByPoolOperator,
);
});
it('Should fail to remove a maker when called by someone other than the pool operator', async () => {
// test parameters
const operatorAddress = users[0];
const operatorShare = 39;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingWrapper);
const makerAddress = users[1];
const maker = new MakerActor(makerAddress, stakingWrapper);
const notOperatorAddress = users[2];
// create pool
const poolId = await poolOperator.createStakingPoolAsync(operatorShare);
expect(poolId).to.be.equal(stakingConstants.INITIAL_POOL_ID);
// add maker to pool
const makerApproval = maker.signApprovalForStakingPool(poolId);
await poolOperator.addMakerToStakingPoolAsync(poolId, makerAddress, makerApproval.signature);
// try to remove the maker address from an address other than the operator
await expectTransactionFailedAsync(
stakingWrapper.removeMakerFromStakingPoolAsync(poolId, makerAddress, notOperatorAddress),
RevertReason.OnlyCallableByPoolOperatorOrMaker,
);
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@@ -0,0 +1,358 @@
import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { chaiSetup, expectTransactionFailedAsync, provider, web3Wrapper } from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { RevertReason } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { Simulation } from './utils/Simulation';
import { StakingWrapper } from './utils/staking_wrapper';
chaiSetup.configure();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
// tslint:disable:no-unnecessary-type-assertion
describe('End-To-End Simulations', () => {
// constants
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
// tokens & addresses
let accounts: string[];
let owner: string;
let exchange: string;
let users: string[];
let zrxTokenContract: DummyERC20TokenContract;
let erc20ProxyContract: ERC20ProxyContract;
// wrappers
let stakingWrapper: StakingWrapper;
let erc20Wrapper: ERC20Wrapper;
// tests
before(async () => {
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
before(async () => {
// create accounts
accounts = await web3Wrapper.getAvailableAddressesAsync();
owner = accounts[0];
exchange = accounts[1];
users = accounts.slice(2);
users = [...users, ...users]; // @TODO figure out how to get more addresses from `web3Wrapper`
// deploy erc20 proxy
erc20Wrapper = new ERC20Wrapper(provider, accounts, owner);
erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
// deploy zrx token
[zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, ZRX_TOKEN_DECIMALS);
await erc20Wrapper.setBalancesAndAllowancesAsync();
// deploy staking contracts
stakingWrapper = new StakingWrapper(provider, owner, erc20ProxyContract, zrxTokenContract, accounts);
await stakingWrapper.deployAndConfigureContractsAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('Simulations', () => {
it('Should successfully simulate (no delegators / no shadow balances)', async () => {
// @TODO - get computations more accurate
const simulationParams = {
users,
numberOfPools: 3,
poolOperatorShares: [100, 100, 100],
stakeByPoolOperator: [
StakingWrapper.toBaseUnitAmount(42),
StakingWrapper.toBaseUnitAmount(84),
StakingWrapper.toBaseUnitAmount(97),
],
numberOfMakers: 6,
numberOfMakersPerPool: [1, 2, 3],
protocolFeesByMaker: [
// pool 1
StakingWrapper.toBaseUnitAmount(0.304958),
// pool 2
StakingWrapper.toBaseUnitAmount(3.2),
StakingWrapper.toBaseUnitAmount(12.123258),
// pool 3
StakingWrapper.toBaseUnitAmount(23.577),
StakingWrapper.toBaseUnitAmount(4.54522236),
StakingWrapper.toBaseUnitAmount(0),
],
numberOfDelegators: 0,
numberOfDelegatorsPerPool: [0, 0, 0],
stakeByDelegator: [],
delegateInNextEpoch: false, // no shadow eth
withdrawByUndelegating: false, // profits are withdrawn without undelegating
expectedFeesByPool: [
StakingWrapper.toBaseUnitAmount(0.304958),
StakingWrapper.toBaseUnitAmount(15.323258),
StakingWrapper.toBaseUnitAmount(28.12222236),
],
expectedPayoutByPool: [
new BigNumber('4.75677'), // 4.756772362932728793619590327361600155564384201215274334070
new BigNumber('16.28130'), // 16.28130500394935316563988584956596823402223838026190634525
new BigNumber('20.31028'), // 20.31028447343014834523983759032242063760612769662934308289
],
expectedPayoutByPoolOperator: [
new BigNumber('4.75677'), // 4.756772362932728793619590327361600155564384201215274334070
new BigNumber('16.28130'), // 16.28130500394935316563988584956596823402223838026190634525
new BigNumber('20.31028'), // 20.31028447343014834523983759032242063760612769662934308289
],
expectedMembersPayoutByPool: [new BigNumber('0'), new BigNumber('0'), new BigNumber('0')],
expectedPayoutByDelegator: [],
exchangeAddress: exchange,
};
const simulator = new Simulation(stakingWrapper, simulationParams);
await simulator.runAsync();
});
it('Should successfully simulate (delegators withdraw by undeleating / no shadow balances)', async () => {
// @TODO - get computations more accurate
/*
\ // the expected payouts were computed by hand
// @TODO - get computations more accurate
Pool | Total Fees | Total Stake | Total Delegated Stake | Total Stake (Weighted) | Payout
0 | 0.304958 | 42 | 0 | 42 | 3.0060373...
1 | 15.323258 | 84 | 0 | 84 |
3 | 28.12222236 | 97 | 182 | 260.8
...
Cumulative Fees = 43.75043836
Cumulative Weighted Stake = 386.8
Total Rewards = 43.75043836
*/
const simulationParams = {
users,
numberOfPools: 3,
poolOperatorShares: [39, 59, 43],
stakeByPoolOperator: [
StakingWrapper.toBaseUnitAmount(42),
StakingWrapper.toBaseUnitAmount(84),
StakingWrapper.toBaseUnitAmount(97),
],
numberOfMakers: 6,
numberOfMakersPerPool: [1, 2, 3],
protocolFeesByMaker: [
// pool 1
StakingWrapper.toBaseUnitAmount(0.304958),
// pool 2
StakingWrapper.toBaseUnitAmount(3.2),
StakingWrapper.toBaseUnitAmount(12.123258),
// pool 3
StakingWrapper.toBaseUnitAmount(23.577),
StakingWrapper.toBaseUnitAmount(4.54522236),
StakingWrapper.toBaseUnitAmount(0),
],
numberOfDelegators: 3,
numberOfDelegatorsPerPool: [0, 0, 3],
stakeByDelegator: [
StakingWrapper.toBaseUnitAmount(17),
StakingWrapper.toBaseUnitAmount(75),
StakingWrapper.toBaseUnitAmount(90),
],
delegateInNextEpoch: false, // delegated stake is included in payout computation + no shadow ether
withdrawByUndelegating: false, // profits are withdrawn without undelegating
expectedFeesByPool: [
StakingWrapper.toBaseUnitAmount(0.304958),
StakingWrapper.toBaseUnitAmount(15.323258),
StakingWrapper.toBaseUnitAmount(28.12222236),
],
expectedPayoutByPool: [
new BigNumber('3.00603'), // 3.006037310109530277237724562632303034914024715508955780682
new BigNumber('10.28895'), // 10.28895363598396754741643198605226143579652264694121578135
new BigNumber('29.26472'), // 29.26473180250053106672049765968527817034954761113582833460
],
expectedPayoutByPoolOperator: [
new BigNumber('1.17235'), // 0.39 * 3.00603
new BigNumber('6.07048'), // 0.59 * 10.28895
new BigNumber('12.58383'), // 0.43 * 29.26472
],
expectedMembersPayoutByPool: [
new BigNumber('1.83368'), // (1 - 0.39) * 3.00603
new BigNumber('4.21847'), // (1 - 0.59) * 10.28895
new BigNumber('16.68089'), // (1 - 0.43) * 29.26472
],
expectedPayoutByDelegator: [
// note that the on-chain values may be slightly different due to rounding down on each entry
// there is a carry over between calls, which we account for here. the result is that delegators
// who withdraw later on will scoop up any rounding spillover from those who have already withdrawn.
new BigNumber('1.55810'), // (17 / 182) * 16.6809
new BigNumber('6.87399'), // (75 / 182) * 16.6809
new BigNumber('8.24879'), // (90 / 182) * 16.6809
],
exchangeAddress: exchange,
};
const simulator = new Simulation(stakingWrapper, simulationParams);
await simulator.runAsync();
});
it('Should successfully simulate (delegators withdraw by undelegating / includes shadow balances / delegators enter after reward payouts)', async () => {
// @TODO - get computations more accurate
/*
Pool | Total Fees | Total Stake | Total Delegated Stake | Total Stake (Scaled)
0 | 0.304958 | 42 | 0 | 42
1 | 15.323258 | 84 | 0 | 84
3 | 28.12222236 | 97 | 182 | 260.8
...
Cumulative Fees = 43.75043836
Cumulative Weighted Stake = 386.8
Total Rewards = 43.75043836
// In this case, there was already a pot of ETH in the delegator pool that nobody had claimed.
// The first delegator got to claim it all. This is due to the necessary conservation of payouts.
// When a new delegator arrives, their new stake should not affect existing delegator payouts.
// In this case, there was unclaimed $$ in the delegator pool - which is claimed by the first delegator.
*/
const simulationParams = {
users,
numberOfPools: 3,
poolOperatorShares: [39, 59, 43],
stakeByPoolOperator: [
StakingWrapper.toBaseUnitAmount(42),
StakingWrapper.toBaseUnitAmount(84),
StakingWrapper.toBaseUnitAmount(97),
],
numberOfMakers: 6,
numberOfMakersPerPool: [1, 2, 3],
protocolFeesByMaker: [
// pool 1
StakingWrapper.toBaseUnitAmount(0.304958),
// pool 2
StakingWrapper.toBaseUnitAmount(3.2),
StakingWrapper.toBaseUnitAmount(12.123258),
// pool 3
StakingWrapper.toBaseUnitAmount(23.577),
StakingWrapper.toBaseUnitAmount(4.54522236),
StakingWrapper.toBaseUnitAmount(0),
],
numberOfDelegators: 3,
numberOfDelegatorsPerPool: [0, 0, 3],
stakeByDelegator: [
StakingWrapper.toBaseUnitAmount(17),
StakingWrapper.toBaseUnitAmount(75),
StakingWrapper.toBaseUnitAmount(90),
],
delegateInNextEpoch: true, // delegated stake is included in payout computation + forces shadow eth
withdrawByUndelegating: true, // profits are withdrawn as result of undelegating
expectedFeesByPool: [
StakingWrapper.toBaseUnitAmount(0.304958),
StakingWrapper.toBaseUnitAmount(15.323258),
StakingWrapper.toBaseUnitAmount(28.12222236),
],
expectedPayoutByPool: [
new BigNumber('4.75677'), // 4.756772362932728793619590327361600155564384201215274334070
new BigNumber('16.28130'), // 16.28130500394935316563988584956596823402223838026190634525
new BigNumber('20.31028'), // 20.31028447343014834523983759032242063760612769662934308289
],
expectedPayoutByPoolOperator: [
new BigNumber('1.85514'), // 0.39 * 4.75677
new BigNumber('9.60597'), // 0.59 * 16.28130
new BigNumber('8.73342'), // 0.43 * 20.31028
],
expectedMembersPayoutByPool: [
new BigNumber('2.90163'), // (1 - 0.39) * 4.75677
new BigNumber('6.67533'), // (1 - 0.59) * 16.28130
new BigNumber('11.57686'), // (1 - 0.43) * 20.31028
],
expectedPayoutByDelegator: [
new BigNumber('11.57686'), // (1 - 0.43) * 20.31028
new BigNumber(0),
new BigNumber(0),
],
exchangeAddress: exchange,
};
const simulator = new Simulation(stakingWrapper, simulationParams);
await simulator.runAsync();
});
it('Should successfully simulate (delegators withdraw without undelegating / includes shadow balances / delegators enter after reward payouts)', async () => {
// @TODO - get computations more accurate
/*
Pool | Total Fees | Total Stake | Total Delegated Stake | Total Stake (Scaled)
0 | 0.304958 | 42 | 0 | 42
1 | 15.323258 | 84 | 0 | 84
3 | 28.12222236 | 97 | 182 | 260.8
...
Cumulative Fees = 43.75043836
Cumulative Weighted Stake = 386.8
Total Rewards = 43.75043836
// In this case, there was already a pot of ETH in the delegator pool that nobody had claimed.
// The first delegator got to claim it all. This is due to the necessary conservation of payouts.
// When a new delegator arrives, their new stake should not affect existing delegator payouts.
// In this case, there was unclaimed $$ in the delegator pool - which is claimed by the first delegator.
*/
const simulationParams = {
users,
numberOfPools: 3,
poolOperatorShares: [39, 59, 43],
stakeByPoolOperator: [
StakingWrapper.toBaseUnitAmount(42),
StakingWrapper.toBaseUnitAmount(84),
StakingWrapper.toBaseUnitAmount(97),
],
numberOfMakers: 6,
numberOfMakersPerPool: [1, 2, 3],
protocolFeesByMaker: [
// pool 1
StakingWrapper.toBaseUnitAmount(0.304958),
// pool 2
StakingWrapper.toBaseUnitAmount(3.2),
StakingWrapper.toBaseUnitAmount(12.123258),
// pool 3
StakingWrapper.toBaseUnitAmount(23.577),
StakingWrapper.toBaseUnitAmount(4.54522236),
StakingWrapper.toBaseUnitAmount(0),
],
numberOfDelegators: 3,
numberOfDelegatorsPerPool: [0, 0, 3],
stakeByDelegator: [
StakingWrapper.toBaseUnitAmount(17),
StakingWrapper.toBaseUnitAmount(75),
StakingWrapper.toBaseUnitAmount(90),
],
delegateInNextEpoch: true, // delegated stake is included in payout computation + forces shadow eth
withdrawByUndelegating: false, // profits are withdrawn without undelegating
expectedFeesByPool: [
StakingWrapper.toBaseUnitAmount(0.304958),
StakingWrapper.toBaseUnitAmount(15.323258),
StakingWrapper.toBaseUnitAmount(28.12222236),
],
expectedPayoutByPool: [
new BigNumber('4.75677'), // 4.756772362932728793619590327361600155564384201215274334070
new BigNumber('16.28130'), // 16.28130500394935316563988584956596823402223838026190634525
new BigNumber('20.31028'), // 20.31028447343014834523983759032242063760612769662934308289
],
expectedPayoutByPoolOperator: [
new BigNumber('1.85514'), // 0.39 * 4.75677
new BigNumber('9.60597'), // 0.59 * 16.28130
new BigNumber('8.73342'), // 0.43 * 20.31028
],
expectedMembersPayoutByPool: [
new BigNumber('2.90163'), // (1 - 0.39) * 4.75677
new BigNumber('6.67533'), // (1 - 0.59) * 16.28130
new BigNumber('11.57686'), // (1 - 0.43) * 20.31028
],
expectedPayoutByDelegator: [
new BigNumber('11.57686'), // (1 - 0.43) * 20.31028
new BigNumber(0),
new BigNumber(0),
],
exchangeAddress: exchange,
};
const simulator = new Simulation(stakingWrapper, simulationParams);
await simulator.runAsync();
});
it('Should not be able to record a protocol fee from an unknown exchange', async () => {
const makerAddress = users[1];
const protocolFee = new BigNumber(1);
await expectTransactionFailedAsync(
stakingWrapper.payProtocolFeeAsync(makerAddress, protocolFee, owner),
RevertReason.OnlyCallableByExchange,
);
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@@ -0,0 +1,111 @@
import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { chaiSetup, provider, web3Wrapper } from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { RevertReason } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { DelegatorActor } from './actors/delegator_actor';
import { StakerActor } from './actors/staker_actor';
import { StakingWrapper } from './utils/staking_wrapper';
chaiSetup.configure();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
// tslint:disable:no-unnecessary-type-assertion
describe('Staking & Delegating', () => {
// constants
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
// tokens & addresses
let accounts: string[];
let owner: string;
let stakers: string[];
let zrxTokenContract: DummyERC20TokenContract;
let erc20ProxyContract: ERC20ProxyContract;
// wrappers
let stakingWrapper: StakingWrapper;
let erc20Wrapper: ERC20Wrapper;
// tests
before(async () => {
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
before(async () => {
// create accounts
accounts = await web3Wrapper.getAvailableAddressesAsync();
owner = accounts[0];
stakers = accounts.slice(2, 5);
// deploy erc20 proxy
erc20Wrapper = new ERC20Wrapper(provider, accounts, owner);
erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
// deploy zrx token
[zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, ZRX_TOKEN_DECIMALS);
await erc20Wrapper.setBalancesAndAllowancesAsync();
// deploy staking contracts
stakingWrapper = new StakingWrapper(provider, owner, erc20ProxyContract, zrxTokenContract, accounts);
await stakingWrapper.deployAndConfigureContractsAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('Staking', () => {
it('basic staking/unstaking', async () => {
// setup test parameters
const amountToStake = StakingWrapper.toBaseUnitAmount(10);
const amountToDeactivate = StakingWrapper.toBaseUnitAmount(4);
const amountToReactivate = StakingWrapper.toBaseUnitAmount(1);
const amountToWithdraw = StakingWrapper.toBaseUnitAmount(1.5);
// run test - this actor will validate its own state
const staker = new StakerActor(stakers[0], stakingWrapper);
await staker.depositZrxAndMintActivatedStakeAsync(amountToStake);
await staker.deactivateAndTimeLockStakeAsync(amountToDeactivate);
// note - we cannot re-activate this timeLocked stake until at least one full timeLock period has passed.
// attempting to do so should revert.
await staker.activateStakeAsync(amountToReactivate, RevertReason.InsufficientBalance);
await staker.skipToNextTimeLockPeriodAsync();
await staker.activateStakeAsync(amountToReactivate, RevertReason.InsufficientBalance);
await staker.skipToNextTimeLockPeriodAsync();
// this forces the internal state to update; it is not necessary to activate stake, but
// allows us to check that state is updated correctly after a timeLock period rolls over.
await staker.forceTimeLockSyncAsync();
// now we can activate stake
await staker.activateStakeAsync(amountToReactivate);
await staker.burnDeactivatedStakeAndWithdrawZrxAsync(amountToWithdraw);
});
});
describe('Delegating', () => {
it('basic delegating/undelegating', async () => {
// setup test parameters
const amountToDelegate = StakingWrapper.toBaseUnitAmount(10);
const amountToDeactivate = StakingWrapper.toBaseUnitAmount(4);
const amountToReactivate = StakingWrapper.toBaseUnitAmount(1);
const amountToWithdraw = StakingWrapper.toBaseUnitAmount(1.5);
const poolOperator = stakers[1];
const operatorShare = 39;
const poolId = await stakingWrapper.createStakingPoolAsync(poolOperator, operatorShare);
// run test
const delegator = new DelegatorActor(stakers[0], stakingWrapper);
await delegator.depositZrxAndDelegateToStakingPoolAsync(poolId, amountToDelegate);
await delegator.deactivateAndTimeLockDelegatedStakeAsync(poolId, amountToDeactivate);
// note - we cannot re-activate this timeLocked stake until at least one full timeLock period has passed.
// attempting to do so should revert.
await delegator.activateStakeAsync(amountToReactivate, RevertReason.InsufficientBalance);
await delegator.skipToNextTimeLockPeriodAsync();
await delegator.activateStakeAsync(amountToReactivate, RevertReason.InsufficientBalance);
await delegator.skipToNextTimeLockPeriodAsync();
// this forces the internal state to update; it is not necessary to activate stake, but
// allows us to check that state is updated correctly after a timeLock period rolls over.
await delegator.forceTimeLockSyncAsync();
// now we can activate stake
await delegator.activateAndDelegateStakeAsync(poolId, amountToReactivate);
await delegator.burnDeactivatedStakeAndWithdrawZrxAsync(amountToWithdraw);
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@@ -0,0 +1,274 @@
import { chaiSetup } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import { DelegatorActor } from '../actors/delegator_actor';
import { MakerActor } from '../actors/maker_actor';
import { PoolOperatorActor } from '../actors/pool_operator_actor';
import { Queue } from './queue';
import { StakingWrapper } from './staking_wrapper';
import { SimulationParams } from './types';
chaiSetup.configure();
const expect = chai.expect;
export class Simulation {
private readonly _stakingWrapper: StakingWrapper;
private readonly _p: SimulationParams;
private _userQueue: Queue<string>;
private readonly _poolOperators: PoolOperatorActor[];
private readonly _poolOperatorsAsDelegators: DelegatorActor[];
private readonly _poolIds: string[];
private readonly _makers: MakerActor[];
private readonly _delegators: DelegatorActor[];
constructor(stakingWrapper: StakingWrapper, simulationParams: SimulationParams) {
this._stakingWrapper = stakingWrapper;
this._p = simulationParams;
this._userQueue = new Queue<string>();
this._poolOperators = [];
this._poolOperatorsAsDelegators = [];
this._poolIds = [];
this._makers = [];
this._delegators = [];
}
public async runAsync(): Promise<void> {
this._userQueue = new Queue<string>(this._p.users);
await this._stakingWrapper.addExchangeAddressAsync(this._p.exchangeAddress);
await this._setupPoolsAsync(this._p);
await this._setupMakersAsync(this._p);
await this._payProtocolFeesAsync(this._p);
if (this._p.delegateInNextEpoch) {
// this property forces the staking contracts to use shadow ether
await this._stakingWrapper.skipToNextEpochAsync();
}
await this._setupDelegatorsAsync(this._p);
await this._stakingWrapper.skipToNextEpochAsync();
// everyone has been paid out into the vault. check balances.
await this._assertVaultBalancesAsync(this._p);
await this._withdrawRewardForStakingPoolMemberForOperatorsAsync(this._p);
if (this._p.withdrawByUndelegating) {
await this._withdrawRewardForStakingPoolMemberForDelegatorsAsync(this._p);
} else {
await this._withdrawRewardForStakingPoolMemberForDelegatorsByUndelegatingAsync(this._p);
}
// @TODO cleanup state and verify the staking contract is empty
}
private async _withdrawRewardForStakingPoolMemberForDelegatorsByUndelegatingAsync(
p: SimulationParams,
): Promise<void> {
let delegatorIdx = 0;
let poolIdx = 0;
for (const numberOfDelegatorsInPool of p.numberOfDelegatorsPerPool) {
const poolId = this._poolIds[poolIdx];
// tslint:disable-next-line no-unused-variable
for (const j of _.range(numberOfDelegatorsInPool)) {
const delegator = this._delegators[delegatorIdx];
const delegatorAddress = delegator.getOwner();
const amountOfStakeDelegated = p.stakeByDelegator[delegatorIdx];
const initEthBalance = await this._stakingWrapper.getEthBalanceAsync(delegatorAddress);
await delegator.deactivateAndTimeLockDelegatedStakeAsync(poolId, amountOfStakeDelegated);
const finalEthBalance = await this._stakingWrapper.getEthBalanceAsync(delegatorAddress);
const reward = finalEthBalance.minus(initEthBalance);
const rewardTrimmed = StakingWrapper.trimFloat(StakingWrapper.toFloatingPoint(reward, 18), 5);
const expectedReward = p.expectedPayoutByDelegator[delegatorIdx];
expect(
rewardTrimmed,
`reward withdrawn from pool ${poolId} for delegator ${delegatorAddress}`,
).to.be.bignumber.equal(expectedReward);
delegatorIdx += 1;
}
poolIdx += 1;
}
}
private async _withdrawRewardForStakingPoolMemberForDelegatorsAsync(p: SimulationParams): Promise<void> {
let delegatorIdx = 0;
let poolIdx = 0;
for (const numberOfDelegatorsInPool of p.numberOfDelegatorsPerPool) {
const poolId = this._poolIds[poolIdx];
// tslint:disable-next-line no-unused-variable
for (const j of _.range(numberOfDelegatorsInPool)) {
const delegator = this._delegators[delegatorIdx];
const delegatorAddress = delegator.getOwner();
const initEthBalance = await this._stakingWrapper.getEthBalanceAsync(delegatorAddress);
await this._stakingWrapper.withdrawTotalRewardForStakingPoolMemberAsync(poolId, delegatorAddress);
const finalEthBalance = await this._stakingWrapper.getEthBalanceAsync(delegatorAddress);
const reward = finalEthBalance.minus(initEthBalance);
const rewardTrimmed = StakingWrapper.trimFloat(StakingWrapper.toFloatingPoint(reward, 18), 5);
const expectedReward = p.expectedPayoutByDelegator[delegatorIdx];
expect(
rewardTrimmed,
`reward withdrawn from pool ${poolId} for delegator ${delegatorAddress}`,
).to.be.bignumber.equal(expectedReward);
delegatorIdx += 1;
}
poolIdx += 1;
}
}
private async _setupPoolsAsync(p: SimulationParams): Promise<void> {
// tslint:disable-next-line no-unused-variable
for (const i of _.range(p.numberOfPools)) {
// create operator
const poolOperatorAddress = this._userQueue.popFront();
const poolOperator = new PoolOperatorActor(poolOperatorAddress, this._stakingWrapper);
this._poolOperators.push(poolOperator);
// create a pool id for this operator
const poolId = await poolOperator.createStakingPoolAsync(p.poolOperatorShares[i]);
this._poolIds.push(poolId);
// each pool operator can also be a staker/delegator
const poolOperatorAsDelegator = new DelegatorActor(poolOperatorAddress, this._stakingWrapper);
this._poolOperatorsAsDelegators.push(poolOperatorAsDelegator);
// add stake to the operator's pool
const amountOfStake = p.stakeByPoolOperator[i];
await poolOperatorAsDelegator.depositZrxAndMintActivatedStakeAsync(amountOfStake);
}
}
private async _setupMakersAsync(p: SimulationParams): Promise<void> {
// create makers
// tslint:disable-next-line no-unused-variable
for (const i of _.range(p.numberOfMakers)) {
const makerAddress = this._userQueue.popFront();
const maker = new MakerActor(makerAddress, this._stakingWrapper);
this._makers.push(maker);
}
// add each maker to their respective pool
let makerIdx = 0;
let poolIdx = 0;
for (const numberOfMakersInPool of p.numberOfMakersPerPool) {
const poolId = this._poolIds[poolIdx];
const poolOperator = this._poolOperators[poolIdx];
// tslint:disable-next-line no-unused-variable
for (const j of _.range(numberOfMakersInPool)) {
const maker = this._makers[makerIdx];
const makerApproval = maker.signApprovalForStakingPool(poolId);
const makerAddress = maker.getOwner();
await poolOperator.addMakerToStakingPoolAsync(poolId, makerAddress, makerApproval.signature);
makerIdx += 1;
}
poolIdx += 1;
}
}
private async _setupDelegatorsAsync(p: SimulationParams): Promise<void> {
// create delegators
// tslint:disable-next-line no-unused-variable
for (const i of _.range(p.numberOfDelegators)) {
const delegatorAddress = this._userQueue.popFront();
const delegator = new DelegatorActor(delegatorAddress, this._stakingWrapper);
this._delegators.push(delegator);
}
// delegate to pools
// currently each actor delegates to a single pool
let delegatorIdx = 0;
let poolIdx = 0;
for (const numberOfDelegatorsInPool of p.numberOfDelegatorsPerPool) {
const poolId = this._poolIds[poolIdx];
// tslint:disable-next-line no-unused-variable
for (const j of _.range(numberOfDelegatorsInPool)) {
const delegator = this._delegators[delegatorIdx];
const amount = p.stakeByDelegator[delegatorIdx];
await delegator.depositZrxAndDelegateToStakingPoolAsync(poolId, amount);
delegatorIdx += 1;
}
poolIdx += 1;
}
}
private async _payProtocolFeesAsync(p: SimulationParams): Promise<void> {
// pay fees
// tslint:disable-next-line no-unused-variable
for (const i of _.range(this._makers.length)) {
const maker = this._makers[i];
const makerAddress = maker.getOwner();
const feeAmount = p.protocolFeesByMaker[i];
await this._stakingWrapper.payProtocolFeeAsync(makerAddress, feeAmount, p.exchangeAddress);
}
// validate fees per pool
let expectedTotalFeesThisEpoch = new BigNumber(0);
// tslint:disable-next-line no-unused-variable
for (const i of _.range(this._poolIds.length)) {
const poolId = this._poolIds[i];
const expectedFees = p.expectedFeesByPool[i];
const fees = await this._stakingWrapper.getProtocolFeesThisEpochByPoolAsync(poolId);
expect(fees, `fees for pool ${poolId}`).to.be.bignumber.equal(expectedFees);
expectedTotalFeesThisEpoch = expectedTotalFeesThisEpoch.plus(fees);
}
// validate total fees
const totalFeesThisEpoch = await this._stakingWrapper.getTotalProtocolFeesThisEpochAsync();
expect(expectedTotalFeesThisEpoch, 'total fees earned').to.be.bignumber.equal(totalFeesThisEpoch);
}
private async _assertVaultBalancesAsync(p: SimulationParams): Promise<void> {
// tslint:disable-next-line no-unused-variable
for (const i of _.range(p.numberOfPools)) {
// @TODO - we trim balances in here because payouts are accurate only to 5 decimal places.
// update once more accurate.
// check pool balance in vault
const poolId = this._poolIds[i];
const rewardVaultBalance = await this._stakingWrapper.rewardVaultBalanceOfAsync(poolId);
const rewardVaultBalanceTrimmed = StakingWrapper.trimFloat(
StakingWrapper.toFloatingPoint(rewardVaultBalance, 18),
5,
);
const expectedRewardBalance = p.expectedPayoutByPool[i];
expect(
rewardVaultBalanceTrimmed,
`expected balance in vault for pool with id ${poolId}`,
).to.be.bignumber.equal(expectedRewardBalance);
// check operator's balance
const poolOperatorVaultBalance = await this._stakingWrapper.getRewardBalanceOfStakingPoolOperatorAsync(
poolId,
);
const poolOperatorVaultBalanceTrimmed = StakingWrapper.trimFloat(
StakingWrapper.toFloatingPoint(poolOperatorVaultBalance, 18),
5,
);
const expectedPoolOperatorVaultBalance = p.expectedPayoutByPoolOperator[i];
expect(
poolOperatorVaultBalanceTrimmed,
`operator balance in vault for pool with id ${poolId}`,
).to.be.bignumber.equal(expectedPoolOperatorVaultBalance);
// check balance of pool members
const membersVaultBalance = await this._stakingWrapper.getRewardBalanceOfStakingPoolMembersAsync(poolId);
const membersVaultBalanceTrimmed = StakingWrapper.trimFloat(
StakingWrapper.toFloatingPoint(membersVaultBalance, 18),
5,
);
const expectedMembersVaultBalance = p.expectedMembersPayoutByPool[i];
expect(
membersVaultBalanceTrimmed,
`members balance in vault for pool with id ${poolId}`,
).to.be.bignumber.equal(expectedMembersVaultBalance);
// @TODO compute balance of each member
}
}
private async _withdrawRewardForStakingPoolMemberForOperatorsAsync(p: SimulationParams): Promise<void> {
// tslint:disable-next-line no-unused-variable
for (const i of _.range(p.numberOfPools)) {
// @TODO - we trim balances in here because payouts are accurate only to 5 decimal places.
// update once more accurate.
// check pool balance in vault
const poolId = this._poolIds[i];
const poolOperator = this._poolOperators[i];
const poolOperatorAddress = poolOperator.getOwner();
const initEthBalance = await this._stakingWrapper.getEthBalanceAsync(poolOperatorAddress);
await this._stakingWrapper.withdrawTotalRewardForStakingPoolOperatorAsync(poolId, poolOperatorAddress);
const finalEthBalance = await this._stakingWrapper.getEthBalanceAsync(poolOperatorAddress);
const reward = finalEthBalance.minus(initEthBalance);
const rewardTrimmed = StakingWrapper.trimFloat(StakingWrapper.toFloatingPoint(reward, 18), 5);
const expectedReward = p.expectedPayoutByPoolOperator[i];
expect(rewardTrimmed, `reward withdrawn from pool ${poolId} for operator`).to.be.bignumber.equal(
expectedReward,
);
}
}
}

View File

@@ -0,0 +1,40 @@
import { signingUtils } from '@0x/contracts-test-utils';
import { SignatureType } from '@0x/types';
import * as ethUtil from 'ethereumjs-util';
import { hashUtils } from './hash_utils';
import { SignedStakingPoolApproval } from './types';
export class ApprovalFactory {
private readonly _privateKey: Buffer;
private readonly _verifyingContractAddress: string;
private readonly _chainId: number;
constructor(privateKey: Buffer, verifyingContractAddress: string, chainId: number) {
this._privateKey = privateKey;
this._verifyingContractAddress = verifyingContractAddress;
this._chainId = chainId;
}
public newSignedApproval(
poolId: string,
makerAddress: string,
signatureType: SignatureType = SignatureType.EthSign,
): SignedStakingPoolApproval {
const approvalHashBuff = hashUtils.getStakingPoolApprovalHashBuffer(
poolId,
makerAddress,
this._verifyingContractAddress,
this._chainId,
);
const signatureBuff = signingUtils.signMessage(approvalHashBuff, this._privateKey, signatureType);
const signedApproval = {
makerAddress,
poolId,
verifyingContractAddress: this._verifyingContractAddress,
chainId: this._chainId,
signature: ethUtil.addHexPrefix(signatureBuff.toString('hex')),
};
return signedApproval;
}
}

View File

@@ -0,0 +1,14 @@
import { BigNumber } from '@0x/utils';
export const constants = {
MAX_UINT_64: new BigNumber(2).pow(256).minus(1),
TOKEN_MULTIPLIER: new BigNumber(10).pow(18),
INITIAL_POOL_ID: '0x0000000000000000000000000000000100000000000000000000000000000000',
NIL_POOL_ID: '0x0000000000000000000000000000000000000000000000000000000000000000',
NIL_ADDRESS: '0x0000000000000000000000000000000000000000',
INITIAL_EPOCH: new BigNumber(0),
INITIAL_TIMELOCK_PERIOD: new BigNumber(0),
EPOCH_DURATION_IN_SECONDS: new BigNumber(1000), // @TODO SET FOR DEPLOYMENT*/
TIMELOCK_DURATION_IN_EPOCHS: new BigNumber(3), // @TODO SET FOR DEPLOYMENT
CHAIN_ID: 1,
};

View File

@@ -0,0 +1,32 @@
import { eip712Utils } from '@0x/order-utils';
import { signTypedDataUtils } from '@0x/utils';
import * as _ from 'lodash';
export const hashUtils = {
getStakingPoolApprovalHashBuffer(
poolId: string,
makerAddress: string,
verifyingContractAddress: string,
chainId: number,
): Buffer {
const typedData = eip712Utils.createStakingPoolApprovalTypedData(
poolId,
makerAddress,
verifyingContractAddress,
chainId,
);
const hashBuffer = signTypedDataUtils.generateTypedDataHash(typedData);
return hashBuffer;
},
getStakingPoolApprovalHashHex(
poolId: string,
makerAddress: string,
verifyingContractAddress: string,
chainId: number,
): string {
const hashHex = `0x${hashUtils
.getStakingPoolApprovalHashBuffer(poolId, makerAddress, verifyingContractAddress, chainId)
.toString('hex')}`;
return hashHex;
},
};

View File

@@ -0,0 +1,39 @@
import * as _ from 'lodash';
export class Queue<T> {
private _store: T[] = [];
constructor(store?: T[]) {
this._store = store !== undefined ? _.cloneDeep(store) : [];
}
public pushBack(val: T): void {
this._store.push(val);
}
public pushFront(val: T): void {
this._store.unshift(val);
}
public popFront(): T {
if (this._store.length === 0) {
throw new Error('Queue is empty');
}
return this._store.shift() as T;
}
public popBack(): T {
if (this._store.length === 0) {
throw new Error('Queue is empty');
}
const backElement = this._store.splice(-1, 1)[0];
return backElement;
}
public mergeBack(q: Queue<T>): void {
this._store = this._store.concat(q._store);
}
public mergeFront(q: Queue<T>): void {
this._store = q._store.concat(this._store);
}
public getStore(): T[] {
return this._store;
}
public peekFront(): T | undefined {
return this._store.length >= 0 ? this._store[0] : undefined;
}
}

View File

@@ -0,0 +1,822 @@
import { ERC20ProxyContract } from '@0x/contracts-asset-proxy';
import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20';
import { constants as testUtilsConstants, LogDecoder, txDefaults } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils';
import { SignatureType } from '@0x/types';
import { BigNumber, logUtils } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import {
artifacts,
LibFeeMathTestContract,
StakingContract,
StakingPoolRewardVaultContract,
StakingProxyContract,
ZrxVaultContract,
} from '../../src';
import { ApprovalFactory } from './approval_factory';
import { constants } from './constants';
import { SignedStakingPoolApproval } from './types';
export class StakingWrapper {
private readonly _web3Wrapper: Web3Wrapper;
private readonly _provider: Provider;
private readonly _logDecoder: LogDecoder;
private readonly _ownerAddress: string;
private readonly _erc20ProxyContract: ERC20ProxyContract;
private readonly _zrxTokenContract: DummyERC20TokenContract;
private readonly _accounts: string[];
private _stakingContractIfExists?: StakingContract;
private _stakingProxyContractIfExists?: StakingProxyContract;
private _zrxVaultContractIfExists?: ZrxVaultContract;
private _rewardVaultContractIfExists?: StakingPoolRewardVaultContract;
private _LibFeeMathTestContractIfExists?: LibFeeMathTestContract;
public static toBaseUnitAmount(amount: BigNumber | number): BigNumber {
const decimals = 18;
const amountAsBigNumber = typeof amount === 'number' ? new BigNumber(amount) : amount;
const baseUnitAmount = Web3Wrapper.toBaseUnitAmount(amountAsBigNumber, decimals);
return baseUnitAmount;
}
public static toFixedPoint(amount: BigNumber | number, decimals: number): BigNumber {
const amountAsBigNumber = typeof amount === 'number' ? new BigNumber(amount) : amount;
const scalar = Math.pow(10, decimals);
const amountAsFixedPoint = amountAsBigNumber.times(scalar);
return amountAsFixedPoint;
}
public static toFloatingPoint(amount: BigNumber | number, decimals: number): BigNumber {
const amountAsBigNumber = typeof amount === 'number' ? new BigNumber(amount) : amount;
const scalar = Math.pow(10, decimals);
const amountAsFloatingPoint = amountAsBigNumber.dividedBy(scalar);
return amountAsFloatingPoint;
}
public static trimFloat(amount: BigNumber | number, decimals: number): BigNumber {
const amountAsBigNumber = typeof amount === 'number' ? new BigNumber(amount) : amount;
const scalar = Math.pow(10, decimals);
const amountAsFloatingPoint = amountAsBigNumber
.multipliedBy(scalar)
.dividedToIntegerBy(1)
.dividedBy(scalar);
return amountAsFloatingPoint;
}
constructor(
provider: Provider,
ownerAddres: string,
erc20ProxyContract: ERC20ProxyContract,
zrxTokenContract: DummyERC20TokenContract,
accounts: string[],
) {
this._web3Wrapper = new Web3Wrapper(provider);
this._provider = provider;
const decoderArtifacts = _.merge(artifacts, erc20Artifacts);
this._logDecoder = new LogDecoder(this._web3Wrapper, decoderArtifacts);
this._ownerAddress = ownerAddres;
this._erc20ProxyContract = erc20ProxyContract;
this._zrxTokenContract = zrxTokenContract;
this._accounts = accounts;
}
public getStakingContract(): StakingContract {
this._validateDeployedOrThrow();
return this._stakingContractIfExists as StakingContract;
}
public getStakingProxyContract(): StakingProxyContract {
this._validateDeployedOrThrow();
return this._stakingProxyContractIfExists as StakingProxyContract;
}
public getZrxVaultContract(): ZrxVaultContract {
this._validateDeployedOrThrow();
return this._zrxVaultContractIfExists as ZrxVaultContract;
}
public getStakingPoolRewardVaultContract(): StakingPoolRewardVaultContract {
this._validateDeployedOrThrow();
return this._rewardVaultContractIfExists as StakingPoolRewardVaultContract;
}
public getLibFeeMathTestContract(): LibFeeMathTestContract {
this._validateDeployedOrThrow();
return this._LibFeeMathTestContractIfExists as LibFeeMathTestContract;
}
public async deployAndConfigureContractsAsync(): Promise<void> {
// deploy zrx vault
const zrxAssetData = assetDataUtils.encodeERC20AssetData(this._zrxTokenContract.address);
this._zrxVaultContractIfExists = await ZrxVaultContract.deployFrom0xArtifactAsync(
artifacts.ZrxVault,
this._provider,
txDefaults,
artifacts,
this._erc20ProxyContract.address,
this._zrxTokenContract.address,
zrxAssetData,
);
// deploy reward vault
this._rewardVaultContractIfExists = await StakingPoolRewardVaultContract.deployFrom0xArtifactAsync(
artifacts.StakingPoolRewardVault,
this._provider,
txDefaults,
artifacts,
);
// configure erc20 proxy to accept calls from zrx vault
await this._erc20ProxyContract.addAuthorizedAddress.awaitTransactionSuccessAsync(
this._zrxVaultContractIfExists.address,
);
// deploy staking contract
this._stakingContractIfExists = await StakingContract.deployFrom0xArtifactAsync(
artifacts.Staking,
this._provider,
txDefaults,
artifacts,
);
// deploy staking proxy
this._stakingProxyContractIfExists = await StakingProxyContract.deployFrom0xArtifactAsync(
artifacts.StakingProxy,
this._provider,
txDefaults,
artifacts,
this._stakingContractIfExists.address,
);
// set staking proxy contract in zrx vault
await this._zrxVaultContractIfExists.setStakingContract.awaitTransactionSuccessAsync(
this._stakingProxyContractIfExists.address,
);
// set zrx vault in staking contract
const setZrxVaultCalldata = this._stakingContractIfExists.setZrxVault.getABIEncodedTransactionData(
this._zrxVaultContractIfExists.address,
);
const setZrxVaultTxData = {
from: this._ownerAddress,
to: this._stakingProxyContractIfExists.address,
data: setZrxVaultCalldata,
};
await this._web3Wrapper.awaitTransactionSuccessAsync(
await this._web3Wrapper.sendTransactionAsync(setZrxVaultTxData),
);
// set staking proxy contract in reward vault
await this._rewardVaultContractIfExists.setStakingContract.awaitTransactionSuccessAsync(
this._stakingProxyContractIfExists.address,
);
// set reward vault in staking contract
const setStakingPoolRewardVaultCalldata = this._stakingContractIfExists.setStakingPoolRewardVault.getABIEncodedTransactionData(
this._rewardVaultContractIfExists.address,
);
const setStakingPoolRewardVaultTxData = {
from: this._ownerAddress,
to: this._stakingProxyContractIfExists.address,
data: setStakingPoolRewardVaultCalldata,
};
await this._web3Wrapper.awaitTransactionSuccessAsync(
await this._web3Wrapper.sendTransactionAsync(setStakingPoolRewardVaultTxData),
);
// deploy libmath test
this._LibFeeMathTestContractIfExists = await LibFeeMathTestContract.deployFrom0xArtifactAsync(
artifacts.LibFeeMathTest,
this._provider,
txDefaults,
artifacts,
);
}
public async getEthBalanceAsync(owner: string): Promise<BigNumber> {
const balance = this._web3Wrapper.getBalanceInWeiAsync(owner);
return balance;
}
///// STAKE /////
public async depositZrxAndMintDeactivatedStakeAsync(
owner: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().depositZrxAndMintDeactivatedStake.getABIEncodedTransactionData(
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async depositZrxAndMintActivatedStakeAsync(
owner: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().depositZrxAndMintActivatedStake.getABIEncodedTransactionData(amount);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async depositZrxAndDelegateToStakingPoolAsync(
owner: string,
poolId: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().depositZrxAndDelegateToStakingPool.getABIEncodedTransactionData(
poolId,
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner, new BigNumber(0), true);
return txReceipt;
}
public async activateStakeAsync(owner: string, amount: BigNumber): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().activateStake.getABIEncodedTransactionData(amount);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async activateAndDelegateStakeAsync(
owner: string,
poolId: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().activateAndDelegateStake.getABIEncodedTransactionData(
poolId,
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async deactivateAndTimeLockStakeAsync(
owner: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().deactivateAndTimeLockStake.getABIEncodedTransactionData(amount);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async deactivateAndTimeLockDelegatedStakeAsync(
owner: string,
poolId: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().deactivateAndTimeLockDelegatedStake.getABIEncodedTransactionData(
poolId,
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner, new BigNumber(0), true);
return txReceipt;
}
public async burnDeactivatedStakeAndWithdrawZrxAsync(
owner: string,
amount: BigNumber,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().burnDeactivatedStakeAndWithdrawZrx.getABIEncodedTransactionData(
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async forceTimeLockSyncAsync(owner: string): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().forceTimeLockSync.getABIEncodedTransactionData(owner);
const txReceipt = await this._executeTransactionAsync(calldata, this._ownerAddress);
return txReceipt;
}
///// STAKE BALANCES /////
public async getTotalStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTotalStake.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTotalStake.getABIDecodedReturnData(returnData);
return value;
}
public async getActivatedStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getActivatedStake.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getActivatedStake.getABIDecodedReturnData(returnData);
return value;
}
public async getDeactivatedStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getDeactivatedStake.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getDeactivatedStake.getABIDecodedReturnData(returnData);
return value;
}
public async getActivatableStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getActivatableStake.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getActivatableStake.getABIDecodedReturnData(returnData);
return value;
}
public async getWithdrawableStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getWithdrawableStake.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getWithdrawableStake.getABIDecodedReturnData(returnData);
return value;
}
public async getTimeLockedStakeAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTimeLockedStake.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTimeLockedStake.getABIDecodedReturnData(returnData);
return value;
}
public async getTimeLockStartAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTimeLockStart.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTimeLockStart.getABIDecodedReturnData(returnData);
return value;
}
public async getStakeDelegatedByOwnerAsync(owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getStakeDelegatedByOwner.getABIEncodedTransactionData(owner);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getStakeDelegatedByOwner.getABIDecodedReturnData(returnData);
return value;
}
public async getStakeDelegatedToPoolByOwnerAsync(poolId: string, owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getStakeDelegatedToPoolByOwner.getABIEncodedTransactionData(
owner,
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getStakeDelegatedToPoolByOwner.getABIDecodedReturnData(returnData);
return value;
}
public async getTotalStakeDelegatedToPoolAsync(poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTotalStakeDelegatedToPool.getABIEncodedTransactionData(poolId);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTotalStakeDelegatedToPool.getABIDecodedReturnData(returnData);
return value;
}
///// POOLS /////
public async getNextStakingPoolIdAsync(): Promise<string> {
const calldata = this.getStakingContract().getNextStakingPoolId.getABIEncodedTransactionData();
const nextPoolId = await this._callAsync(calldata);
return nextPoolId;
}
public async createStakingPoolAsync(operatorAddress: string, operatorShare: number): Promise<string> {
const calldata = this.getStakingContract().createStakingPool.getABIEncodedTransactionData(operatorShare);
const txReceipt = await this._executeTransactionAsync(calldata, operatorAddress);
const createStakingPoolLog = this._logDecoder.decodeLogOrThrow(txReceipt.logs[0]);
const poolId = (createStakingPoolLog as any).args.poolId;
return poolId;
}
public async addMakerToStakingPoolAsync(
poolId: string,
makerAddress: string,
makerSignature: string,
operatorAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().addMakerToStakingPool.getABIEncodedTransactionData(
poolId,
makerAddress,
makerSignature,
);
const txReceipt = await this._executeTransactionAsync(calldata, operatorAddress);
return txReceipt;
}
public async removeMakerFromStakingPoolAsync(
poolId: string,
makerAddress: string,
operatorAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().removeMakerFromStakingPool.getABIEncodedTransactionData(
poolId,
makerAddress,
);
const txReceipt = await this._executeTransactionAsync(calldata, operatorAddress);
return txReceipt;
}
public async getStakingPoolIdOfMakerAsync(makerAddress: string): Promise<string> {
const calldata = this.getStakingContract().getStakingPoolIdOfMaker.getABIEncodedTransactionData(makerAddress);
const poolId = await this._callAsync(calldata);
return poolId;
}
public async getMakersForStakingPoolAsync(poolId: string): Promise<string[]> {
const calldata = this.getStakingContract().getMakersForStakingPool.getABIEncodedTransactionData(poolId);
const returndata = await this._callAsync(calldata);
const makerAddresses = this.getStakingContract().getMakersForStakingPool.getABIDecodedReturnData(returndata);
return makerAddresses;
}
public async isValidMakerSignatureAsync(
poolId: string,
makerAddress: string,
makerSignature: string,
): Promise<boolean> {
const calldata = this.getStakingContract().isValidMakerSignature.getABIEncodedTransactionData(
poolId,
makerAddress,
makerSignature,
);
const returndata = await this._callAsync(calldata);
const isValid = this.getStakingContract().isValidMakerSignature.getABIDecodedReturnData(returndata);
return isValid;
}
public async getStakingPoolApprovalMessageHashAsync(poolId: string, makerAddress: string): Promise<string> {
const calldata = this.getStakingContract().getStakingPoolApprovalMessageHash.getABIEncodedTransactionData(
poolId,
makerAddress,
);
const returndata = await this._callAsync(calldata);
const messageHash = this.getStakingContract().getStakingPoolApprovalMessageHash.getABIDecodedReturnData(
returndata,
);
return messageHash;
}
public signApprovalForStakingPool(
poolId: string,
makerAddress: string,
makerPrivateKeyIfExists?: Buffer,
verifierAddressIfExists?: string,
chainIdIfExists?: number,
signatureType: SignatureType = SignatureType.EthSign,
): SignedStakingPoolApproval {
const makerPrivateKey =
makerPrivateKeyIfExists !== undefined
? makerPrivateKeyIfExists
: testUtilsConstants.TESTRPC_PRIVATE_KEYS[this._accounts.indexOf(makerAddress)];
const verifierAddress =
verifierAddressIfExists !== undefined ? verifierAddressIfExists : this.getStakingProxyContract().address;
const chainId = chainIdIfExists !== undefined ? chainIdIfExists : constants.CHAIN_ID;
const approvalFactory = new ApprovalFactory(makerPrivateKey, verifierAddress, chainId);
const signedStakingPoolApproval = approvalFactory.newSignedApproval(poolId, makerAddress, signatureType);
return signedStakingPoolApproval;
}
///// EPOCHS /////
public async goToNextEpochAsync(): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().finalizeFees.getABIEncodedTransactionData();
const txReceipt = await this._executeTransactionAsync(calldata, undefined, new BigNumber(0), true);
logUtils.log(`Finalization costed ${txReceipt.gasUsed} gas`);
return txReceipt;
}
public async skipToNextEpochAsync(): Promise<TransactionReceiptWithDecodedLogs> {
// increase timestamp of next block
const epochDurationInSeconds = await this.getEpochDurationInSecondsAsync();
await this._web3Wrapper.increaseTimeAsync(epochDurationInSeconds.toNumber());
// mine next block
await this._web3Wrapper.mineBlockAsync();
// increment epoch in contracts
const txReceipt = await this.goToNextEpochAsync();
// mine next block
await this._web3Wrapper.mineBlockAsync();
return txReceipt;
}
public async skipToNextTimeLockPeriodAsync(): Promise<void> {
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 getEpochDurationInSecondsAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getEpochDurationInSeconds.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getEpochDurationInSeconds.getABIDecodedReturnData(returnData);
return value;
}
public async getTimeLockDurationInEpochsAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getTimeLockDurationInEpochs.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTimeLockDurationInEpochs.getABIDecodedReturnData(returnData);
return value;
}
public async getCurrentEpochStartTimeInSecondsAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentEpochStartTimeInSeconds.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentEpochStartTimeInSeconds.getABIDecodedReturnData(returnData);
return value;
}
public async getCurrentTimeLockPeriodStartEpochAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentTimeLockPeriodStartEpoch.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentTimeLockPeriodStartEpoch.getABIDecodedReturnData(returnData);
return value;
}
public async getCurrentEpochEarliestEndTimeInSecondsAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentEpochEarliestEndTimeInSeconds.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentEpochEarliestEndTimeInSeconds.getABIDecodedReturnData(
returnData,
);
return value;
}
public async getCurrentTimeLockPeriodEndEpochAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentTimeLockPeriodEndEpoch.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentTimeLockPeriodEndEpoch.getABIDecodedReturnData(returnData);
return value;
}
public async getCurrentEpochAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentEpoch.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentEpoch.getABIDecodedReturnData(returnData);
return value;
}
public async getCurrentTimeLockPeriodAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getCurrentTimeLockPeriod.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getCurrentTimeLockPeriod.getABIDecodedReturnData(returnData);
return value;
}
///// PROTOCOL FEES /////
public async payProtocolFeeAsync(
makerAddress: string,
amount: BigNumber,
exchangeAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().payProtocolFee.getABIEncodedTransactionData(makerAddress);
const txReceipt = await this._executeTransactionAsync(calldata, exchangeAddress, amount);
return txReceipt;
}
public async getProtocolFeesThisEpochByPoolAsync(poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getProtocolFeesThisEpochByPool.getABIEncodedTransactionData(poolId);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getProtocolFeesThisEpochByPool.getABIDecodedReturnData(returnData);
return value;
}
public async getTotalProtocolFeesThisEpochAsync(): Promise<BigNumber> {
const calldata = this.getStakingContract().getTotalProtocolFeesThisEpoch.getABIEncodedTransactionData();
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTotalProtocolFeesThisEpoch.getABIDecodedReturnData(returnData);
return value;
}
///// EXCHANGES /////
public async isValidExchangeAddressAsync(exchangeAddress: string): Promise<boolean> {
const calldata = this.getStakingContract().isValidExchangeAddress.getABIEncodedTransactionData(exchangeAddress);
const returnData = await this._callAsync(calldata);
const isValid = this.getStakingContract().isValidExchangeAddress.getABIDecodedReturnData(returnData);
return isValid;
}
public async addExchangeAddressAsync(
exchangeAddress: string,
ownerAddressIfExists?: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().addExchangeAddress.getABIEncodedTransactionData(exchangeAddress);
const ownerAddress = ownerAddressIfExists !== undefined ? ownerAddressIfExists : this._ownerAddress;
const txReceipt = await this._executeTransactionAsync(calldata, ownerAddress);
return txReceipt;
}
public async removeExchangeAddressAsync(
exchangeAddress: string,
ownerAddressIfExists?: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().removeExchangeAddress.getABIEncodedTransactionData(exchangeAddress);
const ownerAddress = ownerAddressIfExists !== undefined ? ownerAddressIfExists : this._ownerAddress;
const txReceipt = await this._executeTransactionAsync(calldata, ownerAddress);
return txReceipt;
}
///// REWARDS /////
public async getTotalRewardBalanceOfStakingPoolAsync(poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTotalRewardBalanceOfStakingPool.getABIEncodedTransactionData(
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTotalRewardBalanceOfStakingPool.getABIDecodedReturnData(returnData);
return value;
}
public async getRewardBalanceOfStakingPoolOperatorAsync(poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getRewardBalanceOfStakingPoolOperator.getABIEncodedTransactionData(
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getRewardBalanceOfStakingPoolOperator.getABIDecodedReturnData(
returnData,
);
return value;
}
public async getRewardBalanceOfStakingPoolMembersAsync(poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getRewardBalanceOfStakingPoolMembers.getABIEncodedTransactionData(
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getRewardBalanceOfStakingPoolMembers.getABIDecodedReturnData(
returnData,
);
return value;
}
public async computeRewardBalanceOfStakingPoolMemberAsync(poolId: string, owner: string): Promise<BigNumber> {
const calldata = this.getStakingContract().computeRewardBalanceOfStakingPoolMember.getABIEncodedTransactionData(
poolId,
owner,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().computeRewardBalanceOfStakingPoolMember.getABIDecodedReturnData(
returnData,
);
return value;
}
public async getTotalShadowBalanceOfStakingPoolAsync(poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getTotalShadowBalanceOfStakingPool.getABIEncodedTransactionData(
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getTotalShadowBalanceOfStakingPool.getABIDecodedReturnData(returnData);
return value;
}
public async getShadowBalanceOfStakingPoolMemberAsync(owner: string, poolId: string): Promise<BigNumber> {
const calldata = this.getStakingContract().getShadowBalanceOfStakingPoolMember.getABIEncodedTransactionData(
owner,
poolId,
);
const returnData = await this._callAsync(calldata);
const value = this.getStakingContract().getShadowBalanceOfStakingPoolMember.getABIDecodedReturnData(returnData);
return value;
}
public async withdrawRewardForStakingPoolOperatorAsync(
poolId: string,
amount: BigNumber,
operatorAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().withdrawRewardForStakingPoolOperator.getABIEncodedTransactionData(
poolId,
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, operatorAddress);
return txReceipt;
}
public async withdrawRewardForStakingPoolMemberAsync(
poolId: string,
amount: BigNumber,
owner: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().withdrawRewardForStakingPoolMember.getABIEncodedTransactionData(
poolId,
amount,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
public async withdrawTotalRewardForStakingPoolOperatorAsync(
poolId: string,
operatorAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().withdrawTotalRewardForStakingPoolOperator.getABIEncodedTransactionData(
poolId,
);
const txReceipt = await this._executeTransactionAsync(calldata, operatorAddress);
return txReceipt;
}
public async withdrawTotalRewardForStakingPoolMemberAsync(
poolId: string,
owner: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingContract().withdrawTotalRewardForStakingPoolMember.getABIEncodedTransactionData(
poolId,
);
const txReceipt = await this._executeTransactionAsync(calldata, owner);
return txReceipt;
}
///// REWARD VAULT /////
public async rewardVaultDepositForAsync(
poolId: string,
amount: BigNumber,
stakingContractAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingPoolRewardVaultContract().depositFor.getABIEncodedTransactionData(poolId);
const txReceipt = await this._executeTransactionAsync(calldata, stakingContractAddress, amount);
return txReceipt;
}
public async rewardVaultEnterCatastrophicFailureModeAsync(
zeroExMultisigAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingPoolRewardVaultContract().enterCatostrophicFailure.getABIEncodedTransactionData();
const txReceipt = await this._executeTransactionAsync(calldata, zeroExMultisigAddress);
return txReceipt;
}
public async rewardVaultBalanceOfAsync(poolId: string): Promise<BigNumber> {
const balance = await this.getStakingPoolRewardVaultContract().balanceOf.callAsync(poolId);
return balance;
}
public async rewardVaultBalanceOfOperatorAsync(poolId: string): Promise<BigNumber> {
const balance = await this.getStakingPoolRewardVaultContract().balanceOfOperator.callAsync(poolId);
return balance;
}
public async rewardVaultBalanceOfMembersAsync(poolId: string): Promise<BigNumber> {
const balance = await this.getStakingPoolRewardVaultContract().balanceOfMembers.callAsync(poolId);
return balance;
}
public async rewardVaultRegisterPoolAsync(
poolId: string,
poolOperatorShare: number,
stakingContractAddress: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const calldata = this.getStakingPoolRewardVaultContract().registerStakingPool.getABIEncodedTransactionData(
poolId,
poolOperatorShare,
);
const txReceipt = await this._executeTransactionAsync(calldata, stakingContractAddress);
return txReceipt;
}
///// ZRX VAULT /////
public async getZrxVaultBalanceAsync(holder: string): Promise<BigNumber> {
const balance = await this.getZrxVaultContract().balanceOf.callAsync(holder);
return balance;
}
public async getZrxTokenBalanceAsync(holder: string): Promise<BigNumber> {
const balance = await this._zrxTokenContract.balanceOf.callAsync(holder);
return balance;
}
public async getZrxTokenBalanceOfZrxVaultAsync(): Promise<BigNumber> {
const balance = await this._zrxTokenContract.balanceOf.callAsync(this.getZrxVaultContract().address);
return balance;
}
///// MATH /////
public async nthRootAsync(value: BigNumber, n: BigNumber): Promise<BigNumber> {
// const txReceipt = await this.getLibFeeMathTestContract().nthRoot.await(value, n);
const output = await this.getLibFeeMathTestContract().nthRoot.callAsync(value, n);
return output;
}
public async nthRootFixedPointAsync(value: BigNumber, n: BigNumber): Promise<BigNumber> {
const output = await this.getLibFeeMathTestContract().nthRootFixedPoint.callAsync(value, n);
return output;
}
public async cobbDouglasAsync(
totalRewards: BigNumber,
ownerFees: BigNumber,
totalFees: BigNumber,
ownerStake: BigNumber,
totalStake: BigNumber,
alphaNumerator: BigNumber,
alphaDenominator: BigNumber,
): Promise<BigNumber> {
const output = await this.getLibFeeMathTestContract().cobbDouglas.callAsync(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaNumerator,
alphaDenominator,
);
return output;
}
public async cobbDouglasSimplifiedAsync(
totalRewards: BigNumber,
ownerFees: BigNumber,
totalFees: BigNumber,
ownerStake: BigNumber,
totalStake: BigNumber,
alphaDenominator: BigNumber,
): Promise<BigNumber> {
await this.getLibFeeMathTestContract().cobbDouglasSimplifiedInverse.awaitTransactionSuccessAsync(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaDenominator,
);
const output = await this.getLibFeeMathTestContract().cobbDouglasSimplified.callAsync(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaDenominator,
);
return output;
}
public async cobbDouglasSimplifiedInverseAsync(
totalRewards: BigNumber,
ownerFees: BigNumber,
totalFees: BigNumber,
ownerStake: BigNumber,
totalStake: BigNumber,
alphaDenominator: BigNumber,
): Promise<BigNumber> {
await this.getLibFeeMathTestContract().cobbDouglasSimplifiedInverse.awaitTransactionSuccessAsync(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaDenominator,
);
const output = await this.getLibFeeMathTestContract().cobbDouglasSimplifiedInverse.callAsync(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
alphaDenominator,
);
return output;
}
private async _executeTransactionAsync(
calldata: string,
from?: string,
value?: BigNumber,
includeLogs?: boolean,
): Promise<TransactionReceiptWithDecodedLogs> {
const txData = {
from: from ? from : this._ownerAddress,
to: this.getStakingProxyContract().address,
data: calldata,
gas: 3000000,
gasPrice: 0,
value,
};
const txHash = await this._web3Wrapper.sendTransactionAsync(txData);
const txReceipt = await (includeLogs
? this._logDecoder.getTxWithDecodedLogsAsync(txHash)
: this._web3Wrapper.awaitTransactionSuccessAsync(txHash));
return txReceipt;
}
private async _callAsync(calldata: string, from?: string): Promise<any> {
const txData = {
from: from ? from : this._ownerAddress,
to: this.getStakingProxyContract().address,
data: calldata,
gas: 3000000,
};
const returnValue = await this._web3Wrapper.callAsync(txData);
return returnValue;
}
private _validateDeployedOrThrow(): void {
if (this._stakingContractIfExists === undefined) {
throw new Error('Staking contracts are not deployed. Call `deployStakingContracts`');
}
}
}
// tslint:disable-line:max-file-line-count

View File

@@ -0,0 +1,50 @@
import { BigNumber } from '@0x/utils';
export interface StakingPoolApproval {
makerAddress: string;
poolId: string;
verifyingContractAddress: string;
chainId: number;
}
export interface SignedStakingPoolApproval extends StakingPoolApproval {
signature: string;
}
export interface StakerBalances {
zrxBalance: BigNumber;
stakeBalance: BigNumber;
stakeBalanceInVault: BigNumber;
withdrawableStakeBalance: BigNumber;
activatableStakeBalance: BigNumber;
activatedStakeBalance: BigNumber;
deactivatedStakeBalance: BigNumber;
timeLockedStakeBalance: BigNumber;
}
export interface DelegatorBalances extends StakerBalances {
delegatedStakeBalance: BigNumber;
stakeDelegatedToPoolByOwner: BigNumber[];
stakeDelegatedToPool: BigNumber[];
}
export interface SimulationParams {
users: string[];
numberOfPools: number;
poolOperatorShares: number[];
stakeByPoolOperator: BigNumber[];
numberOfMakers: number;
numberOfMakersPerPool: number[];
protocolFeesByMaker: BigNumber[];
numberOfDelegators: number;
numberOfDelegatorsPerPool: number[];
stakeByDelegator: BigNumber[];
expectedFeesByPool: BigNumber[];
expectedPayoutByPool: BigNumber[];
expectedPayoutByPoolOperator: BigNumber[];
expectedMembersPayoutByPool: BigNumber[];
expectedPayoutByDelegator: BigNumber[];
exchangeAddress: string;
delegateInNextEpoch: boolean;
withdrawByUndelegating: boolean;
}

View File

@@ -0,0 +1,77 @@
import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { chaiSetup, expectTransactionFailedAsync, provider, web3Wrapper } from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { RevertReason } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { StakingWrapper } from './utils/staking_wrapper';
chaiSetup.configure();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
// tslint:disable:no-unnecessary-type-assertion
describe('Staking Vaults', () => {
// constants
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
// tokens & addresses
let accounts: string[];
let owner: string;
let users: string[];
let zrxTokenContract: DummyERC20TokenContract;
let erc20ProxyContract: ERC20ProxyContract;
// wrappers
let stakingWrapper: StakingWrapper;
let erc20Wrapper: ERC20Wrapper;
// tests
before(async () => {
await blockchainLifecycle.startAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
before(async () => {
// create accounts
accounts = await web3Wrapper.getAvailableAddressesAsync();
owner = accounts[0];
users = accounts.slice(1);
// deploy erc20 proxy
erc20Wrapper = new ERC20Wrapper(provider, accounts, owner);
erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
// deploy zrx token
[zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, ZRX_TOKEN_DECIMALS);
await erc20Wrapper.setBalancesAndAllowancesAsync();
// deploy staking contracts
stakingWrapper = new StakingWrapper(provider, owner, erc20ProxyContract, zrxTokenContract, accounts);
await stakingWrapper.deployAndConfigureContractsAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('Reward Vault', () => {
it.skip('basic management', async () => {
// 1 setup test parameters
const poolOperator = users[0];
const operatorShare = 39;
const poolId = await stakingWrapper.createStakingPoolAsync(poolOperator, operatorShare);
const stakingContractAddress = stakingWrapper.getStakingContract().address;
const notStakingContractAddress = poolOperator;
// create pool in vault
await stakingWrapper.rewardVaultRegisterPoolAsync(poolId, operatorShare, stakingContractAddress);
// should fail to create pool if it already exists
await expectTransactionFailedAsync(
stakingWrapper.rewardVaultRegisterPoolAsync(poolId, operatorShare, stakingContractAddress),
RevertReason.PoolAlreadyExists,
);
// should fail to create a pool from an address other than the staking contract
await expectTransactionFailedAsync(
stakingWrapper.rewardVaultRegisterPoolAsync(poolId, operatorShare, notStakingContractAddress),
RevertReason.OnlyCallableByStakingContract,
);
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@@ -2,6 +2,43 @@
"extends": "../../tsconfig",
"compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true },
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"files": ["generated-artifacts/IStaking.json", "generated-artifacts/Staking.json"],
"files": [
"generated-artifacts/IStaking.json",
"generated-artifacts/IStakingEvents.json",
"generated-artifacts/IStakingPoolRewardVault.json",
"generated-artifacts/IStakingProxy.json",
"generated-artifacts/IStructs.json",
"generated-artifacts/IVaultCore.json",
"generated-artifacts/IWallet.json",
"generated-artifacts/IZrxVault.json",
"generated-artifacts/LibEIP712Hash.json",
"generated-artifacts/LibFeeMath.json",
"generated-artifacts/LibFeeMathTest.json",
"generated-artifacts/LibRewardMath.json",
"generated-artifacts/LibSafeMath.json",
"generated-artifacts/LibSafeMath64.json",
"generated-artifacts/LibSafeMath96.json",
"generated-artifacts/LibSignatureValidator.json",
"generated-artifacts/MixinConstants.json",
"generated-artifacts/MixinDelegatedStake.json",
"generated-artifacts/MixinDeploymentConstants.json",
"generated-artifacts/MixinExchangeFees.json",
"generated-artifacts/MixinExchangeManager.json",
"generated-artifacts/MixinOwnable.json",
"generated-artifacts/MixinScheduler.json",
"generated-artifacts/MixinStake.json",
"generated-artifacts/MixinStakeBalances.json",
"generated-artifacts/MixinStakingPool.json",
"generated-artifacts/MixinStakingPoolRewardVault.json",
"generated-artifacts/MixinStakingPoolRewards.json",
"generated-artifacts/MixinStorage.json",
"generated-artifacts/MixinTimeLockedStake.json",
"generated-artifacts/MixinVaultCore.json",
"generated-artifacts/MixinZrxVault.json",
"generated-artifacts/Staking.json",
"generated-artifacts/StakingPoolRewardVault.json",
"generated-artifacts/StakingProxy.json",
"generated-artifacts/ZrxVault.json"
],
"exclude": ["./deploy/solc/solc_bin"]
}

View File

@@ -1,4 +1,13 @@
[
{
"version": "1.1.1",
"changes": [
{
"note": "Fixed sorting in artifact generation",
"pr": 1910
}
]
},
{
"version": "1.1.0",
"changes": [

View File

@@ -79,7 +79,7 @@ function generateArtifactsTs(contracts: string[], artifactsDir: string, prettier
const importPath = path.join('..', artifactsDir, `${contractName}.json`);
return `import * as ${contractName} from '${importPath}';`;
});
const sortedImports = _.sortBy(imports);
const sortedImports = _.sortBy(imports, _import => _import.toLowerCase());
const artifacts = _.map(contracts, contract => {
const contractName = path.basename(contract, SOLIDITY_EXTENSION);
if (contractName === 'ZRXToken') {

View File

@@ -65,6 +65,10 @@
{
"note": "Update `IncompleteFillError` to take an `errorCode`, `expectedAssetFillAmount`, and `actualAssetFillAmount` fields.",
"pr": 2075
},
{
"note": "Add EIP712 types for Staking",
"pr": 1910
}
]
},

View File

@@ -149,6 +149,12 @@ export const constants = {
{ name: 'approvalExpirationTimeSeconds', type: 'uint256' },
],
},
STAKING_DOMAIN_NAME: '0x Protocol Staking',
STAKING_DOMAIN_VERSION: '1.0.0',
STAKING_POOL_APPROVAL_SCHEMA: {
name: 'StakingPoolApproval',
parameters: [{ name: 'poolId', type: 'bytes32' }, { name: 'makerAddress', type: 'address' }],
},
ERC20_METHOD_ABI,
ERC721_METHOD_ABI,
MULTI_ASSET_METHOD_ABI,

View File

@@ -125,4 +125,34 @@ export const eip712Utils = {
);
return typedData;
},
/**
* Creates an Coordiantor typedData EIP712TypedData object for use with the Coordinator extension contract
* @return A typed data object
*/
createStakingPoolApprovalTypedData: (
poolId: string,
makerAddress: string,
verifyingContractAddress: string,
chainId: number,
): EIP712TypedData => {
const domain = {
name: constants.STAKING_DOMAIN_NAME,
version: constants.STAKING_DOMAIN_VERSION,
verifyingContractAddress,
chainId,
};
const approval = {
poolId,
makerAddress,
};
const typedData = eip712Utils.createTypedData(
constants.STAKING_POOL_APPROVAL_SCHEMA.name,
{
StakingPoolApproval: constants.STAKING_POOL_APPROVAL_SCHEMA.parameters,
},
approval,
domain,
);
return typedData;
},
};

View File

@@ -9,6 +9,11 @@
{
"note": "Add `OrderInfo`, `FillResults`, `MatchedFillResults`, `BatchMatchedFillResults` types",
"pr": 2031
},
,
{
"note": "Add status types for Staking contracts",
"pr": 1910
}
]
},

View File

@@ -340,6 +340,21 @@ export enum RevertReason {
TargetNotEven = 'TARGET_NOT_EVEN',
UnexpectedStaticCallResult = 'UNEXPECTED_STATIC_CALL_RESULT',
TransfersSuccessful = 'TRANSFERS_SUCCESSFUL',
// Staking
OnlyCallableByPoolOperator = 'ONLY_CALLABLE_BY_POOL_OPERATOR',
OnlyCallableByPoolOperatorOrMaker = 'ONLY_CALLABLE_BY_POOL_OPERATOR_OR_MAKER',
MakerAddressAlreadyRegistered = 'MAKER_ADDRESS_ALREADY_REGISTERED',
MakerAddressNotRegistered = 'MAKER_ADDRESS_NOT_REGISTERED',
OnlyCallableByExchange = 'ONLY_CALLABLE_BY_EXCHANGE',
ExchangeAddressAlreadyRegistered = 'EXCHANGE_ADDRESS_ALREADY_REGISTERED',
ExchangeAddressNotRegistered = 'EXCHANGE_ADDRESS_NOT_REGISTERED',
PoolAlreadyExists = 'POOL_ALREADY_EXISTS',
PoolBalanceIsZero = 'POOL_BALANCE_IS_ZERO',
InvalidOwner = 'INVALID_OWNER',
AmountExceedsBalanceOfPool = 'AMOUNT_EXCEEDS_BALANCE_OF_POOL',
OnlyCallableByStakingContract = 'ONLY_CALLABLE_BY_STAKING_CONTRACT',
InvalidMakerSignature = 'INVALID_MAKER_SIGNATURE',
InsufficientBalance = 'INSUFFICIENT_BALANCE',
}
export enum StatusCodes {

View File

@@ -649,26 +649,6 @@
dependencies:
"@0x/base-contract" "^4.0.3"
"@0x/abi-gen@^2.0.9":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@0x/abi-gen/-/abi-gen-2.1.1.tgz#2ca9072e64a2a46b6149aaea434f09d5dbf0866f"
integrity sha512-QLigDvQEGe248MjafJ58nWfwMbz+Va6KTlCsrADTuQd711XY10c95poDPevOfAXNHFZYpgS6rEzIau0WmY+Kbw==
dependencies:
"@0x/types" "^2.4.0"
"@0x/typescript-typings" "^4.2.3"
"@0x/utils" "^4.4.0"
chalk "^2.3.0"
change-case "^3.0.2"
cli-format "^3.0.9"
ethereum-types "^2.1.3"
glob "^7.1.2"
handlebars "^4.0.11"
lodash "^4.17.11"
mkdirp "^0.5.1"
tmp "^0.0.33"
to-snake-case "^1.0.0"
yargs "^10.0.3"
"@0x/asset-buyer@6.1.8":
version "6.1.8"
resolved "https://registry.yarnpkg.com/@0x/asset-buyer/-/asset-buyer-6.1.8.tgz#71f6abb366e89e62457c256644edb37e12113e94"
@@ -828,7 +808,7 @@
"@0x/web3-wrapper@^4.0.1":
version "4.0.2"
resolved "https://registry.npmjs.org/@0x/web3-wrapper/-/web3-wrapper-4.0.2.tgz#d4e0a4fa1217155e1aed4cd91086654fd99f2959"
resolved "https://registry.yarnpkg.com/@0x/web3-wrapper/-/web3-wrapper-4.0.2.tgz#d4e0a4fa1217155e1aed4cd91086654fd99f2959"
dependencies:
"@0x/assert" "^2.0.2"
"@0x/json-schemas" "^3.0.2"