Read-Only mode in proxy
This commit is contained in:
parent
fc7f2e7fc6
commit
d0c6d9cf2d
107
contracts/staking/contracts/src/ReadOnlyProxy.sol
Normal file
107
contracts/staking/contracts/src/ReadOnlyProxy.sol
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
@ -31,11 +31,13 @@ contract StakingProxy is
|
||||
|
||||
/// @dev Constructor.
|
||||
/// @param _stakingContract Staking contract to delegate calls to.
|
||||
constructor(address _stakingContract)
|
||||
constructor(address _stakingContract, address _readOnlyProxy)
|
||||
public
|
||||
MixinStorage()
|
||||
{
|
||||
stakingContract = _stakingContract;
|
||||
readOnlyProxyCallee = _stakingContract;
|
||||
readOnlyProxy = _readOnlyProxy;
|
||||
}
|
||||
|
||||
/// @dev Delegates calls to the staking contract, if it is set.
|
||||
@ -92,6 +94,7 @@ contract StakingProxy is
|
||||
onlyOwner
|
||||
{
|
||||
stakingContract = _stakingContract;
|
||||
readOnlyProxyCallee = _stakingContract;
|
||||
emit StakingContractAttachedToProxy(_stakingContract);
|
||||
}
|
||||
|
||||
@ -104,4 +107,18 @@ contract StakingProxy is
|
||||
stakingContract = NIL_ADDRESS;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,12 @@ contract MixinStorage is
|
||||
// address of staking contract
|
||||
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
|
||||
// (access using _loadAndSyncBalance or _loadUnsyncedBalance)
|
||||
mapping (address => IStructs.StoredBalance) internal activeStakeByOwner;
|
||||
|
@ -31,6 +31,11 @@ interface IStakingProxy /* is IStaking */
|
||||
/// @dev Emitted by StakingProxy when a staking contract is detached.
|
||||
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.
|
||||
// solhint-disable no-complex-fallback
|
||||
function ()
|
||||
|
@ -37,7 +37,7 @@
|
||||
},
|
||||
"config": {
|
||||
"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": {
|
||||
"type": "git",
|
||||
|
@ -36,6 +36,7 @@ import * as MixinStakingPoolRewardVault from '../generated-artifacts/MixinStakin
|
||||
import * as MixinStorage from '../generated-artifacts/MixinStorage.json';
|
||||
import * as MixinVaultCore from '../generated-artifacts/MixinVaultCore.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 StakingPoolRewardVault from '../generated-artifacts/StakingPoolRewardVault.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 ZrxVault from '../generated-artifacts/ZrxVault.json';
|
||||
export const artifacts = {
|
||||
ReadOnlyProxy: ReadOnlyProxy as ContractArtifact,
|
||||
Staking: Staking as ContractArtifact,
|
||||
StakingProxy: StakingProxy as ContractArtifact,
|
||||
MixinExchangeFees: MixinExchangeFees as ContractArtifact,
|
||||
|
@ -34,6 +34,7 @@ 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/mixin_zrx_vault';
|
||||
export * from '../generated-wrappers/read_only_proxy';
|
||||
export * from '../generated-wrappers/staking';
|
||||
export * from '../generated-wrappers/staking_pool_reward_vault';
|
||||
export * from '../generated-wrappers/staking_proxy';
|
||||
|
89
contracts/staking/test/catastrophy_test.ts
Normal file
89
contracts/staking/test/catastrophy_test.ts
Normal 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
|
@ -10,6 +10,7 @@ import * as _ from 'lodash';
|
||||
import {
|
||||
artifacts,
|
||||
EthVaultContract,
|
||||
ReadOnlyProxyContract,
|
||||
StakingContract,
|
||||
StakingPoolRewardVaultContract,
|
||||
StakingProxyContract,
|
||||
@ -33,6 +34,7 @@ export class StakingWrapper {
|
||||
private _zrxVaultContractIfExists?: ZrxVaultContract;
|
||||
private _ethVaultContractIfExists?: EthVaultContract;
|
||||
private _rewardVaultContractIfExists?: StakingPoolRewardVaultContract;
|
||||
private _readOnlyProxyContractIfExists?: ReadOnlyProxyContract;
|
||||
public static toBaseUnitAmount(amount: BigNumber | number): BigNumber {
|
||||
const decimals = 18;
|
||||
const amountAsBigNumber = typeof amount === 'number' ? new BigNumber(amount) : amount;
|
||||
@ -97,6 +99,13 @@ export class StakingWrapper {
|
||||
return this._rewardVaultContractIfExists as StakingPoolRewardVaultContract;
|
||||
}
|
||||
public async deployAndConfigureContractsAsync(): Promise<void> {
|
||||
// deploy read-only proxy
|
||||
this._readOnlyProxyContractIfExists = await ReadOnlyProxyContract.deployFrom0xArtifactAsync(
|
||||
artifacts.ReadOnlyProxy,
|
||||
this._provider,
|
||||
txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
// deploy zrx vault
|
||||
this._zrxVaultContractIfExists = await ZrxVaultContract.deployFrom0xArtifactAsync(
|
||||
artifacts.ZrxVault,
|
||||
@ -142,6 +151,7 @@ export class StakingWrapper {
|
||||
txDefaults,
|
||||
artifacts,
|
||||
this._stakingContractIfExists.address,
|
||||
this._readOnlyProxyContractIfExists.address,
|
||||
);
|
||||
// set staking proxy contract in zrx vault
|
||||
await this._zrxVaultContractIfExists.setStakingContract.awaitTransactionSuccessAsync(
|
||||
@ -176,6 +186,12 @@ export class StakingWrapper {
|
||||
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> {
|
||||
const balance = this._web3Wrapper.getBalanceInWeiAsync(owner);
|
||||
return balance;
|
||||
|
@ -34,6 +34,7 @@
|
||||
"generated-artifacts/MixinStorage.json",
|
||||
"generated-artifacts/MixinVaultCore.json",
|
||||
"generated-artifacts/MixinZrxVault.json",
|
||||
"generated-artifacts/ReadOnlyProxy.json",
|
||||
"generated-artifacts/Staking.json",
|
||||
"generated-artifacts/StakingPoolRewardVault.json",
|
||||
"generated-artifacts/StakingProxy.json",
|
||||
|
Loading…
x
Reference in New Issue
Block a user