diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index ef834d6638..93d27fa96b 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "2.3.0-beta.4", + "changes": [ + { + "note": "Implement `KyberBridge`.", + "pr": 2352 + } + ] + }, { "version": "2.3.0-beta.3", "changes": [ diff --git a/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol b/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol index 2d12c1f729..499ea19ee3 100644 --- a/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol @@ -61,8 +61,8 @@ contract Eth2DaiBridge is LibERC20Token.approve(fromTokenAddress, address(exchange), uint256(-1)); // Try to sell all of this contract's `fromTokenAddress` token balance. - uint256 boughtAmount = _getEth2DaiContract().sellAllAmount( - address(fromTokenAddress), + uint256 boughtAmount = exchange.sellAllAmount( + fromTokenAddress, IERC20Token(fromTokenAddress).balanceOf(address(this)), toTokenAddress, amount diff --git a/contracts/asset-proxy/contracts/src/bridges/KyberBridge.sol b/contracts/asset-proxy/contracts/src/bridges/KyberBridge.sol new file mode 100644 index 0000000000..8833ae2d9c --- /dev/null +++ b/contracts/asset-proxy/contracts/src/bridges/KyberBridge.sol @@ -0,0 +1,160 @@ +/* + + 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-erc20/contracts/src/interfaces/IERC20Token.sol"; +import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; +import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; +import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; +import "../interfaces/IERC20Bridge.sol"; +import "../interfaces/IKyberNetworkProxy.sol"; + + +// solhint-disable space-after-comma +contract KyberBridge is + IERC20Bridge, + IWallet +{ + // @dev Structure used internally to get around stack limits. + struct TradeState { + IKyberNetworkProxy kyber; + IEtherToken weth; + address fromTokenAddress; + uint256 fromTokenBalance; + uint256 payableAmount; + uint256 minConversionRate; + } + + /// @dev Address of the WETH contract. + address constant public WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + /// @dev Address of the KyberNeworkProxy contract. + address constant public KYBER_NETWORK_PROXY_ADDRESS = 0x818E6FECD516Ecc3849DAf6845e3EC868087B755; + /// @dev Kyber ETH pseudo-address. + address constant public KYBER_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /// @dev Callback for `IKyberBridge`. Tries to buy `amount` of + /// `toTokenAddress` tokens by selling the entirety of the opposing asset + /// to the `KyberNetworkProxy` contract, then transfers the bought + /// tokens to `to`. + /// @param toTokenAddress The token to give to `to`. + /// @param to The recipient of the bought tokens. + /// @param amount Minimum amount of `toTokenAddress` tokens to buy. + /// @param bridgeData The abi-encoeded "from" token address. + /// @return success The magic bytes if successful. + function bridgeTransferFrom( + address toTokenAddress, + address /* from */, + address to, + uint256 amount, + bytes calldata bridgeData + ) + external + returns (bytes4 success) + { + TradeState memory state; + state.kyber = _getKyberContract(); + state.weth = _getWETHContract(); + // Decode the bridge data to get the `fromTokenAddress`. + (state.fromTokenAddress) = abi.decode(bridgeData, (address)); + state.fromTokenBalance = IERC20Token(state.fromTokenAddress).balanceOf(address(this)); + if (state.fromTokenBalance == 0) { + // Do nothing if no input tokens. + return BRIDGE_SUCCESS; + } + state.minConversionRate = (10 ** 18) * amount / state.fromTokenBalance; + if (state.fromTokenAddress == toTokenAddress) { + // Just transfer the tokens if they're the same. + LibERC20Token.transfer(state.fromTokenAddress, to, state.fromTokenBalance); + return BRIDGE_SUCCESS; + } else if (state.fromTokenAddress != address(state.weth)) { + // If the input token is not WETH, grant an allowance to the exchange + // to spend them. + LibERC20Token.approve(state.fromTokenAddress, address(state.kyber), uint256(-1)); + } else { + // If the input token is WETH, unwrap it and attach it to the call. + state.fromTokenAddress = KYBER_ETH_ADDRESS; + state.payableAmount = state.fromTokenBalance; + state.weth.withdraw(state.fromTokenBalance); + } + + // Try to sell all of this contract's input token balance. + uint256 boughtAmount = state.kyber.trade.value(state.payableAmount)( + // Input token. + state.fromTokenAddress, + // Sell amount. + state.fromTokenBalance, + // Output token. + toTokenAddress == address(state.weth) ? + KYBER_ETH_ADDRESS : + toTokenAddress, + // Transfer to this contract if converting to ETH, otherwise + // transfer directly to the recipient. + toTokenAddress == address(state.weth) ? + address(uint160(address(this))) : + address(uint160(to)), + // Buy as much as possible. + uint256(-1), + // Minimum conversion rate. + state.minConversionRate, + // No affiliate address. + address(0) + ); + // Wrap ETH output and transfer to recipient. + if (toTokenAddress == address(state.weth)) { + state.weth.deposit.value(boughtAmount)(); + state.weth.transfer(to, boughtAmount); + } + return BRIDGE_SUCCESS; + } + + /// @dev `SignatureType.Wallet` callback, so that this bridge can be the maker + /// and sign for itself in orders. Always succeeds. + /// @return magicValue Magic success bytes, always. + function isValidSignature( + bytes32, + bytes calldata + ) + external + view + returns (bytes4 magicValue) + { + return LEGACY_WALLET_MAGIC_VALUE; + } + + /// @dev Overridable way to get the `KyberNetworkProxy` contract. + /// @return kyber The `IKyberNetworkProxy` contract. + function _getKyberContract() + internal + view + returns (IKyberNetworkProxy kyber) + { + return IKyberNetworkProxy(KYBER_NETWORK_PROXY_ADDRESS); + } + + /// @dev Overridable way to get the WETH contract. + /// @return weth The WETH contract. + function _getWETHContract() + internal + view + returns (IEtherToken weth) + { + return IEtherToken(WETH_ADDRESS); + } +} diff --git a/contracts/asset-proxy/contracts/src/interfaces/IKyberNetworkProxy.sol b/contracts/asset-proxy/contracts/src/interfaces/IKyberNetworkProxy.sol new file mode 100644 index 0000000000..b17a6f9fee --- /dev/null +++ b/contracts/asset-proxy/contracts/src/interfaces/IKyberNetworkProxy.sol @@ -0,0 +1,46 @@ +/* + + 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; + + +interface IKyberNetworkProxy { + + /// @dev Sells `sellTokenAddress` tokens for `buyTokenAddress` tokens. + /// @param sellTokenAddress Token to sell. + /// @param sellAmount Amount of tokens to sell. + /// @param buyTokenAddress Token to buy. + /// @param recipientAddress Address to send bought tokens to. + /// @param maxBuyTokenAmount A limit on the amount of tokens to buy. + /// @param minConversionRate The minimal conversion rate. If actual rate + /// is lower, trade is canceled. + /// @param walletId The wallet ID to send part of the fees + /// @return boughtAmount Amount of tokens bought. + function trade( + address sellTokenAddress, + uint256 sellAmount, + address buyTokenAddress, + address payable recipientAddress, + uint256 maxBuyTokenAmount, + uint256 minConversionRate, + address walletId + ) + external + payable + returns(uint256 boughtAmount); +} diff --git a/contracts/asset-proxy/contracts/test/TestKyberBridge.sol b/contracts/asset-proxy/contracts/test/TestKyberBridge.sol new file mode 100644 index 0000000000..9e816be1a8 --- /dev/null +++ b/contracts/asset-proxy/contracts/test/TestKyberBridge.sol @@ -0,0 +1,325 @@ +/* + + 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-erc20/contracts/src/interfaces/IERC20Token.sol"; +import "../src/bridges/KyberBridge.sol"; +import "../src/interfaces/IKyberNetworkProxy.sol"; + + +// solhint-disable no-simple-event-func-name +interface ITestContract { + + function wethWithdraw( + address payable ownerAddress, + uint256 amount + ) + external; + + function wethDeposit( + address ownerAddress + ) + external + payable; + + function tokenTransfer( + address ownerAddress, + address recipientAddress, + uint256 amount + ) + external + returns (bool success); + + function tokenApprove( + address ownerAddress, + address spenderAddress, + uint256 allowance + ) + external + returns (bool success); + + function tokenBalanceOf( + address ownerAddress + ) + external + view + returns (uint256 balance); +} + + +/// @dev A minimalist ERC20/WETH token. +contract TestToken { + + ITestContract private _testContract; + + constructor() public { + _testContract = ITestContract(msg.sender); + } + + function approve(address spender, uint256 allowance) + external + returns (bool) + { + return _testContract.tokenApprove( + msg.sender, + spender, + allowance + ); + } + + function transfer(address recipient, uint256 amount) + external + returns (bool) + { + return _testContract.tokenTransfer( + msg.sender, + recipient, + amount + ); + } + + function withdraw(uint256 amount) + external + { + return _testContract.wethWithdraw(msg.sender, amount); + } + + function deposit() + external + payable + { + return _testContract.wethDeposit.value(msg.value)(msg.sender); + } + + function balanceOf(address owner) + external + view + returns (uint256) + { + return _testContract.tokenBalanceOf(owner); + } +} + + +/// @dev Eth2DaiBridge overridden to mock tokens and +/// implement IEth2Dai. +contract TestKyberBridge is + KyberBridge, + ITestContract, + IKyberNetworkProxy +{ + event KyberBridgeTrade( + uint256 msgValue, + address sellTokenAddress, + uint256 sellAmount, + address buyTokenAddress, + address payable recipientAddress, + uint256 maxBuyTokenAmount, + uint256 minConversionRate, + address walletId + ); + + event KyberBridgeWethWithdraw( + address ownerAddress, + uint256 amount + ); + + event KyberBridgeWethDeposit( + uint256 msgValue, + address ownerAddress, + uint256 amount + ); + + event KyberBridgeTokenApprove( + address tokenAddress, + address ownerAddress, + address spenderAddress, + uint256 allowance + ); + + event KyberBridgeTokenTransfer( + address tokenAddress, + address ownerAddress, + address recipientAddress, + uint256 amount + ); + + IEtherToken public weth; + mapping (address => mapping (address => uint256)) private _tokenBalances; + uint256 private _nextFillAmount; + + constructor() public { + weth = IEtherToken(address(new TestToken())); + } + + /// @dev Implementation of `IKyberNetworkProxy.trade()` + function trade( + address sellTokenAddress, + uint256 sellAmount, + address buyTokenAddress, + address payable recipientAddress, + uint256 maxBuyTokenAmount, + uint256 minConversionRate, + address walletId + ) + external + payable + returns(uint256 boughtAmount) + { + emit KyberBridgeTrade( + msg.value, + sellTokenAddress, + sellAmount, + buyTokenAddress, + recipientAddress, + maxBuyTokenAmount, + minConversionRate, + walletId + ); + return _nextFillAmount; + } + + function createToken() + external + returns (address tokenAddress) + { + return address(new TestToken()); + } + + function setNextFillAmount(uint256 amount) + external + payable + { + if (msg.value != 0) { + require(amount == msg.value, "VALUE_AMOUNT_MISMATCH"); + grantTokensTo(address(weth), address(this), msg.value); + } + _nextFillAmount = amount; + } + + function wethDeposit( + address ownerAddress + ) + external + payable + { + require(msg.sender == address(weth), "ONLY_WETH"); + grantTokensTo(address(weth), ownerAddress, msg.value); + emit KyberBridgeWethDeposit( + msg.value, + ownerAddress, + msg.value + ); + } + + function wethWithdraw( + address payable ownerAddress, + uint256 amount + ) + external + { + require(msg.sender == address(weth), "ONLY_WETH"); + _tokenBalances[address(weth)][ownerAddress] -= amount; + if (ownerAddress != address(this)) { + ownerAddress.transfer(amount); + } + emit KyberBridgeWethWithdraw( + ownerAddress, + amount + ); + } + + function tokenApprove( + address ownerAddress, + address spenderAddress, + uint256 allowance + ) + external + returns (bool success) + { + emit KyberBridgeTokenApprove( + msg.sender, + ownerAddress, + spenderAddress, + allowance + ); + return true; + } + + function tokenTransfer( + address ownerAddress, + address recipientAddress, + uint256 amount + ) + external + returns (bool success) + { + _tokenBalances[msg.sender][ownerAddress] -= amount; + _tokenBalances[msg.sender][recipientAddress] += amount; + emit KyberBridgeTokenTransfer( + msg.sender, + ownerAddress, + recipientAddress, + amount + ); + return true; + } + + function tokenBalanceOf( + address ownerAddress + ) + external + view + returns (uint256 balance) + { + return _tokenBalances[msg.sender][ownerAddress]; + } + + function grantTokensTo(address tokenAddress, address ownerAddress, uint256 amount) + public + payable + { + _tokenBalances[tokenAddress][ownerAddress] += amount; + if (tokenAddress != address(weth)) { + // Send back ether if not WETH. + msg.sender.transfer(msg.value); + } else { + require(msg.value == amount, "VALUE_AMOUNT_MISMATCH"); + } + } + + // @dev overridden to point to this contract. + function _getKyberContract() + internal + view + returns (IKyberNetworkProxy kyber) + { + return IKyberNetworkProxy(address(this)); + } + + // @dev overridden to point to test WETH. + function _getWETHContract() + internal + view + returns (IEtherToken weth_) + { + return weth; + } +} diff --git a/contracts/asset-proxy/package.json b/contracts/asset-proxy/package.json index d85798d5c0..d83a30609e 100644 --- a/contracts/asset-proxy/package.json +++ b/contracts/asset-proxy/package.json @@ -38,8 +38,8 @@ "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { - "publicInterfaceContracts": "ERC1155Proxy,ERC20Proxy,ERC721Proxy,MultiAssetProxy,StaticCallProxy,ERC20BridgeProxy,Eth2DaiBridge,IAssetData,IAssetProxy,UniswapBridge,TestStaticCallTarget", - "abis": "./test/generated-artifacts/@(ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IERC20Bridge|IEth2Dai|IUniswapExchange|IUniswapExchangeFactory|MixinAssetProxyDispatcher|MixinAuthorizable|MultiAssetProxy|Ownable|StaticCallProxy|TestERC20Bridge|TestEth2DaiBridge|TestStaticCallTarget|TestUniswapBridge|UniswapBridge).json", + "publicInterfaceContracts": "ERC1155Proxy,ERC20Proxy,ERC721Proxy,MultiAssetProxy,StaticCallProxy,ERC20BridgeProxy,Eth2DaiBridge,IAssetData,IAssetProxy,UniswapBridge,KyberBridge,TestStaticCallTarget", + "abis": "./test/generated-artifacts/@(ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IERC20Bridge|IEth2Dai|IKyberNetworkProxy|IUniswapExchange|IUniswapExchangeFactory|KyberBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MultiAssetProxy|Ownable|StaticCallProxy|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|UniswapBridge).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/asset-proxy/src/artifacts.ts b/contracts/asset-proxy/src/artifacts.ts index 1dd886dedf..14ed1ee378 100644 --- a/contracts/asset-proxy/src/artifacts.ts +++ b/contracts/asset-proxy/src/artifacts.ts @@ -12,6 +12,7 @@ import * as ERC721Proxy from '../generated-artifacts/ERC721Proxy.json'; import * as Eth2DaiBridge from '../generated-artifacts/Eth2DaiBridge.json'; import * as IAssetData from '../generated-artifacts/IAssetData.json'; import * as IAssetProxy from '../generated-artifacts/IAssetProxy.json'; +import * as KyberBridge from '../generated-artifacts/KyberBridge.json'; import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json'; import * as TestStaticCallTarget from '../generated-artifacts/TestStaticCallTarget.json'; @@ -27,5 +28,6 @@ export const artifacts = { IAssetData: IAssetData as ContractArtifact, IAssetProxy: IAssetProxy as ContractArtifact, UniswapBridge: UniswapBridge as ContractArtifact, + KyberBridge: KyberBridge as ContractArtifact, TestStaticCallTarget: TestStaticCallTarget as ContractArtifact, }; diff --git a/contracts/asset-proxy/src/wrappers.ts b/contracts/asset-proxy/src/wrappers.ts index 0fa37a911c..7b01c84334 100644 --- a/contracts/asset-proxy/src/wrappers.ts +++ b/contracts/asset-proxy/src/wrappers.ts @@ -10,6 +10,7 @@ export * from '../generated-wrappers/erc721_proxy'; export * from '../generated-wrappers/eth2_dai_bridge'; export * from '../generated-wrappers/i_asset_data'; export * from '../generated-wrappers/i_asset_proxy'; +export * from '../generated-wrappers/kyber_bridge'; export * from '../generated-wrappers/multi_asset_proxy'; export * from '../generated-wrappers/static_call_proxy'; export * from '../generated-wrappers/test_static_call_target'; diff --git a/contracts/asset-proxy/test/artifacts.ts b/contracts/asset-proxy/test/artifacts.ts index 54e7e8757e..54d46b2ee1 100644 --- a/contracts/asset-proxy/test/artifacts.ts +++ b/contracts/asset-proxy/test/artifacts.ts @@ -16,8 +16,10 @@ import * as IAssetProxyDispatcher from '../test/generated-artifacts/IAssetProxyD import * as IAuthorizable from '../test/generated-artifacts/IAuthorizable.json'; import * as IERC20Bridge from '../test/generated-artifacts/IERC20Bridge.json'; import * as IEth2Dai from '../test/generated-artifacts/IEth2Dai.json'; +import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkProxy.json'; import * as IUniswapExchange from '../test/generated-artifacts/IUniswapExchange.json'; import * as IUniswapExchangeFactory from '../test/generated-artifacts/IUniswapExchangeFactory.json'; +import * as KyberBridge from '../test/generated-artifacts/KyberBridge.json'; import * as MixinAssetProxyDispatcher from '../test/generated-artifacts/MixinAssetProxyDispatcher.json'; import * as MixinAuthorizable from '../test/generated-artifacts/MixinAuthorizable.json'; import * as MultiAssetProxy from '../test/generated-artifacts/MultiAssetProxy.json'; @@ -25,6 +27,7 @@ import * as Ownable from '../test/generated-artifacts/Ownable.json'; import * as StaticCallProxy from '../test/generated-artifacts/StaticCallProxy.json'; import * as TestERC20Bridge from '../test/generated-artifacts/TestERC20Bridge.json'; import * as TestEth2DaiBridge from '../test/generated-artifacts/TestEth2DaiBridge.json'; +import * as TestKyberBridge from '../test/generated-artifacts/TestKyberBridge.json'; import * as TestStaticCallTarget from '../test/generated-artifacts/TestStaticCallTarget.json'; import * as TestUniswapBridge from '../test/generated-artifacts/TestUniswapBridge.json'; import * as UniswapBridge from '../test/generated-artifacts/UniswapBridge.json'; @@ -39,6 +42,7 @@ export const artifacts = { MultiAssetProxy: MultiAssetProxy as ContractArtifact, StaticCallProxy: StaticCallProxy as ContractArtifact, Eth2DaiBridge: Eth2DaiBridge as ContractArtifact, + KyberBridge: KyberBridge as ContractArtifact, UniswapBridge: UniswapBridge as ContractArtifact, IAssetData: IAssetData as ContractArtifact, IAssetProxy: IAssetProxy as ContractArtifact, @@ -46,10 +50,12 @@ export const artifacts = { IAuthorizable: IAuthorizable as ContractArtifact, IERC20Bridge: IERC20Bridge as ContractArtifact, IEth2Dai: IEth2Dai as ContractArtifact, + IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact, IUniswapExchange: IUniswapExchange as ContractArtifact, IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, TestERC20Bridge: TestERC20Bridge as ContractArtifact, TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact, + TestKyberBridge: TestKyberBridge as ContractArtifact, TestStaticCallTarget: TestStaticCallTarget as ContractArtifact, TestUniswapBridge: TestUniswapBridge as ContractArtifact, }; diff --git a/contracts/asset-proxy/test/kyber_bridge.ts b/contracts/asset-proxy/test/kyber_bridge.ts new file mode 100644 index 0000000000..b61c154958 --- /dev/null +++ b/contracts/asset-proxy/test/kyber_bridge.ts @@ -0,0 +1,272 @@ +import { + blockchainTests, + constants, + expect, + getRandomInteger, + hexLeftPad, + hexRandom, + randomAddress, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { AssetProxyId } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { DecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { artifacts } from './artifacts'; + +import { TestKyberBridgeContract, TestKyberBridgeEvents } from './wrappers'; + +blockchainTests.resets('KyberBridge unit tests', env => { + const KYBER_ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; + let testContract: TestKyberBridgeContract; + + before(async () => { + testContract = await TestKyberBridgeContract.deployFrom0xArtifactAsync( + artifacts.TestKyberBridge, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + describe('isValidSignature()', () => { + it('returns success bytes', async () => { + const LEGACY_WALLET_MAGIC_VALUE = '0xb0671381'; + const result = await testContract.isValidSignature(hexRandom(), hexRandom(_.random(0, 32))).callAsync(); + expect(result).to.eq(LEGACY_WALLET_MAGIC_VALUE); + }); + }); + + describe('bridgeTransferFrom()', () => { + let fromTokenAddress: string; + let toTokenAddress: string; + let wethAddress: string; + + before(async () => { + wethAddress = await testContract.weth().callAsync(); + fromTokenAddress = await testContract.createToken().callAsync(); + await testContract.createToken().awaitTransactionSuccessAsync(); + toTokenAddress = await testContract.createToken().callAsync(); + await testContract.createToken().awaitTransactionSuccessAsync(); + }); + + const STATIC_KYBER_TRADE_ARGS = { + maxBuyTokenAmount: constants.MAX_UINT256, + walletId: constants.NULL_ADDRESS, + }; + + interface TransferFromOpts { + toTokenAddress: string; + fromTokenAddress: string; + toAddress: string; + // Amount to pass into `bridgeTransferFrom()` + amount: BigNumber; + // Amount to convert in `trade()`. + fillAmount: BigNumber; + // Token balance of the bridge. + fromTokenBalance: BigNumber; + } + + interface TransferFromResult { + opts: TransferFromOpts; + result: string; + logs: DecodedLogs; + } + + function createTransferFromOpts(opts?: Partial): TransferFromOpts { + return { + fromTokenAddress, + toTokenAddress, + toAddress: randomAddress(), + amount: getRandomInteger(1, 10e18), + fillAmount: getRandomInteger(1, 10e18), + fromTokenBalance: getRandomInteger(1, 10e18), + ...opts, + }; + } + + async function withdrawToAsync(opts?: Partial): Promise { + const _opts = createTransferFromOpts(opts); + // Fund the contract with input tokens. + await testContract + .grantTokensTo(_opts.fromTokenAddress, testContract.address, _opts.fromTokenBalance) + .awaitTransactionSuccessAsync({ value: _opts.fromTokenBalance }); + // Fund the contract with output tokens. + await testContract.setNextFillAmount(_opts.fillAmount).awaitTransactionSuccessAsync({ + value: _opts.toTokenAddress === wethAddress ? _opts.fillAmount : constants.ZERO_AMOUNT, + }); + // Call bridgeTransferFrom(). + const bridgeTransferFromFn = testContract.bridgeTransferFrom( + // Output token + _opts.toTokenAddress, + // Random maker address. + randomAddress(), + // Recipient address. + _opts.toAddress, + // Transfer amount. + _opts.amount, + // ABI-encode the input token address as the bridge data. + hexLeftPad(_opts.fromTokenAddress), + ); + const result = await bridgeTransferFromFn.callAsync(); + const { logs } = await bridgeTransferFromFn.awaitTransactionSuccessAsync(); + return { + opts: _opts, + result, + logs: (logs as any) as DecodedLogs, + }; + } + + function getMinimumConversionRate(opts: TransferFromOpts): BigNumber { + return opts.amount + .times(constants.ONE_ETHER) + .div(opts.fromTokenBalance) + .integerValue(BigNumber.ROUND_DOWN); + } + + it('returns magic bytes on success', async () => { + const BRIDGE_SUCCESS_RETURN_DATA = AssetProxyId.ERC20Bridge; + const { result } = await withdrawToAsync(); + expect(result).to.eq(BRIDGE_SUCCESS_RETURN_DATA); + }); + + it('can trade token -> token', async () => { + const { opts, logs } = await withdrawToAsync(); + verifyEventsFromLogs( + logs, + [ + { + sellTokenAddress: opts.fromTokenAddress, + buyTokenAddress: opts.toTokenAddress, + sellAmount: opts.fromTokenBalance, + recipientAddress: opts.toAddress, + minConversionRate: getMinimumConversionRate(opts), + msgValue: constants.ZERO_AMOUNT, + ...STATIC_KYBER_TRADE_ARGS, + }, + ], + TestKyberBridgeEvents.KyberBridgeTrade, + ); + }); + + it('can trade token -> ETH', async () => { + const { opts, logs } = await withdrawToAsync({ + toTokenAddress: wethAddress, + }); + verifyEventsFromLogs( + logs, + [ + { + sellTokenAddress: opts.fromTokenAddress, + buyTokenAddress: KYBER_ETH_ADDRESS, + sellAmount: opts.fromTokenBalance, + recipientAddress: testContract.address, + minConversionRate: getMinimumConversionRate(opts), + msgValue: constants.ZERO_AMOUNT, + ...STATIC_KYBER_TRADE_ARGS, + }, + ], + TestKyberBridgeEvents.KyberBridgeTrade, + ); + }); + + it('can trade ETH -> token', async () => { + const { opts, logs } = await withdrawToAsync({ + fromTokenAddress: wethAddress, + }); + verifyEventsFromLogs( + logs, + [ + { + sellTokenAddress: KYBER_ETH_ADDRESS, + buyTokenAddress: opts.toTokenAddress, + sellAmount: opts.fromTokenBalance, + recipientAddress: opts.toAddress, + minConversionRate: getMinimumConversionRate(opts), + msgValue: opts.fromTokenBalance, + ...STATIC_KYBER_TRADE_ARGS, + }, + ], + TestKyberBridgeEvents.KyberBridgeTrade, + ); + }); + + it('does nothing if bridge has no token balance', async () => { + const { logs } = await withdrawToAsync({ + fromTokenBalance: constants.ZERO_AMOUNT, + }); + expect(logs).to.be.length(0); + }); + + it('only transfers the token if trading the same token', async () => { + const { opts, logs } = await withdrawToAsync({ + toTokenAddress: fromTokenAddress, + }); + verifyEventsFromLogs( + logs, + [ + { + tokenAddress: fromTokenAddress, + ownerAddress: testContract.address, + recipientAddress: opts.toAddress, + amount: opts.fromTokenBalance, + }, + ], + TestKyberBridgeEvents.KyberBridgeTokenTransfer, + ); + }); + + it('grants Kyber an allowance when selling non-WETH', async () => { + const { opts, logs } = await withdrawToAsync(); + verifyEventsFromLogs( + logs, + [ + { + tokenAddress: opts.fromTokenAddress, + ownerAddress: testContract.address, + spenderAddress: testContract.address, + allowance: constants.MAX_UINT256, + }, + ], + TestKyberBridgeEvents.KyberBridgeTokenApprove, + ); + }); + + it('does not grant Kyber an allowance when selling WETH', async () => { + const { logs } = await withdrawToAsync({ + fromTokenAddress: wethAddress, + }); + verifyEventsFromLogs(logs, [], TestKyberBridgeEvents.KyberBridgeTokenApprove); + }); + + it('withdraws WETH and passes it to Kyber when selling WETH', async () => { + const { opts, logs } = await withdrawToAsync({ + fromTokenAddress: wethAddress, + }); + expect(logs[0].event).to.eq(TestKyberBridgeEvents.KyberBridgeWethWithdraw); + expect(logs[0].args).to.deep.eq({ + ownerAddress: testContract.address, + amount: opts.fromTokenBalance, + }); + expect(logs[1].event).to.eq(TestKyberBridgeEvents.KyberBridgeTrade); + expect(logs[1].args.msgValue).to.bignumber.eq(opts.fromTokenBalance); + }); + + it('wraps WETH and transfers it to the recipient when buyng WETH', async () => { + const { opts, logs } = await withdrawToAsync({ + toTokenAddress: wethAddress, + }); + expect(logs[0].event).to.eq(TestKyberBridgeEvents.KyberBridgeTokenApprove); + expect(logs[0].args.tokenAddress).to.eq(opts.fromTokenAddress); + expect(logs[1].event).to.eq(TestKyberBridgeEvents.KyberBridgeTrade); + expect(logs[1].args.recipientAddress).to.eq(testContract.address); + expect(logs[2].event).to.eq(TestKyberBridgeEvents.KyberBridgeWethDeposit); + expect(logs[2].args).to.deep.eq({ + msgValue: opts.fillAmount, + ownerAddress: testContract.address, + amount: opts.fillAmount, + }); + }); + }); +}); diff --git a/contracts/asset-proxy/test/wrappers.ts b/contracts/asset-proxy/test/wrappers.ts index af9dcfc25d..3ae5cf51d8 100644 --- a/contracts/asset-proxy/test/wrappers.ts +++ b/contracts/asset-proxy/test/wrappers.ts @@ -14,8 +14,10 @@ export * from '../test/generated-wrappers/i_asset_proxy_dispatcher'; export * from '../test/generated-wrappers/i_authorizable'; export * from '../test/generated-wrappers/i_erc20_bridge'; export * from '../test/generated-wrappers/i_eth2_dai'; +export * from '../test/generated-wrappers/i_kyber_network_proxy'; export * from '../test/generated-wrappers/i_uniswap_exchange'; export * from '../test/generated-wrappers/i_uniswap_exchange_factory'; +export * from '../test/generated-wrappers/kyber_bridge'; export * from '../test/generated-wrappers/mixin_asset_proxy_dispatcher'; export * from '../test/generated-wrappers/mixin_authorizable'; export * from '../test/generated-wrappers/multi_asset_proxy'; @@ -23,6 +25,7 @@ export * from '../test/generated-wrappers/ownable'; export * from '../test/generated-wrappers/static_call_proxy'; export * from '../test/generated-wrappers/test_erc20_bridge'; export * from '../test/generated-wrappers/test_eth2_dai_bridge'; +export * from '../test/generated-wrappers/test_kyber_bridge'; export * from '../test/generated-wrappers/test_static_call_target'; export * from '../test/generated-wrappers/test_uniswap_bridge'; export * from '../test/generated-wrappers/uniswap_bridge'; diff --git a/contracts/asset-proxy/tsconfig.json b/contracts/asset-proxy/tsconfig.json index f96ede121d..d75379ddcf 100644 --- a/contracts/asset-proxy/tsconfig.json +++ b/contracts/asset-proxy/tsconfig.json @@ -10,6 +10,7 @@ "generated-artifacts/Eth2DaiBridge.json", "generated-artifacts/IAssetData.json", "generated-artifacts/IAssetProxy.json", + "generated-artifacts/KyberBridge.json", "generated-artifacts/MultiAssetProxy.json", "generated-artifacts/StaticCallProxy.json", "generated-artifacts/TestStaticCallTarget.json", @@ -25,8 +26,10 @@ "test/generated-artifacts/IAuthorizable.json", "test/generated-artifacts/IERC20Bridge.json", "test/generated-artifacts/IEth2Dai.json", + "test/generated-artifacts/IKyberNetworkProxy.json", "test/generated-artifacts/IUniswapExchange.json", "test/generated-artifacts/IUniswapExchangeFactory.json", + "test/generated-artifacts/KyberBridge.json", "test/generated-artifacts/MixinAssetProxyDispatcher.json", "test/generated-artifacts/MixinAuthorizable.json", "test/generated-artifacts/MultiAssetProxy.json", @@ -34,6 +37,7 @@ "test/generated-artifacts/StaticCallProxy.json", "test/generated-artifacts/TestERC20Bridge.json", "test/generated-artifacts/TestEth2DaiBridge.json", + "test/generated-artifacts/TestKyberBridge.json", "test/generated-artifacts/TestStaticCallTarget.json", "test/generated-artifacts/TestUniswapBridge.json", "test/generated-artifacts/UniswapBridge.json"