@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", | ||||
|                 "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" | ||||
|     }, | ||||
|     "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." | ||||
|     }, | ||||
|     "repository": { | ||||
|   | ||||
| @@ -6,23 +6,27 @@ | ||||
| import { ContractArtifact } from 'ethereum-types'; | ||||
|  | ||||
| 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 ERC721Proxy from '../generated-artifacts/ERC721Proxy.json'; | ||||
| import * as IAssetData from '../generated-artifacts/IAssetData.json'; | ||||
| import * as IAssetProxy from '../generated-artifacts/IAssetProxy.json'; | ||||
| import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispatcher.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 MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json'; | ||||
| import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; | ||||
| import * as Ownable from '../generated-artifacts/Ownable.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'; | ||||
| export const artifacts = { | ||||
|     MixinAssetProxyDispatcher: MixinAssetProxyDispatcher as ContractArtifact, | ||||
|     MixinAuthorizable: MixinAuthorizable as ContractArtifact, | ||||
|     Ownable: Ownable as ContractArtifact, | ||||
|     ERC1155Proxy: ERC1155Proxy as ContractArtifact, | ||||
|     ERC20BridgeProxy: ERC20BridgeProxy as ContractArtifact, | ||||
|     ERC20Proxy: ERC20Proxy as ContractArtifact, | ||||
|     ERC721Proxy: ERC721Proxy as ContractArtifact, | ||||
|     MultiAssetProxy: MultiAssetProxy as ContractArtifact, | ||||
| @@ -31,5 +35,7 @@ export const artifacts = { | ||||
|     IAssetProxy: IAssetProxy as ContractArtifact, | ||||
|     IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact, | ||||
|     IAuthorizable: IAuthorizable as ContractArtifact, | ||||
|     IERC20Bridge: IERC20Bridge as ContractArtifact, | ||||
|     TestERC20Bridge: TestERC20Bridge as ContractArtifact, | ||||
|     TestStaticCallTarget: TestStaticCallTarget as ContractArtifact, | ||||
| }; | ||||
|   | ||||
| @@ -4,15 +4,18 @@ | ||||
|  * ----------------------------------------------------------------------------- | ||||
|  */ | ||||
| export * from '../generated-wrappers/erc1155_proxy'; | ||||
| export * from '../generated-wrappers/erc20_bridge_proxy'; | ||||
| export * from '../generated-wrappers/erc20_proxy'; | ||||
| export * from '../generated-wrappers/erc721_proxy'; | ||||
| export * from '../generated-wrappers/i_asset_data'; | ||||
| export * from '../generated-wrappers/i_asset_proxy'; | ||||
| export * from '../generated-wrappers/i_asset_proxy_dispatcher'; | ||||
| 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_authorizable'; | ||||
| export * from '../generated-wrappers/multi_asset_proxy'; | ||||
| export * from '../generated-wrappers/ownable'; | ||||
| export * from '../generated-wrappers/static_call_proxy'; | ||||
| export * from '../generated-wrappers/test_erc20_bridge'; | ||||
| 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/**/*"], | ||||
|     "files": [ | ||||
|         "generated-artifacts/ERC1155Proxy.json", | ||||
|         "generated-artifacts/ERC20BridgeProxy.json", | ||||
|         "generated-artifacts/ERC20Proxy.json", | ||||
|         "generated-artifacts/ERC721Proxy.json", | ||||
|         "generated-artifacts/IAssetData.json", | ||||
|         "generated-artifacts/IAssetProxy.json", | ||||
|         "generated-artifacts/IAssetProxyDispatcher.json", | ||||
|         "generated-artifacts/IAuthorizable.json", | ||||
|         "generated-artifacts/IERC20Bridge.json", | ||||
|         "generated-artifacts/MixinAssetProxyDispatcher.json", | ||||
|         "generated-artifacts/MixinAuthorizable.json", | ||||
|         "generated-artifacts/MultiAssetProxy.json", | ||||
|         "generated-artifacts/Ownable.json", | ||||
|         "generated-artifacts/StaticCallProxy.json", | ||||
|         "generated-artifacts/TestERC20Bridge.json", | ||||
|         "generated-artifacts/TestStaticCallTarget.json" | ||||
|     ], | ||||
|     "exclude": ["./deploy/solc/solc_bin"] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user