Merge pull request #2310 from 0xProject/tests/3.0/StakingProxyUnitTests

Staking Proxy Unit Tests + Fallback Reverts if No Staking Contract
This commit is contained in:
Greg Hysz
2019-11-01 10:26:54 -07:00
committed by GitHub
10 changed files with 470 additions and 3 deletions

View File

@@ -17,6 +17,10 @@
{
"note": "Call `StakingProxy.assertValidStorageParams()` in `MixinParams.setParams()`",
"pr": 2279
},
{
"note": "The fallback function in `StakingProxy` reverts if there is no staking contract attached",
"pr": 2310
}
]
},

View File

@@ -54,8 +54,16 @@ contract StakingProxy is
external
payable
{
// Sanity check that we have a staking contract to call
address stakingContract_ = stakingContract;
if (stakingContract_ == NIL_ADDRESS) {
LibRichErrors.rrevert(
LibStakingRichErrors.ProxyDestinationCannotBeNilError()
);
}
// Call the staking contract with the provided calldata.
(bool success, bytes memory returnData) = stakingContract.delegatecall(msg.data);
(bool success, bytes memory returnData) = stakingContract_.delegatecall(msg.data);
// Revert on failure or return on success.
assembly {
@@ -104,7 +112,7 @@ contract StakingProxy is
address staking = stakingContract;
// Ensure that a staking contract has been attached to the proxy.
if (staking == address(0)) {
if (staking == NIL_ADDRESS) {
LibRichErrors.rrevert(
LibStakingRichErrors.ProxyDestinationCannotBeNilError()
);
@@ -186,6 +194,7 @@ contract StakingProxy is
(bool didInitSucceed, bytes memory initReturnData) = stakingContract.delegatecall(
abi.encodeWithSelector(IStorageInit(0).init.selector)
);
if (!didInitSucceed) {
assembly {
revert(add(initReturnData, 0x20), mload(initReturnData))

View File

@@ -0,0 +1,82 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "../src/Staking.sol";
contract TestProxyDestination is
Staking
{
// Init will revert if this flag is set to `true`
bool public initFailFlag;
/// @dev Emitted when `init` is called
event InitCalled(
bool initCalled
);
/// @dev returns the input string
function echo(string calldata val)
external
returns (string memory)
{
return val;
}
/// @dev Just a function that'll do some math on input
function doMath(uint256 a, uint256 b)
external
returns (uint256 sum, uint256 difference)
{
return (
a + b,
a - b
);
}
/// @dev reverts with "Goodbye, World!"
function die()
external
{
revert("Goodbye, World!");
}
/// @dev Called when attached to the StakingProxy.
/// Reverts if `initFailFlag` is set, otherwise
/// sets storage params and emits `InitCalled`.
function init()
public
{
if (initFailFlag) {
revert("INIT_FAIL_FLAG_SET");
}
// Set params such that they'll pass `StakingProxy.assertValidStorageParams`
epochDurationInSeconds = 5 days;
cobbDouglasAlphaNumerator = 1;
cobbDouglasAlphaDenominator = 1;
rewardDelegatedStakeWeight = PPM_DENOMINATOR;
minimumPoolStake = 100;
// Emit event to notify that `init` was called
emit InitCalled(true);
}
}

View File

@@ -0,0 +1,62 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "../src/StakingProxy.sol";
contract TestStakingProxyUnit is
StakingProxy
{
// Storage Params - these are tested by StakingProxy.assertValidStorageParams.
struct TestStorageParams {
uint256 epochDurationInSeconds;
uint32 cobbDouglasAlphaNumerator;
uint32 cobbDouglasAlphaDenominator;
uint32 rewardDelegatedStakeWeight;
uint256 minimumPoolStake;
}
// If this is set then the `init` call will revert in the `TestProxyDestination` contract
bool public initFailFlag;
// solhint-disable no-empty-blocks
constructor(address _stakingContract)
public
StakingProxy( _stakingContract)
{}
// Setters to modify the
function setInitFailFlag()
external
{
initFailFlag = true;
}
/// @dev Sets storage params with test values
function setTestStorageParams(TestStorageParams calldata params)
external
{
epochDurationInSeconds = params.epochDurationInSeconds;
cobbDouglasAlphaNumerator = params.cobbDouglasAlphaNumerator;
cobbDouglasAlphaDenominator = params.cobbDouglasAlphaDenominator;
rewardDelegatedStakeWeight = params.rewardDelegatedStakeWeight;
minimumPoolStake = params.minimumPoolStake;
}
}

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|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeBalances|TestMixinStakeStorage|TestMixinStakingPool|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault).json"
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeBalances|TestMixinStakeStorage|TestMixinStakingPool|TestProtocolFees|TestProxyDestination|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStakingProxyUnit|TestStorageLayoutAndConstants|ZrxVault).json"
},
"repository": {
"type": "git",

View File

@@ -49,9 +49,11 @@ import * as TestMixinStakeBalances from '../generated-artifacts/TestMixinStakeBa
import * as TestMixinStakeStorage from '../generated-artifacts/TestMixinStakeStorage.json';
import * as TestMixinStakingPool from '../generated-artifacts/TestMixinStakingPool.json';
import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json';
import * as TestProxyDestination from '../generated-artifacts/TestProxyDestination.json';
import * as TestStaking from '../generated-artifacts/TestStaking.json';
import * as TestStakingNoWETH from '../generated-artifacts/TestStakingNoWETH.json';
import * as TestStakingProxy from '../generated-artifacts/TestStakingProxy.json';
import * as TestStakingProxyUnit from '../generated-artifacts/TestStakingProxyUnit.json';
import * as TestStorageLayoutAndConstants from '../generated-artifacts/TestStorageLayoutAndConstants.json';
import * as ZrxVault from '../generated-artifacts/ZrxVault.json';
export const artifacts = {
@@ -100,8 +102,10 @@ export const artifacts = {
TestMixinStakeStorage: TestMixinStakeStorage as ContractArtifact,
TestMixinStakingPool: TestMixinStakingPool as ContractArtifact,
TestProtocolFees: TestProtocolFees as ContractArtifact,
TestProxyDestination: TestProxyDestination as ContractArtifact,
TestStaking: TestStaking as ContractArtifact,
TestStakingNoWETH: TestStakingNoWETH as ContractArtifact,
TestStakingProxy: TestStakingProxy as ContractArtifact,
TestStakingProxyUnit: TestStakingProxyUnit as ContractArtifact,
TestStorageLayoutAndConstants: TestStorageLayoutAndConstants as ContractArtifact,
};

View File

@@ -47,8 +47,10 @@ export * from '../generated-wrappers/test_mixin_stake_balances';
export * from '../generated-wrappers/test_mixin_stake_storage';
export * from '../generated-wrappers/test_mixin_staking_pool';
export * from '../generated-wrappers/test_protocol_fees';
export * from '../generated-wrappers/test_proxy_destination';
export * from '../generated-wrappers/test_staking';
export * from '../generated-wrappers/test_staking_no_w_e_t_h';
export * from '../generated-wrappers/test_staking_proxy';
export * from '../generated-wrappers/test_staking_proxy_unit';
export * from '../generated-wrappers/test_storage_layout_and_constants';
export * from '../generated-wrappers/zrx_vault';

View File

@@ -0,0 +1,301 @@
import { blockchainTests, constants, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { AuthorizableRevertErrors, BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import {
artifacts,
StakingProxyEvents,
TestProxyDestinationContract,
TestProxyDestinationEvents,
TestStakingProxyUnitContract,
} from '../../src';
import { constants as stakingConstants } from '../utils/constants';
blockchainTests.resets('StakingProxy unit tests', env => {
const testString = 'Hello, World!';
const testRevertString = 'Goodbye, World!';
let accounts: string[];
let owner: string;
let authorizedAddress: string;
let notAuthorizedAddresses: string[];
let testProxyContract: TestStakingProxyUnitContract;
let testContractViaProxy: TestProxyDestinationContract;
let testContract: TestProxyDestinationContract;
let testContract2: TestProxyDestinationContract;
before(async () => {
// Create accounts
accounts = await env.getAccountAddressesAsync();
[owner, authorizedAddress, ...notAuthorizedAddresses] = accounts;
// Deploy contracts
testContract = await TestProxyDestinationContract.deployFrom0xArtifactAsync(
artifacts.TestProxyDestination,
env.provider,
env.txDefaults,
artifacts,
);
testContract2 = await TestProxyDestinationContract.deployFrom0xArtifactAsync(
artifacts.TestProxyDestination,
env.provider,
env.txDefaults,
artifacts,
);
testProxyContract = await TestStakingProxyUnitContract.deployFrom0xArtifactAsync(
artifacts.TestStakingProxyUnit,
env.provider,
env.txDefaults,
artifacts,
testContract.address,
);
const logDecoderDependencies = _.mapValues(artifacts, v => v.compilerOutput.abi);
testContractViaProxy = new TestProxyDestinationContract(
testProxyContract.address,
env.provider,
env.txDefaults,
logDecoderDependencies,
);
// Add authorized address to Staking Proxy
await testProxyContract.addAuthorizedAddress.sendTransactionAsync(authorizedAddress, { from: owner });
});
describe('Fallback function', () => {
it('should pass back the return value of the destination contract', async () => {
const returnValue = await testContractViaProxy.echo.callAsync(testString);
expect(returnValue).to.equal(testString);
});
it('should revert with correct value when destination reverts', async () => {
return expect(testContractViaProxy.die.callAsync()).to.revertWith(testRevertString);
});
it('should revert if no staking contract is attached', async () => {
await testProxyContract.detachStakingContract.awaitTransactionSuccessAsync({ from: authorizedAddress });
const expectedError = new StakingRevertErrors.ProxyDestinationCannotBeNilError();
const tx = testContractViaProxy.echo.callAsync(testString);
return expect(tx).to.revertWith(expectedError);
});
});
describe('attachStakingContract', () => {
it('should successfully attaching a new staking contract', async () => {
// Cache existing staking contract and attach a new one
const initStakingContractAddress = await testProxyContract.stakingContract.callAsync();
const txReceipt = await testProxyContract.attachStakingContract.awaitTransactionSuccessAsync(
testContract2.address,
{ from: authorizedAddress },
);
// Validate `ContractAttachedToProxy` event
verifyEventsFromLogs(
txReceipt.logs,
[
{
newStakingContractAddress: testContract2.address,
},
],
StakingProxyEvents.StakingContractAttachedToProxy,
);
// Check that `init` was called on destination contract
verifyEventsFromLogs(
txReceipt.logs,
[
{
initCalled: true,
},
],
TestProxyDestinationEvents.InitCalled,
);
// Validate new staking contract address
const finalStakingContractAddress = await testProxyContract.stakingContract.callAsync();
expect(finalStakingContractAddress).to.be.equal(testContract2.address);
expect(finalStakingContractAddress).to.not.equal(initStakingContractAddress);
});
it('should revert if call to `init` on new staking contract fails', async () => {
await testProxyContract.setInitFailFlag.awaitTransactionSuccessAsync();
const tx = testProxyContract.attachStakingContract.awaitTransactionSuccessAsync(testContract2.address, {
from: authorizedAddress,
});
const expectedError = 'INIT_FAIL_FLAG_SET';
return expect(tx).to.revertWith(expectedError);
});
it('should revert if called by unauthorized address', async () => {
const tx = testProxyContract.attachStakingContract.awaitTransactionSuccessAsync(testContract2.address, {
from: notAuthorizedAddresses[0],
});
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorizedAddresses[0]);
return expect(tx).to.revertWith(expectedError);
});
});
describe('detachStakingContract', () => {
it('should detach staking contract', async () => {
// Cache existing staking contract and attach a new one
const initStakingContractAddress = await testProxyContract.stakingContract.callAsync();
const txReceipt = await testProxyContract.detachStakingContract.awaitTransactionSuccessAsync({
from: authorizedAddress,
});
// Validate that event was emitted
verifyEventsFromLogs(txReceipt.logs, [{}], StakingProxyEvents.StakingContractDetachedFromProxy);
// Validate staking contract address was unset
const finalStakingContractAddress = await testProxyContract.stakingContract.callAsync();
expect(finalStakingContractAddress).to.be.equal(stakingConstants.NIL_ADDRESS);
expect(finalStakingContractAddress).to.not.equal(initStakingContractAddress);
});
it('should revert if called by unauthorized address', async () => {
const tx = testProxyContract.detachStakingContract.awaitTransactionSuccessAsync({
from: notAuthorizedAddresses[0],
});
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorizedAddresses[0]);
return expect(tx).to.revertWith(expectedError);
});
});
describe('batchExecute', () => {
it('should execute no-op if no calls to make', async () => {
await testProxyContract.batchExecute.awaitTransactionSuccessAsync([]);
});
it('should call one function and return the output', async () => {
const calls = [testContract.echo.getABIEncodedTransactionData(testString)];
const rawResults = await testProxyContract.batchExecute.callAsync(calls);
expect(rawResults.length).to.equal(1);
const returnValues = [testContract.echo.getABIDecodedReturnData(rawResults[0])];
expect(returnValues[0]).to.equal(testString);
});
it('should call multiple functions and return their outputs', async () => {
const calls = [
testContract.echo.getABIEncodedTransactionData(testString),
testContract.doMath.getABIEncodedTransactionData(new BigNumber(2), new BigNumber(1)),
];
const rawResults = await testProxyContract.batchExecute.callAsync(calls);
expect(rawResults.length).to.equal(2);
const returnValues = [
testContract.echo.getABIDecodedReturnData(rawResults[0]),
testContract.doMath.getABIDecodedReturnData(rawResults[1]),
];
expect(returnValues[0]).to.equal(testString);
expect(returnValues[1][0]).to.bignumber.equal(new BigNumber(3));
expect(returnValues[1][1]).to.bignumber.equal(new BigNumber(1));
});
it('should revert if a call reverts', async () => {
const calls = [
testContract.echo.getABIEncodedTransactionData(testString),
testContract.die.getABIEncodedTransactionData(),
testContract.doMath.getABIEncodedTransactionData(new BigNumber(2), new BigNumber(1)),
];
const tx = testProxyContract.batchExecute.callAsync(calls);
const expectedError = testRevertString;
return expect(tx).to.revertWith(expectedError);
});
it('should revert if no staking contract is attached', async () => {
await testProxyContract.detachStakingContract.awaitTransactionSuccessAsync({ from: authorizedAddress });
const calls = [testContract.echo.getABIEncodedTransactionData(testString)];
const tx = testProxyContract.batchExecute.callAsync(calls);
const expectedError = new StakingRevertErrors.ProxyDestinationCannotBeNilError();
return expect(tx).to.revertWith(expectedError);
});
});
describe('assertValidStorageParams', () => {
const validStorageParams = {
epochDurationInSeconds: new BigNumber(stakingConstants.ONE_DAY_IN_SECONDS * 5),
cobbDouglasAlphaNumerator: new BigNumber(1),
cobbDouglasAlphaDenominator: new BigNumber(1),
rewardDelegatedStakeWeight: constants.PPM_DENOMINATOR,
minimumPoolStake: new BigNumber(100),
};
it('should not revert if all storage params are valid', async () => {
await testProxyContract.setTestStorageParams.awaitTransactionSuccessAsync(validStorageParams);
await testProxyContract.assertValidStorageParams.callAsync();
});
it('should revert if `epochDurationInSeconds` is less than 5 days', async () => {
const invalidStorageParams = {
...validStorageParams,
epochDurationInSeconds: new BigNumber(0),
};
await testProxyContract.setTestStorageParams.awaitTransactionSuccessAsync(invalidStorageParams);
const tx = testProxyContract.assertValidStorageParams.callAsync();
const expectedError = new StakingRevertErrors.InvalidParamValueError(
StakingRevertErrors.InvalidParamValueErrorCodes.InvalidEpochDuration,
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `epochDurationInSeconds` is greater than 30 days', async () => {
const invalidStorageParams = {
...validStorageParams,
epochDurationInSeconds: new BigNumber(stakingConstants.ONE_DAY_IN_SECONDS * 31),
};
await testProxyContract.setTestStorageParams.awaitTransactionSuccessAsync(invalidStorageParams);
const tx = testProxyContract.assertValidStorageParams.callAsync();
const expectedError = new StakingRevertErrors.InvalidParamValueError(
StakingRevertErrors.InvalidParamValueErrorCodes.InvalidEpochDuration,
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `cobbDouglasAlphaNumerator` is greater than `cobbDouglasAlphaDenominator`', async () => {
const invalidStorageParams = {
...validStorageParams,
cobbDouglasAlphaNumerator: new BigNumber(2),
cobbDouglasAlphaDenominator: new BigNumber(1),
};
await testProxyContract.setTestStorageParams.awaitTransactionSuccessAsync(invalidStorageParams);
const tx = testProxyContract.assertValidStorageParams.callAsync();
const expectedError = new StakingRevertErrors.InvalidParamValueError(
StakingRevertErrors.InvalidParamValueErrorCodes.InvalidCobbDouglasAlpha,
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `cobbDouglasAlphaDenominator` equals zero', async () => {
const invalidStorageParams = {
...validStorageParams,
cobbDouglasAlphaDenominator: new BigNumber(0),
};
await testProxyContract.setTestStorageParams.awaitTransactionSuccessAsync(invalidStorageParams);
const tx = testProxyContract.assertValidStorageParams.callAsync();
const expectedError = new StakingRevertErrors.InvalidParamValueError(
StakingRevertErrors.InvalidParamValueErrorCodes.InvalidCobbDouglasAlpha,
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `rewardDelegatedStakeWeight` is greater than PPM_DENOMINATOR', async () => {
const invalidStorageParams = {
...validStorageParams,
rewardDelegatedStakeWeight: new BigNumber(constants.PPM_DENOMINATOR + 1),
};
await testProxyContract.setTestStorageParams.awaitTransactionSuccessAsync(invalidStorageParams);
const tx = testProxyContract.assertValidStorageParams.callAsync();
const expectedError = new StakingRevertErrors.InvalidParamValueError(
StakingRevertErrors.InvalidParamValueErrorCodes.InvalidRewardDelegatedStakeWeight,
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `minimumPoolStake` is less than two', async () => {
const invalidStorageParams = {
...validStorageParams,
minimumPoolStake: new BigNumber(1),
};
await testProxyContract.setTestStorageParams.awaitTransactionSuccessAsync(invalidStorageParams);
const tx = testProxyContract.assertValidStorageParams.callAsync();
const expectedError = new StakingRevertErrors.InvalidParamValueError(
StakingRevertErrors.InvalidParamValueErrorCodes.InvalidMinimumPoolStake,
);
return expect(tx).to.revertWith(expectedError);
});
});
});
// tslint:disable: max-file-line-count

View File

@@ -18,4 +18,5 @@ export const constants = {
cobbDouglasAlphaDenominator: new BigNumber(3),
},
PPM,
ONE_DAY_IN_SECONDS: 24 * 60 * 60,
};

View File

@@ -47,9 +47,11 @@
"generated-artifacts/TestMixinStakeStorage.json",
"generated-artifacts/TestMixinStakingPool.json",
"generated-artifacts/TestProtocolFees.json",
"generated-artifacts/TestProxyDestination.json",
"generated-artifacts/TestStaking.json",
"generated-artifacts/TestStakingNoWETH.json",
"generated-artifacts/TestStakingProxy.json",
"generated-artifacts/TestStakingProxyUnit.json",
"generated-artifacts/TestStorageLayoutAndConstants.json",
"generated-artifacts/ZrxVault.json"
],