@0x/contracts-asset-proxy
: Add KyberBridge
.
This commit is contained in:
parent
1166b43946
commit
5da1fc8445
@ -1,4 +1,13 @@
|
||||
[
|
||||
{
|
||||
"version": "2.3.0-beta.4",
|
||||
"changes": [
|
||||
{
|
||||
"note": "Implement `KyberBridge`.",
|
||||
"pr": 2352
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.3.0-beta.3",
|
||||
"changes": [
|
||||
|
@ -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
|
||||
|
160
contracts/asset-proxy/contracts/src/bridges/KyberBridge.sol
Normal file
160
contracts/asset-proxy/contracts/src/bridges/KyberBridge.sol
Normal file
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
325
contracts/asset-proxy/contracts/test/TestKyberBridge.sol
Normal file
325
contracts/asset-proxy/contracts/test/TestKyberBridge.sol
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
};
|
||||
|
272
contracts/asset-proxy/test/kyber_bridge.ts
Normal file
272
contracts/asset-proxy/test/kyber_bridge.ts
Normal file
@ -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>): TransferFromOpts {
|
||||
return {
|
||||
fromTokenAddress,
|
||||
toTokenAddress,
|
||||
toAddress: randomAddress(),
|
||||
amount: getRandomInteger(1, 10e18),
|
||||
fillAmount: getRandomInteger(1, 10e18),
|
||||
fromTokenBalance: getRandomInteger(1, 10e18),
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
async function withdrawToAsync(opts?: Partial<TransferFromOpts>): Promise<TransferFromResult> {
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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';
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user