@0x/contracts-asset-proxy: Add ERC20BridgeProxy and tests.
				
					
				
			This commit is contained in:
		| @@ -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" | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|   | |||||||
							
								
								
									
										128
									
								
								contracts/asset-proxy/contracts/src/ERC20BridgeProxy.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								contracts/asset-proxy/contracts/src/ERC20BridgeProxy.sol
									
									
									
									
									
										Normal 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  | } | ||||||
							
								
								
									
										108
									
								
								contracts/asset-proxy/contracts/test/TestERC20Bridge.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								contracts/asset-proxy/contracts/test/TestERC20Bridge.sol
									
									
									
									
									
										Normal 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)) } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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": { | ||||||
|   | |||||||
| @@ -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, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -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'; | ||||||
|   | |||||||
							
								
								
									
										296
									
								
								contracts/asset-proxy/test/erc20bridge_proxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								contracts/asset-proxy/test/erc20bridge_proxy.ts
									
									
									
									
									
										Normal 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); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
| @@ -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"] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user