Merge remote-tracking branch 'origin/feat/exchange-proxy/post-cd-audit' into fix/ep/meta-transactions

This commit is contained in:
Lawrence Forman
2020-09-01 09:48:17 -04:00
29 changed files with 547 additions and 74 deletions

View File

@@ -101,6 +101,7 @@
"@0x/contracts-multisig": "^4.1.7",
"@0x/contracts-staking": "^2.0.14",
"@0x/contracts-test-utils": "^5.3.4",
"@0x/contracts-zero-ex": "^0.2.0",
"@0x/subproviders": "^6.1.1",
"@0x/types": "^3.2.0",
"@0x/typescript-typings": "^5.1.1",

View File

@@ -0,0 +1,304 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20';
import { IExchangeContract } from '@0x/contracts-exchange';
import { blockchainTests, constants, expect, getRandomPortion, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import {
artifacts as exchangeProxyArtifacts,
IZeroExContract,
LogMetadataTransformerContract,
signCallData,
} from '@0x/contracts-zero-ex';
import { migrateOnceAsync } from '@0x/migrations';
import {
assetDataUtils,
encodeFillQuoteTransformerData,
encodePayTakerTransformerData,
ETH_TOKEN_ADDRESS,
FillQuoteTransformerSide,
findTransformerNonce,
signatureUtils,
SignedExchangeProxyMetaTransaction,
} from '@0x/order-utils';
import { AssetProxyId, Order, SignedOrder } from '@0x/types';
import { BigNumber, hexUtils } from '@0x/utils';
import * as ethjs from 'ethereumjs-util';
const { MAX_UINT256, NULL_ADDRESS, NULL_BYTES, NULL_BYTES32, ZERO_AMOUNT } = constants;
blockchainTests.resets.only('exchange proxy - meta-transactions', env => {
const quoteSignerKey = hexUtils.random();
const quoteSigner = hexUtils.toHex(ethjs.privateToAddress(ethjs.toBuffer(quoteSignerKey)));
let owner: string;
let relayer: string;
let maker: string;
let taker: string;
let flashWalletAddress: string;
let zeroEx: IZeroExContract;
let exchange: IExchangeContract;
let inputToken: DummyERC20TokenContract;
let outputToken: DummyERC20TokenContract;
let feeToken: DummyERC20TokenContract;
let addresses: ContractAddresses;
let protocolFee: BigNumber;
let metadataTransformer: LogMetadataTransformerContract;
const GAS_PRICE = new BigNumber('1e9');
const MAKER_BALANCE = new BigNumber('100e18');
const TAKER_BALANCE = new BigNumber('100e18');
const TAKER_FEE_BALANCE = new BigNumber('100e18');
before(async () => {
[, relayer, maker, taker] = await env.getAccountAddressesAsync();
addresses = await migrateOnceAsync(env.provider);
zeroEx = new IZeroExContract(addresses.exchangeProxy, env.provider, env.txDefaults, {
LogMetadataTransformer: LogMetadataTransformerContract.ABI(),
DummyERC20Token: DummyERC20TokenContract.ABI(),
});
exchange = new IExchangeContract(addresses.exchange, env.provider, env.txDefaults);
[inputToken, outputToken, feeToken] = await Promise.all(
[...new Array(3)].map(i =>
DummyERC20TokenContract.deployFrom0xArtifactAsync(
erc20Artifacts.DummyERC20Token,
env.provider,
env.txDefaults,
{},
`DummyToken-${i}`,
`TOK${i}`,
new BigNumber(18),
BigNumber.max(MAKER_BALANCE, TAKER_BALANCE),
),
),
);
// LogMetadataTransformer is not deployed in migrations.
metadataTransformer = await LogMetadataTransformerContract.deployFrom0xArtifactAsync(
exchangeProxyArtifacts.LogMetadataTransformer,
env.provider,
{
...env.txDefaults,
from: addresses.exchangeProxyTransformerDeployer,
},
{},
);
owner = await zeroEx.owner().callAsync();
protocolFee = await exchange.protocolFeeMultiplier().callAsync();
flashWalletAddress = await zeroEx.getTransformWallet().callAsync();
const erc20Proxy = await exchange.getAssetProxy(AssetProxyId.ERC20).callAsync();
const allowanceTarget = await zeroEx.getAllowanceTarget().callAsync();
await outputToken.mint(MAKER_BALANCE).awaitTransactionSuccessAsync({ from: maker });
await inputToken.mint(TAKER_BALANCE).awaitTransactionSuccessAsync({ from: taker });
await feeToken.mint(TAKER_FEE_BALANCE).awaitTransactionSuccessAsync({ from: taker });
await outputToken.approve(erc20Proxy, MAX_UINT256).awaitTransactionSuccessAsync({ from: maker });
await inputToken.approve(allowanceTarget, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
await feeToken.approve(allowanceTarget, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
await zeroEx.setQuoteSigner(quoteSigner).awaitTransactionSuccessAsync({ from: owner });
});
interface Transformation {
deploymentNonce: number;
data: string;
}
interface SwapInfo {
inputTokenAddress: string;
outputTokenAddress: string;
inputTokenAmount: BigNumber;
minOutputTokenAmount: BigNumber;
transformations: Transformation[];
orders: SignedOrder[];
}
async function generateSwapAsync(orderFields: Partial<Order> = {}): Promise<SwapInfo> {
const order = await signatureUtils.ecSignTypedDataOrderAsync(
env.provider,
{
chainId: 1337,
exchangeAddress: exchange.address,
expirationTimeSeconds: new BigNumber(Date.now()),
salt: new BigNumber(hexUtils.random()),
feeRecipientAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
takerAddress: flashWalletAddress,
makerAddress: maker,
makerAssetData: assetDataUtils.encodeERC20AssetData(outputToken.address),
takerAssetData: assetDataUtils.encodeERC20AssetData(inputToken.address),
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
takerAssetAmount: getRandomPortion(TAKER_BALANCE),
makerAssetAmount: getRandomPortion(MAKER_BALANCE),
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
...orderFields,
},
maker,
);
const transformations = [
{
deploymentNonce: findTransformerNonce(
addresses.transformers.fillQuoteTransformer,
addresses.exchangeProxyTransformerDeployer,
),
data: encodeFillQuoteTransformerData({
orders: [order],
signatures: [order.signature],
buyToken: outputToken.address,
sellToken: inputToken.address,
fillAmount: order.takerAssetAmount,
maxOrderFillAmounts: [],
refundReceiver: hexUtils.leftPad(2, 20), // Send refund to sender.
side: FillQuoteTransformerSide.Sell,
}),
},
{
deploymentNonce: findTransformerNonce(
addresses.transformers.payTakerTransformer,
addresses.exchangeProxyTransformerDeployer,
),
data: encodePayTakerTransformerData({
tokens: [inputToken.address, outputToken.address, ETH_TOKEN_ADDRESS],
amounts: [MAX_UINT256, MAX_UINT256, MAX_UINT256],
}),
},
{
deploymentNonce: findTransformerNonce(
metadataTransformer.address,
addresses.exchangeProxyTransformerDeployer,
),
data: NULL_BYTES,
},
];
return {
transformations,
orders: [order],
inputTokenAddress: inputToken.address,
outputTokenAddress: outputToken.address,
inputTokenAmount: order.takerAssetAmount,
minOutputTokenAmount: order.makerAssetAmount,
};
}
function getSwapData(swap: SwapInfo): string {
return zeroEx
.transformERC20(
swap.inputTokenAddress,
swap.outputTokenAddress,
swap.inputTokenAmount,
swap.minOutputTokenAmount,
swap.transformations,
)
.getABIEncodedTransactionData();
}
function getSignedSwapData(swap: SwapInfo, signerKey?: string): string {
return signCallData(
zeroEx
.transformERC20(
swap.inputTokenAddress,
swap.outputTokenAddress,
swap.inputTokenAmount,
swap.minOutputTokenAmount,
swap.transformations,
)
.getABIEncodedTransactionData(),
signerKey ? signerKey : quoteSignerKey,
);
}
async function createMetaTransactionAsync(
data: string,
value: BigNumber,
): Promise<SignedExchangeProxyMetaTransaction> {
return signatureUtils.ecSignTypedDataExchangeProxyMetaTransactionAsync(
env.provider,
{
value,
signer: taker,
sender: relayer,
minGasPrice: GAS_PRICE,
maxGasPrice: GAS_PRICE,
expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / 1000) + 60),
salt: new BigNumber(hexUtils.random()),
callData: data,
feeToken: feeToken.address,
feeAmount: getRandomPortion(TAKER_FEE_BALANCE),
domain: {
chainId: 1,
name: 'ZeroEx',
version: '1.0.0',
verifyingContract: zeroEx.address,
},
},
taker,
);
}
it('can call `transformERC20()` with signed calldata and a relayer fee', async () => {
const swap = await generateSwapAsync();
const callDataHash = hexUtils.hash(getSwapData(swap));
const signedSwapData = getSignedSwapData(swap);
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
const mtx = await createMetaTransactionAsync(signedSwapData, _protocolFee);
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
const receipt = await zeroEx
.executeMetaTransaction(mtx, mtx.signature)
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
const relayerEthRefund = relayerEthBalanceBefore
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
.minus(GAS_PRICE.times(receipt.gasUsed));
// Ensure the relayer got back the unused protocol fees.
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
// Ensure the relayer got paid mtx fees.
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(mtx.feeAmount);
// Ensure the taker got output tokens.
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
// Ensure the maker got input tokens.
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
// Check events.
verifyEventsFromLogs(
receipt.logs,
[
{
taker,
callDataHash,
sender: zeroEx.address,
data: NULL_BYTES,
},
],
'TransformerMetadata',
);
});
it('can call `transformERC20()` with wrongly signed calldata and a relayer fee', async () => {
const swap = await generateSwapAsync();
const signedSwapData = getSignedSwapData(swap, hexUtils.random());
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
const mtx = await createMetaTransactionAsync(signedSwapData, _protocolFee);
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
const receipt = await zeroEx
.executeMetaTransaction(mtx, mtx.signature)
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
const relayerEthRefund = relayerEthBalanceBefore
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
.minus(GAS_PRICE.times(receipt.gasUsed));
// Ensure the relayer got back the unused protocol fees.
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
// Ensure the relayer got paid mtx fees.
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(mtx.feeAmount);
// Ensure the taker got output tokens.
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
// Ensure the maker got input tokens.
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
// Check events.
verifyEventsFromLogs(
receipt.logs,
[
{
taker,
// Only signed calldata should have a nonzero hash.
callDataHash: NULL_BYTES32,
sender: zeroEx.address,
data: NULL_BYTES,
},
],
'TransformerMetadata',
);
});
});

View File

@@ -21,6 +21,14 @@
{
"note": "Fix `TransformerDeployer.kill()` calling the wrong `die()` interface.",
"pr": 2624
},
{
"note": "Address CD post-audit feedback",
"pr": 2657
},
{
"note": "Add `LogMetadataTransformer`",
"pr": 2657
}
]
},

View File

@@ -77,6 +77,7 @@ contract Bootstrap is
/// @dev Self-destructs this contract.
/// Can only be called by the deployer.
function die() external {
assert(address(this) == _implementation);
if (msg.sender != _deployer) {
LibProxyRichErrors.InvalidDieCallerError(msg.sender, _deployer).rrevert();
}

View File

@@ -271,7 +271,9 @@ contract MetaTransactions is
_validateMetaTransaction(state);
// Mark the transaction executed.
// Mark the transaction executed by storing the block at which it was executed.
// Currently the block number just indicates that the mtx was executed and
// serves no other purpose from within this contract.
LibMetaTransactionsStorage.getStorage()
.mtxHashToExecutedBlockNumber[state.hash] = block.number;

View File

@@ -44,10 +44,6 @@ contract Ownable is
using LibRichErrorsV06 for bytes;
constructor() public FixinCommon() {
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initializes this feature. The intial owner will be set to this (ZeroEx)
/// to allow the bootstrappers to call `extend()`. Ownership should be
/// transferred to the real owner by the bootstrapper after

View File

@@ -49,10 +49,6 @@ contract SignatureValidator is
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
constructor() public FixinCommon() {
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initialize and register this feature.
/// Should be delegatecalled by `Migrate.migrate()`.
/// @return success `LibMigrate.SUCCESS` on success.

View File

@@ -42,10 +42,6 @@ contract SimpleFunctionRegistry is
using LibRichErrorsV06 for bytes;
constructor() public FixinCommon() {
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initializes this feature, registering its own functions.
/// @return success Magic bytes if successful.
function bootstrap()

View File

@@ -48,10 +48,6 @@ contract TokenSpender is
using LibRichErrorsV06 for bytes;
constructor() public FixinCommon() {
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initialize and register this feature. Should be delegatecalled
/// into during a `Migrate.migrate()`.
/// @param allowanceTarget An `allowanceTarget` instance, configured to have

View File

@@ -61,10 +61,6 @@ contract TransformERC20 is
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 2, 0);
constructor() public FixinCommon() {
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initialize and register this feature.
/// Should be delegatecalled by `Migrate.migrate()`.
/// @param transformerDeployer The trusted deployer for transformers.
@@ -257,16 +253,15 @@ contract TransformERC20 is
// Compute how much output token has been transferred to the taker.
state.takerOutputTokenBalanceAfter =
LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.taker);
if (state.takerOutputTokenBalanceAfter > state.takerOutputTokenBalanceBefore) {
outputTokenAmount = state.takerOutputTokenBalanceAfter.safeSub(
state.takerOutputTokenBalanceBefore
);
} else if (state.takerOutputTokenBalanceAfter < state.takerOutputTokenBalanceBefore) {
if (state.takerOutputTokenBalanceAfter < state.takerOutputTokenBalanceBefore) {
LibTransformERC20RichErrors.NegativeTransformERC20OutputError(
address(args.outputToken),
state.takerOutputTokenBalanceBefore - state.takerOutputTokenBalanceAfter
).rrevert();
}
outputTokenAmount = state.takerOutputTokenBalanceAfter.safeSub(
state.takerOutputTokenBalanceBefore
);
// Ensure enough output token has been sent to the taker.
if (outputTokenAmount < args.minOutputTokenAmount) {
LibTransformERC20RichErrors.IncompleteTransformERC20Error(

View File

@@ -32,7 +32,6 @@ import "./LibERC20Transformer.sol";
contract AffiliateFeeTransformer is
Transformer
{
// solhint-disable no-empty-blocks
using LibRichErrorsV06 for bytes;
using LibSafeMathV06 for uint256;
using LibERC20Transformer for IERC20TokenV06;
@@ -51,12 +50,6 @@ contract AffiliateFeeTransformer is
uint256 private constant MAX_UINT256 = uint256(-1);
/// @dev Create this contract.
constructor()
public
Transformer()
{}
/// @dev Transfers tokens to recipients.
/// @param context Context information.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).

View File

@@ -0,0 +1,46 @@
/*
Copyright 2020 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.6.5;
pragma experimental ABIEncoderV2;
import "./Transformer.sol";
import "./LibERC20Transformer.sol";
/// @dev A transformer that just emits an event with an arbitrary byte payload.
contract LogMetadataTransformer is
Transformer
{
event TransformerMetadata(bytes32 callDataHash, address sender, address taker, bytes data);
/// @dev Maximum uint256 value.
uint256 private constant MAX_UINT256 = uint256(-1);
/// @dev Emits an event.
/// @param context Context information.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform(TransformContext calldata context)
external
override
returns (bytes4 success)
{
emit TransformerMetadata(context.callDataHash, context.sender, context.taker, context.data);
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
}

View File

@@ -39,9 +39,9 @@
"publish:private": "yarn build && gitpkg publish"
},
"config": {
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer,Ownable,SimpleFunctionRegistry,TransformERC20,TokenSpender,AffiliateFeeTransformer,SignatureValidator,MetaTransactions,BridgeAdapter",
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer,Ownable,SimpleFunctionRegistry,TransformERC20,TokenSpender,AffiliateFeeTransformer,SignatureValidator,MetaTransactions,LogMetadataTransformer",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IMetaTransactions|IOwnable|ISignatureValidator|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|MetaTransactions|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|Ownable|PayTakerTransformer|SignatureValidator|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json"
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinGasToken|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IMetaTransactions|IOwnable|ISignatureValidator|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LogMetadataTransformer|MetaTransactions|Ownable|PayTakerTransformer|SignatureValidator|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json"
},
"repository": {
"type": "git",

View File

@@ -18,6 +18,7 @@ import * as ISimpleFunctionRegistry from '../generated-artifacts/ISimpleFunction
import * as ITokenSpender from '../generated-artifacts/ITokenSpender.json';
import * as ITransformERC20 from '../generated-artifacts/ITransformERC20.json';
import * as IZeroEx from '../generated-artifacts/IZeroEx.json';
import * as LogMetadataTransformer from '../generated-artifacts/LogMetadataTransformer.json';
import * as MetaTransactions from '../generated-artifacts/MetaTransactions.json';
import * as Ownable from '../generated-artifacts/Ownable.json';
import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json';
@@ -50,4 +51,5 @@ export const artifacts = {
SignatureValidator: SignatureValidator as ContractArtifact,
MetaTransactions: MetaTransactions as ContractArtifact,
BridgeAdapter: BridgeAdapter as ContractArtifact,
LogMetadataTransformer: LogMetadataTransformer as ContractArtifact,
};

View File

@@ -16,6 +16,7 @@ export * from '../generated-wrappers/i_token_spender';
export * from '../generated-wrappers/i_transform_erc20';
export * from '../generated-wrappers/i_zero_ex';
export * from '../generated-wrappers/initial_migration';
export * from '../generated-wrappers/log_metadata_transformer';
export * from '../generated-wrappers/meta_transactions';
export * from '../generated-wrappers/ownable';
export * from '../generated-wrappers/pay_taker_transformer';

View File

@@ -12,6 +12,7 @@ import * as BridgeAdapter from '../test/generated-artifacts/BridgeAdapter.json';
import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json';
import * as FixinCommon from '../test/generated-artifacts/FixinCommon.json';
import * as FixinEIP712 from '../test/generated-artifacts/FixinEIP712.json';
import * as FixinReentrancyGuard from '../test/generated-artifacts/FixinReentrancyGuard.json';
import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json';
import * as FullMigration from '../test/generated-artifacts/FullMigration.json';
import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json';
@@ -53,6 +54,7 @@ import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpe
import * as LibTransformERC20RichErrors from '../test/generated-artifacts/LibTransformERC20RichErrors.json';
import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json';
import * as LibWalletRichErrors from '../test/generated-artifacts/LibWalletRichErrors.json';
import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json';
import * as MetaTransactions from '../test/generated-artifacts/MetaTransactions.json';
import * as MixinAdapterAddresses from '../test/generated-artifacts/MixinAdapterAddresses.json';
import * as MixinBalancer from '../test/generated-artifacts/MixinBalancer.json';
@@ -141,6 +143,7 @@ export const artifacts = {
LibSignedCallData: LibSignedCallData as ContractArtifact,
FixinCommon: FixinCommon as ContractArtifact,
FixinEIP712: FixinEIP712 as ContractArtifact,
FixinReentrancyGuard: FixinReentrancyGuard as ContractArtifact,
FullMigration: FullMigration as ContractArtifact,
InitialMigration: InitialMigration as ContractArtifact,
LibBootstrap: LibBootstrap as ContractArtifact,
@@ -157,6 +160,7 @@ export const artifacts = {
FillQuoteTransformer: FillQuoteTransformer as ContractArtifact,
IERC20Transformer: IERC20Transformer as ContractArtifact,
LibERC20Transformer: LibERC20Transformer as ContractArtifact,
LogMetadataTransformer: LogMetadataTransformer as ContractArtifact,
PayTakerTransformer: PayTakerTransformer as ContractArtifact,
Transformer: Transformer as ContractArtifact,
WethTransformer: WethTransformer as ContractArtifact,

View File

@@ -539,6 +539,72 @@ blockchainTests.resets('MetaTransactions feature', env => {
),
);
});
it('cannot reenter `executeMetaTransaction()`', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
value: TRANSFORM_ERC20_REENTER_VALUE,
});
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.maxGasPrice,
value: mtx.value,
};
const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
mtxHash,
undefined,
new ZeroExRevertErrors.Common.IllegalReentrancyError(
feature.getSelector('executeMetaTransaction'),
REENTRANCY_FLAG_MTX,
).encode(),
),
);
});
it('cannot reenter `batchExecuteMetaTransactions()`', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
value: TRANSFORM_ERC20_BATCH_REENTER_VALUE,
});
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.maxGasPrice,
value: mtx.value,
};
const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
mtxHash,
undefined,
new ZeroExRevertErrors.Common.IllegalReentrancyError(
feature.getSelector('batchExecuteMetaTransactions'),
REENTRANCY_FLAG_MTX,
).encode(),
),
);
});
});
describe('batchExecuteMetaTransactions()', () => {

View File

@@ -10,6 +10,7 @@ export * from '../test/generated-wrappers/bridge_adapter';
export * from '../test/generated-wrappers/fill_quote_transformer';
export * from '../test/generated-wrappers/fixin_common';
export * from '../test/generated-wrappers/fixin_e_i_p712';
export * from '../test/generated-wrappers/fixin_reentrancy_guard';
export * from '../test/generated-wrappers/flash_wallet';
export * from '../test/generated-wrappers/full_migration';
export * from '../test/generated-wrappers/i_allowance_target';
@@ -51,6 +52,7 @@ export * from '../test/generated-wrappers/lib_token_spender_storage';
export * from '../test/generated-wrappers/lib_transform_erc20_rich_errors';
export * from '../test/generated-wrappers/lib_transform_erc20_storage';
export * from '../test/generated-wrappers/lib_wallet_rich_errors';
export * from '../test/generated-wrappers/log_metadata_transformer';
export * from '../test/generated-wrappers/meta_transactions';
export * from '../test/generated-wrappers/mixin_adapter_addresses';
export * from '../test/generated-wrappers/mixin_balancer';

View File

@@ -16,6 +16,7 @@
"generated-artifacts/ITransformERC20.json",
"generated-artifacts/IZeroEx.json",
"generated-artifacts/InitialMigration.json",
"generated-artifacts/LogMetadataTransformer.json",
"generated-artifacts/MetaTransactions.json",
"generated-artifacts/Ownable.json",
"generated-artifacts/PayTakerTransformer.json",
@@ -74,6 +75,7 @@
"test/generated-artifacts/LibTransformERC20RichErrors.json",
"test/generated-artifacts/LibTransformERC20Storage.json",
"test/generated-artifacts/LibWalletRichErrors.json",
"test/generated-artifacts/LogMetadataTransformer.json",
"test/generated-artifacts/MetaTransactions.json",
"test/generated-artifacts/MixinAdapterAddresses.json",
"test/generated-artifacts/MixinBalancer.json",

View File

@@ -7,10 +7,10 @@ import {
encodeWethTransformerData,
ETH_TOKEN_ADDRESS,
FillQuoteTransformerSide,
findTransformerNonce,
} from '@0x/order-utils';
import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import * as ethjs from 'ethereumjs-util';
import * as _ from 'lodash';
import { constants } from '../constants';
@@ -32,7 +32,6 @@ import { getTokenFromAssetData } from '../utils/utils';
// tslint:disable-next-line:custom-no-magic-numbers
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
const { NULL_ADDRESS } = constants;
const MAX_NONCE_GUESSES = 2048;
export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
public readonly provider: ZeroExProvider;
@@ -230,32 +229,3 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
return quote.type === MarketOperation.Buy;
}
/**
* Find the nonce for a transformer given its deployer.
* If `deployer` is the null address, zero will always be returned.
*/
export function findTransformerNonce(transformer: string, deployer: string = NULL_ADDRESS): number {
if (deployer === NULL_ADDRESS) {
return 0;
}
const lowercaseTransformer = transformer.toLowerCase();
// Try to guess the nonce.
for (let nonce = 0; nonce < MAX_NONCE_GUESSES; ++nonce) {
const deployedAddress = getTransformerAddress(deployer, nonce);
if (deployedAddress === lowercaseTransformer) {
return nonce;
}
}
throw new Error(`${deployer} did not deploy ${transformer}!`);
}
/**
* Compute the deployed address for a transformer given a deployer and nonce.
*/
export function getTransformerAddress(deployer: string, nonce: number): string {
return ethjs.bufferToHex(
// tslint:disable-next-line: custom-no-magic-numbers
ethjs.rlphash([deployer, nonce] as any).slice(12),
);
}

View File

@@ -8,6 +8,7 @@ import {
decodeWethTransformerData,
ETH_TOKEN_ADDRESS,
FillQuoteTransformerSide,
getTransformerAddress,
} from '@0x/order-utils';
import { Order } from '@0x/types';
import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils';
@@ -16,10 +17,7 @@ import * as _ from 'lodash';
import 'mocha';
import { constants } from '../src/constants';
import {
ExchangeProxySwapQuoteConsumer,
getTransformerAddress,
} from '../src/quote_consumers/exchange_proxy_swap_quote_consumer';
import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_proxy_swap_quote_consumer';
import { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types';
import { OptimizedMarketOrder } from '../src/utils/market_operation_utils/types';

View File

@@ -1,4 +1,13 @@
[
{
"version": "5.2.0",
"changes": [
{
"note": "Add `exchangeProxyMetaTransactionSchema`",
"pr": 2657
}
]
},
{
"version": "5.1.0",
"changes": [

View File

@@ -0,0 +1,30 @@
{
"id": "/exchangeProxyMetaTransactionSchema",
"properties": {
"signer": { "$ref": "/addressSchema" },
"sender": { "$ref": "/addressSchema" },
"minGasPrice": { "$ref": "/wholeNumberSchema" },
"maxGasPrice": { "$ref": "/wholeNumberSchema" },
"expirationTimeSeconds": { "$ref": "/wholeNumberSchema" },
"salt": { "$ref": "/wholeNumberSchema" },
"callData": { "$ref": "/hexSchema" },
"value": { "$ref": "/wholeNumberSchema" },
"feeToken": { "$ref": "/addressSchema" },
"feeAmount": { "$ref": "/wholeNumberSchema" },
"domain": { "$ref": "/eip712DomainSchema" }
},
"required": [
"signer",
"sender",
"minGasPrice",
"maxGasPrice",
"expirationTimeSeconds",
"salt",
"callData",
"value",
"feeToken",
"feeAmount",
"domain"
],
"type": "object"
}

View File

@@ -7,6 +7,7 @@ import * as ecSignatureParameterSchema from '../schemas/ec_signature_parameter_s
import * as ecSignatureSchema from '../schemas/ec_signature_schema.json';
import * as eip712DomainSchema from '../schemas/eip712_domain_schema.json';
import * as eip712TypedDataSchema from '../schemas/eip712_typed_data_schema.json';
import * as exchangeProxyMetaTransactionSchema from '../schemas/exchange_proxy_meta_transaction_schema.json';
import * as hexSchema from '../schemas/hex_schema.json';
import * as indexFilterValuesSchema from '../schemas/index_filter_values_schema.json';
import * as jsNumber from '../schemas/js_number_schema.json';
@@ -87,5 +88,6 @@ export const schemas = {
relayerApiOrdersResponseSchema,
relayerApiAssetDataPairsSchema,
zeroExTransactionSchema,
exchangeProxyMetaTransactionSchema,
wholeNumberSchema,
};

View File

@@ -50,6 +50,7 @@
"./schemas/orderbook_request_schema.json",
"./schemas/orders_request_opts_schema.json",
"./schemas/paged_request_opts_schema.json",
"./schemas/order_config_request_schema.json"
"./schemas/order_config_request_schema.json",
"./schemas/exchange_proxy_meta_transaction_schema.json"
]
}

View File

@@ -13,6 +13,14 @@
{
"note": "Add `refundReceiver` field to `FillQuoteTransformer.TransformData`.",
"pr": 2657
},
{
"note": "Add `findTransformerNonce()` and `getTransformerAddress()` functions.",
"pr": 2657
},
{
"note": "Fix EP signature utils schema assertion.",
"pr": 2657
}
]
},

View File

@@ -77,7 +77,9 @@ export {
AffiliateFeeTransformerData,
encodeAffiliateFeeTransformerData,
decodeAffiliateFeeTransformerData,
} from './transformer_data_encoders';
findTransformerNonce,
getTransformerAddress,
} from './transformer_utils';
export { getOrderHash, getExchangeMetaTransactionHash, getExchangeProxyMetaTransactionHash } from './hash_utils';

View File

@@ -206,7 +206,9 @@ export const signatureUtils = {
transaction: ExchangeProxyMetaTransaction,
signerAddress: string,
): Promise<SignedExchangeProxyMetaTransaction> {
assert.doesConformToSchema('transaction', transaction, schemas.zeroExTransactionSchema, [schemas.hexSchema]);
assert.doesConformToSchema('transaction', transaction, schemas.exchangeProxyMetaTransactionSchema, [
schemas.hexSchema,
]);
try {
const signedTransaction = await signatureUtils.ecSignTypedDataExchangeProxyMetaTransactionAsync(
supportedProvider,
@@ -253,7 +255,9 @@ export const signatureUtils = {
): Promise<SignedExchangeProxyMetaTransaction> {
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isETHAddressHex('signerAddress', signerAddress);
assert.doesConformToSchema('transaction', transaction, schemas.zeroExTransactionSchema, [schemas.hexSchema]);
assert.doesConformToSchema('transaction', transaction, schemas.exchangeProxyMetaTransactionSchema, [
schemas.hexSchema,
]);
const web3Wrapper = new Web3Wrapper(provider);
await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper);
const normalizedSignerAddress = signerAddress.toLowerCase();

View File

@@ -1,5 +1,10 @@
import { Order } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils';
import * as ethjs from 'ethereumjs-util';
import { constants } from './constants';
const { NULL_ADDRESS } = constants;
const ORDER_ABI_COMPONENTS = [
{ name: 'makerAddress', type: 'address' },
@@ -187,3 +192,36 @@ export function encodeAffiliateFeeTransformerData(data: AffiliateFeeTransformerD
export function decodeAffiliateFeeTransformerData(encoded: string): AffiliateFeeTransformerData {
return affiliateFeeTransformerDataEncoder.decode(encoded);
}
/**
* Find the nonce for a transformer given its deployer.
* If `deployer` is the null address, zero will always be returned.
*/
export function findTransformerNonce(
transformer: string,
deployer: string = NULL_ADDRESS,
maxGuesses: number = 1024,
): number {
if (deployer === NULL_ADDRESS) {
return 0;
}
const lowercaseTransformer = transformer.toLowerCase();
// Try to guess the nonce.
for (let nonce = 0; nonce < maxGuesses; ++nonce) {
const deployedAddress = getTransformerAddress(deployer, nonce);
if (deployedAddress === lowercaseTransformer) {
return nonce;
}
}
throw new Error(`${deployer} did not deploy ${transformer}!`);
}
/**
* Compute the deployed address for a transformer given a deployer and nonce.
*/
export function getTransformerAddress(deployer: string, nonce: number): string {
return ethjs.bufferToHex(
// tslint:disable-next-line: custom-no-magic-numbers
ethjs.rlphash([deployer, nonce] as any).slice(12),
);
}