Read-Only mode in proxy

This commit is contained in:
Greg Hysen 2019-09-04 04:21:41 -07:00
parent fc7f2e7fc6
commit d0c6d9cf2d
10 changed files with 246 additions and 2 deletions

View File

@ -0,0 +1,107 @@
/*
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 "./immutable/MixinStorage.sol";
contract ReadOnlyProxy is
MixinStorage
{
/// @dev Executes a read-only call to the staking contract, via `revertDelegateCall`.
/// By routing through `revertDelegateCall` any state changes are reverted.
// solhint-disable no-complex-fallback
function ()
external
{
address thisAddress = address(this);
bytes4 revertDelegateCallSelector = this.revertDelegateCall.selector;
assembly {
// store selector of destination function
mstore(0x0, revertDelegateCallSelector)
// copy calldata to memory
calldatacopy(
0x4,
0x0,
calldatasize()
)
// delegate call into staking contract
let success := delegatecall(
gas, // forward all gas
thisAddress, // calling staking contract
0x0, // start of input (calldata)
add(calldatasize(), 4), // length of input (calldata)
0x0, // write output over input
0 // length of output is unknown
)
// copy return data to memory and *return*
returndatacopy(
0x0,
0x0,
returndatasize()
)
return(0, returndatasize())
}
}
/// @dev Executes a delegate call to the staking contract, if it is set.
/// This function always reverts with the return data.
function revertDelegateCall()
external
{
address _readOnlyProxyCallee = readOnlyProxyCallee;
if (_readOnlyProxyCallee == address(0)) {
return;
}
assembly {
// copy calldata to memory
calldatacopy(
0x0,
0x4,
calldatasize()
)
// delegate call into staking contract
let success := delegatecall(
gas, // forward all gas
_readOnlyProxyCallee, // calling staking contract
0x0, // start of input (calldata)
sub(calldatasize(), 4), // length of input (calldata)
0x0, // write output over input
0 // length of output is unknown
)
// copy return data to memory and *revert*
returndatacopy(
0x0,
0x0,
returndatasize()
)
revert(0, returndatasize())
}
}
}

View File

@ -31,11 +31,13 @@ contract StakingProxy is
/// @dev Constructor. /// @dev Constructor.
/// @param _stakingContract Staking contract to delegate calls to. /// @param _stakingContract Staking contract to delegate calls to.
constructor(address _stakingContract) constructor(address _stakingContract, address _readOnlyProxy)
public public
MixinStorage() MixinStorage()
{ {
stakingContract = _stakingContract; stakingContract = _stakingContract;
readOnlyProxyCallee = _stakingContract;
readOnlyProxy = _readOnlyProxy;
} }
/// @dev Delegates calls to the staking contract, if it is set. /// @dev Delegates calls to the staking contract, if it is set.
@ -92,6 +94,7 @@ contract StakingProxy is
onlyOwner onlyOwner
{ {
stakingContract = _stakingContract; stakingContract = _stakingContract;
readOnlyProxyCallee = _stakingContract;
emit StakingContractAttachedToProxy(_stakingContract); emit StakingContractAttachedToProxy(_stakingContract);
} }
@ -104,4 +107,18 @@ contract StakingProxy is
stakingContract = NIL_ADDRESS; stakingContract = NIL_ADDRESS;
emit StakingContractDetachedFromProxy(); emit StakingContractDetachedFromProxy();
} }
/// @dev Set read-only mode (state cannot be changed).
function setReadOnlyMode(bool readOnlyMode)
external
onlyOwner
{
if (readOnlyMode) {
stakingContract = readOnlyProxy;
} else {
stakingContract = readOnlyProxyCallee;
}
emit ReadOnlyModeSet(readOnlyMode);
}
} }

View File

@ -41,6 +41,12 @@ contract MixinStorage is
// address of staking contract // address of staking contract
address internal stakingContract; address internal stakingContract;
// address of read-only proxy
address internal readOnlyProxy;
// address for read-only proxy to call
address internal readOnlyProxyCallee;
// mapping from Owner to Amount of Active Stake // mapping from Owner to Amount of Active Stake
// (access using _loadAndSyncBalance or _loadUnsyncedBalance) // (access using _loadAndSyncBalance or _loadUnsyncedBalance)
mapping (address => IStructs.StoredBalance) internal activeStakeByOwner; mapping (address => IStructs.StoredBalance) internal activeStakeByOwner;

View File

@ -31,6 +31,11 @@ interface IStakingProxy /* is IStaking */
/// @dev Emitted by StakingProxy when a staking contract is detached. /// @dev Emitted by StakingProxy when a staking contract is detached.
event StakingContractDetachedFromProxy(); event StakingContractDetachedFromProxy();
/// @dev Emitted by StakingProxy when read-only mode is set.
event ReadOnlyModeSet(
bool readOnlyMode
);
/// @dev Delegates calls to the staking contract, if it is set. /// @dev Delegates calls to the staking contract, if it is set.
// solhint-disable no-complex-fallback // solhint-disable no-complex-fallback
function () function ()

View File

@ -37,7 +37,7 @@
}, },
"config": { "config": {
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./generated-artifacts/@(EthVault|IEthVault|IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStructs|IVaultCore|IWallet|IZrxVault|LibEIP712Hash|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibSignatureValidator|LibStakingRichErrors|MixinConstants|MixinDeploymentConstants|MixinEthVault|MixinExchangeFees|MixinExchangeManager|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|MixinZrxVault|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestLibFixedMath|TestStorageLayout|ZrxVault).json" "abis": "./generated-artifacts/@(EthVault|IEthVault|IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStructs|IVaultCore|IWallet|IZrxVault|LibEIP712Hash|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibSignatureValidator|LibStakingRichErrors|MixinConstants|MixinDeploymentConstants|MixinEthVault|MixinExchangeFees|MixinExchangeManager|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|MixinZrxVault|ReadOnlyProxy|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestLibFixedMath|TestStorageLayout|ZrxVault).json"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -36,6 +36,7 @@ import * as MixinStakingPoolRewardVault from '../generated-artifacts/MixinStakin
import * as MixinStorage from '../generated-artifacts/MixinStorage.json'; import * as MixinStorage from '../generated-artifacts/MixinStorage.json';
import * as MixinVaultCore from '../generated-artifacts/MixinVaultCore.json'; import * as MixinVaultCore from '../generated-artifacts/MixinVaultCore.json';
import * as MixinZrxVault from '../generated-artifacts/MixinZrxVault.json'; import * as MixinZrxVault from '../generated-artifacts/MixinZrxVault.json';
import * as ReadOnlyProxy from '../generated-artifacts/ReadOnlyProxy.json';
import * as Staking from '../generated-artifacts/Staking.json'; import * as Staking from '../generated-artifacts/Staking.json';
import * as StakingPoolRewardVault from '../generated-artifacts/StakingPoolRewardVault.json'; import * as StakingPoolRewardVault from '../generated-artifacts/StakingPoolRewardVault.json';
import * as StakingProxy from '../generated-artifacts/StakingProxy.json'; import * as StakingProxy from '../generated-artifacts/StakingProxy.json';
@ -44,6 +45,7 @@ import * as TestLibFixedMath from '../generated-artifacts/TestLibFixedMath.json'
import * as TestStorageLayout from '../generated-artifacts/TestStorageLayout.json'; import * as TestStorageLayout from '../generated-artifacts/TestStorageLayout.json';
import * as ZrxVault from '../generated-artifacts/ZrxVault.json'; import * as ZrxVault from '../generated-artifacts/ZrxVault.json';
export const artifacts = { export const artifacts = {
ReadOnlyProxy: ReadOnlyProxy as ContractArtifact,
Staking: Staking as ContractArtifact, Staking: Staking as ContractArtifact,
StakingProxy: StakingProxy as ContractArtifact, StakingProxy: StakingProxy as ContractArtifact,
MixinExchangeFees: MixinExchangeFees as ContractArtifact, MixinExchangeFees: MixinExchangeFees as ContractArtifact,

View File

@ -34,6 +34,7 @@ export * from '../generated-wrappers/mixin_staking_pool_rewards';
export * from '../generated-wrappers/mixin_storage'; export * from '../generated-wrappers/mixin_storage';
export * from '../generated-wrappers/mixin_vault_core'; export * from '../generated-wrappers/mixin_vault_core';
export * from '../generated-wrappers/mixin_zrx_vault'; export * from '../generated-wrappers/mixin_zrx_vault';
export * from '../generated-wrappers/read_only_proxy';
export * from '../generated-wrappers/staking'; export * from '../generated-wrappers/staking';
export * from '../generated-wrappers/staking_pool_reward_vault'; export * from '../generated-wrappers/staking_pool_reward_vault';
export * from '../generated-wrappers/staking_proxy'; export * from '../generated-wrappers/staking_proxy';

View File

@ -0,0 +1,89 @@
import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { blockchainTests, describe, expect, provider, web3Wrapper } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { LogWithDecodedArgs } from 'ethereum-types';
import * as _ from 'lodash';
import { StakingProxyReadOnlyModeSetEventArgs } from '../src';
import { StakingWrapper } from './utils/staking_wrapper';
// tslint:disable:no-unnecessary-type-assertion
blockchainTests.resets('Catastrophy Tests', () => {
// constants
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
const ZERO = new BigNumber(0);
// tokens & addresses
let accounts: string[];
let owner: string;
let actors: string[];
let zrxTokenContract: DummyERC20TokenContract;
let erc20ProxyContract: ERC20ProxyContract;
// wrappers
let stakingWrapper: StakingWrapper;
let erc20Wrapper: ERC20Wrapper;
// tests
before(async () => {
// create accounts
accounts = await web3Wrapper.getAvailableAddressesAsync();
owner = accounts[0];
actors = 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();
});
describe('Read-Only Mode', () => {
it('should be able to change state by default', async () => {
// stake some zrx and assert the balance
const amountToStake = StakingWrapper.toBaseUnitAmount(10);
await stakingWrapper.stakeAsync(actors[0], amountToStake);
const activeStakeBalance = await stakingWrapper.getActiveStakeAsync(actors[0]);
expect(activeStakeBalance.currentEpochBalance).to.be.bignumber.equal(amountToStake);
});
it('should not change state when in read-only mode', async () => {
// set to read-only mode
await stakingWrapper.setReadOnlyModeAsync(true);
// try to stake
const amountToStake = StakingWrapper.toBaseUnitAmount(10);
await stakingWrapper.stakeAsync(actors[0], amountToStake);
const activeStakeBalance = await stakingWrapper.getActiveStakeAsync(actors[0]);
expect(activeStakeBalance.currentEpochBalance).to.be.bignumber.equal(ZERO);
});
it('should read values correctly when in read-only mode', async () => {
// stake some zrx
const amountToStake = StakingWrapper.toBaseUnitAmount(10);
await stakingWrapper.stakeAsync(actors[0], amountToStake);
// set to read-only mode
await stakingWrapper.setReadOnlyModeAsync(true);
// read stake balance in read-only mode
const activeStakeBalanceReadOnly = await stakingWrapper.getActiveStakeAsync(actors[0]);
expect(activeStakeBalanceReadOnly.currentEpochBalance).to.be.bignumber.equal(amountToStake);
});
it('should exit read-only mode', async () => {
// set to read-only mode
await stakingWrapper.setReadOnlyModeAsync(true);
await stakingWrapper.setReadOnlyModeAsync(false);
// try to stake
const amountToStake = StakingWrapper.toBaseUnitAmount(10);
await stakingWrapper.stakeAsync(actors[0], amountToStake);
const activeStakeBalance = await stakingWrapper.getActiveStakeAsync(actors[0]);
expect(activeStakeBalance.currentEpochBalance).to.be.bignumber.equal(amountToStake);
});
it('should emit event when setting read-only mode', async () => {
// set to read-only mode
const txReceipt = await stakingWrapper.setReadOnlyModeAsync(true);
expect(txReceipt.logs.length).to.be.equal(1);
const trueLog = txReceipt.logs[0] as LogWithDecodedArgs<StakingProxyReadOnlyModeSetEventArgs>;
expect(trueLog.args.readOnlyMode).to.be.true();
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@ -10,6 +10,7 @@ import * as _ from 'lodash';
import { import {
artifacts, artifacts,
EthVaultContract, EthVaultContract,
ReadOnlyProxyContract,
StakingContract, StakingContract,
StakingPoolRewardVaultContract, StakingPoolRewardVaultContract,
StakingProxyContract, StakingProxyContract,
@ -33,6 +34,7 @@ export class StakingWrapper {
private _zrxVaultContractIfExists?: ZrxVaultContract; private _zrxVaultContractIfExists?: ZrxVaultContract;
private _ethVaultContractIfExists?: EthVaultContract; private _ethVaultContractIfExists?: EthVaultContract;
private _rewardVaultContractIfExists?: StakingPoolRewardVaultContract; private _rewardVaultContractIfExists?: StakingPoolRewardVaultContract;
private _readOnlyProxyContractIfExists?: ReadOnlyProxyContract;
public static toBaseUnitAmount(amount: BigNumber | number): BigNumber { public static toBaseUnitAmount(amount: BigNumber | number): BigNumber {
const decimals = 18; const decimals = 18;
const amountAsBigNumber = typeof amount === 'number' ? new BigNumber(amount) : amount; const amountAsBigNumber = typeof amount === 'number' ? new BigNumber(amount) : amount;
@ -97,6 +99,13 @@ export class StakingWrapper {
return this._rewardVaultContractIfExists as StakingPoolRewardVaultContract; return this._rewardVaultContractIfExists as StakingPoolRewardVaultContract;
} }
public async deployAndConfigureContractsAsync(): Promise<void> { public async deployAndConfigureContractsAsync(): Promise<void> {
// deploy read-only proxy
this._readOnlyProxyContractIfExists = await ReadOnlyProxyContract.deployFrom0xArtifactAsync(
artifacts.ReadOnlyProxy,
this._provider,
txDefaults,
artifacts,
);
// deploy zrx vault // deploy zrx vault
this._zrxVaultContractIfExists = await ZrxVaultContract.deployFrom0xArtifactAsync( this._zrxVaultContractIfExists = await ZrxVaultContract.deployFrom0xArtifactAsync(
artifacts.ZrxVault, artifacts.ZrxVault,
@ -142,6 +151,7 @@ export class StakingWrapper {
txDefaults, txDefaults,
artifacts, artifacts,
this._stakingContractIfExists.address, this._stakingContractIfExists.address,
this._readOnlyProxyContractIfExists.address,
); );
// set staking proxy contract in zrx vault // set staking proxy contract in zrx vault
await this._zrxVaultContractIfExists.setStakingContract.awaitTransactionSuccessAsync( await this._zrxVaultContractIfExists.setStakingContract.awaitTransactionSuccessAsync(
@ -176,6 +186,12 @@ export class StakingWrapper {
await this._web3Wrapper.sendTransactionAsync(setStakingPoolRewardVaultTxData), await this._web3Wrapper.sendTransactionAsync(setStakingPoolRewardVaultTxData),
); );
} }
public async setReadOnlyModeAsync(readOnlyMode: boolean): Promise<TransactionReceiptWithDecodedLogs> {
const txReceipt = await this.getStakingProxyContract().setReadOnlyMode.awaitTransactionSuccessAsync(
readOnlyMode,
);
return txReceipt;
}
public async getEthBalanceAsync(owner: string): Promise<BigNumber> { public async getEthBalanceAsync(owner: string): Promise<BigNumber> {
const balance = this._web3Wrapper.getBalanceInWeiAsync(owner); const balance = this._web3Wrapper.getBalanceInWeiAsync(owner);
return balance; return balance;

View File

@ -34,6 +34,7 @@
"generated-artifacts/MixinStorage.json", "generated-artifacts/MixinStorage.json",
"generated-artifacts/MixinVaultCore.json", "generated-artifacts/MixinVaultCore.json",
"generated-artifacts/MixinZrxVault.json", "generated-artifacts/MixinZrxVault.json",
"generated-artifacts/ReadOnlyProxy.json",
"generated-artifacts/Staking.json", "generated-artifacts/Staking.json",
"generated-artifacts/StakingPoolRewardVault.json", "generated-artifacts/StakingPoolRewardVault.json",
"generated-artifacts/StakingProxy.json", "generated-artifacts/StakingProxy.json",