diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 2af4fb80..00000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -contracts/ diff --git a/.prettierrc.json b/.prettierrc.json index 54dc7996..be4d2d3b 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,5 +3,18 @@ "parser": "typescript", "tabWidth": 2, "trailingComma": "all", - "arrowParens": "always" + "arrowParens": "always", + "overrides": [ + { + "files": "*.sol", + "options": { + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "singleQuote": false, + "bracketSpacing": true, + "explicitTypes": "always" + } + } + ] } diff --git a/README.md b/README.md index 86b8250d..6830acb2 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,18 @@ yarn ### Run Contract Tests -`yarn test` to run compiled contracts +`yarn test` to run compiled contracts (executes on network localhost, you need to have `yarn chain` running) -OR `yarn test:clean` if contracts have been typings need to be updated +OR `yarn test:clean` if contract typings need to be updated + +### Run Integration Tests + +`yarn chain:fork:ethereum` in one terminal to run chain fork. replace ethereum with polygon or optimism if needed, see package.json + +`yarn test:integration:ethereum` in another terminal, replace chain again as needed + +To run an individual test on e.g. a later block, use (replace path): +`LATESTBLOCK=15508111 INTEGRATIONTEST=true VERBOSE=true npx hardhat test ./test/integration/ethereum/flashMintWrappedIntegration.spec.ts --network localhost` ### Run Coverage Report for Tests diff --git a/contracts/exchangeIssuance/DEXAdapter.sol b/contracts/exchangeIssuance/DEXAdapter.sol index c55b9598..6afd4375 100644 --- a/contracts/exchangeIssuance/DEXAdapter.sol +++ b/contracts/exchangeIssuance/DEXAdapter.sol @@ -682,7 +682,13 @@ library DEXAdapter { returns (uint256) { _safeApprove(IERC20(_path[0]), address(_router), _amountIn); - return _router.swapExactTokensForTokens(_amountIn, _minAmountOut, _path, address(this), block.timestamp)[1]; + // NOTE: The following was changed from always returning result at position [1] to returning the last element of the result array + // With this change, the actual output is correctly returned also for multi-hop swaps + // See https://github.com/IndexCoop/index-coop-smart-contracts/pull/116 + uint256[] memory result = _router.swapExactTokensForTokens(_amountIn, _minAmountOut, _path, address(this), block.timestamp); + // result = uint[] memory The input token amount and all subsequent output token amounts. + // we are usually only interested in the actual amount of the output token (so result element at the last place) + return result[result.length-1]; } /** diff --git a/contracts/exchangeIssuance/FlashMintWrapped.sol b/contracts/exchangeIssuance/FlashMintWrapped.sol index 41dacace..77b6e2cb 100644 --- a/contracts/exchangeIssuance/FlashMintWrapped.sol +++ b/contracts/exchangeIssuance/FlashMintWrapped.sol @@ -17,220 +17,1011 @@ pragma solidity 0.6.10; pragma experimental ABIEncoderV2; -import { Address } from "@openzeppelin/contracts/utils/Address.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Math } from "@openzeppelin/contracts/math/Math.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; -import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { Address} from "@openzeppelin/contracts/utils/Address.sol"; +import { IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; +import { ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import { IController } from "../interfaces/IController.sol"; -import { IDebtIssuanceModule } from "../interfaces/IDebtIssuanceModule.sol"; -import { ISetToken } from "../interfaces/ISetToken.sol"; -import { IWETH } from "../interfaces/IWETH.sol"; -import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; +import { IIntegrationRegistry } from "../interfaces/IIntegrationRegistry.sol"; +import { IWrapV2Adapter} from "../interfaces/IWrapV2Adapter.sol"; +import { IDebtIssuanceModule} from "../interfaces/IDebtIssuanceModule.sol"; +import { ISetToken} from "../interfaces/ISetToken.sol"; +import { IWETH} from "../interfaces/IWETH.sol"; +import { IWrapModuleV2} from "../interfaces/IWrapModuleV2.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { DEXAdapter } from "./DEXAdapter.sol"; + +import "hardhat/console.sol"; /** * @title FlashMintWrapped * - * Flash issues SetTokens whose components are purely made up of wrapped tokens. - * In particular, for issuance, the contract needs to: - * 1. For all components in a SetToken, swap input token into matching unwrapped component - * 2. For each unwrapped component, execute the wrapping function - * 3. Call on issuanceModule to issue tokens + * Flash issues SetTokens whose components contain wrapped tokens. + * + * Compatible with: + * IssuanceModules: DebtIssuanceModule, DebtIssuanceModuleV2 + * WrapAdapters: IWrapV2Adapter + * + * Supports flash minting for sets that contain both unwrapped and wrapped components. + * Does not support debt positions on Set token. + * Wrapping / Unwrapping is skipped for a component if ComponentSwapData[component_index].underlyingERC20 address == set component address + * + * If the set contains both the wrapped and unwrapped version of a token (e.g. DAI and cDAI) + * -> two separate component data points have to be supplied in _swapData and _wrapData + * e.g. for issue + * Set components at index 0 = DAI; then -> ComponentSwapData[0].underlyingERC20 = DAI; (no wrapping will happen) + * Set components at index 1 = cDAI; then -> ComponentSwapData[1].underlyingERC20 = DAI; (wrapping will happen) */ -contract FlashMintWrapped is ReentrancyGuard { - - using Address for address payable; - using SafeMath for uint256; - using PreciseUnitMath for uint256; - using SafeERC20 for IERC20; - using SafeERC20 for ISetToken; - - struct WrappedCalldata { - address target; - uint256 quantity; - bytes callData; - } +contract FlashMintWrapped is Ownable, ReentrancyGuard { + using DEXAdapter for DEXAdapter.Addresses; + using Address for address payable; + using Address for address; + using SafeMath for uint256; + using SafeERC20 for IERC20; + using SafeERC20 for ISetToken; - /* ============ Enums ============ */ + /* ============ Structs ============ */ - /* ============ Constants ============= */ + struct ComponentSwapData { + // unwrapped token version, e.g. DAI + address underlyingERC20; + // swap data for DEX operation: fees, path, etc. see DEXAdapter.SwapData + DEXAdapter.SwapData dexData; + // ONLY relevant for issue, not used for redeem: + // amount that has to be bought of the unwrapped token version (to cover required wrapped component amounts for issuance) + // this amount has to be computed beforehand through the exchange rate of wrapped Component <> unwrappedComponent + // i.e. getRequiredComponentIssuanceUnits() on the IssuanceModule and then convert units through exchange rate to unwrapped component units + // e.g. 300 cDAI needed for issuance of 1 Set token. exchange rate 1cDAI = 0.05 DAI. -> buyUnderlyingAmount = 0.05 DAI * 300 = 15 DAI + uint256 buyUnderlyingAmount; + } - uint256 constant private MAX_UINT96 = 2**96 - 1; - address constant public ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + struct ComponentWrapData { + string integrationName; // wrap adapter integration name as listed in the IntegrationRegistry for the wrapModule + bytes wrapData; // optional wrapData passed to the wrapAdapter + } - /* ============ State Variables ============ */ + /* ============ Constants ============= */ - IController public immutable controller; + uint256 private constant MAX_UINT256 = type(uint256).max; - /* ============ Events ============ */ + /* ============ Immutables ============ */ - event ExchangeIssue( - address indexed _recipient, // The recipient address of the issued SetTokens - ISetToken indexed _setToken, // The issued SetToken - IERC20 indexed _inputToken, // The address of the input asset(ERC20/ETH) used to issue the SetTokens - uint256 _amountInputToken, // The amount of input tokens used for issuance - uint256 _amountSetIssued // The amount of SetTokens received by the recipient - ); + IController public immutable setController; + IDebtIssuanceModule public immutable issuanceModule; // interface is compatible with DebtIssuanceModuleV2 + address public immutable wrapModule; // used to obtain a valid wrap adapter - event ExchangeRedeem( - address indexed _recipient, // The recipient address which redeemed the SetTokens - ISetToken indexed _setToken, // The redeemed SetToken - IERC20 indexed _outputToken, // The address of output asset(ERC20/ETH) received by the recipient - uint256 _amountSetRedeemed, // The amount of SetTokens redeemed for output tokens - uint256 _amountOutputToken // The amount of output tokens received by the recipient - ); + /* ============ State Variables ============ */ - /* ============ Modifiers ============ */ + DEXAdapter.Addresses public dexAdapter; - modifier isSetToken(ISetToken _setToken) { - require(controller.isSet(address(_setToken)), "ExchangeIssuance: INVALID SET"); - _; - } + /* ============ Events ============ */ - modifier isValidModule(address _issuanceModule) { - require(controller.isModule(_issuanceModule), "ExchangeIssuance: INVALID ISSUANCE MODULE"); - _; - } + event FlashMint( + address indexed _recipient, // The recipient address of the minted Set token + ISetToken indexed _setToken, // The minted Set token + IERC20 indexed _inputToken, // The address of the input asset(ERC20/ETH) used to mint the Set tokens + uint256 _amountInputToken, // The amount of input tokens used for minting + uint256 _amountSetIssued // The amount of Set tokens received by the recipient + ); - /* ========== Constructor ========== */ + event FlashRedeem( + address indexed _recipient, // The recipient address which redeemed the Set token + ISetToken indexed _setToken, // The redeemed Set token + IERC20 indexed _outputToken, // The address of output asset(ERC20/ETH) received by the recipient + uint256 _amountSetRedeemed, // The amount of Set token redeemed for output tokens + uint256 _amountOutputToken // The amount of output tokens received by the recipient + ); - constructor(address _controller) { - controller = IController(_controller); - } + /* ============ Modifiers ============ */ - /* ============ External Functions ============ */ - - /** - * Runs all the necessary approval functions required for a given ERC20 token. - * This function can be called when a new token is added to a SetToken during a - * rebalance. - * - * @param _token Address of the token which needs approval - * @param _spender Address of the spender which will be approved to spend token. (Must be a whitlisted issuance module) - */ - function approveToken(IERC20 _token, address _spender) external isValidModule(_spender) { - } + /** + * checks that _setToken is a valid listed set token on the setController + * + * @param _setToken set token to check + */ + modifier isSetToken(ISetToken _setToken) { + require(setController.isSet(address(_setToken)), "FlashMint: INVALID_SET"); + _; + } - /** - * Runs all the necessary approval functions required for a list of ERC20 tokens. - * - * @param _tokens Addresses of the tokens which need approval - * @param _spender Address of the spender which will be approved to spend token. (Must be a whitlisted issuance module) - */ - function approveTokens(IERC20[] calldata _tokens, address _spender) external { + /** + * checks that _inputToken is the first adress in _path and _outputToken is the last address in _path + * + * @param _path Array of addresses for a DEX swap path + * @param _inputToken input token of DEX swap + * @param _outputToken output token of DEX swap + */ + modifier isValidPath( + address[] memory _path, + address _inputToken, + address _outputToken + ) { + if (_inputToken != _outputToken) { + require( + _path[0] == _inputToken || + (_inputToken == dexAdapter.weth && _path[0] == DEXAdapter.ETH_ADDRESS), + "FlashMint: INPUT_TOKEN_NOT_IN_PATH" + ); + require( + _path[_path.length - 1] == _outputToken || + (_outputToken == dexAdapter.weth && _path[_path.length - 1] == DEXAdapter.ETH_ADDRESS), + "FlashMint: OUTPUT_TOKEN_NOT_IN_PATH" + ); } + _; + } + + /* ========== Constructor ========== */ + + /** + * Constructor initializes various addresses + * + * @param _dexAddresses Address of quickRouter, sushiRouter, uniV3Router, uniV3Router, curveAddressProvider, curveCalculator and weth. + * @param _setController Set token controller used to verify a given token is a set + * @param _issuanceModule IDebtIssuanceModule used to issue and redeem tokens + * @param _wrapModule WrapModuleV2 used to obtain a valid wrap adapter + */ + constructor( + DEXAdapter.Addresses memory _dexAddresses, + IController _setController, + IDebtIssuanceModule _issuanceModule, + address _wrapModule + ) public { + dexAdapter = _dexAddresses; + setController = _setController; + issuanceModule = _issuanceModule; + wrapModule = _wrapModule; + } + + /* ============ External Functions ============ */ + receive() external payable { + // required for weth.withdraw() to work properly + require(msg.sender == dexAdapter.weth, "FlashMint: DEPOSITS_NOT_ALLOWED"); + } - /** - * Runs all the necessary approval functions required before issuing - * or redeeming a SetToken. This function need to be called only once before the first time - * this smart contract is used on any particular SetToken. - * - * @param _setToken Address of the SetToken being initialized - * @param _issuanceModule Address of the issuance module which will be approved to spend component tokens. - */ - function approveSetToken(ISetToken _setToken, address _issuanceModule) external { + /** + * Withdraw slippage to selected address + * + * @param _tokens Addresses of tokens to withdraw, specifiy ETH_ADDRESS to withdraw ETH + * @param _to Address to send the tokens to + */ + function withdrawTokens(IERC20[] calldata _tokens, address payable _to) external onlyOwner payable { + for(uint256 i = 0; i < _tokens.length; i++) { + if(address(_tokens[i]) == DEXAdapter.ETH_ADDRESS){ + _to.sendValue(address(this).balance); + } + else{ + _tokens[i].safeTransfer(_to, _tokens[i].balanceOf(address(this))); + } + } + } + + /** + * Runs all the necessary approval functions required before issuing + * or redeeming a SetToken. This function need to be called only once before the first time + * this smart contract is used on any particular SetToken. + * + * @param _setToken Address of the SetToken being initialized + */ + function approveSetToken(ISetToken _setToken) external isSetToken(_setToken) { + address[] memory _components = _setToken.getComponents(); + for (uint256 i = 0; i < _components.length; ++i) { + DEXAdapter._safeApprove(IERC20(_components[i]), address(issuanceModule), MAX_UINT256); } + } - /** - * Issues an exact amount of SetTokens for given amount of input ERC20 tokens. - * The excess amount of tokens is returned in an equivalent amount of ether. - * - * @param _setToken Address of the SetToken to be issued - * @param _inputToken Address of the input token - * @param _amountSetToken Amount of SetTokens to issue - * @param _calldata The calldata needed to execute all the wrapping of tokens - * - * @return totalInputTokenSold Amount of input token spent for issuance - */ - function issueExactSetFromToken( - ISetToken _setToken, - IERC20 _inputToken, - uint256 _amountSetToken, - WrappedCalldata[] memory _calldata - ) - external - nonReentrant - returns (uint256) + /** + * Issues an exact amount of SetTokens for given amount of input ERC20 tokens. + * The excess amount of input tokens is returned. + * The sender must have approved the _maxAmountInputToken for input token to this contract. + * + * @param _setToken Address of the SetToken to be issued + * @param _inputToken Address of the ERC20 input token + * @param _amountSetToken Amount of SetTokens to issue + * @param _maxAmountInputToken Maximum amount of input tokens to be used + * @param _swapData ComponentSwapData (inputToken -> component) for each set component in the same order + * @param _wrapData ComponentWrapData (underlyingERC20 -> wrapped component) for each required set token component in the exact same order + * + * @return totalInputTokenSold Amount of input tokens spent for issuance + */ + function issueExactSetFromERC20( + ISetToken _setToken, + IERC20 _inputToken, + uint256 _amountSetToken, + uint256 _maxAmountInputToken, + ComponentSwapData[] calldata _swapData, + ComponentWrapData[] calldata _wrapData + ) external nonReentrant returns (uint256) { + return + _issueExactSet( + _setToken, + _inputToken, + _amountSetToken, + _maxAmountInputToken, + _swapData, + _wrapData, + false + ); + } + + /** + * Issues an exact amount of SetTokens for given amount of ETH. Max amount of ETH used is the transferred amount in msg.value. + * The excess amount of input ETH is returned. + * + * @param _setToken Address of the SetToken to be issued + * @param _amountSetToken Amount of SetTokens to issue + * @param _swapData ComponentSwapData (WETH -> component) for each set component in the same order + * @param _wrapData ComponentWrapData (underlyingERC20 -> wrapped component) for each required set token component in the exact same order + * + * @return totalETHSold Amount of ETH spent for issuance + */ + function issueExactSetFromETH( + ISetToken _setToken, + uint256 _amountSetToken, + ComponentSwapData[] calldata _swapData, + ComponentWrapData[] calldata _wrapData + ) external payable nonReentrant returns (uint256) { + // input token for all operations is WETH (any sent in ETH will be wrapped) + IERC20 inputToken = IERC20(dexAdapter.weth); + uint256 maxAmountInputToken = msg.value; // = deposited amount ETH -> WETH + + return + _issueExactSet( + _setToken, + inputToken, + _amountSetToken, + maxAmountInputToken, + _swapData, + _wrapData, + true + ); + } + + /** + * Redeems an exact amount of SetTokens for an ERC20 token. + * The sender must have approved the _amountSetToken of _setToken to this contract. + * + * @param _setToken Address of the SetToken to be redeemed + * @param _outputToken Address of the ERC20 output token + * @param _amountSetToken Amount of SetTokens to redeem + * @param _minOutputReceive Minimum amount of output tokens to be received + * @param _swapData ComponentSwapData (underlyingERC20 -> output token) for each _redeemComponents in the same order + * @param _unwrapData ComponentWrapData (wrapped Set component -> underlyingERC20) for each required set token component in the exact same order + * + * @return outputAmount Amount of output tokens sent to the caller + */ + function redeemExactSetForERC20( + ISetToken _setToken, + IERC20 _outputToken, + uint256 _amountSetToken, + uint256 _minOutputReceive, + ComponentSwapData[] calldata _swapData, + ComponentWrapData[] calldata _unwrapData + ) external nonReentrant returns (uint256) { + return + _redeemExactSet( + _setToken, + _outputToken, + _amountSetToken, + _minOutputReceive, + _swapData, + _unwrapData, + false + ); + } + + /** + * Redeems an exact amount of SetTokens for ETH. + * The sender must have approved the _amountSetToken of _setToken to this contract. + * + * @param _setToken Address of the SetToken to be redemeed + * @param _amountSetToken Amount of SetTokens to redeem + * @param _minOutputReceive Minimum amount of output tokens to be received + * @param _swapData ComponentSwapData (underlyingERC20 -> output token) for each _redeemComponents in the same order + * @param _unwrapData ComponentWrapData (wrapped Set component -> underlyingERC20) for each required set token component in the exact same order + * + * @return outputAmount Amount of ETH sent to the caller + */ + function redeemExactSetForETH( + ISetToken _setToken, + uint256 _amountSetToken, + uint256 _minOutputReceive, + ComponentSwapData[] calldata _swapData, + ComponentWrapData[] calldata _unwrapData + ) external nonReentrant returns (uint256) { + // output token for all operations is WETH (it will be unwrapped in the end and sent as ETH) + IERC20 outputToken = IERC20(dexAdapter.weth); + + return + _redeemExactSet( + _setToken, + outputToken, + _amountSetToken, + _minOutputReceive, + _swapData, + _unwrapData, + true + ); + } + + /** + * ESTIMATES the amount of output ERC20 tokens required to issue an exact amount of SetTokens based on component swap data. + * Simulates swapping input to all components in swap data. + * This function is not marked view, but should be static called off-chain. + * + * @param _setToken Address of the SetToken being issued + * @param _inputToken Address of the input token used to pay for issuance + * @param _setAmount Amount of SetTokens to issue + * @param _swapData ComponentSwapData (inputToken -> component) for each set component in the same order + * + * @return amountInputNeeded Amount of tokens needed to issue specified amount of SetTokens + */ + function getIssueExactSet( + ISetToken _setToken, + address _inputToken, + uint256 _setAmount, + ComponentSwapData[] calldata _swapData + ) + external + isSetToken(_setToken) + returns(uint256 amountInputNeeded) + { + require(_setAmount > 0, "FlashMint: INVALID_INPUTS"); + require(_inputToken != address(0), "FlashMint: INVALID_INPUTS"); + + for (uint256 i = 0; i < _swapData.length; ++i) { + // if the input token is the swap target token, no swapping is needed + + if (_inputToken == _swapData[i].underlyingERC20) { + amountInputNeeded = amountInputNeeded.add(_swapData[i].buyUnderlyingAmount); + continue; + } + + // add required input amount to swap to desired buyUnderlyingAmount + uint256 amountInNeeded = dexAdapter.getAmountIn(_swapData[i].dexData, _swapData[i].buyUnderlyingAmount); + amountInputNeeded = amountInputNeeded.add(amountInNeeded); + } + + } + + /** + * ESTIMATES the amount of output ERC20 tokens received when redeeming an exact amount of SetTokens based on component swap data. + * Simulates swapping all components to the output token in swap data. + * This function is not marked view, but should be static called off-chain. + * + * Note that _swapData.buyUnderlyingAmount has to be specified here with the expected underlying amount received after unwrapping + * + * @param _setToken Address of the SetToken being redeemed + * @param _outputToken Address of the output token expected to teceive (if redeeming to ETH, outputToken here is WETH) + * @param _setAmount Amount of SetTokens to redeem + * @param _swapData ComponentSwapData (component -> outputToken) for each set component in the same order + * + * @return amountOutputReceived Amount of output tokens received + */ + function getRedeemExactSet( + ISetToken _setToken, + address _outputToken, + uint256 _setAmount, + ComponentSwapData[] calldata _swapData + ) + external + isSetToken(_setToken) + returns(uint256 amountOutputReceived) + { + require(_setAmount > 0, "FlashMint: INVALID_INPUTS"); + require(_outputToken != address(0), "FlashMint: INVALID_INPUTS"); + + for (uint256 i = 0; i < _swapData.length; ++i) { + // if the output token is the swap target token, no swapping is needed + + if (_outputToken == _swapData[i].underlyingERC20) { + amountOutputReceived = amountOutputReceived.add(_swapData[i].buyUnderlyingAmount); + continue; + } + + // add received output amount from swap + uint256 swapAmountOut = dexAdapter.getAmountOut(_swapData[i].dexData, _swapData[i].buyUnderlyingAmount); + amountOutputReceived = amountOutputReceived.add(swapAmountOut); + } + + } + + /* ============ Internal Functions ============ */ + + /** + * Issues an exact amount of SetTokens for given amount of input. Excess amounts are returned + * + * @param _setToken Address of the SetToken to be issued + * @param _inputToken Address of the ERC20 input token + * @param _amountSetToken Amount of SetTokens to issue + * @param _maxAmountInputToken Maximum amount of input tokens to be used + * @param _swapData ComponentSwapData (input token -> underlyingERC20) for each _requiredComponents in the same order + * @param _wrapData ComponentWrapData (underlyingERC20 -> wrapped component) for each required set token component in the exact same order + * @param _issueFromETH boolean flag to identify if issuing from ETH or from ERC20 tokens + * + * @return totalInputSold Amount of input token spent for issuance + */ + function _issueExactSet( + ISetToken _setToken, + IERC20 _inputToken, + uint256 _amountSetToken, + uint256 _maxAmountInputToken, + ComponentSwapData[] calldata _swapData, + ComponentWrapData[] calldata _wrapData, + bool _issueFromETH + ) internal returns (uint256) { + // 1. validate input params, get required components with amounts and snapshot input token balance before + require(address(_inputToken) != address(0), "FlashMint: INVALID_INPUTS"); + uint256 inputTokenBalanceBefore = IERC20(_inputToken).balanceOf(address(this)); + + // Prevent stack too deep { - return 0; + ( + address[] memory requiredComponents, + uint256[] memory requiredAmounts + ) = _validateIssueParams( + _setToken, + _amountSetToken, + _maxAmountInputToken, + _swapData, + _wrapData + ); + + // 2. transfer input to this contract + if (_issueFromETH) { + // wrap sent in ETH to WETH for all operations + IWETH(dexAdapter.weth).deposit{value: msg.value}(); + } else { + _inputToken.safeTransferFrom(msg.sender, address(this), _maxAmountInputToken); + } + + // 3. swap input token to all components, then wrap them if needed + _swapAndWrapComponents( + _inputToken, + _maxAmountInputToken, + _swapData, + _wrapData, + requiredComponents, + requiredAmounts + ); } + // 4. issue set tokens + issuanceModule.issue(_setToken, _amountSetToken, msg.sender); - /** - * Issues an exact amount of SetTokens for given amount of ETH. - * The excess amount of tokens is returned in an equivalent amount of ether. - * - * @param _setToken Address of the SetToken to be issued - * @param _amountSetToken Amount of SetTokens to issue - * @param _componentQuotes The encoded 0x transactions to execute - * - * @return amountEthReturn Amount of ether returned to the caller - */ - function issueExactSetFromETH( - ISetToken _setToken, - uint256 _amountSetToken - ) - external - nonReentrant - payable - returns (uint256) - { - return 0; + // 5. ensure not too much of input token was spent (covers case where initial input token balance was > 0) + uint256 spentInputTokenAmount = _validateMaxAmountInputToken( + _inputToken, + inputTokenBalanceBefore, + _maxAmountInputToken + ); + + // 6. return excess inputs + _returnExcessInput(_inputToken, _maxAmountInputToken, spentInputTokenAmount, _issueFromETH); + + // 7. emit event and return amount spent + emit FlashMint( + msg.sender, + _setToken, + _issueFromETH ? IERC20(DEXAdapter.ETH_ADDRESS) : _inputToken, + spentInputTokenAmount, + _amountSetToken + ); + + return spentInputTokenAmount; + } + + /** + * Redeems an exact amount of SetTokens. + * + * @param _setToken Address of the SetToken to be issued + * @param _outputToken Address of the ERC20 output token + * @param _amountSetToken Amount of SetTokens to redeem + * @param _minOutputReceive Minimum amount of output tokens to be received + * @param _swapData ComponentSwapData (underlyingERC20 -> output token) for each _redeemComponents in the same order + * @param _unwrapData ComponentWrapData (wrapped Set component -> underlyingERC20) for each _redeemComponents in the same order + * @param _redeemToETH boolean flag to identify if redeeming to ETH or to ERC20 tokens + * + * @return outputAmount Amount of output received + */ + function _redeemExactSet( + ISetToken _setToken, + IERC20 _outputToken, + uint256 _amountSetToken, + uint256 _minOutputReceive, + ComponentSwapData[] calldata _swapData, + ComponentWrapData[] calldata _unwrapData, + bool _redeemToETH + ) internal returns (uint256) { + // 1. validate input params and get required components + (address[] memory redeemComponents, uint256[] memory redeemAmounts) = _validateRedeemParams( + _setToken, + _outputToken, + _amountSetToken, + _swapData, + _unwrapData + ); + + // 2. transfer set tokens to be redeemed to this + _setToken.safeTransferFrom(msg.sender, address(this), _amountSetToken); + + // 3. redeem set tokens + issuanceModule.redeem(_setToken, _amountSetToken, address(this)); + + // 4. unwrap all components if needed and swap them to output token + uint256 totalOutputTokenObtained = _unwrapAndSwapComponents( + _outputToken, + _swapData, + _unwrapData, + redeemComponents, + redeemAmounts + ); + + // 5. ensure expected minimum output amount has been obtained + require(totalOutputTokenObtained >= _minOutputReceive, "FlashMint: INSUFFICIENT_OUTPUT_AMOUNT"); + + // 6. transfer obtained output tokens to msg.sender + _sendObtainedOutputToSender(_outputToken, totalOutputTokenObtained, _redeemToETH); + + // 7. emit event and return amount obtained + emit FlashRedeem( + msg.sender, + _setToken, + _redeemToETH ? IERC20(DEXAdapter.ETH_ADDRESS) : _outputToken, + _amountSetToken, + totalOutputTokenObtained + ); + + return totalOutputTokenObtained; + } + + /** + * Validates input params for _issueExactSet operations + * + * @param _setToken Address of the SetToken to be redeemed + * @param _amountSetToken Amount of SetTokens to issue + * @param _maxAmountToken Maximum amount of input token to spend + * @param _swapData ComponentSwapData (input token -> underlyingERC20) for each _requiredComponents in the same order + * @param _wrapData ComponentWrapData (underlyingERC20 -> wrapped Set component) for each _requiredComponents in the same order + * + * @return requiredComponents Array of required issuance components gotten from IDebtIssuanceModule.getRequiredComponentIssuanceUnits() + * @return requiredAmounts Array of required issuance component amounts gotten from IDebtIssuanceModule.getRequiredComponentIssuanceUnits() + */ + function _validateIssueParams( + ISetToken _setToken, + uint256 _amountSetToken, + uint256 _maxAmountToken, + ComponentSwapData[] calldata _swapData, + ComponentWrapData[] calldata _wrapData + ) + internal + view + isSetToken(_setToken) + returns (address[] memory requiredComponents, uint256[] memory requiredAmounts) + { + require(_amountSetToken > 0 && _maxAmountToken > 0, "FlashMint: INVALID_INPUTS"); + + (requiredComponents, requiredAmounts, ) = issuanceModule.getRequiredComponentIssuanceUnits( + _setToken, + _amountSetToken + ); + + require( + _wrapData.length == _swapData.length && _wrapData.length == requiredComponents.length, + "FlashMint: MISMATCH_INPUT_ARRAYS" + ); + } + + /** + * Validates input params for _redeemExactSet operations + * + * @param _setToken Address of the SetToken to be redeemed + * @param _outputToken Output token that will be redeemed to + * @param _amountSetToken Amount of SetTokens to redeem + * @param _swapData ComponentSwapData (underlyingERC20 -> output token) for each _redeemComponents in the same order + * @param _unwrapData ComponentWrapData (wrapped Set component -> underlyingERC20) for each _redeemComponents in the same order + * + * @return redeemComponents Array of redemption components gotten from IDebtIssuanceModule.getRequiredComponentRedemptionUnits() + * @return redeemAmounts Array of redemption component amounts gotten from IDebtIssuanceModule.getRequiredComponentRedemptionUnits() + */ + function _validateRedeemParams( + ISetToken _setToken, + IERC20 _outputToken, + uint256 _amountSetToken, + ComponentSwapData[] calldata _swapData, + ComponentWrapData[] calldata _unwrapData + ) + internal + view + isSetToken(_setToken) + returns (address[] memory redeemComponents, uint256[] memory redeemAmounts) + { + require( + _amountSetToken > 0 && address(_outputToken) != address(0), + "FlashMint: INVALID_INPUTS" + ); + + (redeemComponents, redeemAmounts, ) = issuanceModule.getRequiredComponentRedemptionUnits( + _setToken, + _amountSetToken + ); + + require( + _unwrapData.length == _swapData.length && _unwrapData.length == redeemComponents.length, + "FlashMint: MISMATCH_INPUT_ARRAYS" + ); + } + + /** + * Swaps and then wraps each _requiredComponents sequentially based on _swapData and _wrapData + * + * @param _inputToken Input token that will be sold + * @param _maxAmountInputToken Maximum amount of input token that can be spent + * @param _swapData ComponentSwapData (input token -> underlyingERC20) for each _requiredComponents in the same order + * @param _wrapData ComponentWrapData (underlyingERC20 -> wrapped Set component) for each _requiredComponents in the same order + * @param _requiredComponents Issuance components gotten from IDebtIssuanceModule.getRequiredComponentIssuanceUnits() + * @param _requiredAmounts Issuance units gotten from IDebtIssuanceModule.getRequiredComponentIssuanceUnits() + */ + function _swapAndWrapComponents( + IERC20 _inputToken, + uint256 _maxAmountInputToken, + ComponentSwapData[] calldata _swapData, + ComponentWrapData[] calldata _wrapData, + address[] memory _requiredComponents, + uint256[] memory _requiredAmounts + ) internal { + // if the required set components contain the input token, we have to make sure that the required amount + // for issuance is actually still left over at the end of swapping and wrapping + uint256 requiredLeftOverInputTokenAmount = 0; + + // for each component in the swapData / wrapData / requiredComponents array: + // 1. swap from input token to unwrapped component (exact to buyUnderlyingAmount) + // 2. wrap from unwrapped component to wrapped component (unless unwrapped component == wrapped component) + // 3. ensure amount in contract covers required amount for issuance + for (uint256 i = 0; i < _requiredComponents.length; ++i) { + // if the required set component is the input token, no swapping or wrapping is needed + uint256 requiredAmount = _requiredAmounts[i]; + if (address(_inputToken) == _requiredComponents[i]) { + requiredLeftOverInputTokenAmount = requiredAmount; + continue; + } + + // snapshot balance of required component before swap and wrap operations + uint256 componentBalanceBefore = IERC20(_requiredComponents[i]).balanceOf(address(this)); + + // swap input token to underlying token + _swapToExact( + _inputToken, // input + IERC20(_swapData[i].underlyingERC20), // output + _swapData[i].buyUnderlyingAmount, // buy amount: must come from flash mint caller, we do not know the exchange rate wrapped <> unwrapped + _maxAmountInputToken, // maximum spend amount: _maxAmountInputToken as transferred by the flash mint caller + _swapData[i].dexData // dex path fees data etc. + ); + + // transform underlying token into wrapped version (unless it's the same) + if (_swapData[i].underlyingERC20 != _requiredComponents[i]) { + _wrapComponent( + _wrapData[i], + _swapData[i].buyUnderlyingAmount, + _swapData[i].underlyingERC20, + _requiredComponents[i] + ); + } + + // ensure obtained component amount covers required component amount for issuance + // this is not already covered through _swapToExact because it does not take wrapping into consideration + uint256 componentBalanceAfter = IERC20(_requiredComponents[i]).balanceOf(address(this)); + uint256 componentAmountObtained = componentBalanceAfter.sub(componentBalanceBefore); + require(componentAmountObtained >= requiredAmount, "FlashMint: UNDERBOUGHT_COMPONENT"); } - /** - * Redeems an exact amount of SetTokens for an ERC20 token. - * The SetToken must be approved by the sender to this contract. - * - * @param _setToken Address of the SetToken being redeemed - * @param _outputToken Address of output token - * @param _amountSetToken Amount SetTokens to redeem - * @param _minOutputReceive Minimum amount of output token to receive - * @param _componentQuotes The encoded 0x transactions execute (components -> WETH). - * @param _issuanceModule Address of issuance Module to use - * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module - * - * @return outputAmount Amount of output tokens sent to the caller - */ - function redeemExactSetForToken( - ISetToken _setToken, - IERC20 _outputToken, - uint256 _amountSetToken, - uint256 _minOutputReceive, - address _issuanceModule - ) - external - nonReentrant - returns (uint256) - { - return 0; + // ensure left over input token amount covers issuance for component if input token is one of the Set components + require( + IERC20(_inputToken).balanceOf(address(this)) >= requiredLeftOverInputTokenAmount, + "FlashMint: NOT_ENOUGH_INPUT" + ); + } + + /** + * Unwraps and then swaps each _redeemComponents sequentially based on _swapData and _unwrapData + * + * @param _outputToken Output token that will be bought + * @param _swapData ComponentSwapData (underlyingERC20 -> output token) for each _redeemComponents in the same order + * @param _unwrapData ComponentWrapData (wrapped Set component -> underlyingERC20) for each _redeemComponents in the same order + * @param _redeemComponents redemption components gotten from IDebtIssuanceModule.getRequiredComponentRedemptionUnits() + * @param _redeemAmounts redemption units gotten from IDebtIssuanceModule.getRequiredComponentRedemptionUnits() + * + * @return totalOutputTokenObtained total output token amount obtained + */ + function _unwrapAndSwapComponents( + IERC20 _outputToken, + ComponentSwapData[] calldata _swapData, + ComponentWrapData[] calldata _unwrapData, + address[] memory _redeemComponents, + uint256[] memory _redeemAmounts + ) internal returns (uint256 totalOutputTokenObtained) { + // for each component in the swapData / wrapData / redeemComponents array: + // 1. unwrap from wrapped set component to unwrapped underlyingERC20 in swapData + // 2. swap from underlyingERC20 token to output token (exact from obtained underlyingERC20 amount) + + for (uint256 i = 0; i < _redeemComponents.length; ++i) { + // default redeemed amount is maximum possible amount that was redeemed for this component + // this is recomputed if the redeemed amount is unwrapped to the actual unwrapped amount + uint256 redeemedAmount = _redeemAmounts[i]; + + // if the set component is the output token, no swapping or wrapping is needed + if (address(_outputToken) == _redeemComponents[i]) { + // add maximum possible amount that was redeemed for this component to totalOutputTokenObtained (=redeemedAmount) + totalOutputTokenObtained = totalOutputTokenObtained.add(redeemedAmount); + continue; + } + + // transform wrapped token into unwrapped version (unless it's the same) + if (_swapData[i].underlyingERC20 != _redeemComponents[i]) { + // snapshot unwrapped balance before to compute the actual redeemed amount of underlying token (due to unknown exchange rate) + uint256 unwrappedBalanceBefore = IERC20(_swapData[i].underlyingERC20).balanceOf( + address(this) + ); + + _unwrapComponent( + _unwrapData[i], + redeemedAmount, + _swapData[i].underlyingERC20, + _redeemComponents[i] + ); + + // recompute actual redeemed amount to the underlyingERC20 token amount obtained through unwrapping + uint256 unwrappedBalanceAfter = IERC20(_swapData[i].underlyingERC20).balanceOf( + address(this) + ); + redeemedAmount = unwrappedBalanceAfter.sub(unwrappedBalanceBefore); + } + + // swap redeemed and unwrapped component to output token + uint256 boughtOutputTokenAmount = _swapFromExact( + IERC20(_swapData[i].underlyingERC20), // input + _outputToken, // output + redeemedAmount, // sell amount of input token + _swapData[i].dexData // dex path fees data etc. + ); + + totalOutputTokenObtained = totalOutputTokenObtained.add(boughtOutputTokenAmount); } + } - /** - * Redeems an exact amount of SetTokens for ETH. - * The SetToken must be approved by the sender to this contract. - * - * @param _setToken Address of the SetToken being redeemed - * @param _amountSetToken Amount SetTokens to redeem - * @param _minEthReceive Minimum amount of Eth to receive - * @param _componentQuotes The encoded 0x transactions execute - * @param _issuanceModule Address of issuance Module to use - * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module - * - * @return outputAmount Amount of output tokens sent to the caller - */ - function redeemExactSetForETH( - ISetToken _setToken, - uint256 _amountSetToken, - uint256 _minEthReceive, - address _issuanceModule - ) - external - nonReentrant + /** + * Swaps _inputToken to exact _amount to _outputToken through _swapDexData + * + * @param _inputToken Input token that will be sold + * @param _outputToken Output token that will be bought + * @param _amount Amount that will be bought + * @param _maxAmountIn Maximum aount of input token that can be spent + * @param _swapDexData DEXAdapter.SwapData with path, fees, etc. for inputToken -> outputToken swap + * + * @return Amount of spent _inputToken + */ + function _swapToExact( + IERC20 _inputToken, + IERC20 _outputToken, + uint256 _amount, + uint256 _maxAmountIn, + DEXAdapter.SwapData calldata _swapDexData + ) + internal + isValidPath(_swapDexData.path, address(_inputToken), address(_outputToken)) returns (uint256) - { - return 0; + { + // safe approves are done right in the dexAdapter library + // swaps are skipped there too if inputToken == outputToken (depending on path) + return dexAdapter.swapTokensForExactTokens(_amount, _maxAmountIn, _swapDexData); + } + + /** + * Swaps exact _amount of _inputToken to _outputToken through _swapDexData + * + * @param _inputToken Input token that will be sold + * @param _outputToken Output token that will be bought + * @param _amount Amount that will be sold + * @param _swapDexData DEXAdapter.SwapData with path, fees, etc. for inputToken -> outputToken swap + * + * @return amount of received _outputToken + */ + function _swapFromExact( + IERC20 _inputToken, + IERC20 _outputToken, + uint256 _amount, + DEXAdapter.SwapData calldata _swapDexData + ) + internal + isValidPath(_swapDexData.path, address(_inputToken), address(_outputToken)) + returns (uint256) + { + // safe approves are done right in the dexAdapter library + return + dexAdapter.swapExactTokensForTokens( + _amount, + // _minAmountOut is 0 here since we don't know what to check against because for wrapped components + // we only have the required amounts for the wrapped component, but not for the underlying we swap to here + // This is covered indirectly in later checks though + // e.g. directly through the issue call (not enough _outputToken -> wrappedComponent -> issue will fail) + 0, + _swapDexData + ); + } + + /** + * Wraps _wrapAmount of _underlyingToken to _wrappedComponent component + * + * @param _wrapData ComponentWrapData including integration name and optional wrap calldata bytes + * @param _wrapAmount amount of _underlyingToken to wrap + * @param _underlyingToken underlying (unwrapped) token to wrap from (e.g. DAI) + * @param _wrappedToken wrapped token to wrap to + */ + function _wrapComponent( + ComponentWrapData calldata _wrapData, + uint256 _wrapAmount, + address _underlyingToken, + address _wrappedToken + ) internal { + // 1. get the wrap adapter directly from the integration registry + + // Note we could get the address of the adapter directly in the params instead of the integration name + // but that would allow integrators to use their own potentially somehow malicious WrapAdapter + // by directly fetching it from our IntegrationRegistry we can be sure that it behaves as expected + IWrapV2Adapter _wrapAdapter = IWrapV2Adapter(_getAndValidateAdapter(_wrapData.integrationName)); + + // 2. get wrap call info from adapter + (address _wrapCallTarget, uint256 _wrapCallValue, bytes memory _wrapCallData) = _wrapAdapter + .getWrapCallData( + _underlyingToken, + _wrappedToken, + _wrapAmount, + address(this), + _wrapData.wrapData + ); + + // 3. approve token transfer from this to _wrapCallTarget + DEXAdapter._safeApprove( + IERC20(_underlyingToken), + _wrapCallTarget, + _wrapAmount + ); + + // 4. invoke wrap function call. we can't check any response value because the implementation might be different + // between wrapCallTargets... e.g. compoundV2 would return uint256 with value 0 if successful + _wrapCallTarget.functionCallWithValue(_wrapCallData, _wrapCallValue); + } + + /** + * Unwraps _unwrapAmount of _wrappedToken to _underlyingToken + * + * @param _unwrapData ComponentWrapData including integration name and optional wrap calldata bytes + * @param _unwrapAmount amount of _underlyingToken to wrap + * @param _underlyingToken underlying (unwrapped) token to unwrap to (e.g. DAI) + * @param _wrappedToken wrapped token to unwrap from + */ + function _unwrapComponent( + ComponentWrapData calldata _unwrapData, + uint256 _unwrapAmount, + address _underlyingToken, + address _wrappedToken + ) internal { + // 1. get the wrap adapter directly from the integration registry + + // Note we could get the address of the adapter directly in the params instead of the integration name + // but that would allow integrators to use their own potentially somehow malicious WrapAdapter + // by directly fetching it from our IntegrationRegistry we can be sure that it behaves as expected + IWrapV2Adapter _wrapAdapter = IWrapV2Adapter(_getAndValidateAdapter(_unwrapData.integrationName)); + + // 2. get unwrap call info from adapter + (address _wrapCallTarget, uint256 _wrapCallValue, bytes memory _wrapCallData) = _wrapAdapter + .getUnwrapCallData( + _underlyingToken, + _wrappedToken, + _unwrapAmount, + address(this), + _unwrapData.wrapData + ); + + // 3. invoke unwrap function call. we can't check any response value because the implementation might be different + // between wrapCallTargets... e.g. compoundV2 would return uint256 with value 0 if successful + _wrapCallTarget.functionCallWithValue(_wrapCallData, _wrapCallValue); + } + + /** + * Validates that not more than the requested max amount of the input token has been spent + * + * @param _inputToken Address of the input token to return + * @param _inputTokenBalanceBefore input token balance before at the beginning of the operation + * @param _maxAmountInputToken maximum amount that could be spent + + * @return spentInputTokenAmount actual spent amount of the input token + */ + function _validateMaxAmountInputToken( + IERC20 _inputToken, + uint256 _inputTokenBalanceBefore, + uint256 _maxAmountInputToken + ) internal view returns (uint256 spentInputTokenAmount) { + uint256 inputTokenBalanceAfter = _inputToken.balanceOf(address(this)); + + // _maxAmountInputToken amount has been transferred to this contract after _inputTokenBalanceBefore snapshot + spentInputTokenAmount = _inputTokenBalanceBefore.add(_maxAmountInputToken).sub( + inputTokenBalanceAfter + ); + + require(spentInputTokenAmount <= _maxAmountInputToken, "FlashMint: OVERSPENT_INPUT_TOKEN"); + } + + /** + * Returns excess input token + * + * @param _inputToken Address of the input token to return + * @param _receivedAmount Amount received by the caller + * @param _spentAmount Amount spent for issuance + * @param _returnETH Boolean flag to identify if ETH should be returned or the input token + */ + function _returnExcessInput( + IERC20 _inputToken, + uint256 _receivedAmount, + uint256 _spentAmount, + bool _returnETH + ) internal { + uint256 amountTokenReturn = _receivedAmount.sub(_spentAmount); + if (amountTokenReturn > 0) { + if (_returnETH) { + // unwrap from WETH -> ETH and send ETH amount back to sender + IWETH(dexAdapter.weth).withdraw(amountTokenReturn); + (payable(msg.sender)).sendValue(amountTokenReturn); + } else { + _inputToken.safeTransfer(msg.sender, amountTokenReturn); + } } -} \ No newline at end of file + } + + /** + * Sends the obtained amount of output token / ETH to msg.sender + * + * @param _outputToken Address of the output token to return + * @param _amount Amount to transfer + * @param _redeemToETH Boolean flag to identify if ETH or the output token should be sent + */ + function _sendObtainedOutputToSender( + IERC20 _outputToken, + uint256 _amount, + bool _redeemToETH + ) internal { + if (_redeemToETH) { + // unwrap from WETH -> ETH and send ETH amount back to sender + IWETH(dexAdapter.weth).withdraw(_amount); + (payable(msg.sender)).sendValue(_amount); + } else { + _outputToken.safeTransfer(msg.sender, _amount); + } + } + + /** + * Gets the integration for the passed in integration name listed on the wrapModule. Validates that the address is not empty + * + * @param _integrationName Name of wrap adapter integration (mapping on integration registry) + * + * @return adapter address of the wrap adapter + */ + function _getAndValidateAdapter(string memory _integrationName) + internal + view + returns (address adapter) + { + // integration registry has resourceId 0, see library ResourceIdentifier + // @dev could also be accomplished with using ResourceIdentifier for IController but this results in less bloat in the repo + IIntegrationRegistry integrationRegistry = IIntegrationRegistry(setController.resourceId(0)); + + adapter = integrationRegistry.getIntegrationAdapterWithHash( + wrapModule, + keccak256(bytes(_integrationName)) + ); + + require(adapter != address(0), "FlashMint: WRAP_ADAPTER_INVALID"); + } +} diff --git a/contracts/interfaces/IIntegrationRegistry.sol b/contracts/interfaces/IIntegrationRegistry.sol new file mode 100644 index 00000000..3ac72305 --- /dev/null +++ b/contracts/interfaces/IIntegrationRegistry.sol @@ -0,0 +1,38 @@ +/* + 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; + +interface IIntegrationRegistry { + function addIntegration( + address _module, + string memory _id, + address _wrapper + ) external; + + function getIntegrationAdapter(address _module, string memory _id) + external + view + returns (address); + + function getIntegrationAdapterWithHash(address _module, bytes32 _id) + external + view + returns (address); + + function isValidIntegration(address _module, string memory _id) external view returns (bool); +} diff --git a/contracts/interfaces/IWrapModuleV2.sol b/contracts/interfaces/IWrapModuleV2.sol new file mode 100644 index 00000000..bb69a38e --- /dev/null +++ b/contracts/interfaces/IWrapModuleV2.sol @@ -0,0 +1,60 @@ +/* + Copyright 2021 Index Coop. + + 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 +*/ + +import { ISetToken } from "./ISetToken.sol"; + +pragma solidity 0.6.10; + +interface IWrapModuleV2 { + + function initialize(ISetToken _setToken) external; + + function wrap( + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _underlyingUnits, + string calldata _integrationName, + bytes memory _wrapData + ) external; + + function wrapWithEther( + ISetToken _setToken, + address _wrappedToken, + uint256 _underlyingUnits, + string calldata _integrationName, + bytes memory _wrapData + ) external; + + function unwrap( + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _wrappedUnits, + string calldata _integrationName, + bytes memory _unwrapData + ) external; + + function unwrapWithEther( + ISetToken _setToken, + address _wrappedToken, + uint256 _wrappedUnits, + string calldata _integrationName, + bytes memory _unwrapData + ) external; +} \ No newline at end of file diff --git a/contracts/interfaces/IWrapV2Adapter.sol b/contracts/interfaces/IWrapV2Adapter.sol new file mode 100644 index 00000000..3e37eca1 --- /dev/null +++ b/contracts/interfaces/IWrapV2Adapter.sol @@ -0,0 +1,61 @@ +/* + Copyright 2021 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; + +/** + * @title IWrapV2Adapter + * @author Set Protocol + */ +interface IWrapV2Adapter { + function ETH_TOKEN_ADDRESS() external view returns (address); + + function getWrapCallData( + address _underlyingToken, + address _wrappedToken, + uint256 _underlyingUnits, + address _to, + bytes memory _wrapData + ) + external + view + returns ( + address _subject, + uint256 _value, + bytes memory _calldata + ); + + function getUnwrapCallData( + address _underlyingToken, + address _wrappedToken, + uint256 _wrappedTokenUnits, + address _to, + bytes memory _unwrapData + ) + external + view + returns ( + address _subject, + uint256 _value, + bytes memory _calldata + ); + + function getSpenderAddress(address _underlyingToken, address _wrappedToken) + external + view + returns (address); +} diff --git a/external/abi/set/Compound.json b/external/abi/set/Compound.json index 486726c1..e1441f62 100644 --- a/external/abi/set/Compound.json +++ b/external/abi/set/Compound.json @@ -35,7 +35,8 @@ } ], "stateMutability": "pure", - "type": "function" + "type": "function", + "gas": "0xa7d8c0" }, { "inputs": [ @@ -69,7 +70,8 @@ } ], "stateMutability": "pure", - "type": "function" + "type": "function", + "gas": "0xa7d8c0" }, { "inputs": [ @@ -103,7 +105,8 @@ } ], "stateMutability": "pure", - "type": "function" + "type": "function", + "gas": "0xa7d8c0" }, { "inputs": [ @@ -137,7 +140,8 @@ } ], "stateMutability": "pure", - "type": "function" + "type": "function", + "gas": "0xa7d8c0" }, { "inputs": [ @@ -171,7 +175,43 @@ } ], "stateMutability": "pure", - "type": "function" + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ICErc20", + "name": "_cToken", + "type": "ICErc20" + }, + { + "internalType": "uint256", + "name": "_redeemNotional", + "type": "uint256" + } + ], + "name": "getRedeemCalldata", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" }, { "inputs": [ @@ -205,7 +245,8 @@ } ], "stateMutability": "pure", - "type": "function" + "type": "function", + "gas": "0xa7d8c0" }, { "inputs": [ @@ -239,7 +280,8 @@ } ], "stateMutability": "pure", - "type": "function" + "type": "function", + "gas": "0xa7d8c0" }, { "inputs": [ @@ -273,11 +315,12 @@ } ], "stateMutability": "pure", - "type": "function" + "type": "function", + "gas": "0xa7d8c0" } ], - "bytecode": "0x611715610026600b82828239805160001a60731461001957fe5b30600052607381538281f3fe730000000000000000000000000000000000000000301460806040526004361061010a5760003560e01c80636d4aada5116100a1578063aed2dfc911610070578063aed2dfc91461041b578063b17faf1c1461045e578063beee4b4b146104a3578063fa872cfa146104e65761010a565b80636d4aada51461036b5780639653f87914610397578063a2ca57e0146103c3578063a753734e146103ef5761010a565b80633c77439c116100dd5780633c77439c1461028b57806343472132146102b95780635d202591146102e5578063690f6561146103285761010a565b806301fd16b21461010f57806309c09c90146101d357806309c8202b1461021a5780632c66b57e1461025d575b600080fd5b61013b6004803603604081101561012557600080fd5b506001600160a01b038135169060200135610529565b60405180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561019657818101518382015260200161017e565b50505050905090810190601f1680156101c35780820380516001836020036101000a031916815260200191505b5094505050505060405180910390f35b8180156101df57600080fd5b50610218600480360360608110156101f657600080fd5b506001600160a01b038135811691602081013582169160409091013516610567565b005b81801561022657600080fd5b506102186004803603606081101561023d57600080fd5b506001600160a01b0381358116916020810135909116906040013561078c565b61013b6004803603604081101561027357600080fd5b506001600160a01b03813581169160200135166109a8565b61013b600480360360408110156102a157600080fd5b506001600160a01b0381358116916020013516610a80565b61013b600480360360408110156102cf57600080fd5b506001600160a01b038135169060200135610acb565b8180156102f157600080fd5b506102186004803603606081101561030857600080fd5b506001600160a01b03813581169160208101359091169060400135610afb565b81801561033457600080fd5b506102186004803603606081101561034b57600080fd5b506001600160a01b03813581169160208101359091169060400135610cc7565b61013b6004803603604081101561038157600080fd5b506001600160a01b038135169060200135610ccf565b61013b600480360360408110156103ad57600080fd5b506001600160a01b038135169060200135610cff565b61013b600480360360408110156103d957600080fd5b506001600160a01b038135169060200135610d3e565b61013b6004803603604081101561040557600080fd5b506001600160a01b038135169060200135610d7d565b81801561042757600080fd5b506102186004803603606081101561043e57600080fd5b506001600160a01b03813581169160208101359091169060400135610dbc565b81801561046a57600080fd5b506102186004803603606081101561048157600080fd5b506001600160a01b038135811691602081013582169160409091013516610fda565b8180156104af57600080fd5b50610218600480360360608110156104c657600080fd5b506001600160a01b038135811691602081013590911690604001356112a4565b8180156104f257600080fd5b506102186004803603606081101561050957600080fd5b506001600160a01b038135811691602081013590911690604001356114c2565b604080516024808201939093528151808203909301835260440190526020810180516001600160e01b031663317afabb60e21b179052909160009190565b60606105738383610a80565b92505050836001600160a01b0316638f6f0332836000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156105f45781810151838201526020016105dc565b50505050905090810190601f1680156106215780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561064257600080fd5b505af1158015610656573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052602081101561067f57600080fd5b8101908080516040519392919084600160201b82111561069e57600080fd5b9083019060208201858111156106b357600080fd5b8251600160201b8111828201881017156106cc57600080fd5b82525081516020918201929091019080838360005b838110156106f95781810151838201526020016106e1565b50505050905090810190601f1680156107265780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561074157600080fd5b505115610786576040805162461bcd60e51b815260206004820152600e60248201526d115e1a5d1a5b99c819985a5b195960921b604482015290519081900360640190fd5b50505050565b60606107988383610cff565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610819578181015183820152602001610801565b50505050905090810190601f1680156108465780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561086757600080fd5b505af115801561087b573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156108a457600080fd5b8101908080516040519392919084600160201b8211156108c357600080fd5b9083019060208201858111156108d857600080fd5b8251600160201b8111828201881017156108f157600080fd5b82525081516020918201929091019080838360005b8381101561091e578181015183820152602001610906565b50505050905090810190601f16801561094b5780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561096657600080fd5b505115610786576040805162461bcd60e51b815260206004820152600b60248201526a135a5b9d0819985a5b195960aa1b604482015290519081900360640190fd5b60408051600180825281830190925260009182916060918291906020808301908036833701905050905085816000815181106109e057fe5b6001600160a01b039092166020928302919091018201526040516024810182815283516044830152835160609385938392606490910191858101910280838360005b83811015610a3a578181015183820152602001610a22565b50506040805193909501838103601f1901845290945250602081018051631853304760e31b6001600160e01b039091161790529a9c60009c509950505050505050505050565b604080516001600160a01b039390931660248085019190915281518085039091018152604490930190526020820180516001600160e01b0316630ede4edd60e41b1790529160009190565b6040805160048152602481019091526020810180516001600160e01b0316632726cff560e11b1790529192909190565b6060610b078383610acb565b92505050836001600160a01b0316638f6f03328484846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610b87578181015183820152602001610b6f565b50505050905090810190601f168015610bb45780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b158015610bd557600080fd5b505af1158015610be9573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526020811015610c1257600080fd5b8101908080516040519392919084600160201b821115610c3157600080fd5b908301906020820185811115610c4657600080fd5b8251600160201b811182820188101715610c5f57600080fd5b82525081516020918201929091019080838360005b83811015610c8c578181015183820152602001610c74565b50505050905090810190601f168015610cb95780820380516001836020036101000a031916815260200191505b506040525050505050505050565b6060610b0783835b6040805160048152602481019091526020810180516001600160e01b0316631249c58b60e01b1790529192909190565b6040805160248082018490528251808303909101815260449091019091526020810180516001600160e01b031663140e25ad60e31b1790529192909190565b6040805160248082018490528251808303909101815260449091019091526020810180516001600160e01b031663852a12e360e01b1790529192909190565b6040805160248082018490528251808303909101815260449091019091526020810180516001600160e01b031663073a938160e11b1790529192909190565b6060610dc88383610d3e565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610e49578181015183820152602001610e31565b50505050905090810190601f168015610e765780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b158015610e9757600080fd5b505af1158015610eab573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526020811015610ed457600080fd5b8101908080516040519392919084600160201b821115610ef357600080fd5b908301906020820185811115610f0857600080fd5b8251600160201b811182820188101715610f2157600080fd5b82525081516020918201929091019080838360005b83811015610f4e578181015183820152602001610f36565b50505050905090810190601f168015610f7b5780820380516001836020036101000a031916815260200191505b506040525050508060200190516020811015610f9657600080fd5b505115610786576040805162461bcd60e51b815260206004820152600d60248201526c14995919595b4819985a5b1959609a1b604482015290519081900360640190fd5b6060610fe683836109a8565b925050506060846001600160a01b0316638f6f0332846000856040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015611069578181015183820152602001611051565b50505050905090810190601f1680156110965780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b1580156110b757600080fd5b505af11580156110cb573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156110f457600080fd5b8101908080516040519392919084600160201b82111561111357600080fd5b90830190602082018581111561112857600080fd5b8251600160201b81118282018810171561114157600080fd5b82525081516020918201929091019080838360005b8381101561116e578181015183820152602001611156565b50505050905090810190601f16801561119b5780820380516001836020036101000a031916815260200191505b5060405250505080602001905160208110156111b657600080fd5b8101908080516040519392919084600160201b8211156111d557600080fd5b9083019060208201858111156111ea57600080fd5b82518660208202830111600160201b8211171561120657600080fd5b82525081516020918201928201910280838360005b8381101561123357818101518382015260200161121b565b5050505090500160405250505090508060008151811061124f57fe5b602002602001015160001461129d576040805162461bcd60e51b815260206004820152600f60248201526e115b9d195c9a5b99c819985a5b1959608a1b604482015290519081900360640190fd5b5050505050565b60606112b08383610529565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015611331578181015183820152602001611319565b50505050905090810190601f16801561135e5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561137f57600080fd5b505af1158015611393573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156113bc57600080fd5b8101908080516040519392919084600160201b8211156113db57600080fd5b9083019060208201858111156113f057600080fd5b8251600160201b81118282018810171561140957600080fd5b82525081516020918201929091019080838360005b8381101561143657818101518382015260200161141e565b50505050905090810190601f1680156114635780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561147e57600080fd5b505115610786576040805162461bcd60e51b815260206004820152600d60248201526c109bdc9c9bddc819985a5b1959609a1b604482015290519081900360640190fd5b60606114ce8383610d7d565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561154f578181015183820152602001611537565b50505050905090810190601f16801561157c5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561159d57600080fd5b505af11580156115b1573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156115da57600080fd5b8101908080516040519392919084600160201b8211156115f957600080fd5b90830190602082018581111561160e57600080fd5b8251600160201b81118282018810171561162757600080fd5b82525081516020918201929091019080838360005b8381101561165457818101518382015260200161163c565b50505050905090810190601f1680156116815780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561169c57600080fd5b505115610786576040805162461bcd60e51b815260206004820152600c60248201526b14995c185e4819985a5b195960a21b604482015290519081900360640190fdfea2646970667358221220b38f42ea8a6b9a5ac34022dcf4e5a0c42fe4610311ecaba4fad92dc42cba958764736f6c634300060a0033", - "deployedBytecode": "0x730000000000000000000000000000000000000000301460806040526004361061010a5760003560e01c80636d4aada5116100a1578063aed2dfc911610070578063aed2dfc91461041b578063b17faf1c1461045e578063beee4b4b146104a3578063fa872cfa146104e65761010a565b80636d4aada51461036b5780639653f87914610397578063a2ca57e0146103c3578063a753734e146103ef5761010a565b80633c77439c116100dd5780633c77439c1461028b57806343472132146102b95780635d202591146102e5578063690f6561146103285761010a565b806301fd16b21461010f57806309c09c90146101d357806309c8202b1461021a5780632c66b57e1461025d575b600080fd5b61013b6004803603604081101561012557600080fd5b506001600160a01b038135169060200135610529565b60405180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561019657818101518382015260200161017e565b50505050905090810190601f1680156101c35780820380516001836020036101000a031916815260200191505b5094505050505060405180910390f35b8180156101df57600080fd5b50610218600480360360608110156101f657600080fd5b506001600160a01b038135811691602081013582169160409091013516610567565b005b81801561022657600080fd5b506102186004803603606081101561023d57600080fd5b506001600160a01b0381358116916020810135909116906040013561078c565b61013b6004803603604081101561027357600080fd5b506001600160a01b03813581169160200135166109a8565b61013b600480360360408110156102a157600080fd5b506001600160a01b0381358116916020013516610a80565b61013b600480360360408110156102cf57600080fd5b506001600160a01b038135169060200135610acb565b8180156102f157600080fd5b506102186004803603606081101561030857600080fd5b506001600160a01b03813581169160208101359091169060400135610afb565b81801561033457600080fd5b506102186004803603606081101561034b57600080fd5b506001600160a01b03813581169160208101359091169060400135610cc7565b61013b6004803603604081101561038157600080fd5b506001600160a01b038135169060200135610ccf565b61013b600480360360408110156103ad57600080fd5b506001600160a01b038135169060200135610cff565b61013b600480360360408110156103d957600080fd5b506001600160a01b038135169060200135610d3e565b61013b6004803603604081101561040557600080fd5b506001600160a01b038135169060200135610d7d565b81801561042757600080fd5b506102186004803603606081101561043e57600080fd5b506001600160a01b03813581169160208101359091169060400135610dbc565b81801561046a57600080fd5b506102186004803603606081101561048157600080fd5b506001600160a01b038135811691602081013582169160409091013516610fda565b8180156104af57600080fd5b50610218600480360360608110156104c657600080fd5b506001600160a01b038135811691602081013590911690604001356112a4565b8180156104f257600080fd5b506102186004803603606081101561050957600080fd5b506001600160a01b038135811691602081013590911690604001356114c2565b604080516024808201939093528151808203909301835260440190526020810180516001600160e01b031663317afabb60e21b179052909160009190565b60606105738383610a80565b92505050836001600160a01b0316638f6f0332836000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156105f45781810151838201526020016105dc565b50505050905090810190601f1680156106215780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561064257600080fd5b505af1158015610656573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052602081101561067f57600080fd5b8101908080516040519392919084600160201b82111561069e57600080fd5b9083019060208201858111156106b357600080fd5b8251600160201b8111828201881017156106cc57600080fd5b82525081516020918201929091019080838360005b838110156106f95781810151838201526020016106e1565b50505050905090810190601f1680156107265780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561074157600080fd5b505115610786576040805162461bcd60e51b815260206004820152600e60248201526d115e1a5d1a5b99c819985a5b195960921b604482015290519081900360640190fd5b50505050565b60606107988383610cff565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610819578181015183820152602001610801565b50505050905090810190601f1680156108465780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561086757600080fd5b505af115801561087b573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156108a457600080fd5b8101908080516040519392919084600160201b8211156108c357600080fd5b9083019060208201858111156108d857600080fd5b8251600160201b8111828201881017156108f157600080fd5b82525081516020918201929091019080838360005b8381101561091e578181015183820152602001610906565b50505050905090810190601f16801561094b5780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561096657600080fd5b505115610786576040805162461bcd60e51b815260206004820152600b60248201526a135a5b9d0819985a5b195960aa1b604482015290519081900360640190fd5b60408051600180825281830190925260009182916060918291906020808301908036833701905050905085816000815181106109e057fe5b6001600160a01b039092166020928302919091018201526040516024810182815283516044830152835160609385938392606490910191858101910280838360005b83811015610a3a578181015183820152602001610a22565b50506040805193909501838103601f1901845290945250602081018051631853304760e31b6001600160e01b039091161790529a9c60009c509950505050505050505050565b604080516001600160a01b039390931660248085019190915281518085039091018152604490930190526020820180516001600160e01b0316630ede4edd60e41b1790529160009190565b6040805160048152602481019091526020810180516001600160e01b0316632726cff560e11b1790529192909190565b6060610b078383610acb565b92505050836001600160a01b0316638f6f03328484846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610b87578181015183820152602001610b6f565b50505050905090810190601f168015610bb45780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b158015610bd557600080fd5b505af1158015610be9573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526020811015610c1257600080fd5b8101908080516040519392919084600160201b821115610c3157600080fd5b908301906020820185811115610c4657600080fd5b8251600160201b811182820188101715610c5f57600080fd5b82525081516020918201929091019080838360005b83811015610c8c578181015183820152602001610c74565b50505050905090810190601f168015610cb95780820380516001836020036101000a031916815260200191505b506040525050505050505050565b6060610b0783835b6040805160048152602481019091526020810180516001600160e01b0316631249c58b60e01b1790529192909190565b6040805160248082018490528251808303909101815260449091019091526020810180516001600160e01b031663140e25ad60e31b1790529192909190565b6040805160248082018490528251808303909101815260449091019091526020810180516001600160e01b031663852a12e360e01b1790529192909190565b6040805160248082018490528251808303909101815260449091019091526020810180516001600160e01b031663073a938160e11b1790529192909190565b6060610dc88383610d3e565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610e49578181015183820152602001610e31565b50505050905090810190601f168015610e765780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b158015610e9757600080fd5b505af1158015610eab573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526020811015610ed457600080fd5b8101908080516040519392919084600160201b821115610ef357600080fd5b908301906020820185811115610f0857600080fd5b8251600160201b811182820188101715610f2157600080fd5b82525081516020918201929091019080838360005b83811015610f4e578181015183820152602001610f36565b50505050905090810190601f168015610f7b5780820380516001836020036101000a031916815260200191505b506040525050508060200190516020811015610f9657600080fd5b505115610786576040805162461bcd60e51b815260206004820152600d60248201526c14995919595b4819985a5b1959609a1b604482015290519081900360640190fd5b6060610fe683836109a8565b925050506060846001600160a01b0316638f6f0332846000856040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015611069578181015183820152602001611051565b50505050905090810190601f1680156110965780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b1580156110b757600080fd5b505af11580156110cb573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156110f457600080fd5b8101908080516040519392919084600160201b82111561111357600080fd5b90830190602082018581111561112857600080fd5b8251600160201b81118282018810171561114157600080fd5b82525081516020918201929091019080838360005b8381101561116e578181015183820152602001611156565b50505050905090810190601f16801561119b5780820380516001836020036101000a031916815260200191505b5060405250505080602001905160208110156111b657600080fd5b8101908080516040519392919084600160201b8211156111d557600080fd5b9083019060208201858111156111ea57600080fd5b82518660208202830111600160201b8211171561120657600080fd5b82525081516020918201928201910280838360005b8381101561123357818101518382015260200161121b565b5050505090500160405250505090508060008151811061124f57fe5b602002602001015160001461129d576040805162461bcd60e51b815260206004820152600f60248201526e115b9d195c9a5b99c819985a5b1959608a1b604482015290519081900360640190fd5b5050505050565b60606112b08383610529565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015611331578181015183820152602001611319565b50505050905090810190601f16801561135e5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561137f57600080fd5b505af1158015611393573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156113bc57600080fd5b8101908080516040519392919084600160201b8211156113db57600080fd5b9083019060208201858111156113f057600080fd5b8251600160201b81118282018810171561140957600080fd5b82525081516020918201929091019080838360005b8381101561143657818101518382015260200161141e565b50505050905090810190601f1680156114635780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561147e57600080fd5b505115610786576040805162461bcd60e51b815260206004820152600d60248201526c109bdc9c9bddc819985a5b1959609a1b604482015290519081900360640190fd5b60606114ce8383610d7d565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561154f578181015183820152602001611537565b50505050905090810190601f16801561157c5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561159d57600080fd5b505af11580156115b1573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156115da57600080fd5b8101908080516040519392919084600160201b8211156115f957600080fd5b90830190602082018581111561160e57600080fd5b8251600160201b81118282018810171561162757600080fd5b82525081516020918201929091019080838360005b8381101561165457818101518382015260200161163c565b50505050905090810190601f1680156116815780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561169c57600080fd5b505115610786576040805162461bcd60e51b815260206004820152600c60248201526b14995c185e4819985a5b195960a21b604482015290519081900360640190fdfea2646970667358221220b38f42ea8a6b9a5ac34022dcf4e5a0c42fe4610311ecaba4fad92dc42cba958764736f6c634300060a0033", + "bytecode": "0x611a07610026600b82828239805160001a60731461001957fe5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600436106101205760003560e01c8063944d008d116100ac578063a753734e1161007b578063a753734e14610474578063aed2dfc9146104a0578063b17faf1c146104e3578063beee4b4b14610528578063fa872cfa1461056b57610120565b8063944d008d146103ad5780639653f879146103f0578063a2ca57e01461041c578063a2f976581461044857610120565b80633c77439c116100f35780633c77439c146102a157806343472132146102cf5780635d202591146102fb578063690f65611461033e5780636d4aada51461038157610120565b806301fd16b21461012557806309c09c90146101e957806309c8202b146102305780632c66b57e14610273575b600080fd5b6101516004803603604081101561013b57600080fd5b506001600160a01b0381351690602001356105ae565b60405180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156101ac578181015183820152602001610194565b50505050905090810190601f1680156101d95780820380516001836020036101000a031916815260200191505b5094505050505060405180910390f35b8180156101f557600080fd5b5061022e6004803603606081101561020c57600080fd5b506001600160a01b0381358116916020810135821691604090910135166105ec565b005b81801561023c57600080fd5b5061022e6004803603606081101561025357600080fd5b506001600160a01b03813581169160208101359091169060400135610811565b6101516004803603604081101561028957600080fd5b506001600160a01b0381358116916020013516610a2d565b610151600480360360408110156102b757600080fd5b506001600160a01b0381358116916020013516610b05565b610151600480360360408110156102e557600080fd5b506001600160a01b038135169060200135610b50565b81801561030757600080fd5b5061022e6004803603606081101561031e57600080fd5b506001600160a01b03813581169160208101359091169060400135610b80565b81801561034a57600080fd5b5061022e6004803603606081101561036157600080fd5b506001600160a01b03813581169160208101359091169060400135610d4c565b6101516004803603604081101561039757600080fd5b506001600160a01b038135169060200135610d54565b8180156103b957600080fd5b5061022e600480360360608110156103d057600080fd5b506001600160a01b03813581169160208101359091169060400135610d84565b6101516004803603604081101561040657600080fd5b506001600160a01b038135169060200135610fa2565b6101516004803603604081101561043257600080fd5b506001600160a01b038135169060200135610fe1565b6101516004803603604081101561045e57600080fd5b506001600160a01b038135169060200135611020565b6101516004803603604081101561048a57600080fd5b506001600160a01b03813516906020013561105f565b8180156104ac57600080fd5b5061022e600480360360608110156104c357600080fd5b506001600160a01b0381358116916020810135909116906040013561109e565b8180156104ef57600080fd5b5061022e6004803603606081101561050657600080fd5b506001600160a01b0381358116916020810135821691604090910135166112cc565b81801561053457600080fd5b5061022e6004803603606081101561054b57600080fd5b506001600160a01b03813581169160208101359091169060400135611596565b81801561057757600080fd5b5061022e6004803603606081101561058e57600080fd5b506001600160a01b038135811691602081013590911690604001356117b4565b604080516024808201939093528151808203909301835260440190526020810180516001600160e01b031663317afabb60e21b179052909160009190565b60606105f88383610b05565b92505050836001600160a01b0316638f6f0332836000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610679578181015183820152602001610661565b50505050905090810190601f1680156106a65780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b1580156106c757600080fd5b505af11580156106db573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052602081101561070457600080fd5b8101908080516040519392919084600160201b82111561072357600080fd5b90830190602082018581111561073857600080fd5b8251600160201b81118282018810171561075157600080fd5b82525081516020918201929091019080838360005b8381101561077e578181015183820152602001610766565b50505050905090810190601f1680156107ab5780820380516001836020036101000a031916815260200191505b5060405250505080602001905160208110156107c657600080fd5b50511561080b576040805162461bcd60e51b815260206004820152600e60248201526d115e1a5d1a5b99c819985a5b195960921b604482015290519081900360640190fd5b50505050565b606061081d8383610fa2565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561089e578181015183820152602001610886565b50505050905090810190601f1680156108cb5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b1580156108ec57600080fd5b505af1158015610900573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052602081101561092957600080fd5b8101908080516040519392919084600160201b82111561094857600080fd5b90830190602082018581111561095d57600080fd5b8251600160201b81118282018810171561097657600080fd5b82525081516020918201929091019080838360005b838110156109a357818101518382015260200161098b565b50505050905090810190601f1680156109d05780820380516001836020036101000a031916815260200191505b5060405250505080602001905160208110156109eb57600080fd5b50511561080b576040805162461bcd60e51b815260206004820152600b60248201526a135a5b9d0819985a5b195960aa1b604482015290519081900360640190fd5b6040805160018082528183019092526000918291606091829190602080830190803683370190505090508581600081518110610a6557fe5b6001600160a01b039092166020928302919091018201526040516024810182815283516044830152835160609385938392606490910191858101910280838360005b83811015610abf578181015183820152602001610aa7565b50506040805193909501838103601f1901845290945250602081018051631853304760e31b6001600160e01b039091161790529a9c60009c509950505050505050505050565b604080516001600160a01b039390931660248085019190915281518085039091018152604490930190526020820180516001600160e01b0316630ede4edd60e41b1790529160009190565b6040805160048152602481019091526020810180516001600160e01b0316632726cff560e11b1790529192909190565b6060610b8c8383610b50565b92505050836001600160a01b0316638f6f03328484846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610c0c578181015183820152602001610bf4565b50505050905090810190601f168015610c395780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b158015610c5a57600080fd5b505af1158015610c6e573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526020811015610c9757600080fd5b8101908080516040519392919084600160201b821115610cb657600080fd5b908301906020820185811115610ccb57600080fd5b8251600160201b811182820188101715610ce457600080fd5b82525081516020918201929091019080838360005b83811015610d11578181015183820152602001610cf9565b50505050905090810190601f168015610d3e5780820380516001836020036101000a031916815260200191505b506040525050505050505050565b6060610b8c83835b6040805160048152602481019091526020810180516001600160e01b0316631249c58b60e01b1790529192909190565b6060610d908383611020565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610e11578181015183820152602001610df9565b50505050905090810190601f168015610e3e5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b158015610e5f57600080fd5b505af1158015610e73573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526020811015610e9c57600080fd5b8101908080516040519392919084600160201b821115610ebb57600080fd5b908301906020820185811115610ed057600080fd5b8251600160201b811182820188101715610ee957600080fd5b82525081516020918201929091019080838360005b83811015610f16578181015183820152602001610efe565b50505050905090810190601f168015610f435780820380516001836020036101000a031916815260200191505b506040525050508060200190516020811015610f5e57600080fd5b50511561080b576040805162461bcd60e51b815260206004820152600d60248201526c14995919595b4819985a5b1959609a1b604482015290519081900360640190fd5b6040805160248082018490528251808303909101815260449091019091526020810180516001600160e01b031663140e25ad60e31b1790529192909190565b6040805160248082018490528251808303909101815260449091019091526020810180516001600160e01b031663852a12e360e01b1790529192909190565b6040805160248082018490528251808303909101815260449091019091526020810180516001600160e01b031663db006a7560e01b1790529192909190565b6040805160248082018490528251808303909101815260449091019091526020810180516001600160e01b031663073a938160e11b1790529192909190565b60606110aa8383610fe1565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561112b578181015183820152602001611113565b50505050905090810190601f1680156111585780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561117957600080fd5b505af115801561118d573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156111b657600080fd5b8101908080516040519392919084600160201b8211156111d557600080fd5b9083019060208201858111156111ea57600080fd5b8251600160201b81118282018810171561120357600080fd5b82525081516020918201929091019080838360005b83811015611230578181015183820152602001611218565b50505050905090810190601f16801561125d5780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561127857600080fd5b50511561080b576040805162461bcd60e51b815260206004820152601860248201527f52656465656d20756e6465726c79696e67206661696c65640000000000000000604482015290519081900360640190fd5b60606112d88383610a2d565b925050506060846001600160a01b0316638f6f0332846000856040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561135b578181015183820152602001611343565b50505050905090810190601f1680156113885780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b1580156113a957600080fd5b505af11580156113bd573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156113e657600080fd5b8101908080516040519392919084600160201b82111561140557600080fd5b90830190602082018581111561141a57600080fd5b8251600160201b81118282018810171561143357600080fd5b82525081516020918201929091019080838360005b83811015611460578181015183820152602001611448565b50505050905090810190601f16801561148d5780820380516001836020036101000a031916815260200191505b5060405250505080602001905160208110156114a857600080fd5b8101908080516040519392919084600160201b8211156114c757600080fd5b9083019060208201858111156114dc57600080fd5b82518660208202830111600160201b821117156114f857600080fd5b82525081516020918201928201910280838360005b8381101561152557818101518382015260200161150d565b5050505090500160405250505090508060008151811061154157fe5b602002602001015160001461158f576040805162461bcd60e51b815260206004820152600f60248201526e115b9d195c9a5b99c819985a5b1959608a1b604482015290519081900360640190fd5b5050505050565b60606115a283836105ae565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561162357818101518382015260200161160b565b50505050905090810190601f1680156116505780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561167157600080fd5b505af1158015611685573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156116ae57600080fd5b8101908080516040519392919084600160201b8211156116cd57600080fd5b9083019060208201858111156116e257600080fd5b8251600160201b8111828201881017156116fb57600080fd5b82525081516020918201929091019080838360005b83811015611728578181015183820152602001611710565b50505050905090810190601f1680156117555780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561177057600080fd5b50511561080b576040805162461bcd60e51b815260206004820152600d60248201526c109bdc9c9bddc819985a5b1959609a1b604482015290519081900360640190fd5b60606117c0838361105f565b92505050836001600160a01b0316638f6f0332846000846040518463ffffffff1660e01b815260040180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015611841578181015183820152602001611829565b50505050905090810190601f16801561186e5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561188f57600080fd5b505af11580156118a3573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405260208110156118cc57600080fd5b8101908080516040519392919084600160201b8211156118eb57600080fd5b90830190602082018581111561190057600080fd5b8251600160201b81118282018810171561191957600080fd5b82525081516020918201929091019080838360005b8381101561194657818101518382015260200161192e565b50505050905090810190601f1680156119735780820380516001836020036101000a031916815260200191505b50604052505050806020019051602081101561198e57600080fd5b50511561080b576040805162461bcd60e51b815260206004820152600c60248201526b14995c185e4819985a5b195960a21b604482015290519081900360640190fdfea264697066735822122032c7491ea3a968c3b0f6143c85651a14144a46c61ef8959a2388b02575b0814164736f6c634300060a0033", + "deployedBytecode": "", "linkReferences": {}, "deployedLinkReferences": {} } diff --git a/external/abi/set/CompoundWrapV2Adapter.json b/external/abi/set/CompoundWrapV2Adapter.json new file mode 100644 index 00000000..7446b728 --- /dev/null +++ b/external/abi/set/CompoundWrapV2Adapter.json @@ -0,0 +1,149 @@ +{ + "contractName": "CompoundWrapV2Adapter", + "abi": [ + { + "inputs": [], + "name": "ETH_TOKEN_ADDRESS", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "_wrappedToken", + "type": "address" + } + ], + "name": "getSpenderAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "_wrappedToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_wrappedTokenUnits", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "getUnwrapCallData", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_underlyingToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_wrappedToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_underlyingUnits", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "getWrapCallData", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b506105d7806100206000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c80631878d1f11461005157806390f0f9381461006f578063d91462ca14610091578063de68a3da146100a4575b600080fd5b6100596100b7565b604051610066919061048c565b60405180910390f35b61008261007d366004610338565b6100cf565b604051610066939291906104a0565b61008261009f366004610338565b61017f565b6100596100b2366004610300565b6102fb565b73eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee81565b600080606080876001600160a01b031673__$059b1e3c35e6526bf44b3e0b6a2a76e329$__63a2f976589091896040518363ffffffff1660e01b81526004016101199291906104e9565b60006040518083038186803b15801561013157600080fd5b505af4158015610145573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261016d91908101906103f7565b999b60009b5098505050505050505050565b600080606081816001600160a01b038a1673eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee141561024e57604051636d4aada560e01b815288925073__$059b1e3c35e6526bf44b3e0b6a2a76e329$__90636d4aada5906101f0906001600160a01b038d169086906004016104e9565b60006040518083038186803b15801561020857600080fd5b505af415801561021c573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261024491908101906103f7565b92506102ed915050565b604051639653f87960e01b81526000925073__$059b1e3c35e6526bf44b3e0b6a2a76e329$__90639653f87990610294906001600160a01b038d16908c906004016104e9565b60006040518083038186803b1580156102ac57600080fd5b505af41580156102c0573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526102e891908101906103f7565b925050505b979990985095505050505050565b919050565b60008060408385031215610312578182fd5b823561031d81610589565b9150602083013561032d81610589565b809150509250929050565b600080600080600060a0868803121561034f578081fd5b853561035a81610589565b9450602086013561036a81610589565b935060408601359250606086013561038181610589565b9150608086013567ffffffffffffffff81111561039c578182fd5b80870188601f8201126103ad578283fd5b803591506103c26103bd83610529565b610502565b8281528960208484010111156103d6578384fd5b6103e783602083016020850161054d565b8093505050509295509295909350565b60008060006060848603121561040b578283fd5b835161041681610589565b60208501516040860151919450925067ffffffffffffffff811115610439578182fd5b80850186601f82011261044a578283fd5b8051915061045a6103bd83610529565b82815287602084840101111561046e578384fd5b61047f836020830160208501610559565b8093505050509250925092565b6001600160a01b0391909116815260200190565b600060018060a01b03851682528360208301526060604083015282518060608401526104d3816080850160208701610559565b601f01601f191691909101608001949350505050565b6001600160a01b03929092168252602082015260400190565b60405181810167ffffffffffffffff8111828210171561052157600080fd5b604052919050565b600067ffffffffffffffff82111561053f578081fd5b50601f01601f191660200190565b82818337506000910152565b60005b8381101561057457818101518382015260200161055c565b83811115610583576000848401525b50505050565b6001600160a01b038116811461059e57600080fd5b5056fea2646970667358221220b418be267fc5f641d13c4fe41ff4b575c1124db227c8a52d125bfb68180cef1464736f6c634300060a0033", + "deployedBytecode": "", + "linkReferences": {}, + "deployedLinkReferences": {} + } + \ No newline at end of file diff --git a/external/contracts/set/Compound.sol b/external/contracts/set/Compound.sol index b31dd871..17001085 100644 --- a/external/contracts/set/Compound.sol +++ b/external/contracts/set/Compound.sol @@ -1,5 +1,5 @@ /* - Copyright 2020 Set Labs Inc. + Copyright 2021 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. @@ -145,7 +145,7 @@ library Compound { } /** - * Get redeem calldata + * Get redeem underlying calldata */ function getRedeemUnderlyingCalldata( ICErc20 _cToken, @@ -166,9 +166,38 @@ library Compound { */ function invokeRedeemUnderlying(ISetToken _setToken, ICErc20 _cToken, uint256 _redeemNotional) external { ( , , bytes memory redeemUnderlyingCalldata) = getRedeemUnderlyingCalldata(_cToken, _redeemNotional); - + require( abi.decode(_setToken.invoke(address(_cToken), 0, redeemUnderlyingCalldata), (uint256)) == 0, + "Redeem underlying failed" + ); + } + + /** + * Get redeem calldata + */ + function getRedeemCalldata( + ICErc20 _cToken, + uint256 _redeemNotional + ) + public + pure + returns (address, uint256, bytes memory) + { + bytes memory callData = abi.encodeWithSignature("redeem(uint256)", _redeemNotional); + + return (address(_cToken), _redeemNotional, callData); + } + + + /** + * Invoke redeem from the SetToken + */ + function invokeRedeem(ISetToken _setToken, ICErc20 _cToken, uint256 _redeemNotional) external { + ( , , bytes memory redeemCalldata) = getRedeemCalldata(_cToken, _redeemNotional); + + require( + abi.decode(_setToken.invoke(address(_cToken), 0, redeemCalldata), (uint256)) == 0, "Redeem failed" ); } diff --git a/external/contracts/set/CompoundWrapV2Adapter.sol b/external/contracts/set/CompoundWrapV2Adapter.sol new file mode 100644 index 00000000..c20cd2bf --- /dev/null +++ b/external/contracts/set/CompoundWrapV2Adapter.sol @@ -0,0 +1,112 @@ +/* + Copyright 2021 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"; + +import { Compound } from "./Compound.sol"; +import { ICErc20 } from "../../../contracts/interfaces/ICErc20.sol"; +import "hardhat/console.sol"; + +/** + * @title CompoundWrapV2Adapter + * @author Set Protocol + * + * Wrap adapter for Compound that returns data for wraps/unwraps of tokens + */ +contract CompoundWrapV2Adapter { + using Compound for ICErc20; + + + /* ============ Constants ============ */ + + // Compound Mock address to indicate ETH. ETH is used directly in Compound protocol (instead of an abstraction such as WETH) + address public constant ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /* ============ External Getter Functions ============ */ + + /** + * Generates the calldata to wrap an underlying asset into a wrappedToken. + * + * @param _underlyingToken Address of the component to be wrapped + * @param _wrappedToken Address of the desired wrapped token + * @param _underlyingUnits Total quantity of underlying units to wrap + * + * @return address Target contract address + * @return uint256 Total quantity of underlying units (if underlying is ETH) + * @return bytes Wrap calldata + */ + function getWrapCallData( + address _underlyingToken, + address _wrappedToken, + uint256 _underlyingUnits, + address /* _to */, + bytes memory /* _wrapData */ + ) + external + pure + returns (address, uint256, bytes memory) + { + uint256 value; + bytes memory callData; + if (_underlyingToken == ETH_TOKEN_ADDRESS) { + value = _underlyingUnits; + ( , , callData) = ICErc20(_wrappedToken).getMintCEtherCalldata(_underlyingUnits); + } else { + value = 0; + ( , , callData) = ICErc20(_wrappedToken).getMintCTokenCalldata(_underlyingUnits); + } + + return (_wrappedToken, value, callData); + } + + /** + * Generates the calldata to unwrap a wrapped asset into its underlying. + * + * @param _wrappedToken Address of the component to be unwrapped + * @param _wrappedTokenUnits Total quantity of wrapped token units to unwrap + * + * @return address Target contract address + * @return uint256 Total quantity of wrapped token units to unwrap. This will always be 0 for unwrapping + * @return bytes Unwrap calldata + */ + function getUnwrapCallData( + address /* _underlyingToken */, + address _wrappedToken, + uint256 _wrappedTokenUnits, + address /* _to */, + bytes memory /* _unwrapData */ + ) + external + pure + returns (address, uint256, bytes memory) + { + ( , , bytes memory callData) = ICErc20(_wrappedToken).getRedeemCalldata(_wrappedTokenUnits); + return (_wrappedToken, 0, callData); + } + + /** + * Returns the address to approve source tokens for wrapping. + * @param _wrappedToken Address of the wrapped token + * @return address Address of the contract to approve tokens to + */ + function getSpenderAddress(address /* _underlyingToken */, address _wrappedToken) external pure returns(address) { + return address(_wrappedToken); + } + +} diff --git a/test/exchangeIssuance/flashMintWrapped.spec.ts b/test/exchangeIssuance/flashMintWrapped.spec.ts new file mode 100644 index 00000000..f65437c7 --- /dev/null +++ b/test/exchangeIssuance/flashMintWrapped.spec.ts @@ -0,0 +1,1366 @@ +import "module-alias/register"; +import { Address, Account } from "@utils/types"; +import { ADDRESS_ZERO, MAX_UINT_256, ZERO, ZERO_BYTES } from "@utils/constants"; +import { SetToken } from "@utils/contracts/setV2"; +import { + cacheBeforeEach, + ether, + getAccounts, + getSetFixture, + getWaffleExpect, + getUniswapFixture, + getUniswapV3Fixture, + getCompoundFixture, +} from "@utils/index"; +import DeployHelper from "@utils/deploys"; +import { usdc, UnitsUtils } from "@utils/common/unitsUtils"; +import { SetFixture, UniswapFixture, UniswapV3Fixture } from "@utils/fixtures"; +import { BigNumber } from "ethers"; +import { StandardTokenMock, WETH9 } from "@utils/contracts/index"; +import { getTxFee } from "@utils/test"; +import { ethers } from "hardhat"; +import { FlashMintWrapped } from "@typechain/FlashMintWrapped"; +import { CERc20 } from "@typechain/CERc20"; +import { IERC20 } from "@typechain/IERC20"; +import { IERC20__factory } from "@typechain/factories/IERC20__factory"; +import { expectThrowsAsync, getLastBlockTimestamp } from "@utils/test/testingUtils"; + +const expect = getWaffleExpect(); + +//#region types, consts +const compoundWrapAdapterIntegrationName: string = "CompoundWrapV2Adapter"; + +enum Exchange { + None, + Quickswap, + Sushiswap, + UniV3, +} + +type SwapData = { + path: Address[]; + fees: number[]; + pool: Address; + exchange: Exchange; +}; + +type ComponentSwapData = { + // unwrapped token version, e.g. DAI + underlyingERC20: Address; + + // // swap data for DEX operation: fees, path, etc. see DEXAdapter.SwapData + dexData: SwapData; + + // ONLY relevant for issue, not used for redeem: + // amount that has to be bought of the unwrapped token version (to cover required wrapped component amounts for issuance) + // this amount has to be computed beforehand through the exchange rate of wrapped Component <> unwrappedComponent + // i.e. getRequiredComponentIssuanceUnits() on the IssuanceModule and then convert units through exchange rate to unwrapped component units + // e.g. 300 cDAI needed for issuance of 1 Set token. exchange rate 1cDAI = 0.05 DAI. -> buyUnderlyingAmount = 0.05 DAI * 300 = 15 DAI + buyUnderlyingAmount: BigNumber; +}; + +type ComponentWrapData = { + integrationName: string; // wrap adapter integration name as listed in the IntegrationRegistry for the wrapModule + wrapData: string; // optional wrapData passed to the wrapAdapter +}; + +enum SetTokenMix { + WRAPPED_UNWRAPPED_MIXED = "WRAPPED_UNWRAPPED_MIXED", + UNWRAPPED_ONLY = "UNWRAPPED_ONLY", + WRAPPED_ONLY = "WRAPPED_ONLY", +} + +//#endregion + +//#region testHelper +class TestHelper { + readonly setTokenInitialBalance: BigNumber = ether(1); + readonly ethAddress: Address = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; + + owner: Account; + methodologist: Account; + + setV2Setup: SetFixture; + deployer: DeployHelper; + setToken: SetToken; + + controllerAddress: Address; + + sushiswap: UniswapFixture; + uniswapV3: UniswapV3Fixture; + + cDAI: CERc20; + cUSDC: CERc20; + cUSDT: CERc20; + + get issuanceModule() { + return this.setV2Setup.debtIssuanceModule; + } + + ownerBalanceOf(token: StandardTokenMock | IERC20 | CERc20 | WETH9 | "ETH") { + return token === "ETH" ? this.owner.wallet.getBalance() : token.balanceOf(this.owner.address); + } + + async init() { + [this.owner, this.methodologist] = await getAccounts(); + this.deployer = new DeployHelper(this.owner.wallet); + } + + async defaultSetV2Setup() { + this.setV2Setup = getSetFixture(this.owner.address); + await this.setV2Setup.initialize(); + + this.controllerAddress = this.setV2Setup.controller.address; + + // deploy CompoundWrapV2Adapter + const compoundWrapAdapter = await this.deployer.setV2.deployCompoundWrapV2Adapter(); + await this.setV2Setup.integrationRegistry.addIntegration( + this.setV2Setup.wrapModule.address, + compoundWrapAdapterIntegrationName, + compoundWrapAdapter.address, + ); + } + + async compoundSetup() { + const compoundSetup = getCompoundFixture(this.owner.address); + await compoundSetup.initialize(); + + // Mint cTokens + this.cUSDC = await compoundSetup.createAndEnableCToken( + this.setV2Setup.usdc.address, + 100000000000000, + compoundSetup.comptroller.address, + compoundSetup.interestRateModel.address, + "Compound USDC", + "cUSDC", + 8, + ether(0.75), // 75% collateral factor + ether(1000), // IMPORTANT: Compound oracles account for decimals scaled by 10e18. For USDC, this is $1 * 10^18 * 10^18 / 10^6 = 10^30 + ); + + this.cDAI = await compoundSetup.createAndEnableCToken( + this.setV2Setup.dai.address, + 100000000000000, + compoundSetup.comptroller.address, + compoundSetup.interestRateModel.address, + "Compound DAI", + "cDAI", + 8, + ether(0.75), // 75% collateral factor + ether(1000), + ); + + this.cUSDT = await compoundSetup.createAndEnableCToken( + this.setV2Setup.usdt.address, + 100000000000000, + compoundSetup.comptroller.address, + compoundSetup.interestRateModel.address, + "Compound USDT", + "cUSDT", + 8, + ether(0.75), // 75% collateral factor + ether(1000), + ); + await compoundSetup.comptroller._setCompRate(ether(1)); + await compoundSetup.comptroller._addCompMarkets([ + this.cDAI.address, + this.cUSDC.address, + this.cUSDT.address, + ]); + + await this.setV2Setup.dai.approve(this.cDAI.address, MAX_UINT_256); + await this.setV2Setup.usdc.approve(this.cUSDC.address, MAX_UINT_256); + await this.setV2Setup.usdt.approve(this.cUSDT.address, MAX_UINT_256); + + await this.cDAI.mint(ether(100)); + await this.cUSDC.mint(ether(100)); + await this.cUSDT.mint(ether(100)); + } + + async createSetToken(set_token_mix: SetTokenMix) { + let tokens: string[] = []; + let units: BigNumber[] = []; + if (set_token_mix === SetTokenMix.WRAPPED_ONLY) { + tokens = [this.cDAI.address, this.cUSDC.address, this.cUSDT.address]; + units = [ + BigNumber.from(200_000_000), + BigNumber.from(300_000_000), + BigNumber.from(100_000_000), + ]; + } else if (set_token_mix === SetTokenMix.WRAPPED_UNWRAPPED_MIXED) { + tokens = [this.cDAI.address, this.setV2Setup.usdc.address, this.cUSDT.address]; + units = [BigNumber.from(20_000_000), BigNumber.from(300_000), BigNumber.from(10_000_000)]; + } else if (set_token_mix === SetTokenMix.UNWRAPPED_ONLY) { + tokens = [ + this.setV2Setup.dai.address, + this.setV2Setup.usdc.address, + this.setV2Setup.usdt.address, + ]; + units = [ether(2), BigNumber.from(300_000), BigNumber.from(100_000)]; + } + + this.setToken = await this.setV2Setup.createSetToken(tokens, units, [ + this.issuanceModule.address, + this.setV2Setup.streamingFeeModule.address, + this.setV2Setup.wrapModule.address, + ]); + } + + async issueInitialSetTokens() { + await this.cDAI.connect(this.owner.wallet).approve(this.issuanceModule.address, MAX_UINT_256); + await this.cUSDC.connect(this.owner.wallet).approve(this.issuanceModule.address, MAX_UINT_256); + await this.cUSDT.connect(this.owner.wallet).approve(this.issuanceModule.address, MAX_UINT_256); + await this.setV2Setup.dai + .connect(this.owner.wallet) + .approve(this.issuanceModule.address, MAX_UINT_256); + await this.setV2Setup.usdc + .connect(this.owner.wallet) + .approve(this.issuanceModule.address, MAX_UINT_256); + await this.setV2Setup.usdt + .connect(this.owner.wallet) + .approve(this.issuanceModule.address, MAX_UINT_256); + + await this.issuanceModule.issue( + this.setToken.address, + this.setTokenInitialBalance, + this.owner.address, + ); + } + + async deployManager() { + const baseManagerV2 = await this.deployer.manager.deployBaseManagerV2( + this.setToken.address, + this.owner.address, + this.methodologist.address, + ); + await baseManagerV2.connect(this.methodologist.wallet).authorizeInitialization(); + // Transfer ownership to manager + if ((await this.setToken.manager()) == this.owner.address) { + await this.setToken.connect(this.owner.wallet).setManager(baseManagerV2.address); + } + } + + async initializeContracts() { + // Initialize modules + await this.issuanceModule.initialize( + this.setToken.address, + ether(1), + ZERO, + ZERO, + this.owner.address, + ADDRESS_ZERO, + ); + + const streamingFeeSettings = { + feeRecipient: this.owner.address, + maxStreamingFeePercentage: ether(0.1), + streamingFeePercentage: ether(0.02), + lastStreamingFeeTimestamp: ZERO, + }; + await this.setV2Setup.streamingFeeModule.initialize( + this.setToken.address, + streamingFeeSettings, + ); + + await this.setV2Setup.wrapModule.initialize(this.setToken.address); + } + + async setupDexes() { + this.sushiswap = getUniswapFixture(this.owner.address); + await this.sushiswap.initialize( + this.owner, + this.setV2Setup.weth.address, + this.setV2Setup.wbtc.address, + this.setV2Setup.dai.address, + ); + + this.uniswapV3 = getUniswapV3Fixture(this.owner.address); + await this.uniswapV3.initialize( + this.owner, + this.setV2Setup.weth, + 3000, + this.setV2Setup.wbtc, + 40000, + this.setV2Setup.dai, + ); + } + + async seedDexLiquidity() { + // uniV3: WETH <-> USDC, WETH <-> USDT and WETH <-> DAI + await this.setV2Setup.weth.approve(this.uniswapV3.nftPositionManager.address, MAX_UINT_256); + await this.setV2Setup.dai.approve(this.uniswapV3.nftPositionManager.address, MAX_UINT_256); + await this.setV2Setup.usdc.approve(this.uniswapV3.nftPositionManager.address, MAX_UINT_256); + await this.setV2Setup.usdt.approve(this.uniswapV3.nftPositionManager.address, MAX_UINT_256); + + // usdc + await this.uniswapV3.createNewPair(this.setV2Setup.weth, this.setV2Setup.usdc, 3000, 3000); + await this.uniswapV3.addLiquidityWide( + this.setV2Setup.weth, + this.setV2Setup.usdc, + 3000, + ether(1000), + usdc(300_000_000), + this.owner.address, + ); + + // usdt + await this.uniswapV3.createNewPair(this.setV2Setup.weth, this.setV2Setup.usdt, 3000, 3000); + await this.uniswapV3.addLiquidityWide( + this.setV2Setup.weth, + this.setV2Setup.usdt, + 3000, + ether(1000), + usdc(300_000_000), + this.owner.address, + ); + + // dai + await this.uniswapV3.createNewPair(this.setV2Setup.weth, this.setV2Setup.dai, 3000, 3000); + await this.uniswapV3.addLiquidityWide( + this.setV2Setup.weth, + this.setV2Setup.dai, + 3000, + ether(10000), + ether(300000), + this.owner.address, + ); + } + + async deployFlashMintWrappedExtension(): Promise { + return await this.deployer.extensions.deployFlashMintWrappedExtension( + this.setV2Setup.weth.address, + ADDRESS_ZERO, // quickswap router + this.sushiswap.router.address, + this.uniswapV3.swapRouter.address, + this.uniswapV3.quoter.address, + ADDRESS_ZERO, // curveCalculatorAddress + ADDRESS_ZERO, // curveAddressProviderAddress + this.controllerAddress, + this.issuanceModule.address, + this.setV2Setup.wrapModule.address, + ); + } + + async getRequiredIssuanceInputAmount( + setToken: Address, + inputToken: Address, + issueSetAmount: BigNumber, + componentSwapData: ComponentSwapData[], + flashMintContract: FlashMintWrapped, + tolerancePercentage: number = 1, // 1% tolerance + ) { + const estimatedInputAmount: BigNumber = await flashMintContract.callStatic.getIssueExactSet( + setToken, + inputToken, + issueSetAmount, + componentSwapData, + ); + + // add some slight tolerance to the inputAmount to cover minor pricing changes or slippage until + // tx is actually executed + return estimatedInputAmount + .mul(BigNumber.from(100 + tolerancePercentage)) + .div(BigNumber.from(100)); + } + + async getIssuanceComponentSwapData( + inputToken: Address, + issueSetAmount: BigNumber, + set_token_mix: SetTokenMix, + setToken = this.setToken.address, + ) { + // get required issuance components + const [ + issuanceComponents, // cDAI, cUSDC and cUSDT, in that order + issuanceUnits, + ] = await this.issuanceModule.getRequiredComponentIssuanceUnits(setToken, issueSetAmount); + + if ( + JSON.stringify([this.cDAI.address, this.cUSDC.address, this.cUSDT.address]) !== + JSON.stringify(issuanceComponents) && + JSON.stringify([this.cDAI.address, this.setV2Setup.usdc.address, this.cUSDT.address]) !== + JSON.stringify(issuanceComponents) && + JSON.stringify([ + this.setV2Setup.dai.address, + this.setV2Setup.usdc.address, + this.setV2Setup.usdt.address, + ]) !== JSON.stringify(issuanceComponents) + ) { + throw new Error("issuance components test case not implemented"); + } + + // get exchange rates for each of the cTokens + // RETURN: The current exchange rate as an unsigned integer, scaled by 1 * 10^(18 - 8 + Underlying Token Decimals). + // => 1e18 + const exchangeRateDAI = await this.cDAI.callStatic.exchangeRateCurrent(); + const exchangeRateUSDC = await this.cUSDC.callStatic.exchangeRateCurrent(); + const exchangeRateUSDT = await this.cUSDT.callStatic.exchangeRateCurrent(); + + // required cTokens each = 100.000 + // precision good enough for test case, should be done exact in JS library + const requiredDAI = issuanceUnits[0].mul(exchangeRateDAI.div(1e6)).div(1e12); + const requiredUSDC = issuanceUnits[1].mul(exchangeRateUSDC.div(1e6)).div(1e12); + const requiredUSDT = issuanceUnits[2].mul(exchangeRateUSDT.div(1e6)).div(1e12); + + const componentSwapData: ComponentSwapData[] = [ + { + underlyingERC20: this.setV2Setup.dai.address, + buyUnderlyingAmount: + set_token_mix !== SetTokenMix.UNWRAPPED_ONLY ? requiredDAI : issuanceUnits[0], + dexData: { + exchange: Exchange.UniV3, + path: + inputToken === this.setV2Setup.weth.address + ? [inputToken, this.setV2Setup.dai.address] + : [inputToken, this.setV2Setup.weth.address, this.setV2Setup.dai.address], + fees: inputToken === this.setV2Setup.weth.address ? [3000] : [3000, 3000], + pool: ADDRESS_ZERO, + }, + }, + { + underlyingERC20: this.setV2Setup.usdc.address, + // cUSDC is only used in test case WRAPPED_ONLY + buyUnderlyingAmount: + set_token_mix === SetTokenMix.WRAPPED_ONLY ? requiredUSDC : issuanceUnits[1], + dexData: { + exchange: Exchange.UniV3, + path: + inputToken === this.setV2Setup.weth.address + ? [inputToken, this.setV2Setup.usdc.address] + : [inputToken, this.setV2Setup.weth.address, this.setV2Setup.usdc.address], + fees: inputToken === this.setV2Setup.weth.address ? [3000] : [3000, 3000], + pool: ADDRESS_ZERO, + }, + }, + { + underlyingERC20: this.setV2Setup.usdt.address, + buyUnderlyingAmount: + set_token_mix !== SetTokenMix.UNWRAPPED_ONLY ? requiredUSDT : issuanceUnits[2], + dexData: { + exchange: Exchange.UniV3, + path: + inputToken === this.setV2Setup.weth.address + ? [inputToken, this.setV2Setup.usdt.address] + : [inputToken, this.setV2Setup.weth.address, this.setV2Setup.usdt.address], + fees: inputToken === this.setV2Setup.weth.address ? [3000] : [3000, 3000], + pool: ADDRESS_ZERO, + }, + }, + ]; + + return componentSwapData; + } + + getWrapData(set_token_mix: SetTokenMix): ComponentWrapData[] { + if (set_token_mix === SetTokenMix.WRAPPED_ONLY) { + return [ + { + integrationName: compoundWrapAdapterIntegrationName, + wrapData: ZERO_BYTES, + }, + { + integrationName: compoundWrapAdapterIntegrationName, + wrapData: ZERO_BYTES, + }, + { + integrationName: compoundWrapAdapterIntegrationName, + wrapData: ZERO_BYTES, + }, + ]; + } else if (set_token_mix === SetTokenMix.WRAPPED_UNWRAPPED_MIXED) { + return [ + { + integrationName: compoundWrapAdapterIntegrationName, + wrapData: ZERO_BYTES, + }, + { + integrationName: "", + wrapData: ZERO_BYTES, + }, + { + integrationName: compoundWrapAdapterIntegrationName, + wrapData: ZERO_BYTES, + }, + ]; + } else if (set_token_mix === SetTokenMix.UNWRAPPED_ONLY) { + return [ + { + integrationName: "", + wrapData: ZERO_BYTES, + }, + { + integrationName: "", + wrapData: ZERO_BYTES, + }, + { + integrationName: "", + wrapData: ZERO_BYTES, + }, + ]; + } + + throw new Error("NOT IMPLEMENTED"); + } + + async getRedemptionComponentSwapData(outputToken: Address) { + const componentSwapData: ComponentSwapData[] = [ + { + underlyingERC20: this.setV2Setup.dai.address, + buyUnderlyingAmount: ZERO, // not used in redeem + dexData: { + exchange: Exchange.UniV3, + path: + outputToken === this.setV2Setup.weth.address + ? [this.setV2Setup.dai.address, outputToken] + : [this.setV2Setup.dai.address, this.setV2Setup.weth.address, outputToken], + fees: outputToken === this.setV2Setup.weth.address ? [3000] : [3000, 3000], + pool: ADDRESS_ZERO, + }, + }, + { + underlyingERC20: this.setV2Setup.usdc.address, + buyUnderlyingAmount: ZERO, // not used in redeem + dexData: { + exchange: Exchange.UniV3, + path: + outputToken === this.setV2Setup.weth.address + ? [this.setV2Setup.usdc.address, outputToken] + : [this.setV2Setup.usdc.address, this.setV2Setup.weth.address, outputToken], + fees: outputToken === this.setV2Setup.weth.address ? [3000] : [3000, 3000], + pool: ADDRESS_ZERO, + }, + }, + { + underlyingERC20: this.setV2Setup.usdt.address, + buyUnderlyingAmount: ZERO, // not used in redeem + dexData: { + exchange: Exchange.UniV3, + path: + outputToken === this.setV2Setup.weth.address + ? [this.setV2Setup.usdt.address, outputToken] + : [this.setV2Setup.usdt.address, this.setV2Setup.weth.address, outputToken], + fees: outputToken === this.setV2Setup.weth.address ? [3000] : [3000, 3000], + pool: ADDRESS_ZERO, + }, + }, + ]; + + return componentSwapData; + } + + async getRedemptionMinAmountOutput( + setToken: Address, + outputToken: Address, + redeemSetAmount: BigNumber, + componentSwapData: ComponentSwapData[], + flashMintContract: FlashMintWrapped, + set_token_mix: SetTokenMix, + tolerancePercentage: number = 1, // 1% tolerance + ) { + // get received redemption components + const [, redemptionUnits] = await this.issuanceModule.getRequiredComponentRedemptionUnits( + setToken, + redeemSetAmount, + ); + + // get exchange rates for each of the cTokens + const exchangeRateDAI = await this.cDAI.callStatic.exchangeRateCurrent(); + const exchangeRateUSDC = await this.cUSDC.callStatic.exchangeRateCurrent(); + const exchangeRateUSDT = await this.cUSDT.callStatic.exchangeRateCurrent(); + + const expectedDAI = redemptionUnits[0].mul(exchangeRateDAI.div(1e6)).div(1e12); + const expectedUSDC = redemptionUnits[1].mul(exchangeRateUSDC.div(1e6)).div(1e12); + const expectedUSDT = redemptionUnits[2].mul(exchangeRateUSDT.div(1e6)).div(1e12); + + componentSwapData[0].buyUnderlyingAmount = + set_token_mix !== SetTokenMix.UNWRAPPED_ONLY ? expectedDAI : redemptionUnits[0]; + componentSwapData[1].buyUnderlyingAmount = + set_token_mix === SetTokenMix.WRAPPED_ONLY ? expectedUSDC : redemptionUnits[1]; + componentSwapData[2].buyUnderlyingAmount = + set_token_mix !== SetTokenMix.UNWRAPPED_ONLY ? expectedUSDT : redemptionUnits[2]; + + const estimatedOutputAmount: BigNumber = await flashMintContract.callStatic.getRedeemExactSet( + setToken, + outputToken, + redeemSetAmount, + componentSwapData, + ); + + // add some slight tolerance to the expected output to cover minor pricing changes or slippage until + // tx is actually executed + return estimatedOutputAmount + .mul(BigNumber.from(100 - tolerancePercentage)) + .div(BigNumber.from(100)); + } + + async issueSetTokens( + flashMintContract: FlashMintWrapped, + set_token_mix: SetTokenMix, + issueSetAmount: BigNumber = ether(1000), + setToken: Address = this.setToken.address, + ) { + const inputToken = this.setV2Setup.dai; + const componentSwapData = await this.getIssuanceComponentSwapData( + inputToken.address, + issueSetAmount, + set_token_mix, + ); + + const maxAmountInputToken = await this.getRequiredIssuanceInputAmount( + setToken, + inputToken.address, + issueSetAmount, + componentSwapData, + flashMintContract, + ); + + await flashMintContract.approveSetToken(setToken); + + return await flashMintContract.issueExactSetFromERC20( + setToken, + inputToken.address, + issueSetAmount, + maxAmountInputToken, + componentSwapData, + this.getWrapData(set_token_mix), + ); + } +} +//#endregion + +// seed liquidity is +// uniV3: WETH <-> USDC, WETH <-> USDT and WETH <-> DAI +describe("FlashMintWrapped", async () => { + const testHelper = new TestHelper(); + + [...Object.keys(SetTokenMix)].forEach((set_token_mix: string | SetTokenMix) => { + describe(`\n\n\n---------\n\n CASE ${set_token_mix}: `, async () => { + set_token_mix = (set_token_mix as unknown) as SetTokenMix; + //#region basic setup, deployment & constructor + // basic test setup, Setv2, modules, set token, external protocols etc. + cacheBeforeEach(async () => { + await testHelper.init(); + await testHelper.defaultSetV2Setup(); + + // prepare external protocols + await testHelper.compoundSetup(); + await testHelper.setupDexes(); + await testHelper.seedDexLiquidity(); + + // prepare our suite + await testHelper.createSetToken(set_token_mix as SetTokenMix); + await testHelper.initializeContracts(); + await testHelper.deployManager(); + + await testHelper.issueInitialSetTokens(); + }); + + context("basic setup", async () => { + describe("#constructor", async () => { + it("should deploy & set constructor data correctly", async () => { + const flashMintContract: FlashMintWrapped = await testHelper.deployFlashMintWrappedExtension(); + + const addresses = await flashMintContract.dexAdapter(); + + expect(addresses.quickRouter).to.eq(ADDRESS_ZERO, "quickswap"); + expect(addresses.sushiRouter).to.eq(testHelper.sushiswap.router.address, "sushiswap"); + expect(addresses.uniV3Router).to.eq( + testHelper.uniswapV3.swapRouter.address, + "uniswapV3 router", + ); + expect(addresses.uniV3Quoter).to.eq( + testHelper.uniswapV3.quoter.address, + "uniswapV3 quoter", + ); + expect(addresses.curveAddressProvider).to.eq(ADDRESS_ZERO, "curveAddressProvider"); + expect(addresses.curveCalculator).to.eq(ADDRESS_ZERO, "curveCalculator"); + expect(addresses.weth).to.eq(testHelper.setV2Setup.weth.address, "weth"); + + const expectedIssuanceModuleAddress = await flashMintContract.issuanceModule(); + expect(expectedIssuanceModuleAddress).to.eq( + testHelper.issuanceModule.address, + "issuanceModule", + ); + + const expectedControllerAddress = await flashMintContract.setController(); + expect(expectedControllerAddress).to.eq(testHelper.controllerAddress, "controller"); + }); + }); + }); + //#endregion + + context("when flashMint is deployed", async () => { + let flashMintContract: FlashMintWrapped; + cacheBeforeEach(async () => { + flashMintContract = await testHelper.deployFlashMintWrappedExtension(); + + await testHelper.setV2Setup.dai + .connect(testHelper.owner.wallet) + .approve(flashMintContract.address, MAX_UINT_256); + await testHelper.setV2Setup.usdc + .connect(testHelper.owner.wallet) + .approve(flashMintContract.address, MAX_UINT_256); + await testHelper.setV2Setup.usdt + .connect(testHelper.owner.wallet) + .approve(flashMintContract.address, MAX_UINT_256); + }); + + //#region approveSetToken + describe("#approveSetToken", async () => { + async function subject(setToken: Address = testHelper.setToken.address) { + await flashMintContract.approveSetToken(setToken); + } + + it("should approve set token", async () => { + await subject(); + }); + + it("should fail when not a set token", async () => { + await expectThrowsAsync(subject(testHelper.cDAI.address), "FlashMint: INVALID_SET"); + }); + + it("should approve all Set components to issuanceModule", async () => { + // get all set components as IERC20 + const components: IERC20[] = ( + await testHelper.setToken.getComponents() + ).map(component => IERC20__factory.connect(component, ethers.provider)); + + // allowance before should be 0 + for (const component of components) { + const allowanceBefore = await component.allowance( + flashMintContract.address, + testHelper.issuanceModule.address, + ); + expect(allowanceBefore).to.equal(ZERO); + } + + await subject(); + + // allowance after should be MAX_UINT_256 + for (const component of components) { + const allowanceAfter = await ((component as unknown) as IERC20).allowance( + flashMintContract.address, + testHelper.issuanceModule.address, + ); + expect(allowanceAfter).to.equal(MAX_UINT_256); + } + }); + }); + //#endregion + + //#region getters + // all getter functions are indirectly tested through the other tests, only reverts are tested explicitly: + // getIssueExactSet + // getRedeemExactSet + describe("#getIssueExactSet", async () => { + async function subject( + issueSetAmount = ether(10), + inputToken = testHelper.setV2Setup.usdc.address, + setToken = testHelper.setToken.address, + ) { + const componentSwapData: ComponentSwapData[] = []; + await flashMintContract.getIssueExactSet( + setToken, + inputToken, + issueSetAmount, + componentSwapData, + ); + } + + // Note component swap data checks are tested through other tests (e.g. path) + + it("should revert if issueSetAmount is 0", async () => { + const revertReason = "FlashMint: INVALID_INPUTS"; + await expect(subject(ZERO)).to.be.revertedWith(revertReason); + }); + + it("should revert if input token is 0x00", async () => { + const revertReason = "FlashMint: INVALID_INPUTS"; + await expect(subject(ether(10), ADDRESS_ZERO)).to.be.revertedWith(revertReason); + }); + + it("should revert if not a set token", async () => { + const revertReason = "FlashMint: INVALID_SET"; + await expect( + subject( + ether(10), + testHelper.setV2Setup.usdc.address, + testHelper.setV2Setup.wbtc.address, + ), + ).to.be.revertedWith(revertReason); + }); + }); + + describe("#getRedeemExactSet", async () => { + async function subject( + redeeemSetAmount = ether(10), + outputToken = testHelper.setV2Setup.usdc.address, + setToken = testHelper.setToken.address, + ) { + const componentSwapData: ComponentSwapData[] = []; + await flashMintContract.getRedeemExactSet( + setToken, + outputToken, + redeeemSetAmount, + componentSwapData, + ); + } + + // Note component swap data checks are tested through other tests (e.g. path) + + it("should revert if redeeemSetAmount is 0", async () => { + const revertReason = "FlashMint: INVALID_INPUTS"; + await expect(subject(ZERO)).to.be.revertedWith(revertReason); + }); + + it("should revert if output token is 0x00", async () => { + const revertReason = "FlashMint: INVALID_INPUTS"; + await expect(subject(ether(10), ADDRESS_ZERO)).to.be.revertedWith(revertReason); + }); + + it("should revert if not a set token", async () => { + const revertReason = "FlashMint: INVALID_SET"; + await expect( + subject( + ether(10), + testHelper.setV2Setup.usdc.address, + testHelper.setV2Setup.wbtc.address, + ), + ).to.be.revertedWith(revertReason); + }); + }); + //#endregion + + //#region issue + ["DAI", "USDC", "ETH"].forEach(tokenName => { + describe(`-------- #issueExactSetFrom${ + tokenName !== "ETH" ? "ERC20" : tokenName + }: paying with ${tokenName} \n`, async () => { + //#region issue setup + let issueSetAmount: BigNumber; + let setToken: Address; + let maxAmountInputToken: BigNumber; + let componentSwapData: ComponentSwapData[]; + let componentWrapData: ComponentWrapData[]; + let inputToken: StandardTokenMock | WETH9; + let inputAmountSpent: BigNumber; + + beforeEach(async () => { + if (tokenName === "DAI") { + inputToken = testHelper.setV2Setup.dai; + } else if (tokenName === "USDC") { + inputToken = testHelper.setV2Setup.usdc; + } else if (tokenName === "ETH") { + // for ETH, input token for swaps etc. is WETH + inputToken = testHelper.setV2Setup.weth; + } else { + throw new Error("test case not implemented!"); + } + + issueSetAmount = ether(10000); + setToken = testHelper.setToken.address; + + componentSwapData = await testHelper.getIssuanceComponentSwapData( + inputToken.address, + issueSetAmount, + set_token_mix as SetTokenMix, + ); + + componentWrapData = testHelper.getWrapData(set_token_mix as SetTokenMix); + + maxAmountInputToken = await testHelper.getRequiredIssuanceInputAmount( + setToken, + inputToken.address, + issueSetAmount, + componentSwapData, + flashMintContract, + ); + + await flashMintContract.approveSetToken(setToken); + }); + + async function subject(overwrite_input_address?: Address) { + if (tokenName !== "ETH") { + return await flashMintContract.issueExactSetFromERC20( + setToken, + overwrite_input_address || inputToken.address, + issueSetAmount, + maxAmountInputToken, + componentSwapData, + componentWrapData, + ); + } else { + return await flashMintContract.issueExactSetFromETH( + setToken, + issueSetAmount, + componentSwapData, + componentWrapData, + { value: maxAmountInputToken }, + ); + } + } + //#endregion + + //#region issue tests with amount checks + it(`should issue from ${tokenName}`, async () => { + await subject(); + }); + + it(`should not have left over amounts in contract after issue`, async () => { + await subject(); + expect(await testHelper.cDAI.balanceOf(flashMintContract.address)).to.equal(ZERO); + expect(await testHelper.cUSDC.balanceOf(flashMintContract.address)).to.equal(ZERO); + expect(await testHelper.cUSDT.balanceOf(flashMintContract.address)).to.equal(ZERO); + expect(await testHelper.setV2Setup.dai.balanceOf(flashMintContract.address)).to.equal( + ZERO, + ); + expect( + await testHelper.setV2Setup.usdc.balanceOf(flashMintContract.address), + ).to.equal(ZERO); + expect( + await testHelper.setV2Setup.usdt.balanceOf(flashMintContract.address), + ).to.equal(ZERO); + expect( + await testHelper.setV2Setup.weth.balanceOf(flashMintContract.address), + ).to.equal(ZERO); + expect(await ethers.provider.getBalance(flashMintContract.address)).to.equal(ZERO); + }); + + it("should return the requested amount of set", async () => { + const balanceBefore = await testHelper.setToken.balanceOf(testHelper.owner.address); + await subject(); + const balanceAfter = await testHelper.setToken.balanceOf(testHelper.owner.address); + expect(balanceAfter.sub(balanceBefore)).to.equal(issueSetAmount); + }); + + it("should cost less than the max amount", async () => { + const balanceBefore = await testHelper.ownerBalanceOf( + tokenName === "ETH" ? "ETH" : inputToken, + ); + const tx = await subject(); + const balanceAfter = await testHelper.ownerBalanceOf( + tokenName === "ETH" ? "ETH" : inputToken, + ); + inputAmountSpent = balanceBefore.sub(balanceAfter); + if (tokenName == "ETH") { + const transactionFee = await getTxFee(tx); + inputAmountSpent = inputAmountSpent.sub(transactionFee); + } + + expect(inputAmountSpent.gt(0)).to.equal(true); + expect(inputAmountSpent.lt(maxAmountInputToken)).to.equal(true); + }); + + it("should emit ExchangeIssuance event", async () => { + await expect(subject()) + .to.emit(flashMintContract, "FlashMint") + .withArgs( + testHelper.owner.address, + setToken, + tokenName === "ETH" ? testHelper.ethAddress : inputToken.address, + inputAmountSpent, + issueSetAmount, + ); + }); + + it("should return excess Input amount", async () => { + maxAmountInputToken = maxAmountInputToken.mul(3); // send way more than needed + + const balanceBefore = await testHelper.ownerBalanceOf( + tokenName === "ETH" ? "ETH" : inputToken, + ); + const tx = await subject(); + const balanceAfter = await testHelper.ownerBalanceOf( + tokenName === "ETH" ? "ETH" : inputToken, + ); + let inputAmountSpentWithExcess = balanceBefore.sub(balanceAfter); + if (tokenName == "ETH") { + const transactionFee = await getTxFee(tx); + inputAmountSpentWithExcess = inputAmountSpentWithExcess.sub(transactionFee); + } + + expect(inputAmountSpent).to.equal(inputAmountSpentWithExcess); + }); + //#endregion + + //#region issue reverts + it("should revert if maxAmountInputToken is zero", async () => { + maxAmountInputToken = ZERO; + const revertReason = "FlashMint: INVALID_INPUTS"; + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if issueSetAmount is zero", async () => { + issueSetAmount = ZERO; + const revertReason = "FlashMint: INVALID_INPUTS"; + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if input token is address 0x000", async () => { + if (tokenName !== "ETH") { + const revertReason = "FlashMint: INVALID_INPUTS"; + await expect(subject(ADDRESS_ZERO)).to.be.revertedWith(revertReason); + } + }); + + it("should revert if invalid invoke wrap data length", async () => { + const revertReason = "FlashMint: MISMATCH_INPUT_ARRAYS"; + componentWrapData = componentWrapData.slice(0, componentWrapData?.length - 1); + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if invalid swap data length", async () => { + const revertReason = "FlashMint: MISMATCH_INPUT_ARRAYS"; + componentSwapData = componentSwapData.slice(0, componentSwapData?.length - 1); + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if not a set token", async () => { + setToken = testHelper.cDAI.address; + const revertReason = "FlashMint: INVALID_SET"; + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if maxAmountInputToken is too low", async () => { + maxAmountInputToken = BigNumber.from(100); + const revertReason = + set_token_mix === SetTokenMix.UNWRAPPED_ONLY || + tokenName === "ETH" || + (tokenName === "USDC" && set_token_mix === SetTokenMix.WRAPPED_UNWRAPPED_MIXED) + ? "STF" + : "UNDERBOUGHT_COMPONENT"; + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if exchange without liquidity is specified", async () => { + componentSwapData[0].dexData.exchange = Exchange.Sushiswap; + componentSwapData[1].dexData.exchange = Exchange.Sushiswap; + componentSwapData[2].dexData.exchange = Exchange.Sushiswap; + await expect(subject()).to.be.revertedWith(""); + }); + + it("should revert if exchange with too little liquidity is specified", async () => { + // Set up sushiswap with insufficient liquidity + await testHelper.setV2Setup.usdc + .connect(testHelper.owner.wallet) + .approve(testHelper.sushiswap.router.address, MAX_UINT_256); + await testHelper.sushiswap.router + .connect(testHelper.owner.wallet) + .addLiquidityETH( + testHelper.setV2Setup.usdc.address, + UnitsUtils.usdc(10), + MAX_UINT_256, + MAX_UINT_256, + testHelper.owner.address, + (await getLastBlockTimestamp()).add(1), + { value: ether(0.001), gasLimit: 9000000 }, + ); + + await testHelper.setV2Setup.dai + .connect(testHelper.owner.wallet) + .approve(testHelper.sushiswap.router.address, MAX_UINT_256); + await testHelper.sushiswap.router + .connect(testHelper.owner.wallet) + .addLiquidityETH( + testHelper.setV2Setup.dai.address, + UnitsUtils.usdc(10), + MAX_UINT_256, + MAX_UINT_256, + testHelper.owner.address, + (await getLastBlockTimestamp()).add(1), + { value: ether(0.001), gasLimit: 9000000 }, + ); + + await testHelper.setV2Setup.usdt + .connect(testHelper.owner.wallet) + .approve(testHelper.sushiswap.router.address, MAX_UINT_256); + await testHelper.sushiswap.router + .connect(testHelper.owner.wallet) + .addLiquidityETH( + testHelper.setV2Setup.usdt.address, + BigNumber.from(1), + MAX_UINT_256, + MAX_UINT_256, + testHelper.owner.address, + (await getLastBlockTimestamp()).add(1), + { value: ether(0.00001), gasLimit: 9000000 }, + ); + + componentSwapData[0].dexData.exchange = Exchange.Sushiswap; + componentSwapData[1].dexData.exchange = Exchange.Sushiswap; + componentSwapData[2].dexData.exchange = Exchange.Sushiswap; + + await expect(subject()).to.be.revertedWith("ds-math-sub-underflow"); + }); + + it("should revert if invalid swap path input token is given", async () => { + const revertReason = "FlashMint: INPUT_TOKEN_NOT_IN_PATH"; + componentSwapData[2].dexData.path[0] = testHelper.setV2Setup.wrapModule.address; // just some other address + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if invalid swap path output token is given", async () => { + const revertReason = "FlashMint: OUTPUT_TOKEN_NOT_IN_PATH"; + componentSwapData[2].dexData.path[componentSwapData[2].dexData.path.length - 1] = + testHelper.setV2Setup.wrapModule.address; // just some other address + await expect(subject()).to.be.revertedWith(revertReason); + }); + //#endregion + }); + }); + //#endregion + + //#region redeem + ["DAI", "USDC", "ETH"].forEach(tokenName => { + describe(`\n\n\n---------\n\n #redeemExactSetFor${ + tokenName !== "ETH" ? "ERC20" : tokenName + }: receiving ${tokenName}`, async () => { + //#region redeem setup + let redeemSetAmount: BigNumber; + let setToken: Address; + let minAmountOutput: BigNumber; + let componentSwapData: ComponentSwapData[]; + let componentUnwrapData: ComponentWrapData[]; + let outputToken: StandardTokenMock | WETH9; + let outputAmountReceived: BigNumber; + + beforeEach(async () => { + if (tokenName === "DAI") { + outputToken = testHelper.setV2Setup.dai; + } else if (tokenName === "USDC") { + outputToken = testHelper.setV2Setup.usdc; + } else if (tokenName === "ETH") { + // for ETH, output token for swaps etc. is WETH + outputToken = testHelper.setV2Setup.weth; + } else { + throw new Error("test case not implemented!"); + } + + // issue set tokens to be redeemed + await testHelper.issueSetTokens(flashMintContract, set_token_mix as SetTokenMix); + + redeemSetAmount = ether(1000); + setToken = testHelper.setToken.address; + + componentSwapData = await testHelper.getRedemptionComponentSwapData( + outputToken.address, + ); + + componentUnwrapData = testHelper.getWrapData(set_token_mix as SetTokenMix); + + minAmountOutput = await testHelper.getRedemptionMinAmountOutput( + setToken, + outputToken.address, + redeemSetAmount, + componentSwapData, + flashMintContract, + set_token_mix as SetTokenMix, + ); + + await testHelper.setToken.approve(flashMintContract.address, redeemSetAmount); + }); + + async function subject(overwrite_output_address?: Address) { + if (tokenName !== "ETH") { + return await flashMintContract.redeemExactSetForERC20( + setToken, + overwrite_output_address || outputToken.address, + redeemSetAmount, + minAmountOutput, + componentSwapData, + componentUnwrapData, + ); + } else { + return await flashMintContract.redeemExactSetForETH( + setToken, + redeemSetAmount, + minAmountOutput, + componentSwapData, + componentUnwrapData, + ); + } + } + //#endregion + + //#region redeem tests with amount checks + it(`should redeem to ${tokenName}`, async () => { + await subject(); + }); + + it(`should not have left over amounts in contract after redeem`, async () => { + await subject(); + expect(await testHelper.cDAI.balanceOf(flashMintContract.address)).to.equal(ZERO); + expect(await testHelper.cUSDC.balanceOf(flashMintContract.address)).to.equal(ZERO); + expect(await testHelper.cUSDT.balanceOf(flashMintContract.address)).to.equal(ZERO); + expect(await testHelper.setV2Setup.dai.balanceOf(flashMintContract.address)).to.equal( + ZERO, + ); + expect( + await testHelper.setV2Setup.usdc.balanceOf(flashMintContract.address), + ).to.equal(ZERO); + expect( + await testHelper.setV2Setup.usdt.balanceOf(flashMintContract.address), + ).to.equal(ZERO); + expect( + await testHelper.setV2Setup.weth.balanceOf(flashMintContract.address), + ).to.equal(ZERO); + expect(await ethers.provider.getBalance(flashMintContract.address)).to.equal(ZERO); + }); + + it("should redeem the requested amount of set", async () => { + const balanceBefore = await testHelper.setToken.balanceOf(testHelper.owner.address); + await subject(); + const balanceAfter = await testHelper.setToken.balanceOf(testHelper.owner.address); + expect(balanceBefore.sub(balanceAfter)).to.equal(redeemSetAmount); + }); + + it("should return at least the expected output amount", async () => { + const balanceBefore = await testHelper.ownerBalanceOf( + tokenName === "ETH" ? "ETH" : outputToken, + ); + const tx = await subject(); + const balanceAfter = await testHelper.ownerBalanceOf( + tokenName === "ETH" ? "ETH" : outputToken, + ); + outputAmountReceived = balanceAfter.sub(balanceBefore); + if (tokenName == "ETH") { + const transactionFee = await getTxFee(tx); + outputAmountReceived = outputAmountReceived.add(transactionFee); + } + + expect(outputAmountReceived.gt(0)).to.equal(true); + expect(outputAmountReceived.gt(minAmountOutput)).to.equal(true); + }); + + it("should emit ExchangeIssuance event", async () => { + await expect(subject()) + .to.emit(flashMintContract, "FlashRedeem") + .withArgs( + testHelper.owner.address, + setToken, + tokenName === "ETH" ? testHelper.ethAddress : outputToken.address, + redeemSetAmount, + outputAmountReceived, + ); + }); + //#endregion + + // #region redeem reverts + it("should revert if redeemSetAmount is zero", async () => { + redeemSetAmount = ZERO; + const revertReason = "FlashMint: INVALID_INPUTS"; + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if output token is address 0x000", async () => { + if (tokenName !== "ETH") { + const revertReason = "FlashMint: INVALID_INPUTS"; + await expect(subject(ADDRESS_ZERO)).to.be.revertedWith(revertReason); + } + }); + + it("should revert if invalid invoke unwrap data length", async () => { + const revertReason = "FlashMint: MISMATCH_INPUT_ARRAYS"; + componentUnwrapData = componentUnwrapData.slice(0, componentUnwrapData?.length - 1); + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if invalid swap data length", async () => { + const revertReason = "FlashMint: MISMATCH_INPUT_ARRAYS"; + componentSwapData = componentSwapData.slice(0, componentSwapData?.length - 1); + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if not a set token", async () => { + setToken = testHelper.cDAI.address; + const revertReason = "FlashMint: INVALID_SET"; + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if minAmountOutput is too high", async () => { + minAmountOutput = minAmountOutput.mul(BigNumber.from(10)); + const revertReason = "INSUFFICIENT_OUTPUT_AMOUNT"; + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if exchange without liquidity is specified", async () => { + componentSwapData[0].dexData.exchange = Exchange.Sushiswap; + componentSwapData[1].dexData.exchange = Exchange.Sushiswap; + componentSwapData[2].dexData.exchange = Exchange.Sushiswap; + await expect(subject()).to.be.revertedWith(""); + }); + + it("should revert if exchange with too little liquidity is specified", async () => { + // Set up sushiswap with insufficient liquidity + await testHelper.setV2Setup.usdc + .connect(testHelper.owner.wallet) + .approve(testHelper.sushiswap.router.address, MAX_UINT_256); + await testHelper.sushiswap.router + .connect(testHelper.owner.wallet) + .addLiquidityETH( + testHelper.setV2Setup.usdc.address, + BigNumber.from(1), + MAX_UINT_256, + MAX_UINT_256, + testHelper.owner.address, + (await getLastBlockTimestamp()).add(1), + { value: ether(0.00001), gasLimit: 9000000 }, + ); + + await testHelper.setV2Setup.dai + .connect(testHelper.owner.wallet) + .approve(testHelper.sushiswap.router.address, MAX_UINT_256); + await testHelper.sushiswap.router + .connect(testHelper.owner.wallet) + .addLiquidityETH( + testHelper.setV2Setup.dai.address, + BigNumber.from(1), + MAX_UINT_256, + MAX_UINT_256, + testHelper.owner.address, + (await getLastBlockTimestamp()).add(1), + { value: ether(0.00001), gasLimit: 9000000 }, + ); + + await testHelper.setV2Setup.usdt + .connect(testHelper.owner.wallet) + .approve(testHelper.sushiswap.router.address, MAX_UINT_256); + await testHelper.sushiswap.router + .connect(testHelper.owner.wallet) + .addLiquidityETH( + testHelper.setV2Setup.usdt.address, + BigNumber.from(1), + MAX_UINT_256, + MAX_UINT_256, + testHelper.owner.address, + (await getLastBlockTimestamp()).add(1), + { value: ether(0.00001), gasLimit: 9000000 }, + ); + + componentSwapData[0].dexData.exchange = Exchange.Sushiswap; + componentSwapData[1].dexData.exchange = Exchange.Sushiswap; + componentSwapData[2].dexData.exchange = Exchange.Sushiswap; + + await expect(subject()).to.be.revertedWith("INSUFFICIENT_OUTPUT_AMOUNT"); + }); + + it("should revert if invalid swap path input token is given", async () => { + const revertReason = "FlashMint: INPUT_TOKEN_NOT_IN_PATH"; + componentSwapData[2].dexData.path[0] = testHelper.setV2Setup.wrapModule.address; // just some other address + await expect(subject()).to.be.revertedWith(revertReason); + }); + + it("should revert if invalid swap path output token is given", async () => { + const revertReason = "FlashMint: OUTPUT_TOKEN_NOT_IN_PATH"; + componentSwapData[2].dexData.path[componentSwapData[0].dexData.path.length - 1] = + testHelper.setV2Setup.wrapModule.address; // just some other address + await expect(subject()).to.be.revertedWith(revertReason); + }); + // #endregion + }); + }); + //#endregion + }); + }); + }); +}); diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index 709aed61..91a80de2 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -4,13 +4,14 @@ export const PRODUCTION_ADDRESSES = { tokens: { stEthAm: "0x28424507fefb6f7f8E9D3860F56504E4e5f5f390", stEth: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", - dai: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", + dai: "0x6B175474E89094C44Da98b954EedeAC495271d0F", weth: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", icEth: "0x7C07F7aBe10CE8e33DC6C5aD68FE033085256A84", ETH2xFli: "0xAa6E8127831c9DE45ae56bB1b0d4D4Da6e5665BD", cEther: "0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5", USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", cUSDC: "0x39aa39c021dfbae8fac545936693ac917d5e7563", + cDAI: "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", }, dexes: { curve: { diff --git a/test/integration/ethereum/flashMintWrappedIntegration.spec.ts b/test/integration/ethereum/flashMintWrappedIntegration.spec.ts new file mode 100644 index 00000000..e9dc8292 --- /dev/null +++ b/test/integration/ethereum/flashMintWrappedIntegration.spec.ts @@ -0,0 +1,695 @@ +import "module-alias/register"; +import { Account, Address } from "@utils/types"; +import DeployHelper from "@utils/deploys"; +import { getAccounts, getWaffleExpect, usdc, getSetFixture, preciseMul } from "@utils/index"; +import { ethers } from "hardhat"; +import { BigNumber, utils } from "ethers"; +import { + IWETH, + StandardTokenMock, + IUniswapV2Router, + FlashMintWrapped, + IERC20__factory, + IERC20, + IDebtIssuanceModule, + CERc20__factory, + ICErc20__factory, +} from "../../../typechain"; +import { PRODUCTION_ADDRESSES, STAGING_ADDRESSES } from "./addresses"; +import { ADDRESS_ZERO, ZERO, ZERO_BYTES } from "@utils/constants"; +import { ether } from "@utils/index"; +import { SetFixture } from "@utils/fixtures"; +import { getTxFee } from "@utils/test"; + +const expect = getWaffleExpect(); +const addresses = process.env.USE_STAGING_ADDRESSES ? STAGING_ADDRESSES : PRODUCTION_ADDRESSES; + +//#region types, consts +const compoundWrapAdapterIntegrationName: string = "CompoundWrapV2Adapter"; + +enum Exchange { + None, + Quickswap, + Sushiswap, + UniV3, +} + +type SwapData = { + path: Address[]; + fees: number[]; + pool: Address; + exchange: Exchange; +}; + +type ComponentSwapData = { + // unwrapped token version, e.g. DAI + underlyingERC20: Address; + + // // swap data for DEX operation: fees, path, etc. see DEXAdapter.SwapData + dexData: SwapData; + + // ONLY relevant for issue, not used for redeem: + // amount that has to be bought of the unwrapped token version (to cover required wrapped component amounts for issuance) + // this amount has to be computed beforehand through the exchange rate of wrapped Component <> unwrappedComponent + // i.e. getRequiredComponentIssuanceUnits() on the IssuanceModule and then convert units through exchange rate to unwrapped component units + // e.g. 300 cDAI needed for issuance of 1 Set token. exchange rate 1cDAI = 0.05 DAI. -> buyUnderlyingAmount = 0.05 DAI * 300 = 15 DAI + buyUnderlyingAmount: BigNumber; +}; + +type ComponentWrapData = { + integrationName: string; // wrap adapter integration name as listed in the IntegrationRegistry for the wrapModule + wrapData: string; // optional wrapData passed to the wrapAdapter +}; + +//#endregion + +class TestHelper { + async getIssuanceComponentSwapData( + inputToken: Address, + issueSetAmount: BigNumber, + setToken: Address, + issuanceModule: IDebtIssuanceModule, + ) { + // get required issuance components + const [ + issuanceComponents, // cDAI, cUSDC + issuanceUnits, + ] = await issuanceModule.getRequiredComponentIssuanceUnits(setToken, issueSetAmount); + + const cDAI = CERc20__factory.connect(addresses.tokens.cDAI, ethers.provider); + const cUSDC = CERc20__factory.connect(addresses.tokens.cUSDC, ethers.provider); + + if ( + JSON.stringify([cDAI.address, cUSDC.address]).toLowerCase() !== + JSON.stringify(issuanceComponents).toLowerCase() + ) { + throw new Error("issuance components test case not implemented"); + } + + // get exchange rates for each of the cTokens + // RETURN: The current exchange rate as an unsigned integer, scaled by 1 * 10^(18 - 8 + Underlying Token Decimals). + const exchangeRateDAI = await cDAI.callStatic.exchangeRateCurrent(); + const exchangeRateUSDC = await cUSDC.callStatic.exchangeRateCurrent(); + + // precision good enough for test case, should be done exact in JS library + let requiredDAI = issuanceUnits[0].mul(exchangeRateDAI.div(1e6)).div(1e12); + let requiredUSDC = issuanceUnits[1].mul(exchangeRateUSDC.div(1e6)).div(1e12); + + // add a minimum of tolerance...(one unit) + requiredDAI = requiredDAI.add(ether(1)); + requiredUSDC = requiredUSDC.add(usdc(1)); + + const componentSwapData: ComponentSwapData[] = [ + { + underlyingERC20: addresses.tokens.dai, + buyUnderlyingAmount: requiredDAI, + dexData: { + exchange: Exchange.Sushiswap, + path: + inputToken === addresses.dexes.curve.ethAddress + ? [addresses.tokens.weth, addresses.tokens.dai] + : [inputToken, addresses.tokens.weth, addresses.tokens.dai], + fees: inputToken === addresses.dexes.curve.ethAddress ? [500] : [500, 500], // not needed for sushi + pool: ADDRESS_ZERO, + }, + }, + { + underlyingERC20: addresses.tokens.USDC, + buyUnderlyingAmount: requiredUSDC, + dexData: { + exchange: Exchange.Sushiswap, + path: + inputToken === addresses.dexes.curve.ethAddress + ? [addresses.tokens.weth, addresses.tokens.USDC] + : [inputToken, addresses.tokens.weth, addresses.tokens.USDC], + fees: inputToken === addresses.dexes.curve.ethAddress ? [500] : [500, 500], // not needed for sushi + pool: ADDRESS_ZERO, + }, + }, + ]; + + return componentSwapData; + } + + getWrapData(): ComponentWrapData[] { + return [ + { + integrationName: compoundWrapAdapterIntegrationName, + wrapData: ZERO_BYTES, + }, + { + integrationName: compoundWrapAdapterIntegrationName, + wrapData: ZERO_BYTES, + }, + ]; + } + + async getRedemptionComponentSwapData(outputToken: Address) { + const componentSwapData: ComponentSwapData[] = [ + { + underlyingERC20: addresses.tokens.dai, + buyUnderlyingAmount: ZERO, // not used in redeem + dexData: { + exchange: Exchange.Sushiswap, + path: + outputToken === addresses.dexes.curve.ethAddress + ? [addresses.tokens.dai, addresses.tokens.weth] + : [addresses.tokens.dai, addresses.tokens.weth, outputToken], + fees: outputToken === addresses.tokens.weth ? [500] : [500, 500], // not used for sushi + pool: ADDRESS_ZERO, + }, + }, + { + underlyingERC20: addresses.tokens.USDC, + buyUnderlyingAmount: ZERO, // not used in redeem + dexData: { + exchange: Exchange.Sushiswap, + path: + outputToken === addresses.dexes.curve.ethAddress + ? [addresses.tokens.USDC, addresses.tokens.weth] + : [addresses.tokens.USDC, addresses.tokens.weth, outputToken], + fees: outputToken === addresses.tokens.weth ? [500] : [500, 500], // not used for sushi + pool: ADDRESS_ZERO, + }, + }, + ]; + return componentSwapData; + } + + async getRedemptionMinAmountOutput( + setToken: Address, + outputToken: Address, + redeemSetAmount: BigNumber, + componentSwapData: ComponentSwapData[], + flashMintContract: FlashMintWrapped, + issuanceModule: IDebtIssuanceModule, + tolerancePercentage: number = 1, // 1% tolerance + ) { + // get received redemption components + const [, redemptionUnits] = await issuanceModule.getRequiredComponentRedemptionUnits( + setToken, + redeemSetAmount, + ); + + // get exchange rates for each of the cTokens + const exchangeRateDAI = await ICErc20__factory.connect( + addresses.tokens.cDAI, + ethers.provider, + ).callStatic.exchangeRateCurrent(); + + const exchangeRateUSDC = await ICErc20__factory.connect( + addresses.tokens.cUSDC, + ethers.provider, + ).callStatic.exchangeRateCurrent(); + + const expectedDAI = redemptionUnits[0].mul(exchangeRateDAI.div(1e6)).div(1e12); + const expectedUSDC = redemptionUnits[1].mul(exchangeRateUSDC.div(1e6)).div(1e12); + + componentSwapData[0].buyUnderlyingAmount = expectedDAI; + componentSwapData[1].buyUnderlyingAmount = expectedUSDC; + + const estimatedOutputAmount: BigNumber = await flashMintContract.callStatic.getRedeemExactSet( + setToken, + outputToken === addresses.dexes.curve.ethAddress ? addresses.tokens.weth : outputToken, + redeemSetAmount, + componentSwapData, + ); + + // add some slight tolerance to the expected output to cover minor pricing changes or slippage until + // tx is actually executed + return estimatedOutputAmount + .mul(BigNumber.from(100 - tolerancePercentage)) + .div(BigNumber.from(100)); + } + + async issueSetTokens( + flashMintContract: FlashMintWrapped, + setToken: Address, + owner: Account, + issuanceModule: IDebtIssuanceModule, + wrapModule: Address, + issueSetAmount: BigNumber = ether(100), + ) { + const inputToken = IERC20__factory.connect(addresses.tokens.USDC, ethers.provider); + + const amountIn = usdc(10000); + + const uniV2Router = (await ethers.getContractAt( + "IUniswapV2Router", + addresses.dexes.uniV2.router, + )) as IUniswapV2Router; + + await uniV2Router.swapETHForExactTokens( + amountIn, + [addresses.tokens.weth, inputToken.address], + owner.address, + BigNumber.from("1688894490"), + { value: ether(1000) }, + ); + + await inputToken.connect(owner.wallet).approve(flashMintContract.address, amountIn); + + const componentSwapData = await this.getIssuanceComponentSwapData( + inputToken.address, + issueSetAmount, + setToken, + issuanceModule, + ); + + await flashMintContract.approveSetToken(setToken); + + return await flashMintContract.issueExactSetFromERC20( + setToken, + inputToken.address, + issueSetAmount, + amountIn, + componentSwapData, + this.getWrapData(), + ); + } +} + +if (process.env.INTEGRATIONTEST) { + describe("FlashMintWrapped - Integration Test", async () => { + let owner: Account; + let deployer: DeployHelper; + let setToken: StandardTokenMock; + let USDC: StandardTokenMock; + let weth: IWETH; + let setV2Setup: SetFixture; + + before(async () => { + [owner] = await getAccounts(); + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSetFixture(owner.address); + await setV2Setup.initialize(); + + // deploy CompoundWrapV2Adapter + const compoundWrapAdapter = await deployer.setV2.deployCompoundWrapV2Adapter(); + await setV2Setup.integrationRegistry.addIntegration( + setV2Setup.wrapModule.address, + compoundWrapAdapterIntegrationName, + compoundWrapAdapter.address, + ); + + // create set token with cDAI and cUSDC + setToken = await setV2Setup.createSetToken( + [addresses.tokens.cDAI, addresses.tokens.cUSDC], + [BigNumber.from(200_000_000), BigNumber.from(300_000_000)], + [ + setV2Setup.debtIssuanceModule.address, + setV2Setup.streamingFeeModule.address, + setV2Setup.wrapModule.address, + ], + ); + + await setV2Setup.debtIssuanceModule.initialize( + setToken.address, + ZERO, + ZERO, + ZERO, + ADDRESS_ZERO, + ADDRESS_ZERO, + ); + + USDC = (await ethers.getContractAt( + "StandardTokenMock", + addresses.tokens.USDC, + )) as StandardTokenMock; + + weth = (await ethers.getContractAt("IWETH", addresses.tokens.weth)) as IWETH; + }); + + context("When flash mint wrapped is deployed", () => { + let flashMintContract: FlashMintWrapped; + //#region basic setup and constructor with addresses set correctly checks + before(async () => { + flashMintContract = await deployer.extensions.deployFlashMintWrappedExtension( + addresses.tokens.weth, + addresses.dexes.uniV2.router, + addresses.dexes.sushiswap.router, + addresses.dexes.uniV3.router, + addresses.dexes.uniV3.quoter, + addresses.dexes.curve.addressProvider, + addresses.dexes.curve.calculator, + setV2Setup.controller.address, + setV2Setup.debtIssuanceModule.address, + setV2Setup.wrapModule.address, + ); + }); + + it("weth address is set correctly", async () => { + const returnedAddresses = await flashMintContract.dexAdapter(); + expect(returnedAddresses.weth).to.eq(utils.getAddress(addresses.tokens.weth)); + }); + + it("sushi router address is set correctly", async () => { + const returnedAddresses = await flashMintContract.dexAdapter(); + expect(returnedAddresses.sushiRouter).to.eq( + utils.getAddress(addresses.dexes.sushiswap.router), + ); + }); + + it("uniV2 router address is set correctly", async () => { + const returnedAddresses = await flashMintContract.dexAdapter(); + expect(returnedAddresses.quickRouter).to.eq(utils.getAddress(addresses.dexes.uniV2.router)); + }); + + it("uniV3 router address is set correctly", async () => { + const returnedAddresses = await flashMintContract.dexAdapter(); + expect(returnedAddresses.uniV3Router).to.eq(utils.getAddress(addresses.dexes.uniV3.router)); + }); + + it("uniV3 quoter address is set correctly", async () => { + const returnedAddresses = await flashMintContract.dexAdapter(); + expect(returnedAddresses.uniV3Quoter).to.eq(utils.getAddress(addresses.dexes.uniV3.quoter)); + }); + + it("controller address is set correctly", async () => { + expect(await flashMintContract.setController()).to.eq( + utils.getAddress(setV2Setup.controller.address), + ); + }); + + it("debt issuance module address is set correctly", async () => { + expect(await flashMintContract.issuanceModule()).to.eq( + utils.getAddress(setV2Setup.debtIssuanceModule.address), + ); + }); + //#endregion + + describe("When setToken is approved", () => { + before(async () => { + await flashMintContract.approveSetToken(setToken.address); + }); + + ["USDC", "ETH"].forEach(tokenName => { + describe(`When input/output token is ${tokenName}`, () => { + const testHelper = new TestHelper(); + + //#region issue + describe( + tokenName == "ETH" ? "issueExactSetFromETH" : "#issueExactSetFromERC20", + () => { + //#region issue test setup + let inputToken: IERC20; + + let subjectSetToken: Address; + let subjectMaxAmountIn: BigNumber; + + let issueSetAmount: BigNumber; + let inputAmount: BigNumber; + + let componentSwapData: ComponentSwapData[]; + + beforeEach(async () => { + inputAmount = ether(100); + + inputToken = IERC20__factory.connect( + tokenName === "ETH" ? addresses.dexes.curve.ethAddress : USDC.address, + ethers.provider, + ); + + if (tokenName !== "ETH") { + inputAmount = usdc(10000); + + const uniV2Router = (await ethers.getContractAt( + "IUniswapV2Router", + addresses.dexes.uniV2.router, + )) as IUniswapV2Router; + + await uniV2Router.swapETHForExactTokens( + inputAmount, + [weth.address, USDC.address], + owner.address, + BigNumber.from("1688894490"), + { value: ether(100) }, + ); + } + + subjectMaxAmountIn = inputAmount; + + const inputTokenBalance: BigNumber = await (tokenName === "ETH" + ? owner.wallet.getBalance() + : inputToken.balanceOf(owner.address)); + if (tokenName === "ETH") { + subjectMaxAmountIn = inputAmount; + } else { + subjectMaxAmountIn = inputTokenBalance; + + await inputToken + .connect(owner.wallet) + .approve(flashMintContract.address, subjectMaxAmountIn); + } + + subjectSetToken = setToken.address; + issueSetAmount = ether(1000); + + componentSwapData = await testHelper.getIssuanceComponentSwapData( + inputToken.address, + issueSetAmount, + subjectSetToken, + setV2Setup.debtIssuanceModule, + ); + }); + + async function subject() { + if (tokenName !== "ETH") { + return await flashMintContract.issueExactSetFromERC20( + subjectSetToken, + inputToken.address, + issueSetAmount, + subjectMaxAmountIn, + componentSwapData, + testHelper.getWrapData(), + ); + } else { + return await flashMintContract.issueExactSetFromETH( + subjectSetToken, + issueSetAmount, + componentSwapData, + testHelper.getWrapData(), + { value: subjectMaxAmountIn }, + ); + } + } + + async function subjectQuote() { + return flashMintContract.callStatic.getIssueExactSet( + subjectSetToken, + inputToken.address, + issueSetAmount, + componentSwapData, + ); + } + //#endregion + + //#region issue tests + it("should issue the correct amount of tokens", async () => { + const setBalancebefore = await setToken.balanceOf(owner.address); + await subject(); + const setBalanceAfter = await setToken.balanceOf(owner.address); + const setObtained = setBalanceAfter.sub(setBalancebefore); + expect(setObtained).to.eq(issueSetAmount); + }); + + it("should spend less than specified max amount", async () => { + const inputBalanceBefore = + tokenName == "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + await subject(); + const inputBalanceAfter = + tokenName == "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + const inputSpent = inputBalanceBefore.sub(inputBalanceAfter); + expect(inputSpent.gt(0)).to.be.true; + expect(inputSpent.lte(subjectMaxAmountIn)).to.be.true; + }); + + it("should quote the correct input amount", async () => { + const inputBalanceBefore = + tokenName == "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + const result = await subject(); + const inputBalanceAfter = + tokenName == "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + let inputSpent = inputBalanceBefore.sub(inputBalanceAfter); + + if (tokenName == "ETH") { + const gasFee = await flashMintContract.estimateGas.issueExactSetFromETH( + subjectSetToken, + issueSetAmount, + componentSwapData, + testHelper.getWrapData(), + { value: subjectMaxAmountIn }, + ); + const gasCost = gasFee.mul(result.gasPrice); + + inputSpent = inputSpent.sub(gasCost); + } + const quotedInputAmount = await subjectQuote(); + expect(quotedInputAmount).to.gt(preciseMul(inputSpent, ether(0.99))); + expect(quotedInputAmount).to.lt(preciseMul(inputSpent, ether(1.01))); + }); + //#endregion + }, + ); + //#endregion + + //#region redeem + describe( + tokenName == "ETH" ? "redeemExactSetForETH" : "#redeemExactSetForERC20", + () => { + //#region redeem test setup + let outputToken: IERC20; + + let minAmountOutput: BigNumber; + let redeemSetAmount: BigNumber; + + let componentSwapData: ComponentSwapData[]; + + let subjectSetToken: Address; + + beforeEach(async () => { + outputToken = IERC20__factory.connect( + tokenName === "ETH" ? addresses.dexes.curve.ethAddress : USDC.address, + ethers.provider, + ); + + redeemSetAmount = ether(100); + subjectSetToken = setToken.address; + + // issue set tokens to be redeemed + await testHelper.issueSetTokens( + flashMintContract, + subjectSetToken, + owner, + setV2Setup.debtIssuanceModule, + setV2Setup.wrapModule.address, + redeemSetAmount, + ); + + componentSwapData = await testHelper.getRedemptionComponentSwapData( + outputToken.address, + ); + + minAmountOutput = await testHelper.getRedemptionMinAmountOutput( + subjectSetToken, + outputToken.address, + redeemSetAmount, + componentSwapData, + flashMintContract, + setV2Setup.debtIssuanceModule, + ); + + await setToken.approve(flashMintContract.address, redeemSetAmount); + }); + + async function subject() { + if (tokenName == "ETH") { + return flashMintContract.redeemExactSetForETH( + subjectSetToken, + redeemSetAmount, + minAmountOutput, + componentSwapData, + testHelper.getWrapData(), + ); + } + return flashMintContract.redeemExactSetForERC20( + subjectSetToken, + outputToken.address, + redeemSetAmount, + minAmountOutput, + componentSwapData, + testHelper.getWrapData(), + ); + } + + async function subjectQuote(): Promise { + return flashMintContract.callStatic.getRedeemExactSet( + subjectSetToken, + outputToken.address, + redeemSetAmount, + componentSwapData, + ); + } + //#endregion + + //#region redeem tests + it("should redeem the correct amount of tokens", async () => { + const setBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setBalanceAfter = await setToken.balanceOf(owner.address); + const setRedeemed = setBalanceBefore.sub(setBalanceAfter); + expect(setRedeemed).to.eq(redeemSetAmount); + }); + + it("should return at least the specified minimum of output tokens", async () => { + const outputBalanceBefore = + tokenName == "ETH" + ? await owner.wallet.getBalance() + : await outputToken.balanceOf(owner.address); + + const tx = await subject(); + + const outputBalanceAfter = + tokenName == "ETH" + ? await owner.wallet.getBalance() + : await outputToken.balanceOf(owner.address); + + let outputObtained = outputBalanceAfter.sub(outputBalanceBefore); + + if (tokenName == "ETH") { + const transactionFee = await getTxFee(tx); + outputObtained = outputObtained.add(transactionFee); + } + + expect(outputObtained.gte(minAmountOutput)).to.be.true; + }); + + it("should quote the correct output amount", async () => { + let gasCount = BigNumber.from(0); + let gasCost: BigNumber; + let outputObtained: BigNumber; + const outputBalanceBefore = + tokenName == "ETH" + ? await owner.wallet.getBalance() + : await outputToken.balanceOf(owner.address); + if (tokenName == "ETH") { + gasCount = await flashMintContract.estimateGas.redeemExactSetForETH( + subjectSetToken, + redeemSetAmount, + minAmountOutput, + componentSwapData, + testHelper.getWrapData(), + ); + } + const result = await subject(); + const outputBalanceAfter = + tokenName == "ETH" + ? await owner.wallet.getBalance() + : await outputToken.balanceOf(owner.address); + outputObtained = outputBalanceAfter.sub(outputBalanceBefore); + gasCount = preciseMul(gasCount, ether(0.9)); + gasCost = gasCount.mul(result.gasPrice); + outputObtained = outputObtained.add(gasCost); + const outputAmountQuote = await subjectQuote(); + expect(outputAmountQuote).to.gt(preciseMul(outputObtained, ether(0.99))); + expect(outputAmountQuote).to.lt(preciseMul(outputObtained, ether(1.01))); + }); + //#endregion + }, + ); + //#endregion + }); + }); + }); + }); + }); +} diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index 9292db72..6f0fd5bd 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -42,6 +42,8 @@ import { GIMExtension__factory } from "../../typechain/factories/GIMExtension__f import { GovernanceExtension__factory } from "../../typechain/factories/GovernanceExtension__factory"; import { StreamingFeeSplitExtension__factory } from "../../typechain/factories/StreamingFeeSplitExtension__factory"; import { WrapExtension__factory } from "../../typechain/factories/WrapExtension__factory"; +import { FlashMintWrapped__factory } from "../../typechain/factories/FlashMintWrapped__factory"; +import { FlashMintWrapped } from "../../typechain/FlashMintWrapped"; export default class DeployExtensions { private _deployerSigner: Signer; @@ -306,4 +308,45 @@ export default class DeployExtensions { public async deployWrapExtension(manager: Address, wrapModule: Address): Promise { return await new WrapExtension__factory(this._deployerSigner).deploy(manager, wrapModule); } + + public async deployFlashMintWrappedExtension( + wethAddress: Address, + quickRouterAddress: Address, + sushiRouterAddress: Address, + uniV3RouterAddress: Address, + uniswapV3QuoterAddress: Address, + curveCalculatorAddress: Address, + curveAddressProviderAddress: Address, + setControllerAddress: Address, + issuanceModuleAddress: Address, + wrapModuleAddress: Address, + ): Promise { + const dexAdapter = await this.deployDEXAdapter(); + + const linkId = convertLibraryNameToLinkId( + "contracts/exchangeIssuance/DEXAdapter.sol:DEXAdapter", + ); + + return await new FlashMintWrapped__factory( + // @ts-ignore + { + [linkId]: dexAdapter.address, + }, + // @ts-ignore + this._deployerSigner, + ).deploy( + { + quickRouter: quickRouterAddress, + sushiRouter: sushiRouterAddress, + uniV3Router: uniV3RouterAddress, + uniV3Quoter: uniswapV3QuoterAddress, + curveAddressProvider: curveAddressProviderAddress, + curveCalculator: curveCalculatorAddress, + weth: wethAddress, + }, + setControllerAddress, + issuanceModuleAddress, + wrapModuleAddress, + ); + } } diff --git a/utils/deploys/deploySetV2.ts b/utils/deploys/deploySetV2.ts index 0d143e76..6e692c63 100644 --- a/utils/deploys/deploySetV2.ts +++ b/utils/deploys/deploySetV2.ts @@ -48,6 +48,7 @@ import { UniswapV2ExchangeAdapter__factory } from "../../typechain/factories/Uni import { WETH9__factory } from "../../typechain/factories/WETH9__factory"; import { WrapModule__factory } from "../../typechain/factories/WrapModule__factory"; import { SlippageIssuanceModule__factory } from "../../typechain/factories/SlippageIssuanceModule__factory"; +import { CompoundWrapV2Adapter__factory } from "@typechain/factories/CompoundWrapV2Adapter__factory"; export default class DeploySetV2 { private _deployerSigner: Signer; @@ -234,4 +235,14 @@ export default class DeploySetV2 { public async deploySlippageIssuanceModule(controller: Address): Promise { return await new SlippageIssuanceModule__factory(this._deployerSigner).deploy(controller); } + + public async deployCompoundWrapV2Adapter(): Promise { + const compoundLibrary = await new Compound__factory(this._deployerSigner).deploy(); + return await new CompoundWrapV2Adapter__factory( + { + ["__$059b1e3c35e6526bf44b3e0b6a2a76e329$__"]: compoundLibrary.address, + }, + this._deployerSigner, + ).deploy(); + } } diff --git a/utils/fixtures/setFixture.ts b/utils/fixtures/setFixture.ts index bede726c..d6e884bb 100644 --- a/utils/fixtures/setFixture.ts +++ b/utils/fixtures/setFixture.ts @@ -1,7 +1,6 @@ import { JsonRpcProvider, Web3Provider } from "@ethersproject/providers"; import { ContractTransaction, Signer } from "ethers"; import { BigNumber } from "@ethersproject/bignumber"; -import { CEther, CERc20 } from "@utils/contracts/compound"; import { AirdropModule, @@ -59,6 +58,7 @@ export class SetFixture { public usdc: StandardTokenMock; public wbtc: StandardTokenMock; public dai: StandardTokenMock; + public usdt: StandardTokenMock; constructor(provider: Web3Provider | JsonRpcProvider, ownerAddress: Address) { this._provider = provider; @@ -118,16 +118,19 @@ export class SetFixture { this.usdc = await this._deployer.setV2.deployTokenMock(this._ownerAddress, ether(100000), 6); this.wbtc = await this._deployer.setV2.deployTokenMock(this._ownerAddress, ether(100000), 8); this.dai = await this._deployer.setV2.deployTokenMock(this._ownerAddress, ether(1000000), 18); + this.usdt = await this._deployer.setV2.deployTokenMock(this._ownerAddress, ether(100000), 6); await this.weth.deposit({ value: ether(200000) }); await this.weth.approve(this.issuanceModule.address, ether(10000)); await this.usdc.approve(this.issuanceModule.address, ether(10000)); await this.wbtc.approve(this.issuanceModule.address, ether(10000)); await this.dai.approve(this.issuanceModule.address, ether(10000)); + await this.usdt.approve(this.issuanceModule.address, ether(10000)); await this.weth.approve(this.debtIssuanceModule.address, ether(10000)); await this.usdc.approve(this.debtIssuanceModule.address, ether(10000)); await this.wbtc.approve(this.debtIssuanceModule.address, ether(10000)); await this.dai.approve(this.debtIssuanceModule.address, ether(10000)); + await this.usdt.approve(this.debtIssuanceModule.address, ether(10000)); } public async createSetToken( diff --git a/utils/test/testingUtils.ts b/utils/test/testingUtils.ts index b74dab6b..6989ddde 100644 --- a/utils/test/testingUtils.ts +++ b/utils/test/testingUtils.ts @@ -1,4 +1,4 @@ -import chai from "chai"; +import chai, { expect } from "chai"; import { solidity } from "ethereum-waffle"; chai.use(solidity); @@ -44,7 +44,7 @@ export function cacheBeforeEach(initializer: Mocha.AsyncFunc): void { let initialized = false; const blockchain = new Blockchain(provider); - beforeEach(async function () { + beforeEach(async function() { if (!initialized) { await initializer.call(this); SNAPSHOTS.push(await blockchain.saveSnapshotAsync()); @@ -56,7 +56,7 @@ export function cacheBeforeEach(initializer: Mocha.AsyncFunc): void { } }); - after(async function () { + after(async function() { if (initialized) { SNAPSHOTS.pop(); } @@ -76,21 +76,13 @@ export async function mineBlockAsync(): Promise { await sendJSONRpcRequestAsync("evm_mine", []); } -export async function increaseTimeAsync( - duration: BigNumber, -): Promise { +export async function increaseTimeAsync(duration: BigNumber): Promise { await sendJSONRpcRequestAsync("evm_increaseTime", [duration.toNumber()]); await mineBlockAsync(); } -async function sendJSONRpcRequestAsync( - method: string, - params: any[], -): Promise { - return provider.send( - method, - params, - ); +async function sendJSONRpcRequestAsync(method: string, params: any[]): Promise { + return provider.send(method, params); } export async function getTxFee(tx: ContractTransaction) { @@ -100,3 +92,16 @@ export async function getTxFee(tx: ContractTransaction) { const transactionFee = gasPrice.mul(gasUsed); return transactionFee; } + +export const expectThrowsAsync = async (method: Promise, errorMessage: string = "") => { + let error!: Error; + try { + await method; + } catch (err) { + error = (err as unknown) as Error; + } + expect(error).to.be.an("Error"); + if (errorMessage) { + expect(error.message).to.include(errorMessage); + } +};