diff --git a/contracts/protocol/integration/index-exchange/KyberIndexExchangeAdapter.sol b/contracts/protocol/integration/index-exchange/KyberIndexExchangeAdapter.sol new file mode 100644 index 000000000..300f133ab --- /dev/null +++ b/contracts/protocol/integration/index-exchange/KyberIndexExchangeAdapter.sol @@ -0,0 +1,143 @@ +/* + Copyright 2020 Set Labs Inc. + + 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. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +/** + * @title KyberIndexExchangeAdapter + * @author Set Protocol + * + * A Kyber exchange adapter that returns calldata for trading with GeneralIndexModule, + * allows trading a fixed input amount or for a fixed output amount. + */ + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { IIndexExchangeAdapter } from "../../../interfaces/IIndexExchangeAdapter.sol"; +import { IKyberNetworkProxy } from "../../../interfaces/external/IKyberNetworkProxy.sol"; +import { PreciseUnitMath } from "../../../lib/PreciseUnitMath.sol"; + +contract KyberIndexExchangeAdapter is IIndexExchangeAdapter { + using SafeMath for uint256; + using PreciseUnitMath for uint256; + + /* ============ Structs ============ */ + + /** + * Struct containing information for trade function + */ + struct KyberTradeInfo { + uint256 sourceTokenDecimals; // Decimals of the token to send + uint256 destinationTokenDecimals; // Decimals of the token to receive + uint256 conversionRate; // Derived conversion rate from min receive quantity + } + + /* ============ State Variables ============ */ + + // Address of Kyber Network Proxy + address public kyberNetworkProxyAddress; + + /* ============ Constructor ============ */ + + /** + * Set state variables + * + * @param _kyberNetworkProxyAddress Address of Kyber Network Proxy contract + */ + constructor( + address _kyberNetworkProxyAddress + ) + public + { + kyberNetworkProxyAddress = _kyberNetworkProxyAddress; + } + + /* ============ External Getter Functions ============ */ + + /** + * Calculate Kyber trade encoded calldata. To be invoked on the SetToken. + * + * @param _sourceToken Address of source token to be sold + * @param _destinationToken Address of destination token to buy + * @param _destinationAddress Address that output assets should be transferred to + * @param _sourceQuantity Fixed/Max amount of source token to sell + * @param _destinationQuantity Min/Fixed amount of destination tokens to receive + * + * @return address Target contract address + * @return uint256 Call value + * @return bytes Trade calldata + */ + function getTradeCalldata( + address _sourceToken, + address _destinationToken, + address _destinationAddress, + bool /*_isSendTokenFixed*/, + uint256 _sourceQuantity, + uint256 _destinationQuantity, + bytes memory /*_data*/ + ) + override + external + view + returns (address, uint256, bytes memory) + { + KyberTradeInfo memory kyberTradeInfo; + + kyberTradeInfo.sourceTokenDecimals = ERC20(_sourceToken).decimals(); + kyberTradeInfo.destinationTokenDecimals = ERC20(_destinationToken).decimals(); + + // Get conversion rate from minimum receive token quantity. + // dstQty * (10 ** 18) * (10 ** dstDecimals) / (10 ** srcDecimals) / srcQty + kyberTradeInfo.conversionRate = _destinationQuantity + .mul(PreciseUnitMath.preciseUnit()) + .mul(10 ** kyberTradeInfo.sourceTokenDecimals) + .div(10 ** kyberTradeInfo.destinationTokenDecimals) + .div(_sourceQuantity); + + // Encode method data for SetToken to invoke + bytes memory methodData = abi.encodeWithSignature( + "trade(address,uint256,address,address,uint256,uint256,address)", + _sourceToken, + _sourceQuantity, + _destinationToken, + _destinationAddress, + PreciseUnitMath.maxUint256(), // Sell entire amount of sourceToken + kyberTradeInfo.conversionRate, // Trade with implied conversion rate + address(0) // No referrer address + ); + + return (kyberNetworkProxyAddress, 0, methodData); + } + + /** + * Returns the address to approve source tokens to for trading. This is the Kyber Network + * Proxy address + * + * @return address Address of the contract to approve tokens to + */ + function getSpender() + override + external + view + returns (address) + { + return kyberNetworkProxyAddress; + } +} \ No newline at end of file diff --git a/test/protocol/integration/index-exchange/kyberIndexExchangeAdapter.spec.ts b/test/protocol/integration/index-exchange/kyberIndexExchangeAdapter.spec.ts new file mode 100644 index 000000000..f926fc9ef --- /dev/null +++ b/test/protocol/integration/index-exchange/kyberIndexExchangeAdapter.spec.ts @@ -0,0 +1,150 @@ +import "module-alias/register"; + +import { ethers } from "hardhat"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address, Bytes } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { + ADDRESS_ZERO, + EMPTY_BYTES, + ZERO, +} from "@utils/constants"; +import { KyberIndexExchangeAdapter, KyberNetworkProxyMock } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + ether, +} from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getSystemFixture, + getWaffleExpect, +} from "@utils/test/index"; + +import { SystemFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + + +describe("KyberIndexExchangeAdapter", () => { + let owner: Account; + let mockSetToken: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + + let kyberNetworkProxy: KyberNetworkProxyMock; + let wbtcRate: BigNumber; + let kyberIndexExchangeAdapter: KyberIndexExchangeAdapter; + + before(async () => { + [ + owner, + mockSetToken, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + wbtcRate = ether(33); // 1 WBTC = 33 ETH + + // Mock Kyber reserve only allows trading from/to WETH + kyberNetworkProxy = await deployer.mocks.deployKyberNetworkProxyMock(setup.weth.address); + await kyberNetworkProxy.addToken( + setup.wbtc.address, + wbtcRate, + 8 + ); + + kyberIndexExchangeAdapter = await deployer.adapters.deployKyberIndexExchangeAdapter(kyberNetworkProxy.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("constructor", async () => { + let subjectKyberNetworkProxy: Address; + + beforeEach(async () => { + subjectKyberNetworkProxy = kyberNetworkProxy.address; + }); + + async function subject(): Promise { + return await deployer.adapters.deployKyberIndexExchangeAdapter(subjectKyberNetworkProxy); + } + + it("should have the correct Kyber proxy address", async () => { + const deployedKyberIndexExchangeAdapter = await subject(); + + const actualKyberAddress = await deployedKyberIndexExchangeAdapter.kyberNetworkProxyAddress(); + expect(actualKyberAddress).to.eq(kyberNetworkProxy.address); + }); + }); + + + describe("getSpender", async () => { + async function subject(): Promise { + return await kyberIndexExchangeAdapter.getSpender(); + } + + it("should return the correct spender address", async () => { + const spender = await subject(); + + expect(spender).to.eq(kyberNetworkProxy.address); + }); + }); + + describe("getTradeCalldata", async () => { + let sourceAddress: Address; + let destinationAddress: Address; + let sourceQuantity: BigNumber; + let destinationQuantity: BigNumber; + + let subjectMockSetToken: Address; + let subjectSourceToken: Address; + let subjectDestinationToken: Address; + let subjectSourceQuantity: BigNumber; + let subjectDestinationQuantity: BigNumber; + let subjectData: Bytes; + + beforeEach(async () => { + sourceAddress = setup.wbtc.address; // WBTC Address + sourceQuantity = BigNumber.from(100000000); // Trade 1 WBTC + destinationAddress = setup.weth.address; // WETH Address + destinationQuantity = ether(33); // Receive at least 33 ETH + + subjectSourceToken = sourceAddress; + subjectDestinationToken = destinationAddress; + subjectMockSetToken = mockSetToken.address; + subjectSourceQuantity = sourceQuantity; + subjectDestinationQuantity = destinationQuantity; + subjectData = EMPTY_BYTES; + }); + + async function subject(): Promise { + return await kyberIndexExchangeAdapter.getTradeCalldata( + subjectSourceToken, + subjectDestinationToken, + subjectMockSetToken, + true, + subjectSourceQuantity, + subjectDestinationQuantity, + subjectData, + ); + } + + it("should return the correct trade calldata", async () => { + const calldata = await subject(); + const expectedCallData = kyberNetworkProxy.interface.encodeFunctionData("trade", [ + sourceAddress, + sourceQuantity, + destinationAddress, + mockSetToken.address, + ethers.constants.MaxUint256, + wbtcRate, + ADDRESS_ZERO, + ]); + expect(JSON.stringify(calldata)).to.eq(JSON.stringify([kyberNetworkProxy.address, ZERO, expectedCallData])); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index d509201f3..05a854b63 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -40,6 +40,7 @@ export { InvokeMock } from "../../typechain/InvokeMock"; export { ISetValuer } from "../../typechain/ISetValuer"; export { IssuanceModule } from "../../typechain/IssuanceModule"; export { KyberExchangeAdapter } from "../../typechain/KyberExchangeAdapter"; +export { KyberIndexExchangeAdapter } from "../../typechain/KyberIndexExchangeAdapter"; export { KyberNetworkProxyMock } from "../../typechain/KyberNetworkProxyMock"; export { LendToAaveMigrator } from "../../typechain/LendToAaveMigrator"; export { ManagerIssuanceHookMock } from "../../typechain/ManagerIssuanceHookMock"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index 0c579d3ab..3d420a916 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -7,6 +7,7 @@ import { CompoundLikeGovernanceAdapter, CurveStakingAdapter, KyberExchangeAdapter, + KyberIndexExchangeAdapter, OneInchExchangeAdapter, AaveMigrationWrapAdapter, AaveWrapAdapter, @@ -31,6 +32,7 @@ import { BalancerV1IndexExchangeAdapter__factory } from "../../typechain/factori import { CompoundLikeGovernanceAdapter__factory } from "../../typechain/factories/CompoundLikeGovernanceAdapter__factory"; import { CurveStakingAdapter__factory } from "../../typechain/factories/CurveStakingAdapter__factory"; import { KyberExchangeAdapter__factory } from "../../typechain/factories/KyberExchangeAdapter__factory"; +import { KyberIndexExchangeAdapter__factory } from "../../typechain/factories/KyberIndexExchangeAdapter__factory"; import { OneInchExchangeAdapter__factory } from "../../typechain/factories/OneInchExchangeAdapter__factory"; import { ZeroExApiAdapter__factory } from "../../typechain/factories/ZeroExApiAdapter__factory"; import { AaveMigrationWrapAdapter__factory } from "../../typechain/factories/AaveMigrationWrapAdapter__factory"; @@ -57,6 +59,10 @@ export default class DeployAdapters { return await new KyberExchangeAdapter__factory(this._deployerSigner).deploy(kyberNetworkProxy); } + public async deployKyberIndexExchangeAdapter(kyberNetworkProxy: Address): Promise { + return await new KyberIndexExchangeAdapter__factory(this._deployerSigner).deploy(kyberNetworkProxy); + } + public async deployOneInchExchangeAdapter( approveAddress: Address, exchangeAddress: Address,