Merge pull request #2205 from 0xProject/feature/contracts-staking/rip-mixin-vault-core

Consolidate MixinVaultCore and ZrxVault + unit tests
This commit is contained in:
mzhu25 2019-09-24 23:23:32 -07:00 committed by GitHub
commit e4ab832ced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 573 additions and 959 deletions

View File

@ -18,28 +18,41 @@
pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/Authorizable.sol";
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol";
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol";
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "../interfaces/IZrxVault.sol";
import "./MixinVaultCore.sol";
import "./libs/LibStakingRichErrors.sol";
import "./interfaces/IZrxVault.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 catastrophic
/// failure mode, it cannot be returned to normal mode; this prevents
/// corruption of related state in the staking contract.
/// The contract also includes management of the staking contract
/// and setting the vault to "Catastrophic Failure Mode".
/// Catastrophic Failure Mode should only be set 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)`. In Catastrophic Failure Mode, only withdrawals
/// can be made (no deposits). Once Catastrophic Failure Mode is invoked,
/// it cannot be returned to normal mode; this prevents corruption of related
/// state in the staking contract.
contract ZrxVault is
IZrxVault,
MixinVaultCore
Authorizable,
IZrxVault
{
using LibSafeMath for uint256;
// mapping from Owner to ZRX balance
// Address of staking proxy contract
address payable public stakingProxyAddress;
// True iff vault has been set to Catastrophic Failure Mode
bool public isInCatastrophicFailure;
// Mapping from staker to ZRX balance
mapping (address => uint256) internal _balances;
// Zrx Asset Proxy
@ -59,7 +72,10 @@ contract ZrxVault is
address _zrxTokenAddress
)
public
Authorizable()
{
_addAuthorizedAddress(owner);
zrxAssetProxy = IAssetProxy(_zrxProxyAddress);
_zrxToken = IERC20Token(_zrxTokenAddress);
_zrxAssetData = abi.encodeWithSelector(
@ -68,6 +84,28 @@ contract ZrxVault is
);
}
/// @dev Sets the address of the StakingProxy contract.
/// Note that only the contract owner can call this function.
/// @param _stakingProxyAddress Address of Staking proxy contract.
function setStakingProxy(address payable _stakingProxyAddress)
external
onlyAuthorized
{
stakingProxyAddress = _stakingProxyAddress;
emit StakingProxySet(_stakingProxyAddress);
}
/// @dev Vault enters into Catastrophic Failure Mode.
/// *** WARNING - ONCE IN CATOSTROPHIC FAILURE MODE, YOU CAN NEVER GO BACK! ***
/// Note that only the contract owner can call this function.
function enterCatastrophicFailure()
external
onlyAuthorized
{
isInCatastrophicFailure = true;
emit InCatastrophicFailureMode(msg.sender);
}
/// @dev Sets the Zrx proxy.
/// Note that only an authorized address can call this function.
/// Note that this can only be called when *not* in Catastrophic Failure mode.
@ -165,4 +203,48 @@ contract ZrxVault is
amount
);
}
modifier onlyStakingProxy() {
_assertSenderIsStakingProxy();
_;
}
modifier onlyInCatastrophicFailure() {
_assertInCatastrophicFailure();
_;
}
modifier onlyNotInCatastrophicFailure() {
_assertNotInCatastrophicFailure();
_;
}
function _assertSenderIsStakingProxy()
private
view
{
if (msg.sender != stakingProxyAddress) {
LibRichErrors.rrevert(LibStakingRichErrors.OnlyCallableByStakingContractError(
msg.sender
));
}
}
function _assertInCatastrophicFailure()
private
view
{
if (!isInCatastrophicFailure) {
LibRichErrors.rrevert(LibStakingRichErrors.OnlyCallableIfInCatastrophicFailureError());
}
}
function _assertNotInCatastrophicFailure()
private
view
{
if (isInCatastrophicFailure) {
LibRichErrors.rrevert(LibStakingRichErrors.OnlyCallableIfNotInCatastrophicFailureError());
}
}
}

View File

@ -1,53 +0,0 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
/// @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 Catastrophic Failure Mode,
/// a vault cannot be reset to normal mode; this prevents corruption of related
/// state in the staking contract.
interface IVaultCore {
/// @dev Emmitted whenever a StakingProxy is set in a vault.
event StakingProxySet(address stakingProxyAddress);
/// @dev Emitted when the Staking contract is put into Catastrophic Failure Mode
/// @param sender Address of sender (`msg.sender`)
event InCatastrophicFailureMode(address sender);
/// @dev Sets the address of the StakingProxy contract.
/// Note that this is callable only by an authorized address.
/// @param _stakingProxyAddress Address of Staking proxy contract.
function setStakingProxy(address payable _stakingProxyAddress)
external;
/// @dev Vault enters into Catastrophic Failure Mode.
/// *** WARNING - ONCE IN CATOSTROPHIC FAILURE MODE, YOU CAN NEVER GO BACK! ***
/// Note that this is callable only by an authorized address.
function enterCatastrophicFailure()
external;
}

View File

@ -22,12 +22,24 @@ 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 catastrophic
/// failure mode, it cannot be returned to normal mode; this prevents
/// corruption of related state in the staking contract.
/// The contract also includes management of the staking contract
/// and setting the vault to "Catastrophic Failure Mode".
/// Catastrophic Failure Mode should only be set 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)`. In Catastrophic Failure Mode, only withdrawals
/// can be made (no deposits). Once Catastrophic Failure Mode is invoked,
/// it cannot be returned to normal mode; this prevents corruption of related
/// state in the staking contract.
interface IZrxVault {
/// @dev Emmitted whenever a StakingProxy is set in a vault.
event StakingProxySet(address stakingProxyAddress);
/// @dev Emitted when the Staking contract is put into Catastrophic Failure Mode
/// @param sender Address of sender (`msg.sender`)
event InCatastrophicFailureMode(address sender);
/// @dev Emitted when Zrx Tokens are deposited into the vault.
/// @param staker of Zrx Tokens.
/// @param amount of Zrx Tokens deposited.
@ -47,6 +59,18 @@ interface IZrxVault {
/// @dev Emitted whenever the ZRX AssetProxy is set.
event ZrxProxySet(address zrxProxyAddress);
/// @dev Sets the address of the StakingProxy contract.
/// Note that only the contract staker can call this function.
/// @param _stakingProxyAddress Address of Staking proxy contract.
function setStakingProxy(address payable _stakingProxyAddress)
external;
/// @dev Vault enters into Catastrophic Failure Mode.
/// *** WARNING - ONCE IN CATOSTROPHIC FAILURE MODE, YOU CAN NEVER GO BACK! ***
/// Note that only the contract staker can call this function.
function enterCatastrophicFailure()
external;
/// @dev Sets the Zrx proxy.
/// Note that only the contract staker can call this.
/// Note that this can only be called when *not* in Catastrophic Failure mode.

View File

@ -1,103 +0,0 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/Authorizable.sol";
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "../libs/LibStakingRichErrors.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 Catastrophic Failure Mode,
/// a vault cannot be reset to normal mode; this prevents corruption of related
/// status in the staking contract.
contract MixinVaultCore is
Authorizable,
IVaultCore
{
// Address of staking contract
address payable public stakingProxyAddress;
// True iff vault has been set to Catastrophic Failure Mode
bool public isInCatastrophicFailure;
/// @dev Asserts that the sender (`msg.sender`) is the staking contract.
modifier onlyStakingProxy {
if (msg.sender != stakingProxyAddress) {
LibRichErrors.rrevert(LibStakingRichErrors.OnlyCallableByStakingContractError(
msg.sender
));
}
_;
}
/// @dev Asserts that this contract *is in* Catastrophic Failure Mode.
modifier onlyInCatastrophicFailure {
if (!isInCatastrophicFailure) {
LibRichErrors.rrevert(LibStakingRichErrors.OnlyCallableIfInCatastrophicFailureError());
}
_;
}
/// @dev Asserts that this contract *is not in* Catastrophic Failure Mode.
modifier onlyNotInCatastrophicFailure {
if (isInCatastrophicFailure) {
LibRichErrors.rrevert(LibStakingRichErrors.OnlyCallableIfNotInCatastrophicFailureError());
}
_;
}
/// @dev Sets the vault owner and adds owner as an authorized address.
constructor()
public
Authorizable()
{
_addAuthorizedAddress(owner);
}
/// @dev Sets the address of the StakingProxy contract.
/// Note that only an authorized address can call this function.
/// @param _stakingProxyAddress Address of Staking proxy contract.
function setStakingProxy(address payable _stakingProxyAddress)
external
onlyAuthorized
{
stakingProxyAddress = _stakingProxyAddress;
emit StakingProxySet(_stakingProxyAddress);
}
/// @dev Vault enters into Catastrophic Failure Mode.
/// *** WARNING - ONCE IN CATOSTROPHIC FAILURE MODE, YOU CAN NEVER GO BACK! ***
/// Note that only an authorized address can call this function.
function enterCatastrophicFailure()
external
onlyAuthorized
{
isInCatastrophicFailure = true;
emit InCatastrophicFailureMode(msg.sender);
}
}

View File

@ -1,44 +0,0 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
import "../src/vaults/MixinVaultCore.sol";
// solhint-disable no-empty-blocks
contract TestMixinVaultCore is
MixinVaultCore
{
function assertStakingProxy()
external
view
onlyStakingProxy
{}
function assertInCatastrophicFailure()
external
view
onlyInCatastrophicFailure
{}
function assertNotInCatastrophicFailure()
external
view
onlyNotInCatastrophicFailure
{}
}

View File

@ -37,7 +37,7 @@
},
"config": {
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IVaultCore|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|ReadOnlyProxy|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestMixinVaultCore|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault).json"
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|ReadOnlyProxy|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault).json"
},
"repository": {
"type": "git",

View File

@ -11,7 +11,6 @@ import * as IStakingProxy from '../generated-artifacts/IStakingProxy.json';
import * as IStorage from '../generated-artifacts/IStorage.json';
import * as IStorageInit from '../generated-artifacts/IStorageInit.json';
import * as IStructs from '../generated-artifacts/IStructs.json';
import * as IVaultCore from '../generated-artifacts/IVaultCore.json';
import * as IZrxVault from '../generated-artifacts/IZrxVault.json';
import * as LibCobbDouglas from '../generated-artifacts/LibCobbDouglas.json';
import * as LibFixedMath from '../generated-artifacts/LibFixedMath.json';
@ -34,7 +33,6 @@ import * as MixinStakeStorage from '../generated-artifacts/MixinStakeStorage.jso
import * as MixinStakingPool from '../generated-artifacts/MixinStakingPool.json';
import * as MixinStakingPoolRewards from '../generated-artifacts/MixinStakingPoolRewards.json';
import * as MixinStorage from '../generated-artifacts/MixinStorage.json';
import * as MixinVaultCore from '../generated-artifacts/MixinVaultCore.json';
import * as ReadOnlyProxy from '../generated-artifacts/ReadOnlyProxy.json';
import * as Staking from '../generated-artifacts/Staking.json';
import * as StakingProxy from '../generated-artifacts/StakingProxy.json';
@ -48,7 +46,6 @@ import * as TestLibFixedMath from '../generated-artifacts/TestLibFixedMath.json'
import * as TestLibProxy from '../generated-artifacts/TestLibProxy.json';
import * as TestLibProxyReceiver from '../generated-artifacts/TestLibProxyReceiver.json';
import * as TestLibSafeDowncast from '../generated-artifacts/TestLibSafeDowncast.json';
import * as TestMixinVaultCore from '../generated-artifacts/TestMixinVaultCore.json';
import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json';
import * as TestStaking from '../generated-artifacts/TestStaking.json';
import * as TestStakingNoWETH from '../generated-artifacts/TestStakingNoWETH.json';
@ -59,6 +56,7 @@ export const artifacts = {
ReadOnlyProxy: ReadOnlyProxy as ContractArtifact,
Staking: Staking as ContractArtifact,
StakingProxy: StakingProxy as ContractArtifact,
ZrxVault: ZrxVault as ContractArtifact,
MixinExchangeFees: MixinExchangeFees as ContractArtifact,
MixinExchangeManager: MixinExchangeManager as ContractArtifact,
MixinConstants: MixinConstants as ContractArtifact,
@ -70,7 +68,6 @@ export const artifacts = {
IStorage: IStorage as ContractArtifact,
IStorageInit: IStorageInit as ContractArtifact,
IStructs: IStructs as ContractArtifact,
IVaultCore: IVaultCore as ContractArtifact,
IZrxVault: IZrxVault as ContractArtifact,
LibCobbDouglas: LibCobbDouglas as ContractArtifact,
LibFixedMath: LibFixedMath as ContractArtifact,
@ -88,8 +85,6 @@ export const artifacts = {
MixinFinalizer: MixinFinalizer as ContractArtifact,
MixinParams: MixinParams as ContractArtifact,
MixinScheduler: MixinScheduler as ContractArtifact,
MixinVaultCore: MixinVaultCore as ContractArtifact,
ZrxVault: ZrxVault as ContractArtifact,
TestAssertStorageParams: TestAssertStorageParams as ContractArtifact,
TestCobbDouglas: TestCobbDouglas as ContractArtifact,
TestCumulativeRewardTracking: TestCumulativeRewardTracking as ContractArtifact,
@ -100,7 +95,6 @@ export const artifacts = {
TestLibProxy: TestLibProxy as ContractArtifact,
TestLibProxyReceiver: TestLibProxyReceiver as ContractArtifact,
TestLibSafeDowncast: TestLibSafeDowncast as ContractArtifact,
TestMixinVaultCore: TestMixinVaultCore as ContractArtifact,
TestProtocolFees: TestProtocolFees as ContractArtifact,
TestStaking: TestStaking as ContractArtifact,
TestStakingNoWETH: TestStakingNoWETH as ContractArtifact,

View File

@ -9,7 +9,6 @@ export * from '../generated-wrappers/i_staking_proxy';
export * from '../generated-wrappers/i_storage';
export * from '../generated-wrappers/i_storage_init';
export * from '../generated-wrappers/i_structs';
export * from '../generated-wrappers/i_vault_core';
export * from '../generated-wrappers/i_zrx_vault';
export * from '../generated-wrappers/lib_cobb_douglas';
export * from '../generated-wrappers/lib_fixed_math';
@ -32,7 +31,6 @@ export * from '../generated-wrappers/mixin_stake_storage';
export * from '../generated-wrappers/mixin_staking_pool';
export * from '../generated-wrappers/mixin_staking_pool_rewards';
export * from '../generated-wrappers/mixin_storage';
export * from '../generated-wrappers/mixin_vault_core';
export * from '../generated-wrappers/read_only_proxy';
export * from '../generated-wrappers/staking';
export * from '../generated-wrappers/staking_proxy';
@ -46,7 +44,6 @@ export * from '../generated-wrappers/test_lib_fixed_math';
export * from '../generated-wrappers/test_lib_proxy';
export * from '../generated-wrappers/test_lib_proxy_receiver';
export * from '../generated-wrappers/test_lib_safe_downcast';
export * from '../generated-wrappers/test_mixin_vault_core';
export * from '../generated-wrappers/test_protocol_fees';
export * from '../generated-wrappers/test_staking';
export * from '../generated-wrappers/test_staking_no_w_e_t_h';

View File

@ -1,340 +0,0 @@
/*
@TODO (hysz) - update once new staking mechanics are merged
import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { blockchainTests, expect } from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { Simulation } from './utils/Simulation';
import { StakingWrapper } from './utils/staking_wrapper';
// tslint:disable:no-unnecessary-type-assertion
blockchainTests('End-To-End Simulations', env => {
// constants
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
const PPM_ONE = 1e6;
// 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 () => {
// create accounts
accounts = await env.web3Wrapper.getAvailableAddressesAsync();
owner = accounts[0];
exchange = accounts[1];
users = accounts.slice(2);
users = [...users];
// deploy erc20 proxy
erc20Wrapper = new ERC20Wrapper(env.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(env.provider, owner, erc20ProxyContract, zrxTokenContract);
await stakingWrapper.deployAndConfigureContractsAsync();
});
blockchainTests.resets('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].map(v => (v / 100) * PPM_ONE),
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.7567723629327287936195903273616'),
new BigNumber('16.281305003949353165639885849565'),
new BigNumber('20.310284473430148345239837590322'),
],
expectedPayoutByPoolOperator: [
new BigNumber('4.7567723629327287936195903273616'),
new BigNumber('16.281305003949353165639885849565'),
new BigNumber('20.310284473430148345239837590322'),
],
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 undelegating / 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].map(v => (v / 100) * PPM_ONE),
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.0060373101095302067028699237670'),
new BigNumber('10.288953635983966866289393130525'),
new BigNumber('29.264731802500529663161540874979'),
],
expectedPayoutByPoolOperator: [
new BigNumber('1.1723545509427168206625812850596'),
new BigNumber('6.0704826452305401312658116198463'),
new BigNumber('12.583834675075227560217188236544'),
],
expectedMembersPayoutByPool: [
new BigNumber('1.8336827591668133860402886387074'),
new BigNumber('4.2184709907534267350235815106787'),
new BigNumber('16.680897127425302102944352638435'),
],
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.0163987496997496894870114443624'),
new BigNumber('4.4841121310283074536191681368932'),
new BigNumber('5.3809345572339689443430017642717'),
],
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].map(v => (v / 100) * PPM_ONE),
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.7567723629327287476569912989141'),
new BigNumber('16.281305003949352312532097047985'),
new BigNumber('20.310284473430147203349271380151'),
],
expectedPayoutByPoolOperator: [
new BigNumber('1.8551412215437642749591650093188'),
new BigNumber('9.6059699523301173582693060410895'),
new BigNumber('8.7334223235749631621465139389311'),
],
expectedMembersPayoutByPool: [
new BigNumber('2.9016311413889644726978262895953'),
new BigNumber('6.6753350516192349542627910068955'),
new BigNumber('11.576862149855184041202757441220'),
],
expectedPayoutByDelegator: [new BigNumber(0), 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].map(v => (v / 100) * PPM_ONE),
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.7567723629327287476569912989141'),
new BigNumber('16.281305003949352312532097047985'),
new BigNumber('20.310284473430147203349271380151'),
],
expectedPayoutByPoolOperator: [
new BigNumber('1.8551412215437642749591650093188'),
new BigNumber('9.6059699523301173582693060410895'),
new BigNumber('8.7334223235749631621465139389311'),
],
expectedMembersPayoutByPool: [
new BigNumber('2.9016311413889644726978262895953'),
new BigNumber('6.6753350516192349542627910068955'),
new BigNumber('11.576862149855184041202757441220'),
],
expectedPayoutByDelegator: [new BigNumber(0), 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);
// TODO(jalextowle) I need to update this test when I make my PR on adding protocol fees to the Staking contracts
const revertError = new StakingRevertErrors.OnlyCallableByExchangeError(owner);
const tx = stakingWrapper.payProtocolFeeAsync(makerAddress, makerAddress, protocolFee, protocolFee, owner);
await expect(tx).to.revertWith(revertError);
});
});
});
// tslint:enable:no-unnecessary-type-assertion
*/

View File

@ -25,7 +25,7 @@ import {
toBaseUnitAmount,
} from '../utils/number_utils';
blockchainTests.resets('delegator unit rewards', env => {
blockchainTests.resets('Delegator rewards unit tests', env => {
let testContract: TestDelegatorRewardsContract;
before(async () => {

View File

@ -24,7 +24,7 @@ import {
} from '../../src';
import { assertIntegerRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils';
blockchainTests.resets('finalizer unit tests', env => {
blockchainTests.resets('Finalizer unit tests', env => {
const { ZERO_AMOUNT } = constants;
const INITIAL_EPOCH = 0;
const INITIAL_BALANCE = toBaseUnitAmount(32);

View File

@ -2,12 +2,12 @@ import { blockchainTests, Numberish } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { artifacts, TestCobbDouglasContract } from '../src/';
import { artifacts, TestCobbDouglasContract } from '../../src/';
import { assertRoughlyEquals, getRandomInteger, getRandomPortion, toDecimal } from './utils/number_utils';
import { assertRoughlyEquals, getRandomInteger, getRandomPortion, toDecimal } from '../utils/number_utils';
// tslint:disable: no-unnecessary-type-assertion
blockchainTests('Cobb-Douglas', env => {
blockchainTests('LibCobbDouglas unit tests', env => {
const FUZZ_COUNT = 1024;
const PRECISION = 15;

View File

@ -7,7 +7,7 @@ import { artifacts, TestLibFixedMathContract } from '../../src';
import { assertRoughlyEquals, fromFixed, toDecimal, toFixed } from '../utils/number_utils';
blockchainTests('LibFixedMath', env => {
blockchainTests('LibFixedMath unit tests', env => {
let testContract: TestLibFixedMathContract;
before(async () => {

View File

@ -4,7 +4,7 @@ import { cartesianProduct } from 'js-combinatorics';
import { artifacts, TestLibProxyContract, TestLibProxyReceiverContract } from '../../src';
blockchainTests.resets('LibProxy', env => {
blockchainTests.resets('LibProxy unit tests', env => {
let proxy: TestLibProxyContract;
let receiver: TestLibProxyReceiverContract;

View File

@ -3,7 +3,7 @@ import { BigNumber, SafeMathRevertErrors } from '@0x/utils';
import { artifacts, TestLibSafeDowncastContract } from '../../src/';
blockchainTests('LibSafeDowncast', env => {
blockchainTests('LibSafeDowncast unit tests', env => {
let testContract: TestLibSafeDowncastContract;
before(async () => {

View File

@ -1,99 +0,0 @@
import { blockchainTests, expect, filterLogsToArguments } from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { AuthorizableRevertErrors } from '@0x/utils';
import { constants } from '../utils/constants';
import {
artifacts,
TestMixinVaultCoreContract,
TestMixinVaultCoreInCatastrophicFailureModeEventArgs,
TestMixinVaultCoreStakingProxySetEventArgs,
} from '../../src';
blockchainTests.resets('MixinVaultCore', env => {
let owner: string;
let nonOwnerAddresses: string[];
let testContract: TestMixinVaultCoreContract;
before(async () => {
[owner, ...nonOwnerAddresses] = await env.getAccountAddressesAsync();
testContract = await TestMixinVaultCoreContract.deployFrom0xArtifactAsync(
artifacts.TestMixinVaultCore,
env.provider,
env.txDefaults,
artifacts,
);
});
describe('Set staking proxy', () => {
async function testAssertStakingProxyAsync(callerAddress: string): Promise<void> {
const tx = testContract.assertStakingProxy.callAsync({ from: callerAddress });
const expectedError = new StakingRevertErrors.OnlyCallableByStakingContractError(callerAddress);
expect(tx).to.revertWith(expectedError);
}
it('Owner can set staking proxy', async () => {
const newAddress = nonOwnerAddresses[0];
const receipt = await testContract.setStakingProxy.awaitTransactionSuccessAsync(newAddress, {
from: owner,
});
const eventArgs = filterLogsToArguments<TestMixinVaultCoreStakingProxySetEventArgs>(
receipt.logs,
'StakingProxySet',
);
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].stakingProxyAddress).to.equal(newAddress);
expect(await testContract.stakingProxyAddress.callAsync()).to.equal(newAddress);
// The new staking proxy address should be able to pass the modifier check
await testContract.assertStakingProxy.callAsync({ from: newAddress });
return testAssertStakingProxyAsync(owner);
});
it('Non-authorized address cannot set staking proxy', async () => {
const notAuthorized = nonOwnerAddresses[0];
const newAddress = nonOwnerAddresses[1];
const tx = testContract.setStakingProxy.awaitTransactionSuccessAsync(newAddress, {
from: notAuthorized,
});
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
expect(tx).to.revertWith(expectedError);
expect(await testContract.stakingProxyAddress.callAsync()).to.equal(constants.NIL_ADDRESS);
return testAssertStakingProxyAsync(newAddress);
});
});
describe('Catastrophic failure mode', () => {
async function testCatastrophicFailureModeAsync(isInCatastrophicFailure: boolean): Promise<void> {
const [expectToSucceed, expectToRevert] = isInCatastrophicFailure
? [testContract.assertInCatastrophicFailure, testContract.assertNotInCatastrophicFailure]
: [testContract.assertNotInCatastrophicFailure, testContract.assertInCatastrophicFailure];
const expectedError = isInCatastrophicFailure
? new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError()
: new StakingRevertErrors.OnlyCallableIfInCatastrophicFailureError();
await expectToSucceed.callAsync();
expect(expectToRevert.callAsync()).to.revertWith(expectedError);
expect(await testContract.isInCatastrophicFailure.callAsync()).to.equal(isInCatastrophicFailure);
}
it('Owner can turn on catastrophic failure mode', async () => {
await testCatastrophicFailureModeAsync(false);
const receipt = await testContract.enterCatastrophicFailure.awaitTransactionSuccessAsync({ from: owner });
const eventArgs = filterLogsToArguments<TestMixinVaultCoreInCatastrophicFailureModeEventArgs>(
receipt.logs,
'InCatastrophicFailureMode',
);
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].sender).to.equal(owner);
return testCatastrophicFailureModeAsync(true);
});
it('Non-authorized address cannot turn on catastrophic failure mode', async () => {
await testCatastrophicFailureModeAsync(false);
const tx = testContract.enterCatastrophicFailure.awaitTransactionSuccessAsync({
from: nonOwnerAddresses[0],
});
expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(nonOwnerAddresses[0]));
return testCatastrophicFailureModeAsync(false);
});
});
});

View File

@ -3,10 +3,10 @@ import { AuthorizableRevertErrors, BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { artifacts, IStakingEventsParamsSetEventArgs, MixinParamsContract } from '../src/';
import { artifacts, IStakingEventsParamsSetEventArgs, MixinParamsContract } from '../../src/';
import { constants as stakingConstants } from './utils/constants';
import { StakingParams } from './utils/types';
import { constants as stakingConstants } from '../utils/constants';
import { StakingParams } from '../utils/types';
blockchainTests('Configurable Parameters unit tests', env => {
let testContract: MixinParamsContract;

View File

@ -19,11 +19,11 @@ import {
TestProtocolFeesContract,
TestProtocolFeesERC20ProxyTransferFromEventArgs,
TestProtocolFeesEvents,
} from '../src';
} from '../../src';
import { getRandomInteger } from './utils/number_utils';
import { getRandomInteger } from '../utils/number_utils';
blockchainTests('Protocol Fee Unit Tests', env => {
blockchainTests('Protocol Fees unit tests', env => {
let ownerAddress: string;
let exchangeAddress: string;
let notExchangeAddress: string;

View File

@ -0,0 +1,438 @@
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import {
blockchainTests,
constants,
expect,
expectTransactionFailedAsync,
filterLogsToArguments,
} from '@0x/contracts-test-utils';
import { assetDataUtils, StakingRevertErrors } from '@0x/order-utils';
import { RevertReason } from '@0x/types';
import { AuthorizableRevertErrors, BigNumber, SafeMathRevertErrors } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { constants as stakingConstants } from '../utils/constants';
import {
artifacts,
ZrxVaultContract,
ZrxVaultDepositEventArgs,
ZrxVaultInCatastrophicFailureModeEventArgs,
ZrxVaultStakingProxySetEventArgs,
ZrxVaultWithdrawEventArgs,
ZrxVaultZrxProxySetEventArgs,
} from '../../src';
blockchainTests.resets('ZrxVault unit tests', env => {
let accounts: string[];
let owner: string;
let nonOwnerAddresses: string[];
let erc20Wrapper: ERC20Wrapper;
let zrxVault: ZrxVaultContract;
let zrxAssetData: string;
let zrxProxyAddress: string;
before(async () => {
// create accounts
accounts = await env.getAccountAddressesAsync();
[owner, ...nonOwnerAddresses] = accounts;
// set up ERC20Wrapper
erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner);
// deploy erc20 proxy
const erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
zrxProxyAddress = erc20ProxyContract.address;
// deploy zrx token
const [zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, constants.DUMMY_TOKEN_DECIMALS);
zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxTokenContract.address);
await erc20Wrapper.setBalancesAndAllowancesAsync();
zrxVault = await ZrxVaultContract.deployFrom0xArtifactAsync(
artifacts.ZrxVault,
env.provider,
env.txDefaults,
artifacts,
zrxProxyAddress,
zrxTokenContract.address,
);
// configure erc20 proxy to accept calls from zrx vault
await erc20ProxyContract.addAuthorizedAddress.awaitTransactionSuccessAsync(zrxVault.address);
});
enum ZrxTransfer {
Deposit,
Withdrawal,
}
async function verifyTransferPostconditionsAsync(
transferType: ZrxTransfer,
staker: string,
amount: BigNumber,
initialVaultBalance: BigNumber,
initialTokenBalance: BigNumber,
receipt: TransactionReceiptWithDecodedLogs,
): Promise<void> {
const eventArgs =
transferType === ZrxTransfer.Deposit
? filterLogsToArguments<ZrxVaultDepositEventArgs>(receipt.logs, 'Deposit')
: filterLogsToArguments<ZrxVaultWithdrawEventArgs>(receipt.logs, 'Withdraw');
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].staker).to.equal(staker);
expect(eventArgs[0].amount).to.bignumber.equal(amount);
const newVaultBalance = await zrxVault.balanceOf.callAsync(staker);
const newTokenBalance = await erc20Wrapper.getBalanceAsync(staker, zrxAssetData);
const [expectedVaultBalance, expectedTokenBalance] =
transferType === ZrxTransfer.Deposit
? [initialVaultBalance.plus(amount), initialTokenBalance.minus(amount)]
: [initialVaultBalance.minus(amount), initialTokenBalance.plus(amount)];
expect(newVaultBalance).to.bignumber.equal(expectedVaultBalance);
expect(newTokenBalance).to.bignumber.equal(expectedTokenBalance);
}
describe('Normal operation', () => {
describe('Setting proxies', () => {
async function verifyStakingProxySetAsync(
receipt: TransactionReceiptWithDecodedLogs,
newProxy: string,
): Promise<void> {
const eventArgs = filterLogsToArguments<ZrxVaultStakingProxySetEventArgs>(
receipt.logs,
'StakingProxySet',
);
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].stakingProxyAddress).to.equal(newProxy);
const actualAddress = await zrxVault.stakingProxyAddress.callAsync();
expect(actualAddress).to.equal(newProxy);
}
it('Owner can set the ZRX proxy', async () => {
const newProxy = nonOwnerAddresses[0];
const receipt = await zrxVault.setZrxProxy.awaitTransactionSuccessAsync(newProxy, {
from: owner,
});
const eventArgs = filterLogsToArguments<ZrxVaultZrxProxySetEventArgs>(receipt.logs, 'ZrxProxySet');
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].zrxProxyAddress).to.equal(newProxy);
});
it('Authorized address can set the ZRX proxy', async () => {
const [authorized, newProxy] = nonOwnerAddresses;
await zrxVault.addAuthorizedAddress.awaitTransactionSuccessAsync(authorized, { from: owner });
const receipt = await zrxVault.setZrxProxy.awaitTransactionSuccessAsync(newProxy, {
from: authorized,
});
const eventArgs = filterLogsToArguments<ZrxVaultZrxProxySetEventArgs>(receipt.logs, 'ZrxProxySet');
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].zrxProxyAddress).to.equal(newProxy);
});
it('Non-authorized address cannot set the ZRX proxy', async () => {
const [notAuthorized, newProxy] = nonOwnerAddresses;
const tx = zrxVault.setZrxProxy.awaitTransactionSuccessAsync(newProxy, {
from: notAuthorized,
});
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
expect(tx).to.revertWith(expectedError);
});
it('Owner can set the staking proxy', async () => {
const newProxy = nonOwnerAddresses[0];
const receipt = await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(newProxy, {
from: owner,
});
await verifyStakingProxySetAsync(receipt, newProxy);
});
it('Authorized address can set the staking proxy', async () => {
const [authorized, newProxy] = nonOwnerAddresses;
await zrxVault.addAuthorizedAddress.awaitTransactionSuccessAsync(authorized, { from: owner });
const receipt = await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(newProxy, {
from: authorized,
});
await verifyStakingProxySetAsync(receipt, newProxy);
});
it('Non-authorized address cannot set the staking proxy', async () => {
const [notAuthorized, newProxy] = nonOwnerAddresses;
const tx = zrxVault.setStakingProxy.awaitTransactionSuccessAsync(newProxy, {
from: notAuthorized,
});
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
expect(tx).to.revertWith(expectedError);
const actualAddress = await zrxVault.stakingProxyAddress.callAsync();
expect(actualAddress).to.equal(stakingConstants.NIL_ADDRESS);
});
});
describe('ZRX management', () => {
let staker: string;
let stakingProxy: string;
let initialVaultBalance: BigNumber;
let initialTokenBalance: BigNumber;
before(async () => {
[staker, stakingProxy] = nonOwnerAddresses;
await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(stakingProxy, { from: owner });
await zrxVault.depositFrom.awaitTransactionSuccessAsync(staker, new BigNumber(10), {
from: stakingProxy,
});
});
beforeEach(async () => {
initialVaultBalance = await zrxVault.balanceOf.callAsync(staker);
initialTokenBalance = await erc20Wrapper.getBalanceAsync(staker, zrxAssetData);
});
describe('Deposit', () => {
it('Staking proxy can deposit zero amount on behalf of staker', async () => {
const receipt = await zrxVault.depositFrom.awaitTransactionSuccessAsync(
staker,
constants.ZERO_AMOUNT,
{
from: stakingProxy,
},
);
await verifyTransferPostconditionsAsync(
ZrxTransfer.Deposit,
staker,
constants.ZERO_AMOUNT,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Staking proxy can deposit nonzero amount on behalf of staker', async () => {
const receipt = await zrxVault.depositFrom.awaitTransactionSuccessAsync(staker, new BigNumber(1), {
from: stakingProxy,
});
await verifyTransferPostconditionsAsync(
ZrxTransfer.Deposit,
staker,
new BigNumber(1),
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Staking proxy can deposit entire ZRX balance on behalf of staker', async () => {
const receipt = await zrxVault.depositFrom.awaitTransactionSuccessAsync(
staker,
initialTokenBalance,
{
from: stakingProxy,
},
);
await verifyTransferPostconditionsAsync(
ZrxTransfer.Deposit,
staker,
initialTokenBalance,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it("Reverts if attempting to deposit more than staker's ZRX balance", async () => {
const tx = zrxVault.depositFrom.sendTransactionAsync(staker, initialTokenBalance.plus(1), {
from: stakingProxy,
});
expectTransactionFailedAsync(tx, RevertReason.TransferFailed);
});
});
describe('Withdrawal', () => {
it('Staking proxy can withdraw zero amount on behalf of staker', async () => {
const receipt = await zrxVault.withdrawFrom.awaitTransactionSuccessAsync(
staker,
constants.ZERO_AMOUNT,
{
from: stakingProxy,
},
);
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
constants.ZERO_AMOUNT,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Staking proxy can withdraw nonzero amount on behalf of staker', async () => {
const receipt = await zrxVault.withdrawFrom.awaitTransactionSuccessAsync(staker, new BigNumber(1), {
from: stakingProxy,
});
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
new BigNumber(1),
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Staking proxy can withdraw entire vault balance on behalf of staker', async () => {
const receipt = await zrxVault.withdrawFrom.awaitTransactionSuccessAsync(
staker,
initialVaultBalance,
{
from: stakingProxy,
},
);
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
initialVaultBalance,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it("Reverts if attempting to withdraw more than staker's vault balance", async () => {
const tx = zrxVault.withdrawFrom.awaitTransactionSuccessAsync(staker, initialVaultBalance.plus(1), {
from: stakingProxy,
});
const expectedError = new SafeMathRevertErrors.Uint256BinOpError(
SafeMathRevertErrors.BinOpErrorCodes.SubtractionUnderflow,
initialVaultBalance,
initialVaultBalance.plus(1),
);
expect(tx).to.revertWith(expectedError);
});
});
});
});
describe('Catastrophic Failure Mode', () => {
describe('Authorization', () => {
async function verifyCatastrophicFailureModeAsync(
sender: string,
receipt: TransactionReceiptWithDecodedLogs,
): Promise<void> {
const eventArgs = filterLogsToArguments<ZrxVaultInCatastrophicFailureModeEventArgs>(
receipt.logs,
'InCatastrophicFailureMode',
);
expect(eventArgs.length).to.equal(1);
expect(eventArgs[0].sender).to.equal(sender);
expect(await zrxVault.isInCatastrophicFailure.callAsync()).to.be.true();
}
it('Owner can turn on Catastrophic Failure Mode', async () => {
const receipt = await zrxVault.enterCatastrophicFailure.awaitTransactionSuccessAsync({ from: owner });
await verifyCatastrophicFailureModeAsync(owner, receipt);
});
it('Authorized address can turn on Catastrophic Failure Mode', async () => {
const authorized = nonOwnerAddresses[0];
await zrxVault.addAuthorizedAddress.awaitTransactionSuccessAsync(authorized, { from: owner });
const receipt = await zrxVault.enterCatastrophicFailure.awaitTransactionSuccessAsync({
from: authorized,
});
await verifyCatastrophicFailureModeAsync(authorized, receipt);
});
it('Non-authorized address cannot turn on Catastrophic Failure Mode', async () => {
const notAuthorized = nonOwnerAddresses[0];
const tx = zrxVault.enterCatastrophicFailure.awaitTransactionSuccessAsync({
from: notAuthorized,
});
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
expect(tx).to.revertWith(expectedError);
expect(await zrxVault.isInCatastrophicFailure.callAsync()).to.be.false();
});
});
describe('Affected functionality', () => {
let staker: string;
let stakingProxy: string;
let initialVaultBalance: BigNumber;
let initialTokenBalance: BigNumber;
before(async () => {
[staker, stakingProxy, ...nonOwnerAddresses] = nonOwnerAddresses;
await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(stakingProxy, { from: owner });
await zrxVault.depositFrom.awaitTransactionSuccessAsync(staker, new BigNumber(10), {
from: stakingProxy,
});
await zrxVault.enterCatastrophicFailure.awaitTransactionSuccessAsync({ from: owner });
});
beforeEach(async () => {
initialVaultBalance = await zrxVault.balanceOf.callAsync(staker);
initialTokenBalance = await erc20Wrapper.getBalanceAsync(staker, zrxAssetData);
});
it('Owner cannot set the ZRX proxy', async () => {
const newProxy = nonOwnerAddresses[0];
const tx = zrxVault.setZrxProxy.awaitTransactionSuccessAsync(newProxy, {
from: owner,
});
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
expect(tx).to.revertWith(expectedError);
const actualAddress = await zrxVault.zrxAssetProxy.callAsync();
expect(actualAddress).to.equal(zrxProxyAddress);
});
it('Authorized address cannot set the ZRX proxy', async () => {
const [authorized, newProxy] = nonOwnerAddresses;
await zrxVault.addAuthorizedAddress.awaitTransactionSuccessAsync(authorized, { from: owner });
const tx = zrxVault.setZrxProxy.awaitTransactionSuccessAsync(newProxy, {
from: authorized,
});
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
expect(tx).to.revertWith(expectedError);
const actualAddress = await zrxVault.zrxAssetProxy.callAsync();
expect(actualAddress).to.equal(zrxProxyAddress);
});
it('Staking proxy cannot deposit ZRX', async () => {
const tx = zrxVault.depositFrom.awaitTransactionSuccessAsync(staker, new BigNumber(1), {
from: stakingProxy,
});
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
expect(tx).to.revertWith(expectedError);
});
describe('Withdrawal', () => {
it('Staking proxy cannot call `withdrawFrom`', async () => {
const tx = zrxVault.withdrawFrom.awaitTransactionSuccessAsync(staker, new BigNumber(1), {
from: stakingProxy,
});
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
expect(tx).to.revertWith(expectedError);
});
it('Staker can withdraw all their ZRX', async () => {
const receipt = await zrxVault.withdrawAllFrom.awaitTransactionSuccessAsync(staker, {
from: staker,
});
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
initialVaultBalance,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Owner can withdraw ZRX on behalf of a staker', async () => {
const receipt = await zrxVault.withdrawAllFrom.awaitTransactionSuccessAsync(staker, {
from: owner,
});
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
initialVaultBalance,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
it('Non-owner address can withdraw ZRX on behalf of a staker', async () => {
const receipt = await zrxVault.withdrawAllFrom.awaitTransactionSuccessAsync(staker, {
from: nonOwnerAddresses[0],
});
await verifyTransferPostconditionsAsync(
ZrxTransfer.Withdrawal,
staker,
initialVaultBalance,
initialVaultBalance,
initialTokenBalance,
receipt,
);
});
});
});
});
});

View File

@ -1,279 +0,0 @@
/*
@TODO (hysz) - update once new staking mechanics are merged
import { expect } from '@0x/contracts-test-utils';
import { chaiSetup } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
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';
const REWARD_PRECISION = 12;
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[];
private static _assertRewardsEqual(actual: BigNumber, expected: BigNumber, message?: string): void {
expect(
StakingWrapper.trimFloat(StakingWrapper.toFloatingPoint(actual, 18), REWARD_PRECISION),
message,
).to.be.bignumber.equal(StakingWrapper.trimFloat(expected, REWARD_PRECISION));
}
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 status 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 expectedReward = p.expectedPayoutByDelegator[delegatorIdx];
Simulation._assertRewardsEqual(
reward,
expectedReward,
`reward withdrawn from pool ${poolId} for delegator ${delegatorAddress}`,
);
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 expectedReward = p.expectedPayoutByDelegator[delegatorIdx];
Simulation._assertRewardsEqual(
reward,
expectedReward,
`reward withdrawn from pool ${poolId} for delegator ${delegatorAddress}`,
);
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], false);
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.depositZrxAndDelegateToStakingPoolAsync(poolId, 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];
await maker.joinStakingPoolAsMakerAsync(poolId);
const makerAddress = maker.getOwner();
await poolOperator.addMakerToStakingPoolAsync(poolId, makerAddress);
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];
// TODO(jalextowle): I'll need to fix this once I make my PR on protocol fees. The arguments
// I'm adding are just placeholders for now.
await this._stakingWrapper.payProtocolFeeAsync(
makerAddress,
makerAddress,
feeAmount,
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 REWARD_PRECISION decimal places.
// update once more accurate.
// check pool balance in vault
const poolId = this._poolIds[i];
const rewardVaultBalance = await this._stakingWrapper.rewardVaultBalanceOfAsync(poolId);
const expectedRewardBalance = p.expectedPayoutByPool[i];
Simulation._assertRewardsEqual(
rewardVaultBalance,
expectedRewardBalance,
`expected balance in vault for pool with id ${poolId}`,
);
// check operator's balance
const poolOperatorVaultBalance = await this._stakingWrapper.getRewardBalanceOfStakingPoolOperatorAsync(
poolId,
);
const expectedPoolOperatorVaultBalance = p.expectedPayoutByPoolOperator[i];
Simulation._assertRewardsEqual(
poolOperatorVaultBalance,
expectedPoolOperatorVaultBalance,
`operator balance in vault for pool with id ${poolId}`,
);
// check balance of pool members
const membersVaultBalance = await this._stakingWrapper.getRewardBalanceOfStakingPoolMembersAsync(poolId);
const expectedMembersVaultBalance = p.expectedMembersPayoutByPool[i];
Simulation._assertRewardsEqual(
membersVaultBalance,
expectedMembersVaultBalance,
`members balance in vault for pool with id ${poolId}`,
);
// @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 REWARD_PRECISION 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 expectedReward = p.expectedPayoutByPoolOperator[i];
Simulation._assertRewardsEqual(reward, expectedReward, `reward withdrawn from pool ${poolId} for operator`);
}
}
}
*/

View File

@ -9,7 +9,6 @@
"generated-artifacts/IStorage.json",
"generated-artifacts/IStorageInit.json",
"generated-artifacts/IStructs.json",
"generated-artifacts/IVaultCore.json",
"generated-artifacts/IZrxVault.json",
"generated-artifacts/LibCobbDouglas.json",
"generated-artifacts/LibFixedMath.json",
@ -32,7 +31,6 @@
"generated-artifacts/MixinStakingPool.json",
"generated-artifacts/MixinStakingPoolRewards.json",
"generated-artifacts/MixinStorage.json",
"generated-artifacts/MixinVaultCore.json",
"generated-artifacts/ReadOnlyProxy.json",
"generated-artifacts/Staking.json",
"generated-artifacts/StakingProxy.json",
@ -46,7 +44,6 @@
"generated-artifacts/TestLibProxy.json",
"generated-artifacts/TestLibProxyReceiver.json",
"generated-artifacts/TestLibSafeDowncast.json",
"generated-artifacts/TestMixinVaultCore.json",
"generated-artifacts/TestProtocolFees.json",
"generated-artifacts/TestStaking.json",
"generated-artifacts/TestStakingNoWETH.json",