feat: ExchangeProxy FillQuoteTransformer bridge direct (#2608)

* Detect Bridge orders and fill direct

* Mark as external for try/catch

* Initial tests

* discuss: Continue if protocol fee insufficient

* Emit ProtocolFeeUnfunded

* put the clamps on taker balance

* feat: GST free and optimize

* fix: low level GST free call

* fix: review feedback

* remove unused return struct
This commit is contained in:
Jacob Evans 2020-07-07 07:37:26 +10:00 committed by GitHub
parent 41cdbc0ec4
commit 406d2cefc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 522 additions and 103 deletions

View File

@ -31,6 +31,10 @@
{ {
"note": "Introduce fill `TransformERC20` feature.", "note": "Introduce fill `TransformERC20` feature.",
"pr": 2545 "pr": 2545
},
{
"note": "Fill Bridges directly in `FillQuoteTransformer`.",
"pr": 2608
} }
] ]
} }

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;
import "../vendor/v3/IGasToken.sol";
contract FixinGasToken
{
/// @dev Mainnet address of the GST2 contract
address constant private GST_ADDRESS = 0x0000000000b3F879cb30FE243b4Dfee438691c04;
/// @dev Mainnet address of the GST Collector
address constant private GST_COLLECTOR_ADDRESS = 0x000000D3b08566BE75A6DB803C03C85C0c1c5B96;
/// @dev Frees gas tokens using the balance of `from`. Amount freed is based
/// on the gas consumed in the function
modifier freesGasTokensFromCollector() {
uint256 gasBefore = gasleft();
_;
// (gasUsed + FREE_BASE) / (2 * REIMBURSE - FREE_TOKEN)
// 14154 24000 6870
uint256 value = (gasBefore - gasleft() + 14154) / 41130;
GST_ADDRESS.call(
abi.encodeWithSelector(
IGasToken(address(0)).freeFromUpTo.selector,
GST_COLLECTOR_ADDRESS,
value
)
);
}
}

View File

@ -27,18 +27,23 @@ import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol";
import "../errors/LibTransformERC20RichErrors.sol"; import "../errors/LibTransformERC20RichErrors.sol";
import "../vendor/v3/IExchange.sol"; import "../vendor/v3/IExchange.sol";
import "../vendor/v3/IERC20Bridge.sol";
import "./Transformer.sol"; import "./Transformer.sol";
import "./LibERC20Transformer.sol"; import "./LibERC20Transformer.sol";
import "../fixins/FixinGasToken.sol";
/// @dev A transformer that fills an ERC20 market sell/buy quote. /// @dev A transformer that fills an ERC20 market sell/buy quote.
/// This transformer shortcuts bridge orders and fills them directly
contract FillQuoteTransformer is contract FillQuoteTransformer is
Transformer Transformer,
FixinGasToken
{ {
using LibERC20TokenV06 for IERC20TokenV06; using LibERC20TokenV06 for IERC20TokenV06;
using LibERC20Transformer for IERC20TokenV06; using LibERC20Transformer for IERC20TokenV06;
using LibSafeMathV06 for uint256; using LibSafeMathV06 for uint256;
using LibRichErrorsV06 for bytes; using LibRichErrorsV06 for bytes;
using LibBytesV06 for bytes;
/// @dev Whether we are performing a market sell or buy. /// @dev Whether we are performing a market sell or buy.
enum Side { enum Side {
@ -81,8 +86,29 @@ contract FillQuoteTransformer is
uint256 protocolFeePaid; uint256 protocolFeePaid;
} }
/// @dev Intermediate state variables to get around stack limits.
struct FillState {
uint256 ethRemaining;
uint256 boughtAmount;
uint256 soldAmount;
uint256 protocolFee;
uint256 takerTokenBalanceRemaining;
}
/// @dev Emitted when a trade is skipped due to a lack of funds
/// to pay the 0x Protocol fee.
/// @param ethBalance The current eth balance.
/// @param ethNeeded The current eth balance required to pay
/// the protocol fee.
event ProtocolFeeUnfunded(
uint256 ethBalance,
uint256 ethNeeded
);
/// @dev The Exchange ERC20Proxy ID. /// @dev The Exchange ERC20Proxy ID.
bytes4 private constant ERC20_ASSET_PROXY_ID = 0xf47261b0; bytes4 private constant ERC20_ASSET_PROXY_ID = 0xf47261b0;
/// @dev The Exchange ERC20BridgeProxy ID.
bytes4 private constant ERC20_BRIDGE_PROXY_ID = 0xdc1600f3;
/// @dev Maximum uint256 value. /// @dev Maximum uint256 value.
uint256 private constant MAX_UINT256 = uint256(-1); uint256 private constant MAX_UINT256 = uint256(-1);
@ -113,9 +139,11 @@ contract FillQuoteTransformer is
) )
external external
override override
freesGasTokensFromCollector
returns (bytes4 success) returns (bytes4 success)
{ {
TransformData memory data = abi.decode(data_, (TransformData)); TransformData memory data = abi.decode(data_, (TransformData));
FillState memory state;
// Validate data fields. // Validate data fields.
if (data.sellToken.isTokenETH() || data.buyToken.isTokenETH()) { if (data.sellToken.isTokenETH() || data.buyToken.isTokenETH()) {
@ -131,43 +159,35 @@ contract FillQuoteTransformer is
).rrevert(); ).rrevert();
} }
state.takerTokenBalanceRemaining = data.sellToken.getTokenBalanceOf(address(this));
if (data.side == Side.Sell && data.fillAmount == MAX_UINT256) { if (data.side == Side.Sell && data.fillAmount == MAX_UINT256) {
// If `sellAmount == -1 then we are selling // If `sellAmount == -1 then we are selling
// the entire balance of `sellToken`. This is useful in cases where // the entire balance of `sellToken`. This is useful in cases where
// the exact sell amount is not exactly known in advance, like when // the exact sell amount is not exactly known in advance, like when
// unwrapping Chai/cUSDC/cDAI. // unwrapping Chai/cUSDC/cDAI.
data.fillAmount = data.sellToken.getTokenBalanceOf(address(this)); data.fillAmount = state.takerTokenBalanceRemaining;
} }
// Approve the ERC20 proxy to spend `sellToken`. // Approve the ERC20 proxy to spend `sellToken`.
data.sellToken.approveIfBelow(erc20Proxy, data.fillAmount); data.sellToken.approveIfBelow(erc20Proxy, data.fillAmount);
// Fill the orders. // Fill the orders.
uint256 singleProtocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice); state.protocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice);
uint256 ethRemaining = address(this).balance; state.ethRemaining = address(this).balance;
uint256 boughtAmount = 0;
uint256 soldAmount = 0;
for (uint256 i = 0; i < data.orders.length; ++i) { for (uint256 i = 0; i < data.orders.length; ++i) {
// Check if we've hit our targets. // Check if we've hit our targets.
if (data.side == Side.Sell) { if (data.side == Side.Sell) {
// Market sell check. // Market sell check.
if (soldAmount >= data.fillAmount) { if (state.soldAmount >= data.fillAmount) {
break; break;
} }
} else { } else {
// Market buy check. // Market buy check.
if (boughtAmount >= data.fillAmount) { if (state.boughtAmount >= data.fillAmount) {
break; break;
} }
} }
// Ensure we have enough ETH to cover the protocol fee.
if (ethRemaining < singleProtocolFee) {
LibTransformERC20RichErrors
.InsufficientProtocolFeeError(ethRemaining, singleProtocolFee)
.rrevert();
}
// Fill the order. // Fill the order.
FillOrderResults memory results; FillOrderResults memory results;
if (data.side == Side.Sell) { if (data.side == Side.Sell) {
@ -177,12 +197,12 @@ contract FillQuoteTransformer is
data.sellToken, data.sellToken,
data.orders[i], data.orders[i],
data.signatures[i], data.signatures[i],
data.fillAmount.safeSub(soldAmount).min256( data.fillAmount.safeSub(state.soldAmount).min256(
data.maxOrderFillAmounts.length > i data.maxOrderFillAmounts.length > i
? data.maxOrderFillAmounts[i] ? data.maxOrderFillAmounts[i]
: MAX_UINT256 : MAX_UINT256
), ),
singleProtocolFee state
); );
} else { } else {
// Market buy. // Market buy.
@ -191,39 +211,40 @@ contract FillQuoteTransformer is
data.sellToken, data.sellToken,
data.orders[i], data.orders[i],
data.signatures[i], data.signatures[i],
data.fillAmount.safeSub(boughtAmount).min256( data.fillAmount.safeSub(state.boughtAmount).min256(
data.maxOrderFillAmounts.length > i data.maxOrderFillAmounts.length > i
? data.maxOrderFillAmounts[i] ? data.maxOrderFillAmounts[i]
: MAX_UINT256 : MAX_UINT256
), ),
singleProtocolFee state
); );
} }
// Accumulate totals. // Accumulate totals.
soldAmount = soldAmount.safeAdd(results.takerTokenSoldAmount); state.soldAmount = state.soldAmount.safeAdd(results.takerTokenSoldAmount);
boughtAmount = boughtAmount.safeAdd(results.makerTokenBoughtAmount); state.boughtAmount = state.boughtAmount.safeAdd(results.makerTokenBoughtAmount);
ethRemaining = ethRemaining.safeSub(results.protocolFeePaid); state.ethRemaining = state.ethRemaining.safeSub(results.protocolFeePaid);
state.takerTokenBalanceRemaining = state.takerTokenBalanceRemaining.safeSub(results.takerTokenSoldAmount);
} }
// Ensure we hit our targets. // Ensure we hit our targets.
if (data.side == Side.Sell) { if (data.side == Side.Sell) {
// Market sell check. // Market sell check.
if (soldAmount < data.fillAmount) { if (state.soldAmount < data.fillAmount) {
LibTransformERC20RichErrors LibTransformERC20RichErrors
.IncompleteFillSellQuoteError( .IncompleteFillSellQuoteError(
address(data.sellToken), address(data.sellToken),
soldAmount, state.soldAmount,
data.fillAmount data.fillAmount
).rrevert(); ).rrevert();
} }
} else { } else {
// Market buy check. // Market buy check.
if (boughtAmount < data.fillAmount) { if (state.boughtAmount < data.fillAmount) {
LibTransformERC20RichErrors LibTransformERC20RichErrors
.IncompleteFillBuyQuoteError( .IncompleteFillBuyQuoteError(
address(data.buyToken), address(data.buyToken),
boughtAmount, state.boughtAmount,
data.fillAmount data.fillAmount
).rrevert(); ).rrevert();
} }
@ -237,14 +258,14 @@ contract FillQuoteTransformer is
/// @param order The order to fill. /// @param order The order to fill.
/// @param signature The signature for `order`. /// @param signature The signature for `order`.
/// @param sellAmount Amount of taker token to sell. /// @param sellAmount Amount of taker token to sell.
/// @param protocolFee The protocol fee needed to fill `order`. /// @param state Intermediate state variables to get around stack limits.
function _sellToOrder( function _sellToOrder(
IERC20TokenV06 makerToken, IERC20TokenV06 makerToken,
IERC20TokenV06 takerToken, IERC20TokenV06 takerToken,
IExchange.Order memory order, IExchange.Order memory order,
bytes memory signature, bytes memory signature,
uint256 sellAmount, uint256 sellAmount,
uint256 protocolFee FillState memory state
) )
private private
returns (FillOrderResults memory results) returns (FillOrderResults memory results)
@ -281,18 +302,12 @@ contract FillQuoteTransformer is
} }
} }
// Clamp fill amount to order size.
takerTokenFillAmount = LibSafeMathV06.min256(
takerTokenFillAmount,
order.takerAssetAmount
);
// Perform the fill. // Perform the fill.
return _fillOrder( return _fillOrder(
order, order,
signature, signature,
takerTokenFillAmount, takerTokenFillAmount,
protocolFee, state,
makerToken, makerToken,
takerFeeToken == takerToken takerFeeToken == takerToken
); );
@ -304,14 +319,14 @@ contract FillQuoteTransformer is
/// @param order The order to fill. /// @param order The order to fill.
/// @param signature The signature for `order`. /// @param signature The signature for `order`.
/// @param buyAmount Amount of maker token to buy. /// @param buyAmount Amount of maker token to buy.
/// @param protocolFee The protocol fee needed to fill `order`. /// @param state Intermediate state variables to get around stack limits.
function _buyFromOrder( function _buyFromOrder(
IERC20TokenV06 makerToken, IERC20TokenV06 makerToken,
IERC20TokenV06 takerToken, IERC20TokenV06 takerToken,
IExchange.Order memory order, IExchange.Order memory order,
bytes memory signature, bytes memory signature,
uint256 buyAmount, uint256 buyAmount,
uint256 protocolFee FillState memory state
) )
private private
returns (FillOrderResults memory results) returns (FillOrderResults memory results)
@ -351,18 +366,12 @@ contract FillQuoteTransformer is
} }
} }
// Clamp to order size.
takerTokenFillAmount = LibSafeMathV06.min256(
order.takerAssetAmount,
takerTokenFillAmount
);
// Perform the fill. // Perform the fill.
return _fillOrder( return _fillOrder(
order, order,
signature, signature,
takerTokenFillAmount, takerTokenFillAmount,
protocolFee, state,
makerToken, makerToken,
takerFeeToken == takerToken takerFeeToken == takerToken
); );
@ -373,7 +382,7 @@ contract FillQuoteTransformer is
/// @param order The order to fill. /// @param order The order to fill.
/// @param signature The order signature. /// @param signature The order signature.
/// @param takerAssetFillAmount How much taker asset to fill. /// @param takerAssetFillAmount How much taker asset to fill.
/// @param protocolFee The protocol fee needed to fill this order. /// @param state Intermediate state variables to get around stack limits.
/// @param makerToken The maker token. /// @param makerToken The maker token.
/// @param isTakerFeeInTakerToken Whether the taker fee token is the same as the /// @param isTakerFeeInTakerToken Whether the taker fee token is the same as the
/// taker token. /// taker token.
@ -381,38 +390,116 @@ contract FillQuoteTransformer is
IExchange.Order memory order, IExchange.Order memory order,
bytes memory signature, bytes memory signature,
uint256 takerAssetFillAmount, uint256 takerAssetFillAmount,
uint256 protocolFee, FillState memory state,
IERC20TokenV06 makerToken, IERC20TokenV06 makerToken,
bool isTakerFeeInTakerToken bool isTakerFeeInTakerToken
) )
private private
returns (FillOrderResults memory results) returns (FillOrderResults memory results)
{ {
// Track changes in the maker token balance. // Clamp to remaining taker asset amount or order size.
uint256 initialMakerTokenBalance = makerToken.balanceOf(address(this)); uint256 availableTakerAssetFillAmount =
try takerAssetFillAmount.min256(order.takerAssetAmount);
exchange.fillOrder availableTakerAssetFillAmount =
{value: protocolFee} availableTakerAssetFillAmount.min256(state.takerTokenBalanceRemaining);
(order, takerAssetFillAmount, signature) // If it is a Bridge order we fill this directly
returns (IExchange.FillResults memory fillResults) // rather than filling via 0x Exchange
{ if (order.makerAssetData.readBytes4(0) == ERC20_BRIDGE_PROXY_ID) {
// Update maker quantity based on changes in token balances. // Calculate the amount (in maker token) we expect to receive
results.makerTokenBoughtAmount = makerToken.balanceOf(address(this)) // from the bridge
.safeSub(initialMakerTokenBalance); uint256 outputTokenAmount = LibMathV06.getPartialAmountFloor(
// We can trust the other fill result quantities. availableTakerAssetFillAmount,
results.protocolFeePaid = fillResults.protocolFeePaid; order.takerAssetAmount,
results.takerTokenSoldAmount = fillResults.takerAssetFilledAmount; order.makerAssetAmount
// If the taker fee is payable in the taker asset, include the );
// taker fee in the total amount sold. (bool success, bytes memory data) = address(_implementation).delegatecall(
if (isTakerFeeInTakerToken) { abi.encodeWithSelector(
results.takerTokenSoldAmount = this.fillBridgeOrder.selector,
results.takerTokenSoldAmount.safeAdd(fillResults.takerFeePaid); order.makerAddress,
} order.makerAssetData,
} catch (bytes memory) { order.takerAssetData,
availableTakerAssetFillAmount,
outputTokenAmount
)
);
// Swallow failures, leaving all results as zero. // Swallow failures, leaving all results as zero.
// TransformERC20 asserts the overall price is as expected. It is possible
// a subsequent fill can net out at the expected price so we do not assert
// the trade balance
if (success) {
results.makerTokenBoughtAmount = makerToken
.balanceOf(address(this))
.safeSub(state.boughtAmount);
results.takerTokenSoldAmount = availableTakerAssetFillAmount;
// protocol fee paid remains 0
}
} else {
// Emit an event if we do not have sufficient ETH to cover the protocol fee.
if (state.ethRemaining < state.protocolFee) {
emit ProtocolFeeUnfunded(state.ethRemaining, state.protocolFee);
return results;
}
try
exchange.fillOrder
{value: state.protocolFee}
(order, availableTakerAssetFillAmount, signature)
returns (IExchange.FillResults memory fillResults)
{
results.makerTokenBoughtAmount = fillResults.makerAssetFilledAmount;
results.takerTokenSoldAmount = fillResults.takerAssetFilledAmount;
results.protocolFeePaid = fillResults.protocolFeePaid;
// If the taker fee is payable in the taker asset, include the
// taker fee in the total amount sold.
if (isTakerFeeInTakerToken) {
results.takerTokenSoldAmount =
results.takerTokenSoldAmount.safeAdd(fillResults.takerFeePaid);
}
} catch (bytes memory) {
// Swallow failures, leaving all results as zero.
}
} }
} }
/// @dev Attempt to fill an ERC20 Bridge order. If the fill reverts,
/// or the amount filled was not sufficient this reverts.
/// @param makerAddress The address of the maker.
/// @param makerAssetData The encoded ERC20BridgeProxy asset data.
/// @param takerAssetData The encoded ERC20 asset data.
/// @param inputTokenAmount How much taker asset to fill clamped to the available balance.
/// @param outputTokenAmount How much maker asset to receive.
function fillBridgeOrder(
address makerAddress,
bytes calldata makerAssetData,
bytes calldata takerAssetData,
uint256 inputTokenAmount,
uint256 outputTokenAmount
)
external
{
// Track changes in the maker token balance.
(
address tokenAddress,
address bridgeAddress,
bytes memory bridgeData
) = abi.decode(
makerAssetData.sliceDestructive(4, makerAssetData.length),
(address, address, bytes)
);
require(bridgeAddress != address(this), "INVALID_BRIDGE_ADDRESS");
// Transfer the tokens to the bridge to perform the work
_getTokenFromERC20AssetData(takerAssetData).compatTransfer(
bridgeAddress,
inputTokenAmount
);
IERC20Bridge(bridgeAddress).bridgeTransferFrom(
tokenAddress,
makerAddress,
address(this),
outputTokenAmount, // amount to transfer back from the bridge
bridgeData
);
}
/// @dev Extract the token from plain ERC20 asset data. /// @dev Extract the token from plain ERC20 asset data.
/// If the asset-data is empty, a zero token address will be returned. /// If the asset-data is empty, a zero token address will be returned.
/// @param assetData The order asset data. /// @param assetData The order asset data.

View File

@ -33,7 +33,7 @@ abstract contract Transformer is
/// @dev The address of the deployer. /// @dev The address of the deployer.
address public immutable deployer; address public immutable deployer;
/// @dev The original address of this contract. /// @dev The original address of this contract.
address private immutable _implementation; address internal immutable _implementation;
/// @dev Create this contract. /// @dev Create this contract.
constructor() public { constructor() public {

View File

@ -0,0 +1,55 @@
/*
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;
interface IERC20Bridge {
/// @dev Emitted when a trade occurs.
/// @param inputToken The token the bridge is converting from.
/// @param outputToken The token the bridge is converting to.
/// @param inputTokenAmount Amount of input token.
/// @param outputTokenAmount Amount of output token.
/// @param from The `from` address in `bridgeTransferFrom()`
/// @param to The `to` address in `bridgeTransferFrom()`
event ERC20BridgeTransfer(
address inputToken,
address outputToken,
uint256 inputTokenAmount,
uint256 outputTokenAmount,
address from,
address to
);
/// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`.
/// @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.
/// @param bridgeData Arbitrary asset data needed by the bridge contract.
/// @return success The magic bytes `0xdc1600f3` if successful.
function bridgeTransferFrom(
address tokenAddress,
address from,
address to,
uint256 amount,
bytes calldata bridgeData
)
external
returns (bytes4 success);
}

View File

@ -0,0 +1,37 @@
/*
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;
interface IGasToken {
/// @dev Frees up to `value` sub-tokens
/// @param value The amount of tokens to free
/// @return freed How many tokens were freed
function freeUpTo(uint256 value) external returns (uint256 freed);
/// @dev Frees up to `value` sub-tokens owned by `from`
/// @param from The owner of tokens to spend
/// @param value The amount of tokens to free
/// @return freed How many tokens were freed
function freeFromUpTo(address from, uint256 value) external returns (uint256 freed);
/// @dev Mints `value` amount of tokens
/// @param value The amount of tokens to mint
function mint(uint256 value) external;
}

View File

@ -0,0 +1,65 @@
/*
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 "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol";
import "../src/vendor/v3/IERC20Bridge.sol";
import "./TestMintableERC20Token.sol";
contract TestFillQuoteTransformerBridge {
struct FillBehavior {
// Scaling for maker assets minted, in 1e18.
uint256 makerAssetMintRatio;
}
bytes4 private constant ERC20_BRIDGE_PROXY_ID = 0xdc1600f3;
function bridgeTransferFrom(
address tokenAddress,
address from,
address to,
uint256 amount,
bytes calldata bridgeData
)
external
returns (bytes4 success)
{
FillBehavior memory behavior = abi.decode(bridgeData, (FillBehavior));
TestMintableERC20Token(tokenAddress).mint(
to,
LibMathV06.getPartialAmountFloor(
behavior.makerAssetMintRatio,
1e18,
amount
)
);
return ERC20_BRIDGE_PROXY_ID;
}
function encodeBehaviorData(FillBehavior calldata behavior)
external
pure
returns (bytes memory encoded)
{
return abi.encode(behavior);
}
}

View File

@ -35,12 +35,13 @@
"lint-contracts": "#solhint -c ../.solhint.json contracts/**/**/**/**/*.sol", "lint-contracts": "#solhint -c ../.solhint.json contracts/**/**/**/**/*.sol",
"compile:truffle": "truffle compile", "compile:truffle": "truffle compile",
"docs:md": "ts-doc-gen --sourceDir='$PROJECT_FILES' --output=$MD_FILE_DIR --fileExtension=mdx --tsconfig=./typedoc-tsconfig.json", "docs:md": "ts-doc-gen --sourceDir='$PROJECT_FILES' --output=$MD_FILE_DIR --fileExtension=mdx --tsconfig=./typedoc-tsconfig.json",
"docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES",
"publish:private": "yarn build && gitpkg publish"
}, },
"config": { "config": {
"publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer,Ownable,SimpleFunctionRegistry,TransformERC20,TokenSpender,AffiliateFeeTransformer", "publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer,Ownable,SimpleFunctionRegistry,TransformERC20,TokenSpender,AffiliateFeeTransformer",
"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.",
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|IExchange|IFeature|IFlashWallet|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|PayTakerTransformer|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|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|FixinGasToken|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|PayTakerTransformer|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -10,14 +10,17 @@ import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.js
import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json'; import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json';
import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json'; import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json';
import * as FixinCommon from '../test/generated-artifacts/FixinCommon.json'; import * as FixinCommon from '../test/generated-artifacts/FixinCommon.json';
import * as FixinGasToken from '../test/generated-artifacts/FixinGasToken.json';
import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json'; import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json';
import * as FullMigration from '../test/generated-artifacts/FullMigration.json'; import * as FullMigration from '../test/generated-artifacts/FullMigration.json';
import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json'; import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json';
import * as IBootstrap from '../test/generated-artifacts/IBootstrap.json'; import * as IBootstrap from '../test/generated-artifacts/IBootstrap.json';
import * as IERC20Bridge from '../test/generated-artifacts/IERC20Bridge.json';
import * as IERC20Transformer from '../test/generated-artifacts/IERC20Transformer.json'; import * as IERC20Transformer from '../test/generated-artifacts/IERC20Transformer.json';
import * as IExchange from '../test/generated-artifacts/IExchange.json'; import * as IExchange from '../test/generated-artifacts/IExchange.json';
import * as IFeature from '../test/generated-artifacts/IFeature.json'; import * as IFeature from '../test/generated-artifacts/IFeature.json';
import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json'; import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json';
import * as IGasToken from '../test/generated-artifacts/IGasToken.json';
import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json';
import * as IOwnable from '../test/generated-artifacts/IOwnable.json'; import * as IOwnable from '../test/generated-artifacts/IOwnable.json';
import * as ISimpleFunctionRegistry from '../test/generated-artifacts/ISimpleFunctionRegistry.json'; import * as ISimpleFunctionRegistry from '../test/generated-artifacts/ISimpleFunctionRegistry.json';
@ -45,6 +48,7 @@ import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransf
import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json'; import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json';
import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json';
import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json'; import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json';
import * as TestFillQuoteTransformerBridge from '../test/generated-artifacts/TestFillQuoteTransformerBridge.json';
import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/TestFillQuoteTransformerExchange.json'; import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/TestFillQuoteTransformerExchange.json';
import * as TestFillQuoteTransformerHost from '../test/generated-artifacts/TestFillQuoteTransformerHost.json'; import * as TestFillQuoteTransformerHost from '../test/generated-artifacts/TestFillQuoteTransformerHost.json';
import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json'; import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json';
@ -95,6 +99,7 @@ export const artifacts = {
TokenSpender: TokenSpender as ContractArtifact, TokenSpender: TokenSpender as ContractArtifact,
TransformERC20: TransformERC20 as ContractArtifact, TransformERC20: TransformERC20 as ContractArtifact,
FixinCommon: FixinCommon as ContractArtifact, FixinCommon: FixinCommon as ContractArtifact,
FixinGasToken: FixinGasToken as ContractArtifact,
FullMigration: FullMigration as ContractArtifact, FullMigration: FullMigration as ContractArtifact,
InitialMigration: InitialMigration as ContractArtifact, InitialMigration: InitialMigration as ContractArtifact,
LibBootstrap: LibBootstrap as ContractArtifact, LibBootstrap: LibBootstrap as ContractArtifact,
@ -112,10 +117,13 @@ export const artifacts = {
PayTakerTransformer: PayTakerTransformer as ContractArtifact, PayTakerTransformer: PayTakerTransformer as ContractArtifact,
Transformer: Transformer as ContractArtifact, Transformer: Transformer as ContractArtifact,
WethTransformer: WethTransformer as ContractArtifact, WethTransformer: WethTransformer as ContractArtifact,
IERC20Bridge: IERC20Bridge as ContractArtifact,
IExchange: IExchange as ContractArtifact, IExchange: IExchange as ContractArtifact,
IGasToken: IGasToken as ContractArtifact,
ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact,
TestCallTarget: TestCallTarget as ContractArtifact, TestCallTarget: TestCallTarget as ContractArtifact,
TestDelegateCaller: TestDelegateCaller as ContractArtifact, TestDelegateCaller: TestDelegateCaller as ContractArtifact,
TestFillQuoteTransformerBridge: TestFillQuoteTransformerBridge as ContractArtifact,
TestFillQuoteTransformerExchange: TestFillQuoteTransformerExchange as ContractArtifact, TestFillQuoteTransformerExchange: TestFillQuoteTransformerExchange as ContractArtifact,
TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact, TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact,
TestFullMigration: TestFullMigration as ContractArtifact, TestFullMigration: TestFullMigration as ContractArtifact,

View File

@ -18,6 +18,7 @@ import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { artifacts } from '../artifacts'; import { artifacts } from '../artifacts';
import { TestFillQuoteTransformerBridgeContract } from '../generated-wrappers/test_fill_quote_transformer_bridge';
import { import {
FillQuoteTransformerContract, FillQuoteTransformerContract,
TestFillQuoteTransformerExchangeContract, TestFillQuoteTransformerExchangeContract,
@ -31,6 +32,7 @@ blockchainTests.resets('FillQuoteTransformer', env => {
let maker: string; let maker: string;
let feeRecipient: string; let feeRecipient: string;
let exchange: TestFillQuoteTransformerExchangeContract; let exchange: TestFillQuoteTransformerExchangeContract;
let bridge: TestFillQuoteTransformerBridgeContract;
let transformer: FillQuoteTransformerContract; let transformer: FillQuoteTransformerContract;
let host: TestFillQuoteTransformerHostContract; let host: TestFillQuoteTransformerHostContract;
let makerToken: TestMintableERC20TokenContract; let makerToken: TestMintableERC20TokenContract;
@ -64,6 +66,12 @@ blockchainTests.resets('FillQuoteTransformer', env => {
}, },
artifacts, artifacts,
); );
bridge = await TestFillQuoteTransformerBridgeContract.deployFrom0xArtifactAsync(
artifacts.TestFillQuoteTransformerBridge,
env.provider,
env.txDefaults,
artifacts,
);
[makerToken, takerToken, takerFeeToken] = await Promise.all( [makerToken, takerToken, takerFeeToken] = await Promise.all(
_.times(3, async () => _.times(3, async () =>
TestMintableERC20TokenContract.deployFrom0xArtifactAsync( TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
@ -102,6 +110,19 @@ blockchainTests.resets('FillQuoteTransformer', env => {
}; };
} }
function createBridgeOrder(fields: Partial<Order> = {}, bridgeData: string = encodeBridgeBehavior()): FilledOrder {
const order = createOrder(fields);
return {
...order,
makerAddress: bridge.address,
makerAssetData: assetDataUtils.encodeERC20BridgeAssetData(makerToken.address, bridge.address, bridgeData),
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
};
}
interface QuoteFillResults { interface QuoteFillResults {
makerAssetBought: BigNumber; makerAssetBought: BigNumber;
takerAssetSpent: BigNumber; takerAssetSpent: BigNumber;
@ -244,6 +265,17 @@ blockchainTests.resets('FillQuoteTransformer', env => {
); );
} }
function encodeBridgeBehavior(makerAssetMintRatio: Numberish = 1.0): string {
return hexUtils.slice(
bridge
.encodeBehaviorData({
makerAssetMintRatio: new BigNumber(makerAssetMintRatio).times('1e18').integerValue(),
})
.getABIEncodedTransactionData(),
4,
);
}
const ERC20_ASSET_PROXY_ID = '0xf47261b0'; const ERC20_ASSET_PROXY_ID = '0xf47261b0';
describe('sell quotes', () => { describe('sell quotes', () => {
@ -436,9 +468,10 @@ blockchainTests.resets('FillQuoteTransformer', env => {
) )
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid.minus(1) }); .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid.minus(1) });
return expect(tx).to.revertWith( return expect(tx).to.revertWith(
new ZeroExRevertErrors.TransformERC20.InsufficientProtocolFeeError( new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError(
singleProtocolFee.minus(1), takerToken.address,
singleProtocolFee, getExpectedSellQuoteFillResults([...orders.slice(0, 2)]).takerAssetSpent,
qfr.takerAssetSpent,
), ),
); );
}); });
@ -707,36 +740,6 @@ blockchainTests.resets('FillQuoteTransformer', env => {
}); });
}); });
it('succeeds if an order transfers too many maker tokens', async () => {
const orders = _.times(2, () => createOrder());
// First order will mint its tokens + the maker tokens of the second.
const mintScale = orders[1].makerAssetAmount.div(orders[0].makerAssetAmount.minus(1)).plus(1);
const signatures = [
encodeExchangeBehavior(0, mintScale),
...orders.slice(1).map(() => encodeExchangeBehavior()),
];
const qfr = getExpectedBuyQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
side: FillQuoteTransformerSide.Buy,
fillAmount: qfr.makerAssetBought,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: orders[0].makerAssetAmount.times(mintScale).integerValue(BigNumber.ROUND_DOWN),
takerAssetBalance: orders[1].takerAssetAmount.plus(orders[1].takerFee),
protocolFeeBalance: singleProtocolFee,
});
});
it('fails to buy more than available in orders', async () => { it('fails to buy more than available in orders', async () => {
const orders = _.times(3, () => createOrder()); const orders = _.times(3, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior()); const signatures = orders.map(() => encodeExchangeBehavior());
@ -861,4 +864,109 @@ blockchainTests.resets('FillQuoteTransformer', env => {
}); });
}); });
}); });
describe('bridge orders', () => {
it('can fully sell to a single bridge order quote', async () => {
const orders = _.times(1, () => createBridgeOrder());
const signatures = orders.map(() => NULL_BYTES);
const qfr = getExpectedSellQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: ZERO_AMOUNT });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('can sell to a mix of order quote', async () => {
const nativeOrders = [createOrder()];
const bridgeOrders = [createBridgeOrder()];
const orders = [...nativeOrders, ...bridgeOrders];
const signatures = [
...nativeOrders.map(() => encodeExchangeBehavior()), // Valid Signatures
...bridgeOrders.map(() => NULL_BYTES), // Valid Signatures
];
const qfr = getExpectedSellQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: singleProtocolFee.times(nativeOrders.length) });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('can attempt to sell to a mix of order quote handling reverts', async () => {
const nativeOrders = _.times(3, () => createOrder());
const bridgeOrders = [createBridgeOrder()];
const orders = [...nativeOrders, ...bridgeOrders];
const signatures = [
...nativeOrders.map(() => NULL_BYTES), // Invalid Signatures
...bridgeOrders.map(() => NULL_BYTES), // Valid Signatures
];
const qfr = getExpectedSellQuoteFillResults(bridgeOrders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
// Single protocol fee as all Native orders will fail
.awaitTransactionSuccessAsync({ value: singleProtocolFee });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
protocolFeeBalance: singleProtocolFee,
});
});
it('can continue to the bridge order if the native order reverts', async () => {
const nativeOrders = [createOrder()];
const bridgeOrders = [createBridgeOrder()];
const orders = [...nativeOrders, ...bridgeOrders];
const signatures = [
...nativeOrders.map(() => encodeExchangeBehavior()), // Valid Signatures
...bridgeOrders.map(() => NULL_BYTES), // Valid Signatures
];
const qfr = getExpectedSellQuoteFillResults(bridgeOrders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
// Insufficient single protocol fee
.awaitTransactionSuccessAsync({ value: singleProtocolFee.minus(1) });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
protocolFeeBalance: singleProtocolFee,
});
});
});
}); });

View File

@ -8,14 +8,17 @@ export * from '../test/generated-wrappers/allowance_target';
export * from '../test/generated-wrappers/bootstrap'; export * from '../test/generated-wrappers/bootstrap';
export * from '../test/generated-wrappers/fill_quote_transformer'; export * from '../test/generated-wrappers/fill_quote_transformer';
export * from '../test/generated-wrappers/fixin_common'; export * from '../test/generated-wrappers/fixin_common';
export * from '../test/generated-wrappers/fixin_gas_token';
export * from '../test/generated-wrappers/flash_wallet'; export * from '../test/generated-wrappers/flash_wallet';
export * from '../test/generated-wrappers/full_migration'; export * from '../test/generated-wrappers/full_migration';
export * from '../test/generated-wrappers/i_allowance_target'; export * from '../test/generated-wrappers/i_allowance_target';
export * from '../test/generated-wrappers/i_bootstrap'; export * from '../test/generated-wrappers/i_bootstrap';
export * from '../test/generated-wrappers/i_erc20_bridge';
export * from '../test/generated-wrappers/i_erc20_transformer'; export * from '../test/generated-wrappers/i_erc20_transformer';
export * from '../test/generated-wrappers/i_exchange'; export * from '../test/generated-wrappers/i_exchange';
export * from '../test/generated-wrappers/i_feature'; export * from '../test/generated-wrappers/i_feature';
export * from '../test/generated-wrappers/i_flash_wallet'; export * from '../test/generated-wrappers/i_flash_wallet';
export * from '../test/generated-wrappers/i_gas_token';
export * from '../test/generated-wrappers/i_ownable'; export * from '../test/generated-wrappers/i_ownable';
export * from '../test/generated-wrappers/i_simple_function_registry'; export * from '../test/generated-wrappers/i_simple_function_registry';
export * from '../test/generated-wrappers/i_test_simple_function_registry_feature'; export * from '../test/generated-wrappers/i_test_simple_function_registry_feature';
@ -43,6 +46,7 @@ export * from '../test/generated-wrappers/pay_taker_transformer';
export * from '../test/generated-wrappers/simple_function_registry'; export * from '../test/generated-wrappers/simple_function_registry';
export * from '../test/generated-wrappers/test_call_target'; export * from '../test/generated-wrappers/test_call_target';
export * from '../test/generated-wrappers/test_delegate_caller'; export * from '../test/generated-wrappers/test_delegate_caller';
export * from '../test/generated-wrappers/test_fill_quote_transformer_bridge';
export * from '../test/generated-wrappers/test_fill_quote_transformer_exchange'; export * from '../test/generated-wrappers/test_fill_quote_transformer_exchange';
export * from '../test/generated-wrappers/test_fill_quote_transformer_host'; export * from '../test/generated-wrappers/test_fill_quote_transformer_host';
export * from '../test/generated-wrappers/test_full_migration'; export * from '../test/generated-wrappers/test_full_migration';

View File

@ -26,14 +26,17 @@
"test/generated-artifacts/Bootstrap.json", "test/generated-artifacts/Bootstrap.json",
"test/generated-artifacts/FillQuoteTransformer.json", "test/generated-artifacts/FillQuoteTransformer.json",
"test/generated-artifacts/FixinCommon.json", "test/generated-artifacts/FixinCommon.json",
"test/generated-artifacts/FixinGasToken.json",
"test/generated-artifacts/FlashWallet.json", "test/generated-artifacts/FlashWallet.json",
"test/generated-artifacts/FullMigration.json", "test/generated-artifacts/FullMigration.json",
"test/generated-artifacts/IAllowanceTarget.json", "test/generated-artifacts/IAllowanceTarget.json",
"test/generated-artifacts/IBootstrap.json", "test/generated-artifacts/IBootstrap.json",
"test/generated-artifacts/IERC20Bridge.json",
"test/generated-artifacts/IERC20Transformer.json", "test/generated-artifacts/IERC20Transformer.json",
"test/generated-artifacts/IExchange.json", "test/generated-artifacts/IExchange.json",
"test/generated-artifacts/IFeature.json", "test/generated-artifacts/IFeature.json",
"test/generated-artifacts/IFlashWallet.json", "test/generated-artifacts/IFlashWallet.json",
"test/generated-artifacts/IGasToken.json",
"test/generated-artifacts/IOwnable.json", "test/generated-artifacts/IOwnable.json",
"test/generated-artifacts/ISimpleFunctionRegistry.json", "test/generated-artifacts/ISimpleFunctionRegistry.json",
"test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json", "test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json",
@ -61,6 +64,7 @@
"test/generated-artifacts/SimpleFunctionRegistry.json", "test/generated-artifacts/SimpleFunctionRegistry.json",
"test/generated-artifacts/TestCallTarget.json", "test/generated-artifacts/TestCallTarget.json",
"test/generated-artifacts/TestDelegateCaller.json", "test/generated-artifacts/TestDelegateCaller.json",
"test/generated-artifacts/TestFillQuoteTransformerBridge.json",
"test/generated-artifacts/TestFillQuoteTransformerExchange.json", "test/generated-artifacts/TestFillQuoteTransformerExchange.json",
"test/generated-artifacts/TestFillQuoteTransformerHost.json", "test/generated-artifacts/TestFillQuoteTransformerHost.json",
"test/generated-artifacts/TestFullMigration.json", "test/generated-artifacts/TestFullMigration.json",