* `@0x/contracts-asset-proxy`: Fix `UniswapBridge` token -> token transfer logic. `@0x/contract-addresses`: Update `UniswapBridge` mainnet address. * `@0x/asset-proxy`: Fix `KyberBridge` incorrect `minConversionRate` calculation. * `@0x/contract-addresses`: Update `KyberBridge` mainnet address.
		
			
				
	
	
		
			284 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			284 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import {
 | |
|     blockchainTests,
 | |
|     constants,
 | |
|     expect,
 | |
|     getRandomInteger,
 | |
|     getRandomPortion,
 | |
|     randomAddress,
 | |
|     verifyEventsFromLogs,
 | |
| } from '@0x/contracts-test-utils';
 | |
| import { AssetProxyId } from '@0x/types';
 | |
| import { BigNumber, hexUtils } 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';
 | |
|     const FROM_TOKEN_DECIMALS = 6;
 | |
|     const TO_TOKEN_DECIMALS = 18;
 | |
|     const FROM_TOKEN_BASE = new BigNumber(10).pow(FROM_TOKEN_DECIMALS);
 | |
|     const TO_TOKEN_BASE = new BigNumber(10).pow(TO_TOKEN_DECIMALS);
 | |
|     const WETH_BASE = new BigNumber(10).pow(18);
 | |
|     const KYBER_RATE_BASE = WETH_BASE;
 | |
|     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(hexUtils.random(), hexUtils.random(_.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(FROM_TOKEN_DECIMALS).callAsync();
 | |
|             await testContract.createToken(FROM_TOKEN_DECIMALS).awaitTransactionSuccessAsync();
 | |
|             toTokenAddress = await testContract.createToken(TO_TOKEN_DECIMALS).callAsync();
 | |
|             await testContract.createToken(TO_TOKEN_DECIMALS).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 {
 | |
|             const amount = getRandomInteger(1, TO_TOKEN_BASE.times(100));
 | |
|             return {
 | |
|                 fromTokenAddress,
 | |
|                 toTokenAddress,
 | |
|                 amount,
 | |
|                 toAddress: randomAddress(),
 | |
|                 fillAmount: getRandomPortion(amount),
 | |
|                 fromTokenBalance: getRandomInteger(1, FROM_TOKEN_BASE.times(100)),
 | |
|                 ...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.
 | |
|                 hexUtils.leftPad(_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 {
 | |
|             const fromBase = opts.fromTokenAddress === wethAddress ? WETH_BASE : FROM_TOKEN_BASE;
 | |
|             const toBase = opts.toTokenAddress === wethAddress ? WETH_BASE : TO_TOKEN_BASE;
 | |
|             return opts.amount
 | |
|                 .div(toBase)
 | |
|                 .div(opts.fromTokenBalance.div(fromBase))
 | |
|                 .times(KYBER_RATE_BASE)
 | |
|                 .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,
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| });
 |