Merge pull request #2252 from 0xProject/feature/sandstorm/function-assertions

Initial Sandstorm Framework
This commit is contained in:
James Towle
2019-10-22 13:45:42 -07:00
committed by GitHub
18 changed files with 506 additions and 25 deletions

View File

@@ -58,6 +58,16 @@ jobs:
keys:
- repo-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn wsrun test:circleci @0x/contracts-exchange
test-integrations-ganache-3.0:
resource_class: medium+
docker:
- image: nikolaik/python-nodejs:python3.7-nodejs8
working_directory: ~/repo
steps:
- restore_cache:
keys:
- repo-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn wsrun test:circleci @0x/contracts-integrations
test-contracts-rest-ganache-3.0:
resource_class: medium+
docker:
@@ -392,6 +402,9 @@ workflows:
- test-exchange-ganache-3.0:
requires:
- build
- test-integrations-ganache-3.0:
requires:
- build
- test-contracts-rest-ganache-3.0:
requires:
- build

View File

@@ -87,13 +87,13 @@ contract ERC20Token is
balances[_to] += _value;
balances[_from] -= _value;
allowed[_from][msg.sender] -= _value;
emit Transfer(
_from,
_to,
_value
);
return true;
}

View File

@@ -0,0 +1,45 @@
pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
// This contract is intended to be used in the unit tests that test the typescript
// test framework found in `test/utils/`
contract TestFramework {
event Event(string input);
// bytes4(keccak256("RichRevertErrorSelector(string)"))
bytes4 internal constant RICH_REVERT_ERROR_SELECTOR = 0x49a7e246;
function emitEvent(string calldata input)
external
{
emit Event(input);
}
function emptyRevert()
external
{
revert();
}
function stringRevert(string calldata message)
external
{
revert(message);
}
function doNothing()
external
pure
{} // solhint-disable-line no-empty-blocks
function returnInteger(uint256 integer)
external
pure
returns (uint256)
{
return integer;
}
}

View File

@@ -1,15 +0,0 @@
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-staking/contracts/test/TestStaking.sol";
// TODO(jalextowle): This contract can be removed when the added to this package.
contract TestStakingPlaceholder is
TestStaking
{
constructor(address wethAddress, address zrxVaultAddress)
public
TestStaking(wethAddress, zrxVaultAddress)
{} // solhint-disable-line no-empty-blocks
}

View File

@@ -35,7 +35,7 @@
"compile:truffle": "truffle compile"
},
"config": {
"abis": "./generated-artifacts/@(TestStakingPlaceholder).json",
"abis": "./generated-artifacts/@(TestFramework).json",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
},
"repository": {

View File

@@ -5,5 +5,5 @@
*/
import { ContractArtifact } from 'ethereum-types';
import * as TestStakingPlaceholder from '../generated-artifacts/TestStakingPlaceholder.json';
export const artifacts = { TestStakingPlaceholder: TestStakingPlaceholder as ContractArtifact };
import * as TestFramework from '../generated-artifacts/TestFramework.json';
export const artifacts = { TestFramework: TestFramework as ContractArtifact };

View File

@@ -1,2 +1,5 @@
export * from './artifacts';
export * from './wrappers';
export * from '../test/utils/function_assertions';
export * from '../test/utils/deployment_manager';
export * from '../test/utils/address_manager';

View File

@@ -3,4 +3,4 @@
* Warning: This file is auto-generated by contracts-gen. Don't edit manually.
* -----------------------------------------------------------------------------
*/
export * from '../generated-wrappers/test_staking_placeholder';
export * from '../generated-wrappers/test_framework';

View File

@@ -2,7 +2,7 @@ import { DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
import { constants } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { DeploymentManager } from '../deployment/deployment_mananger';
import { DeploymentManager } from '../utils/deployment_manager';
export type Constructor<T = {}> = new (...args: any[]) => T;

View File

@@ -2,7 +2,7 @@ import { Authorizable, Ownable } from '@0x/contracts-exchange';
import { constants as stakingConstants } from '@0x/contracts-staking';
import { blockchainTests, expect } from '@0x/contracts-test-utils';
import { DeploymentManager } from './deployment_mananger';
import { DeploymentManager } from '../utils/deployment_manager';
blockchainTests('Deployment Manager', env => {
let owner: string;

View File

@@ -0,0 +1,133 @@
import {
blockchainTests,
constants,
expect,
filterLogsToArguments,
getRandomInteger,
hexRandom,
} from '@0x/contracts-test-utils';
import { BigNumber, StringRevertError } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { artifacts, TestFrameworkContract, TestFrameworkEventEventArgs, TestFrameworkEvents } from '../../src';
import { FunctionAssertion, Result } from '../utils/function_assertions';
const { ZERO_AMOUNT, MAX_UINT256 } = constants;
blockchainTests.resets('FunctionAssertion Unit Tests', env => {
let exampleContract: TestFrameworkContract;
before(async () => {
exampleContract = await TestFrameworkContract.deployFrom0xArtifactAsync(
artifacts.TestFramework,
env.provider,
env.txDefaults,
artifacts,
);
});
describe('runAsync', () => {
it('should call the before function with the provided arguments', async () => {
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion(exampleContract.returnInteger, {
before: async (input: BigNumber) => {
sideEffectTarget = randomInput;
},
after: async (beforeInfo: any, result: Result, input: BigNumber) => {},
});
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.runAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
});
it('should call the after function with the provided arguments', async () => {
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion(exampleContract.returnInteger, {
before: async (input: BigNumber) => {},
after: async (beforeInfo: any, result: Result, input: BigNumber) => {
sideEffectTarget = input;
},
});
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.runAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
});
it('should not fail immediately if the wrapped function fails', async () => {
const assertion = new FunctionAssertion(exampleContract.emptyRevert, {
before: async () => {},
after: async (beforeInfo: any, result: Result) => {},
});
await assertion.runAsync();
});
it('should pass the return value of "before" to "after"', async () => {
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion(exampleContract.returnInteger, {
before: async (input: BigNumber) => {
return randomInput;
},
after: async (beforeInfo: any, result: Result, input: BigNumber) => {
sideEffectTarget = beforeInfo;
},
});
await assertion.runAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
});
it('should pass the result from the function call to "after"', async () => {
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion(exampleContract.returnInteger, {
before: async (input: BigNumber) => {},
after: async (beforeInfo: any, result: Result, input: BigNumber) => {
sideEffectTarget = result.data;
},
});
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.runAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
});
it('should pass the receipt from the function call to "after"', async () => {
let sideEffectTarget = {} as TransactionReceiptWithDecodedLogs;
const assertion = new FunctionAssertion(exampleContract.emitEvent, {
before: async (input: string) => {},
after: async (beforeInfo: any, result: Result, input: string) => {
if (result.receipt) {
sideEffectTarget = result.receipt;
}
},
});
const input = 'emitted data';
await assertion.runAsync(input);
// Ensure that the correct events were emitted.
const [event] = filterLogsToArguments<TestFrameworkEventEventArgs>(
sideEffectTarget.logs,
TestFrameworkEvents.Event,
);
expect(event).to.be.deep.eq({ input });
});
it('should pass the error to "after" if the function call fails', async () => {
let sideEffectTarget: Error;
const assertion = new FunctionAssertion(exampleContract.stringRevert, {
before: async string => {},
after: async (any, result: Result, string) => {
sideEffectTarget = result.data;
},
});
const message = 'error message';
await assertion.runAsync(message);
const expectedError = new StringRevertError(message);
return expect(
new Promise((resolve, reject) => {
reject(sideEffectTarget);
}),
).to.revertWith(expectedError);
});
});
});

View File

@@ -0,0 +1,135 @@
import { blockchainTests, constants, expect, filterLogsToArguments, OrderFactory } from '@0x/contracts-test-utils';
import { DummyERC20TokenContract, IERC20TokenEvents, IERC20TokenTransferEventArgs } from '@0x/contracts-erc20';
import { IExchangeEvents, IExchangeFillEventArgs } from '@0x/contracts-exchange';
import { IStakingEventsEvents, IStakingEventsStakingPoolActivatedEventArgs } from '@0x/contracts-staking';
import { assetDataUtils, orderHashUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { AddressManager } from '../utils/address_manager';
import { DeploymentManager } from '../utils/deployment_manager';
blockchainTests('Exchange & Staking', env => {
let accounts: string[];
let makerAddress: string;
let takers: string[] = [];
let delegators: string[] = [];
let feeRecipientAddress: string;
let addressManager: AddressManager;
let deploymentManager: DeploymentManager;
let orderFactory: OrderFactory;
let makerAsset: DummyERC20TokenContract;
let takerAsset: DummyERC20TokenContract;
let feeAsset: DummyERC20TokenContract;
const GAS_PRICE = 1e9;
before(async () => {
const chainId = await env.getChainIdAsync();
accounts = await env.getAccountAddressesAsync();
[makerAddress, feeRecipientAddress, takers[0], takers[1], ...delegators] = accounts.slice(1);
deploymentManager = await DeploymentManager.deployAsync(env);
// Create a staking pool with the operator as a maker address.
await deploymentManager.staking.stakingWrapper.createStakingPool.awaitTransactionSuccessAsync(
constants.ZERO_AMOUNT,
true,
{ from: makerAddress },
);
// Set up an address for market making.
addressManager = new AddressManager();
await addressManager.addMakerAsync(
deploymentManager,
{
address: makerAddress,
mainToken: deploymentManager.tokens.erc20[0],
feeToken: deploymentManager.tokens.erc20[2],
},
env,
deploymentManager.tokens.erc20[1],
feeRecipientAddress,
chainId,
);
// Set up two addresses for taking orders.
await Promise.all(
takers.map(taker =>
addressManager.addTakerAsync(deploymentManager, {
address: taker,
mainToken: deploymentManager.tokens.erc20[1],
feeToken: deploymentManager.tokens.erc20[2],
}),
),
);
});
describe('fillOrder', () => {
it('should be able to fill an order', async () => {
const order = await addressManager.makers[0].orderFactory.newSignedOrderAsync({
makerAddress,
makerAssetAmount: new BigNumber(1),
takerAssetAmount: new BigNumber(1),
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
feeRecipientAddress,
});
const receipt = await deploymentManager.exchange.fillOrder.awaitTransactionSuccessAsync(
order,
new BigNumber(1),
order.signature,
{
from: takers[0],
gasPrice: GAS_PRICE,
value: DeploymentManager.protocolFeeMultiplier.times(GAS_PRICE),
},
);
// Ensure that the number of emitted logs is equal to 3. There should have been a fill event
// and two transfer events. A 'StakingPoolActivated' event should not be expected because
// the only staking pool that was created does not have enough stake.
expect(receipt.logs.length).to.be.eq(3);
// Ensure that the fill event was correct.
const fillArgs = filterLogsToArguments<IExchangeFillEventArgs>(receipt.logs, IExchangeEvents.Fill);
expect(fillArgs.length).to.be.eq(1);
expect(fillArgs).to.be.deep.eq([
{
makerAddress,
feeRecipientAddress,
makerAssetData: order.makerAssetData,
takerAssetData: order.takerAssetData,
makerFeeAssetData: order.makerFeeAssetData,
takerFeeAssetData: order.takerFeeAssetData,
orderHash: orderHashUtils.getOrderHashHex(order),
takerAddress: takers[0],
senderAddress: takers[0],
makerAssetFilledAmount: order.makerAssetAmount,
takerAssetFilledAmount: order.takerAssetAmount,
makerFeePaid: constants.ZERO_AMOUNT,
takerFeePaid: constants.ZERO_AMOUNT,
protocolFeePaid: DeploymentManager.protocolFeeMultiplier.times(GAS_PRICE),
},
]);
// Ensure that the transfer events were correctly emitted.
const transferArgs = filterLogsToArguments<IERC20TokenTransferEventArgs>(
receipt.logs,
IERC20TokenEvents.Transfer,
);
expect(transferArgs.length).to.be.eq(2);
expect(transferArgs).to.be.deep.eq([
{
_from: takers[0],
_to: makerAddress,
_value: order.takerAssetAmount,
},
{
_from: makerAddress,
_to: takers[0],
_value: order.makerAssetAmount,
},
]);
});
});
});

View File

@@ -0,0 +1,97 @@
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { constants, OrderFactory, BlockchainTestsEnvironment } from '@0x/contracts-test-utils';
import { assetDataUtils, Order, SignatureType, SignedOrder } from '@0x/order-utils';
import { DeploymentManager } from '../../src';
interface MarketMaker {
address: string;
orderFactory: OrderFactory;
}
interface ConfigurationArgs {
address: string;
mainToken: DummyERC20TokenContract;
feeToken: DummyERC20TokenContract;
}
export class AddressManager {
// A set of addresses that have been configured for market making.
public makers: MarketMaker[];
// A set of addresses that have been configured to take orders.
public takers: string[];
/**
* Sets up an address to take orders.
*/
public async addTakerAsync(deploymentManager: DeploymentManager, configArgs: ConfigurationArgs): Promise<void> {
// Configure the taker address with the taker and fee tokens.
await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.mainToken);
await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.feeToken);
// Add the taker to the list of configured taker addresses.
this.takers.push(configArgs.address);
}
/**
* Sets up an address for market making.
*/
public async addMakerAsync(
deploymentManager: DeploymentManager,
configArgs: ConfigurationArgs,
environment: BlockchainTestsEnvironment,
takerToken: DummyERC20TokenContract,
feeRecipientAddress: string,
chainId: number,
): Promise<void> {
const accounts = await environment.getAccountAddressesAsync();
// Set up order signing for the maker address.
const defaultOrderParams = {
...constants.STATIC_ORDER_PARAMS,
makerAddress: configArgs.address,
makerAssetData: assetDataUtils.encodeERC20AssetData(configArgs.mainToken.address),
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address),
makerFeeAssetData: assetDataUtils.encodeERC20AssetData(configArgs.feeToken.address),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(configArgs.feeToken.address),
feeRecipientAddress,
exchangeAddress: deploymentManager.exchange.address,
chainId,
};
const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(configArgs.address)];
const orderFactory = new OrderFactory(privateKey, defaultOrderParams);
// Configure the maker address with the maker and fee tokens.
await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.mainToken);
await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.feeToken);
// Add the maker to the list of configured maker addresses.
this.makers.push({
address: configArgs.address,
orderFactory,
});
}
/**
* Sets up initial account balances for a token and approves the ERC20 asset proxy
* to transfer the token.
*/
protected async _configureTokenForAddressAsync(
deploymentManager: DeploymentManager,
address: string,
token: DummyERC20TokenContract,
): Promise<void> {
await token.setBalance.awaitTransactionSuccessAsync(address, constants.INITIAL_ERC20_BALANCE);
await token.approve.awaitTransactionSuccessAsync(
deploymentManager.assetProxies.erc20Proxy.address,
constants.MAX_UINT256,
{ from: address },
);
}
constructor() {
this.makers = [];
this.takers = [];
}
}

View File

@@ -149,7 +149,7 @@ export class DeploymentManager {
exchangeArtifacts.Exchange,
environment.provider,
environment.txDefaults,
exchangeArtifacts,
{ ...ERC20Artifacts, ...exchangeArtifacts },
new BigNumber(chainId),
);
const governor = await ZeroExGovernorContract.deployFrom0xArtifactAsync(

View File

@@ -0,0 +1,68 @@
import { PromiseWithTransactionHash } from '@0x/base-contract';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
export interface ContractGetterFunction {
callAsync: (...args: any[]) => Promise<any>;
}
export interface ContractWrapperFunction extends ContractGetterFunction {
awaitTransactionSuccessAsync?: (...args: any[]) => PromiseWithTransactionHash<TransactionReceiptWithDecodedLogs>;
}
export interface Condition {
before: (...args: any[]) => Promise<any>;
after: (beforeInfo: any, result: Result, ...args: any[]) => Promise<any>;
}
export interface Result {
data?: any;
receipt?: TransactionReceiptWithDecodedLogs;
success: boolean;
}
export class FunctionAssertion {
// A before and an after assertion that will be called around the wrapper function.
public condition: Condition;
// The wrapper function that will be wrapped in assertions.
public wrapperFunction: ContractWrapperFunction;
constructor(wrapperFunction: ContractWrapperFunction, condition: Condition) {
this.condition = condition;
this.wrapperFunction = wrapperFunction;
}
/**
* Runs the wrapped function and fails if the before or after assertions fail.
* @param ...args The args to the contract wrapper function.
*/
public async runAsync(...args: any[]): Promise<{ beforeInfo: any; afterInfo: any }> {
// Call the before condition.
const beforeInfo = await this.condition.before(...args);
// Initialize the callResult so that the default success value is true.
let callResult: Result = { success: true };
// Try to make the call to the function. If it is successful, pass the
// result and receipt to the after condition.
try {
callResult.data = await this.wrapperFunction.callAsync(...args);
callResult.receipt =
this.wrapperFunction.awaitTransactionSuccessAsync !== undefined
? await this.wrapperFunction.awaitTransactionSuccessAsync(...args)
: undefined;
} catch (error) {
callResult.data = error;
callResult.success = false;
callResult.receipt = undefined;
}
// Call the after condition.
const afterInfo = await this.condition.after(beforeInfo, callResult, ...args);
return {
beforeInfo,
afterInfo,
};
}
}

View File

@@ -2,5 +2,5 @@
"extends": "../../tsconfig",
"compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true },
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"files": ["generated-artifacts/TestStakingPlaceholder.json"]
"files": ["generated-artifacts/TestFramework.json"]
}

View File

@@ -9,10 +9,12 @@ import { signingUtils } from './signing_utils';
export class OrderFactory {
private readonly _defaultOrderParams: Partial<Order>;
private readonly _privateKey: Buffer;
constructor(privateKey: Buffer, defaultOrderParams: Partial<Order>) {
this._defaultOrderParams = defaultOrderParams;
this._privateKey = privateKey;
}
public async newSignedOrderAsync(
customOrderParams: Partial<Order> = {},
signatureType: SignatureType = SignatureType.EthSign,