@0x:contracts-staking Added unit tests for LibProxy

This commit is contained in:
Alex Towle
2019-09-10 15:58:27 -07:00
parent 036c8fe920
commit be83789bee
9 changed files with 384 additions and 33 deletions

View File

@@ -31,27 +31,44 @@ pragma experimental ABIEncoderV2;
import "../src/libs/LibProxy.sol";
// solhint-disable payable-fallback
contract TestLibProxy {
using LibProxy for address;
// The arguments of `proxyCall()`.
struct ProxyCallArguments {
address destination;
LibProxy.RevertRule revertRule;
bytes4 customEgressSelector;
bool ignoreIngressSelector;
}
// The current arguments that should be passed in the call to `proxyCall()`. This
// state allows us to send in the exact calldata that should be sent to `proxyCall()`
// while still being able to test any combination of inputs to `proxyCall()`.
ProxyCallArguments internal proxyCallArgs;
/// @dev Exposes the `proxyCall()` library function from LibProxy.
/// @param destination Address to call.
/// @param revertRule Describes scenarios in which this function reverts.
/// @param customEgressSelector Custom selector used to call destination contract.
/// @param ignoreIngressSelector Ignore the selector used to call into this contract.
function externalProxyCall(
address destination,
LibProxy.RevertRule revertRule,
bytes4 customEgressSelector,
bool ignoreIngressSelector
)
function ()
external
{
destination.proxyCall(
revertRule,
customEgressSelector,
ignoreIngressSelector
proxyCallArgs.destination.proxyCall(
proxyCallArgs.revertRule,
proxyCallArgs.customEgressSelector,
proxyCallArgs.ignoreIngressSelector
);
}
/// @dev Calls back into this contract with the calldata that should be sent in the call
/// to `proxyCall()` after setting the `proxyCallArgs` appropriately.
/// @param args The struct that should be set to `proxyCallArgs`.
/// @param data The bytes that should be used to call back into this contract.
function publicProxyCall(ProxyCallArguments memory args, bytes memory data)
public
returns (bool success, bytes memory returnData)
{
proxyCallArgs = args;
(success, returnData) = address(this).call(data);
}
}

View File

@@ -0,0 +1,45 @@
/*
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;
// solhint-disable payable-fallback
contract TestLibProxyReceiver {
function ()
external
{
// Done in assembly to allow the return.
assembly {
let calldataSize := calldatasize()
// Copy all calldata into memory.
calldatacopy(0, 0, calldataSize)
// If the calldatasize is equal to 4, revert.
// This allows us to test `proxyCall` with reverts.
if eq(calldataSize, 4) {
revert(0, 4)
}
// Return. This allows us to test `proxyCall` with returns.
return(0, calldataSize)
}
}
}

View File

@@ -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|IStorageInit|IStructs|IVaultCore|IZrxVault|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinConstants|MixinDeploymentConstants|MixinEthVault|MixinExchangeFees|MixinExchangeManager|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|MixinZrxVault|ReadOnlyProxy|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestInitTarget|TestLibFixedMath|TestLibProxy|TestProtocolFees|TestProtocolFeesERC20Proxy|TestStaking|TestStakingProxy|TestStorageLayout|ZrxVault).json"
"abis": "./generated-artifacts/@(EthVault|IEthVault|IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStorageInit|IStructs|IVaultCore|IZrxVault|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinConstants|MixinDeploymentConstants|MixinEthVault|MixinExchangeFees|MixinExchangeManager|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|MixinZrxVault|ReadOnlyProxy|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestProtocolFees|TestProtocolFeesERC20Proxy|TestStaking|TestStakingProxy|TestStorageLayout|ZrxVault).json"
},
"repository": {
"type": "git",

View File

@@ -44,6 +44,7 @@ import * as TestCobbDouglas from '../generated-artifacts/TestCobbDouglas.json';
import * as TestInitTarget from '../generated-artifacts/TestInitTarget.json';
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 TestProtocolFees from '../generated-artifacts/TestProtocolFees.json';
import * as TestProtocolFeesERC20Proxy from '../generated-artifacts/TestProtocolFeesERC20Proxy.json';
import * as TestStaking from '../generated-artifacts/TestStaking.json';
@@ -91,6 +92,7 @@ export const artifacts = {
TestInitTarget: TestInitTarget as ContractArtifact,
TestLibFixedMath: TestLibFixedMath as ContractArtifact,
TestLibProxy: TestLibProxy as ContractArtifact,
TestLibProxyReceiver: TestLibProxyReceiver as ContractArtifact,
TestProtocolFees: TestProtocolFees as ContractArtifact,
TestProtocolFeesERC20Proxy: TestProtocolFeesERC20Proxy as ContractArtifact,
TestStaking: TestStaking as ContractArtifact,

View File

@@ -42,6 +42,7 @@ export * from '../generated-wrappers/test_cobb_douglas';
export * from '../generated-wrappers/test_init_target';
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_protocol_fees';
export * from '../generated-wrappers/test_protocol_fees_erc20_proxy';
export * from '../generated-wrappers/test_staking';

View File

@@ -1,16 +1,16 @@
import { blockchainTests, constants, expect } from '@0x/contracts-test-utils';
import { blockchainTests, constants, expect, hexRandom } from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { artifacts, TestLibProxyContract } from '../../src';
enum RevertRule {
RevertOnError,
AlwaysRevert,
NeverRevert,
}
import { artifacts, TestLibProxyContract, TestLibProxyReceiverContract } from '../../src';
blockchainTests.resets('LibProxy', env => {
let proxy: TestLibProxyContract;
let receiver: TestLibProxyReceiverContract;
// Generates a random bytes4 value.
function randomBytes4(): string {
return hexRandom(4);
}
before(async () => {
proxy = await TestLibProxyContract.deployFrom0xArtifactAsync(
@@ -19,22 +19,306 @@ blockchainTests.resets('LibProxy', env => {
env.txDefaults,
artifacts,
);
receiver = await TestLibProxyReceiverContract.deployFrom0xArtifactAsync(
artifacts.TestLibProxyReceiver,
env.provider,
env.txDefaults,
artifacts,
);
});
enum RevertRule {
RevertOnError,
AlwaysRevert,
NeverRevert,
}
// Choose a random 4 byte string of calldata to send and prepend with `0x00` to ensure
// that it does not call `externalProxyCall` by accident. This calldata will make the fallback
// in `TestLibProxyReceiver` fail because it is 4 bytes long.
function constructRandomFailureCalldata(): string {
return '0x00'.concat(randomBytes4().slice(4, 10));
}
// Choose a random 24 byte string of calldata to send and prepend with `0x00` to ensure
// that it does not call `externalProxyCall` by accident. This calldata will make the fallback
// in `TestLibProxyReceiver` succeed because it isn't 4 bytes long.
function constructRandomSuccessCalldata(): string {
return '0x00'.concat(hexRandom(36).slice(2, 74));
}
interface PublicProxyCallArgs {
destination: string;
revertRule: RevertRule;
customEgressSelector: string;
ignoreIngressSelector: boolean;
calldata: string;
}
// Exposes `publicProxyCall()` with useful default arguments.
async function publicProxyCallAsync(args: Partial<PublicProxyCallArgs>): Promise<[boolean, string]> {
return proxy.publicProxyCall.callAsync(
{
destination: args.destination || receiver.address,
revertRule: args.revertRule || RevertRule.RevertOnError,
customEgressSelector: args.customEgressSelector || constants.NULL_BYTES4,
ignoreIngressSelector: args.ignoreIngressSelector || false,
},
args.calldata || constructRandomSuccessCalldata(),
);
}
describe('proxyCall', () => {
it('should revert when the destination is address zero and the revert rule is `AlwaysRevert`', async () => {
const expectedError = new StakingRevertErrors.ProxyDestinationCannotBeNilError();
const tx = proxy.externalProxyCall.awaitTransactionSuccessAsync(
constants.NULL_ADDRESS,
RevertRule.AlwaysRevert,
);
return expect(tx).to.revertWith(expectedError);
// Verifies that the result of a given call to `proxyCall()` results in specified outcome
function checkEndingConditions(result: [boolean, string], success: boolean, calldata: string): void {
expect(result[0]).to.be.eq(success);
expect(result[1]).to.be.eq(calldata);
}
describe('Failure Conditions', () => {
// Verifies that the result of a given call to `proxyCall()` results in `ProxyDestinationCannotBeNilError`
function checkDestinationZeroError(result: [boolean, string]): void {
const expectedError = new StakingRevertErrors.ProxyDestinationCannotBeNilError();
expect(result[0]).to.be.false();
expect(result[1]).to.be.eq(expectedError.encode());
}
it('should revert when the destination is address zero', async () => {
checkDestinationZeroError(await publicProxyCallAsync({ destination: constants.NULL_ADDRESS }));
});
it('should revert when the destination is address zero and revertRule == AlwaysRevert', async () => {
checkDestinationZeroError(
await publicProxyCallAsync({
destination: constants.NULL_ADDRESS,
revertRule: RevertRule.AlwaysRevert,
}),
);
});
it('should revert when the destination is address zero and revertRule == NeverRevert', async () => {
checkDestinationZeroError(
await publicProxyCallAsync({
destination: constants.NULL_ADDRESS,
revertRule: RevertRule.NeverRevert,
}),
);
});
});
describe('REVERT_ON_ERROR', () => {});
describe('Calldata Checks', () => {
it('should simply forward the calldata and succeed when customEngressSelector == bytes4(0), ignoreIngressSelector == false, and revertRule = RevertOnError', async () => {
const calldata = constructRandomSuccessCalldata();
describe('ALWAYS_REVERT', () => {});
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(await publicProxyCallAsync({ calldata }), true, calldata);
});
describe('NEVER_REVERT', () => {});
it('should send the customEgressSelector followed by the calldata when customEgressSelector != bytes4(0), ignoreIngressSelector == false, and revertRule == RevertOnError', async () => {
const calldata = constructRandomSuccessCalldata();
// Choose a random customEgressSelector selector.
const customEgressSelector = randomBytes4();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
customEgressSelector,
}),
true,
customEgressSelector.concat(calldata.slice(2, calldata.length)),
);
});
it('should send the the calldata without the selector when customEgressSelector == bytes4(0), ignoreIngressSelector == true, and revertRule == RevertOnError', async () => {
const calldata = constructRandomSuccessCalldata();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
ignoreIngressSelector: true,
}),
true,
'0x'.concat(calldata.slice(10, calldata.length)),
);
});
it('should send the calldata with the customEgressSelector replacing its selctor when customEngressSelector != bytes4(0), ignoreIngressSelector == true, and revertRule == RevertOnError', async () => {
const calldata = constructRandomSuccessCalldata();
// Choose a random customEgressSelector selector.
const customEgressSelector = randomBytes4();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
customEgressSelector,
ignoreIngressSelector: true,
}),
true,
customEgressSelector.concat(calldata.slice(10, calldata.length)),
);
});
});
describe('RevertRule Checks', () => {
it('should revert with the correct data when the call succeeds and revertRule = AlwaysRevert', async () => {
const calldata = constructRandomSuccessCalldata();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
revertRule: RevertRule.AlwaysRevert,
}),
false,
calldata,
);
});
it('should revert with the correct data when the call falls and revertRule = AlwaysRevert', async () => {
const calldata = constructRandomFailureCalldata();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
revertRule: RevertRule.AlwaysRevert,
}),
false,
calldata,
);
});
it('should succeed with the correct data when the call succeeds and revertRule = NeverRevert', async () => {
const calldata = constructRandomSuccessCalldata();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
revertRule: RevertRule.NeverRevert,
}),
true,
calldata,
);
});
it('should succeed with the correct data when the call falls and revertRule = NeverRevert', async () => {
const calldata = constructRandomFailureCalldata();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
revertRule: RevertRule.NeverRevert,
}),
true,
calldata,
);
});
it('should succeed with the correct data when the call succeeds and revertRule = RevertOnError', async () => {
const calldata = constructRandomSuccessCalldata();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
}),
true,
calldata,
);
});
it('should revert with the correct data when the call falls and revertRule = RevertOnError', async () => {
// Choose a random 4 byte string of calldata to send and replace the first byte with `0x00` to ensure
// that it does not call `publicProxyCall` by accident.
const calldata = '0x00'.concat(randomBytes4().slice(4, 10));
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
}),
false,
calldata,
);
});
});
// For brevity, only `RevertOnError` was tested by the `customEgressSelector` and `ignoreIngressSelector` tests. These
// cases are intended to prevent regressions from occuring with the other two revert rules.
describe('Mixed Checks', () => {
it('should function correctly when customEgressSelector != bytes4(0) and revertRule == AlwaysRevert', async () => {
const calldata = constructRandomSuccessCalldata();
// Choose a random customEgressSelector selector.
const customEgressSelector = randomBytes4();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
customEgressSelector,
revertRule: RevertRule.AlwaysRevert,
}),
false,
customEgressSelector.concat(calldata.slice(2, calldata.length)),
);
});
it('should function correctly when customEgressSelector != bytes4(0) and revertRule == NeverRevert', async () => {
const calldata = constructRandomSuccessCalldata();
// Choose a random customEgressSelector selector.
const customEgressSelector = randomBytes4();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
customEgressSelector,
revertRule: RevertRule.NeverRevert,
}),
true,
customEgressSelector.concat(calldata.slice(2, calldata.length)),
);
});
it('should function correctly when ignoreIngressSelector == true and revertRule == AlwaysRevert', async () => {
const calldata = constructRandomSuccessCalldata();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
ignoreIngressSelector: true,
revertRule: RevertRule.AlwaysRevert,
}),
false,
'0x'.concat(calldata.slice(10, calldata.length)),
);
});
it('should function correctly when ignoreIngressSelector == true and revertRule == NeverRevert', async () => {
const calldata = constructRandomSuccessCalldata();
// Ensure that the returndata (the provided calldata) is correct.
checkEndingConditions(
await publicProxyCallAsync({
calldata,
ignoreIngressSelector: true,
revertRule: RevertRule.NeverRevert,
}),
true,
'0x'.concat(calldata.slice(10, calldata.length)),
);
});
});
});
});

View File

@@ -42,6 +42,7 @@
"generated-artifacts/TestInitTarget.json",
"generated-artifacts/TestLibFixedMath.json",
"generated-artifacts/TestLibProxy.json",
"generated-artifacts/TestLibProxyReceiver.json",
"generated-artifacts/TestProtocolFees.json",
"generated-artifacts/TestProtocolFeesERC20Proxy.json",
"generated-artifacts/TestStaking.json",

View File

@@ -54,6 +54,7 @@ export const constants = {
NUM_ERC1155_FUNGIBLE_TOKENS_MINT: 4,
NUM_ERC1155_NONFUNGIBLE_TOKENS_MINT: 4,
NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
NULL_BYTES4: '0x00000000',
NULL_BYTES32: '0x0000000000000000000000000000000000000000000000000000000000000000',
UNLIMITED_ALLOWANCE_IN_BASE_UNITS: MAX_UINT256,
MAX_UINT256,