@0x/contracts-asset-proxy: Add ERC20BridgeProxy and tests.

This commit is contained in:
Lawrence Forman
2019-09-27 19:33:33 -04:00
parent cf8d424b9b
commit d6a4d67a14
9 changed files with 589 additions and 1 deletions

View File

@@ -17,6 +17,10 @@
{ {
"note": "Remove unused dependency on IAuthorizable in IAssetProxy", "note": "Remove unused dependency on IAuthorizable in IAssetProxy",
"pr": 1910 "pr": 1910
},
{
"note": "Add `ERC20BridgeProxy`",
"pr": "TODO"
} }
] ]
}, },

View File

@@ -0,0 +1,128 @@
/*
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 "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "@0x/contracts-utils/contracts/src/Authorizable.sol";
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "./interfaces/IAssetProxy.sol";
import "./interfaces/IERC20Bridge.sol";
contract ERC20BridgeProxy is
IAssetProxy,
Authorizable
{
using LibBytes for bytes;
using LibSafeMath for uint256;
// @dev Result of a successful bridge call.
bytes4 constant public BRIDGE_SUCCESS = 0xb5d40d78;
// @dev Id of this proxy.
// bytes4(keccak256("ERC20BridgeProxy(address,address,bytes)"))
bytes4 constant private PROXY_ID = 0x37708e9b;
/// @dev Calls a bridge contract to transfer `amount` of ERC20 from `from`
/// to `to`. Asserts that the balance of `to` has increased by `amount`.
/// @param assetData Abi-encoded data for this asset proxy encoded as:
/// abi.encodeWithSelector(
/// bytes4 PROXY_ID,
/// address tokenAddress,
/// address bridgeAddress,
/// bytes bridgeData
/// )
/// @param from Address to transfer asset from.
/// @param to Address to transfer asset to.
/// @param amount Amount of asset to transfer.
function transferFrom(
bytes calldata assetData,
address from,
address to,
uint256 amount
)
external
onlyAuthorized
{
// Extract asset data fields.
(
address tokenAddress,
address bridgeAddress,
bytes memory bridgeData
) = abi.decode(
assetData.sliceDestructive(4, assetData.length),
(address, address, bytes)
);
// Remember the balance of `to` before calling the bridge.
uint256 balanceBefore = balanceOf(tokenAddress, to);
// Call the bridge, who should transfer `amount` of `tokenAddress` to
// `to`.
bytes4 success = IERC20Bridge(bridgeAddress).transfer(
bridgeData,
tokenAddress,
from,
to,
amount
);
// Bridge must return the magic bytes to indicate success.
require(success == BRIDGE_SUCCESS, "BRIDGE_FAILED");
// Ensure that the balance of `to` has increased by at least `amount`.
require(
balanceBefore.safeAdd(amount) <= balanceOf(tokenAddress, to),
"BRIDGE_UNDERPAY"
);
}
/// @dev Gets the proxy id associated with this asset proxy.
/// @return proxyId The proxy id.
function getProxyId()
external
pure
returns (bytes4 proxyId)
{
return PROXY_ID;
}
/// @dev Retrieves the balance of `owner` for this asset.
/// @return balance The balance of the ERC20 token being transferred by this
/// asset proxy.
function balanceOf(bytes calldata assetData, address owner)
external
view
returns (uint256 balance)
{
(address tokenAddress, ,) = abi.decode(
assetData.sliceDestructive(4, assetData.length),
(address, address, bytes)
);
return balanceOf(tokenAddress, owner);
}
/// @dev Retrieves the balance of `owner` given an ERC20 address.
/// @return balance The balance of the ERC20 token for `owner`.
function balanceOf(address tokenAddress, address owner)
private
view
returns (uint256 balance)
{
return IERC20Token(tokenAddress).balanceOf(owner);
}
}

View File

@@ -0,0 +1,40 @@
/*
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;
contract IERC20Bridge {
/// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`.
/// @param bridgeData Arbitrary asset data needed by the bridge contract.
/// @param tokenAddress The address of the ERC20 token to transfer.
/// @param from Address to transfer asset from.
/// @param to Address to transfer asset to.
/// @param amount Amount of asset to transfer.
/// @return success The magic bytes `0xb5d40d78` if successful.
function transfer(
bytes calldata bridgeData,
address tokenAddress,
address from,
address to,
uint256 amount
)
external
returns (bytes4 success);
}

View File

@@ -0,0 +1,108 @@
/*
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/interfaces/IERC20Bridge.sol";
/// @dev Test bridge token
contract TestERC20BridgeToken {
mapping (address => uint256) private _balances;
function addBalance(address owner, int256 amount)
external
{
setBalance(owner, uint256(int256(balanceOf(owner)) + amount));
}
function setBalance(address owner, uint256 balance)
public
{
_balances[owner] = balance;
}
function balanceOf(address owner)
public
view
returns (uint256)
{
return _balances[owner];
}
}
/// @dev Test bridge contract.
contract TestERC20Bridge is
IERC20Bridge
{
TestERC20BridgeToken public testToken;
event BridgeTransfer(
bytes bridgeData,
address tokenAddress,
address from,
address to,
uint256 amount
);
constructor() public {
testToken = new TestERC20BridgeToken();
}
function setTestTokenBalance(address owner, uint256 balance)
external
{
testToken.setBalance(owner, balance);
}
function transfer(
bytes calldata bridgeData,
address tokenAddress,
address from,
address to,
uint256 amount
)
external
returns (bytes4)
{
emit BridgeTransfer(
bridgeData,
tokenAddress,
from,
to,
amount
);
// Unpack the bridgeData.
(
int256 transferAmount,
bytes memory revertData,
bytes memory returnData
) = abi.decode(bridgeData, (int256, bytes, bytes));
// If `revertData` is set, revert.
if (revertData.length != 0) {
assembly { revert(add(revertData, 0x20), mload(revertData)) }
}
// Increase `to`'s balance by `transferAmount`.
TestERC20BridgeToken(tokenAddress).addBalance(to, transferAmount);
// Return `returnData`.
assembly { return(add(returnData, 0x20), mload(returnData)) }
}
}

View File

@@ -35,7 +35,7 @@
"compile:truffle": "truffle compile" "compile:truffle": "truffle compile"
}, },
"config": { "config": {
"abis": "./generated-artifacts/@(ERC1155Proxy|ERC20Proxy|ERC721Proxy|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|MixinAssetProxyDispatcher|MixinAuthorizable|MultiAssetProxy|Ownable|StaticCallProxy|TestStaticCallTarget).json", "abis": "./generated-artifacts/@(ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IERC20Bridge|MixinAssetProxyDispatcher|MixinAuthorizable|MultiAssetProxy|Ownable|StaticCallProxy|TestERC20Bridge|TestStaticCallTarget).json",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
}, },
"repository": { "repository": {

View File

@@ -6,23 +6,27 @@
import { ContractArtifact } from 'ethereum-types'; import { ContractArtifact } from 'ethereum-types';
import * as ERC1155Proxy from '../generated-artifacts/ERC1155Proxy.json'; import * as ERC1155Proxy from '../generated-artifacts/ERC1155Proxy.json';
import * as ERC20BridgeProxy from '../generated-artifacts/ERC20BridgeProxy.json';
import * as ERC20Proxy from '../generated-artifacts/ERC20Proxy.json'; import * as ERC20Proxy from '../generated-artifacts/ERC20Proxy.json';
import * as ERC721Proxy from '../generated-artifacts/ERC721Proxy.json'; import * as ERC721Proxy from '../generated-artifacts/ERC721Proxy.json';
import * as IAssetData from '../generated-artifacts/IAssetData.json'; import * as IAssetData from '../generated-artifacts/IAssetData.json';
import * as IAssetProxy from '../generated-artifacts/IAssetProxy.json'; import * as IAssetProxy from '../generated-artifacts/IAssetProxy.json';
import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispatcher.json'; import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispatcher.json';
import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json'; import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json';
import * as IERC20Bridge from '../generated-artifacts/IERC20Bridge.json';
import * as MixinAssetProxyDispatcher from '../generated-artifacts/MixinAssetProxyDispatcher.json'; import * as MixinAssetProxyDispatcher from '../generated-artifacts/MixinAssetProxyDispatcher.json';
import * as MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json'; import * as MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json';
import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json';
import * as Ownable from '../generated-artifacts/Ownable.json'; import * as Ownable from '../generated-artifacts/Ownable.json';
import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json'; import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json';
import * as TestERC20Bridge from '../generated-artifacts/TestERC20Bridge.json';
import * as TestStaticCallTarget from '../generated-artifacts/TestStaticCallTarget.json'; import * as TestStaticCallTarget from '../generated-artifacts/TestStaticCallTarget.json';
export const artifacts = { export const artifacts = {
MixinAssetProxyDispatcher: MixinAssetProxyDispatcher as ContractArtifact, MixinAssetProxyDispatcher: MixinAssetProxyDispatcher as ContractArtifact,
MixinAuthorizable: MixinAuthorizable as ContractArtifact, MixinAuthorizable: MixinAuthorizable as ContractArtifact,
Ownable: Ownable as ContractArtifact, Ownable: Ownable as ContractArtifact,
ERC1155Proxy: ERC1155Proxy as ContractArtifact, ERC1155Proxy: ERC1155Proxy as ContractArtifact,
ERC20BridgeProxy: ERC20BridgeProxy as ContractArtifact,
ERC20Proxy: ERC20Proxy as ContractArtifact, ERC20Proxy: ERC20Proxy as ContractArtifact,
ERC721Proxy: ERC721Proxy as ContractArtifact, ERC721Proxy: ERC721Proxy as ContractArtifact,
MultiAssetProxy: MultiAssetProxy as ContractArtifact, MultiAssetProxy: MultiAssetProxy as ContractArtifact,
@@ -31,5 +35,7 @@ export const artifacts = {
IAssetProxy: IAssetProxy as ContractArtifact, IAssetProxy: IAssetProxy as ContractArtifact,
IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact, IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact,
IAuthorizable: IAuthorizable as ContractArtifact, IAuthorizable: IAuthorizable as ContractArtifact,
IERC20Bridge: IERC20Bridge as ContractArtifact,
TestERC20Bridge: TestERC20Bridge as ContractArtifact,
TestStaticCallTarget: TestStaticCallTarget as ContractArtifact, TestStaticCallTarget: TestStaticCallTarget as ContractArtifact,
}; };

View File

@@ -4,15 +4,18 @@
* ----------------------------------------------------------------------------- * -----------------------------------------------------------------------------
*/ */
export * from '../generated-wrappers/erc1155_proxy'; export * from '../generated-wrappers/erc1155_proxy';
export * from '../generated-wrappers/erc20_bridge_proxy';
export * from '../generated-wrappers/erc20_proxy'; export * from '../generated-wrappers/erc20_proxy';
export * from '../generated-wrappers/erc721_proxy'; export * from '../generated-wrappers/erc721_proxy';
export * from '../generated-wrappers/i_asset_data'; export * from '../generated-wrappers/i_asset_data';
export * from '../generated-wrappers/i_asset_proxy'; export * from '../generated-wrappers/i_asset_proxy';
export * from '../generated-wrappers/i_asset_proxy_dispatcher'; export * from '../generated-wrappers/i_asset_proxy_dispatcher';
export * from '../generated-wrappers/i_authorizable'; export * from '../generated-wrappers/i_authorizable';
export * from '../generated-wrappers/i_erc20_bridge';
export * from '../generated-wrappers/mixin_asset_proxy_dispatcher'; export * from '../generated-wrappers/mixin_asset_proxy_dispatcher';
export * from '../generated-wrappers/mixin_authorizable'; export * from '../generated-wrappers/mixin_authorizable';
export * from '../generated-wrappers/multi_asset_proxy'; export * from '../generated-wrappers/multi_asset_proxy';
export * from '../generated-wrappers/ownable'; export * from '../generated-wrappers/ownable';
export * from '../generated-wrappers/static_call_proxy'; export * from '../generated-wrappers/static_call_proxy';
export * from '../generated-wrappers/test_erc20_bridge';
export * from '../generated-wrappers/test_static_call_target'; export * from '../generated-wrappers/test_static_call_target';

View File

@@ -0,0 +1,296 @@
import {
blockchainTests,
constants,
expect,
getRandomInteger,
hexLeftPad,
hexRightPad,
hexSlice,
Numberish,
randomAddress,
} from '@0x/contracts-test-utils';
import { AbiEncoder, AuthorizableRevertErrors, BigNumber, StringRevertError } from '@0x/utils';
import { DecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import {
artifacts,
ERC20BridgeProxyContract,
TestERC20BridgeBridgeTransferEventArgs,
TestERC20BridgeContract,
} from '../src';
blockchainTests.resets.only('ERC20BridgeProxy unit tests', env => {
const BRIDGE_SUCCESS_RETURN_DATA = hexRightPad('0xb5d40d78');
let owner: string;
let badCaller: string;
let assetProxy: ERC20BridgeProxyContract;
let bridgeContract: TestERC20BridgeContract;
let testTokenAddress: string;
before(async () => {
[owner, badCaller] = await env.getAccountAddressesAsync();
assetProxy = await ERC20BridgeProxyContract.deployFrom0xArtifactAsync(
artifacts.ERC20BridgeProxy,
env.provider,
env.txDefaults,
artifacts,
);
bridgeContract = await TestERC20BridgeContract.deployFrom0xArtifactAsync(
artifacts.TestERC20Bridge,
env.provider,
env.txDefaults,
artifacts,
);
testTokenAddress = await bridgeContract.testToken.callAsync();
await assetProxy.addAuthorizedAddress.awaitTransactionSuccessAsync(owner);
});
interface AssetDataOpts {
tokenAddress: string;
bridgeAddress: string;
bridgeData: BridgeDataOpts;
}
interface BridgeDataOpts {
transferAmount: Numberish;
revertError?: string;
returnData: string;
}
function createAssetData(opts?: Partial<AssetDataOpts>): AssetDataOpts {
return _.merge(
{
tokenAddress: testTokenAddress,
bridgeAddress: bridgeContract.address,
bridgeData: createBridgeData(),
},
opts,
);
}
function createBridgeData(opts?: Partial<BridgeDataOpts>): BridgeDataOpts {
return _.merge(
{
transferAmount: constants.ZERO_AMOUNT,
returnData: BRIDGE_SUCCESS_RETURN_DATA,
},
opts,
);
}
function encodeAssetData(opts: AssetDataOpts): string {
const encoder = AbiEncoder.createMethod('ERC20BridgeProxy', [
{ name: 'tokenAddress', type: 'address' },
{ name: 'bridgeAddress', type: 'address' },
{ name: 'bridgeData', type: 'bytes' },
]);
return encoder.encode([opts.tokenAddress, opts.bridgeAddress, encodeBridgeData(opts.bridgeData)]);
}
function encodeBridgeData(opts: BridgeDataOpts): string {
const encoder = AbiEncoder.create([
{ name: 'transferAmount', type: 'int256' },
{ name: 'revertData', type: 'bytes' },
{ name: 'returnData', type: 'bytes' },
]);
const revertErrorBytes =
opts.revertError !== undefined ? new StringRevertError(opts.revertError).encode() : '0x';
return encoder.encode([new BigNumber(opts.transferAmount), revertErrorBytes, opts.returnData]);
}
async function setTestTokenBalanceAsync(_owner: string, balance: Numberish): Promise<void> {
await bridgeContract.setTestTokenBalance.awaitTransactionSuccessAsync(_owner, new BigNumber(balance));
}
describe('transferFrom()', () => {
interface TransferFromOpts {
assetData: AssetDataOpts;
from: string;
to: string;
amount: Numberish;
}
function createTransferFromOpts(opts?: Partial<TransferFromOpts>): TransferFromOpts {
const transferAmount = _.get(opts, ['amount'], getRandomInteger(1, 100e18)) as BigNumber;
return _.merge(
{
assetData: createAssetData({
bridgeData: createBridgeData({
transferAmount,
}),
}),
from: randomAddress(),
to: randomAddress(),
amount: transferAmount,
},
opts,
);
}
async function transferFromAsync(opts?: Partial<TransferFromOpts>, caller?: string): Promise<DecodedLogs> {
const _opts = createTransferFromOpts(opts);
const { logs } = await assetProxy.transferFrom.awaitTransactionSuccessAsync(
encodeAssetData(_opts.assetData),
_opts.from,
_opts.to,
new BigNumber(_opts.amount),
{ from: caller },
);
return (logs as any) as DecodedLogs;
}
it('succeeds if the bridge succeeds and balance increases', async () => {
const tx = transferFromAsync();
return expect(tx).to.be.fulfilled('');
});
it('succeeds if balance increases more than `amount`', async () => {
const amount = getRandomInteger(1, 100e18);
const tx = transferFromAsync({
amount,
assetData: createAssetData({
bridgeData: createBridgeData({
transferAmount: amount.plus(1),
}),
}),
});
return expect(tx).to.be.fulfilled('');
});
it('passes the correct arguments to the bridge contract', async () => {
const opts = createTransferFromOpts();
const logs = await transferFromAsync(opts);
expect(logs.length).to.eq(1);
const args = logs[0].args as TestERC20BridgeBridgeTransferEventArgs;
expect(args.bridgeData).to.eq(encodeBridgeData(opts.assetData.bridgeData));
expect(args.tokenAddress).to.eq(opts.assetData.tokenAddress);
expect(args.from).to.eq(opts.from);
expect(args.to).to.eq(opts.to);
expect(args.amount).to.bignumber.eq(opts.amount);
});
it('fails if not called by an authorized address', async () => {
const tx = transferFromAsync({}, badCaller);
return expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(badCaller));
});
it('fails if asset data is truncated', async () => {
const opts = createTransferFromOpts();
const truncatedAssetData = hexSlice(encodeAssetData(opts.assetData), 0, -1);
const tx = assetProxy.transferFrom.awaitTransactionSuccessAsync(
truncatedAssetData,
opts.from,
opts.to,
new BigNumber(opts.amount),
);
return expect(tx).to.be.rejected();
});
it('fails if bridge returns nothing', async () => {
const tx = transferFromAsync({
assetData: createAssetData({
bridgeData: createBridgeData({
returnData: '0x',
}),
}),
});
// This will actually revert when the AP tries to decode the return
// value.
return expect(tx).to.be.rejected();
});
it('fails if bridge returns true', async () => {
const tx = transferFromAsync({
assetData: createAssetData({
bridgeData: createBridgeData({
returnData: hexLeftPad('0x1'),
}),
}),
});
// This will actually revert when the AP tries to decode the return
// value.
return expect(tx).to.be.rejected();
});
it('fails if bridge returns 0x1', async () => {
const tx = transferFromAsync({
assetData: createAssetData({
bridgeData: createBridgeData({
returnData: hexRightPad('0x1'),
}),
}),
});
return expect(tx).to.revertWith('BRIDGE_FAILED');
});
it('fails if bridge is an EOA', async () => {
const tx = transferFromAsync({
assetData: createAssetData({
bridgeAddress: randomAddress(),
}),
});
// This will actually revert when the AP tries to decode the return
// value.
return expect(tx).to.be.rejected();
});
it('fails if bridge reverts', async () => {
const revertError = 'FOOBAR';
const tx = transferFromAsync({
assetData: createAssetData({
bridgeData: createBridgeData({
revertError,
}),
}),
});
// This will actually revert when the AP tries to decode the return
// value.
return expect(tx).to.revertWith(revertError);
});
it('fails if balance of `to` increases by `amount - 1`', async () => {
const amount = getRandomInteger(1, 100e18);
const tx = transferFromAsync({
amount,
assetData: createAssetData({
bridgeData: createBridgeData({
transferAmount: amount.minus(1),
}),
}),
});
// This will actually revert when the AP tries to decode the return
// value.
return expect(tx).to.revertWith('BRIDGE_UNDERPAY');
});
it('fails if balance of `to` decreases', async () => {
const toAddress = randomAddress();
await setTestTokenBalanceAsync(toAddress, 1e18);
const tx = transferFromAsync({
to: toAddress,
assetData: createAssetData({
bridgeData: createBridgeData({
transferAmount: -1,
}),
}),
});
// This will actually revert when the AP tries to decode the return
// value.
return expect(tx).to.revertWith('BRIDGE_UNDERPAY');
});
});
describe('balanceOf()', () => {
it('retrieves the balance of the encoded token', async () => {
const _owner = randomAddress();
const balance = getRandomInteger(1, 100e18);
await bridgeContract.setTestTokenBalance.awaitTransactionSuccessAsync(_owner, balance);
const assetData = createAssetData({
tokenAddress: testTokenAddress,
});
const actualBalance = await assetProxy.balanceOf.callAsync(encodeAssetData(assetData), _owner);
expect(actualBalance).to.bignumber.eq(balance);
});
});
});

View File

@@ -4,17 +4,20 @@
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"files": [ "files": [
"generated-artifacts/ERC1155Proxy.json", "generated-artifacts/ERC1155Proxy.json",
"generated-artifacts/ERC20BridgeProxy.json",
"generated-artifacts/ERC20Proxy.json", "generated-artifacts/ERC20Proxy.json",
"generated-artifacts/ERC721Proxy.json", "generated-artifacts/ERC721Proxy.json",
"generated-artifacts/IAssetData.json", "generated-artifacts/IAssetData.json",
"generated-artifacts/IAssetProxy.json", "generated-artifacts/IAssetProxy.json",
"generated-artifacts/IAssetProxyDispatcher.json", "generated-artifacts/IAssetProxyDispatcher.json",
"generated-artifacts/IAuthorizable.json", "generated-artifacts/IAuthorizable.json",
"generated-artifacts/IERC20Bridge.json",
"generated-artifacts/MixinAssetProxyDispatcher.json", "generated-artifacts/MixinAssetProxyDispatcher.json",
"generated-artifacts/MixinAuthorizable.json", "generated-artifacts/MixinAuthorizable.json",
"generated-artifacts/MultiAssetProxy.json", "generated-artifacts/MultiAssetProxy.json",
"generated-artifacts/Ownable.json", "generated-artifacts/Ownable.json",
"generated-artifacts/StaticCallProxy.json", "generated-artifacts/StaticCallProxy.json",
"generated-artifacts/TestERC20Bridge.json",
"generated-artifacts/TestStaticCallTarget.json" "generated-artifacts/TestStaticCallTarget.json"
], ],
"exclude": ["./deploy/solc/solc_bin"] "exclude": ["./deploy/solc/solc_bin"]