From 8ac38ead1d0fdffa9e886ad0b6878291ba8ce078 Mon Sep 17 00:00:00 2001 From: ncitron Date: Thu, 12 Aug 2021 17:51:14 -0400 Subject: [PATCH 1/8] add WrapModuleV2 --- contracts/interfaces/IWrapV2Adapter.sol | 47 ++ .../mocks/integrations/WrapV2AdapterMock.sol | 99 ++++ contracts/protocol/modules/WrapModuleV2.sol | 521 ++++++++++++++++++ 3 files changed, 667 insertions(+) create mode 100644 contracts/interfaces/IWrapV2Adapter.sol create mode 100644 contracts/mocks/integrations/WrapV2AdapterMock.sol create mode 100644 contracts/protocol/modules/WrapModuleV2.sol diff --git a/contracts/interfaces/IWrapV2Adapter.sol b/contracts/interfaces/IWrapV2Adapter.sol new file mode 100644 index 000000000..67e0576a8 --- /dev/null +++ b/contracts/interfaces/IWrapV2Adapter.sol @@ -0,0 +1,47 @@ +/* + 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; + + +/** + * @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); +} \ No newline at end of file diff --git a/contracts/mocks/integrations/WrapV2AdapterMock.sol b/contracts/mocks/integrations/WrapV2AdapterMock.sol new file mode 100644 index 000000000..ddca3887b --- /dev/null +++ b/contracts/mocks/integrations/WrapV2AdapterMock.sol @@ -0,0 +1,99 @@ +/* + 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; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + + +/** + * @title WrapV2AdapterMock + * @author Set Protocol + * + * ERC20 contract that doubles as a wrap token. The wrapToken accepts any underlying token and + * mints/burns the WrapAdapter Token. + */ +contract WrapV2AdapterMock is ERC20 { + + address public constant ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /* ============ Constructor ============ */ + constructor() public ERC20("WrapV2Adapter", "WRAPV2") {} + + /* ============ External Functions ============ */ + + /** + * Mints tokens to the sender of the underlying quantity + */ + function deposit(address _underlyingToken, uint256 _underlyingQuantity) payable external { + // Do a transferFrom of the underlyingToken + if (_underlyingToken != ETH_TOKEN_ADDRESS) { + IERC20(_underlyingToken).transferFrom(msg.sender, address(this), _underlyingQuantity); + } + + _mint(msg.sender, _underlyingQuantity); + } + + /** + * Burns tokens from the sender of the wrapped asset and returns the underlying + */ + function withdraw(address _underlyingToken, uint256 _underlyingQuantity) external { + // Transfer the underlying to the sender + if (_underlyingToken == ETH_TOKEN_ADDRESS) { + msg.sender.transfer(_underlyingQuantity); + } else { + IERC20(_underlyingToken).transfer(msg.sender, _underlyingQuantity); + } + + _burn(msg.sender, _underlyingQuantity); + } + + /** + * [x] + */ + function getWrapCallData( + address _underlyingToken, + address /* _wrappedToken */, + uint256 _underlyingUnits, + address /* _to */, + bytes memory /* _wrapData */ + ) external view returns (address _subject, uint256 _value, bytes memory _calldata) { + uint256 value = _underlyingToken == ETH_TOKEN_ADDRESS ? _underlyingUnits : 0; + bytes memory callData = abi.encodeWithSignature("deposit(address,uint256)", _underlyingToken, _underlyingUnits); + return (address(this), value, callData); + } + + function getUnwrapCallData( + address _underlyingToken, + address /* _wrappedToken */, + uint256 _wrappedTokenUnits, + address /* _to */, + bytes memory /* _wrapData */ + ) external view returns (address _subject, uint256 _value, bytes memory _calldata) { + bytes memory callData = abi.encodeWithSignature("withdraw(address,uint256)", _underlyingToken, _wrappedTokenUnits); + return (address(this), 0, callData); + } + + function getSpenderAddress( + address /* _underlyingToken */, + address /* _wrappedToken */ + ) external view returns(address) { + return address(this); + } +} \ No newline at end of file diff --git a/contracts/protocol/modules/WrapModuleV2.sol b/contracts/protocol/modules/WrapModuleV2.sol new file mode 100644 index 000000000..94c1c7484 --- /dev/null +++ b/contracts/protocol/modules/WrapModuleV2.sol @@ -0,0 +1,521 @@ +/* + Copyright 2020 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { IController } from "../../interfaces/IController.sol"; +import { IIntegrationRegistry } from "../../interfaces/IIntegrationRegistry.sol"; +import { Invoke } from "../lib/Invoke.sol"; +import { ISetToken } from "../../interfaces/ISetToken.sol"; +import { IWETH } from "../../interfaces/external/IWETH.sol"; +import { IWrapV2Adapter } from "../../interfaces/IWrapV2Adapter.sol"; +import { ModuleBase } from "../lib/ModuleBase.sol"; +import { Position } from "../lib/Position.sol"; +import { PreciseUnitMath } from "../../lib/PreciseUnitMath.sol"; + +/** + * @title WrapModuleV2 + * @author Set Protocol + * + * Module that enables the wrapping of ERC20 and Ether positions via third party protocols. The WrapModuleV2 + * works in conjunction with WrapV2Adapters, in which the wrapAdapterID / integrationNames are stored on the + * integration registry. + * + * Some examples of wrap actions include wrapping, DAI to cDAI (Compound) or Dai to aDai (AAVE). + */ +contract WrapModuleV2 is ModuleBase, ReentrancyGuard { + using SafeCast for int256; + using PreciseUnitMath for uint256; + using Position for uint256; + using SafeMath for uint256; + + using Invoke for ISetToken; + using Position for ISetToken.Position; + using Position for ISetToken; + + /* ============ Events ============ */ + + event ComponentWrapped( + ISetToken indexed _setToken, + address indexed _underlyingToken, + address indexed _wrappedToken, + uint256 _underlyingQuantity, + uint256 _wrappedQuantity, + string _integrationName + ); + + event ComponentUnwrapped( + ISetToken indexed _setToken, + address indexed _underlyingToken, + address indexed _wrappedToken, + uint256 _underlyingQuantity, + uint256 _wrappedQuantity, + string _integrationName + ); + + /* ============ State Variables ============ */ + + // Wrapped ETH address + IWETH public weth; + + /* ============ Constructor ============ */ + + /** + * @param _controller Address of controller contract + * @param _weth Address of wrapped eth + */ + constructor(IController _controller, IWETH _weth) public ModuleBase(_controller) { + weth = _weth; + } + + /* ============ External Functions ============ */ + + /** + * MANAGER-ONLY: Instructs the SetToken to wrap an underlying asset into a wrappedToken via a specified adapter. + * + * @param _setToken Instance of the SetToken + * @param _underlyingToken Address of the component to be wrapped + * @param _wrappedToken Address of the desired wrapped token + * @param _underlyingUnits Quantity of underlying units in Position units + * @param _integrationName Name of wrap module integration (mapping on integration registry) + */ + function wrap( + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _underlyingUnits, + string calldata _integrationName, + bytes memory _wrapData + ) + external + nonReentrant + onlyManagerAndValidSet(_setToken) + { + ( + uint256 notionalUnderlyingWrapped, + uint256 notionalWrapped + ) = _validateWrapAndUpdate( + _integrationName, + _setToken, + _underlyingToken, + _wrappedToken, + _underlyingUnits, + _wrapData, + false // does not use Ether + ); + + emit ComponentWrapped( + _setToken, + _underlyingToken, + _wrappedToken, + notionalUnderlyingWrapped, + notionalWrapped, + _integrationName + ); + } + + /** + * MANAGER-ONLY: Instructs the SetToken to wrap Ether into a wrappedToken via a specified adapter. Since SetTokens + * only hold WETH, in order to support protocols that collateralize with Ether the SetToken's WETH must be unwrapped + * first before sending to the external protocol. + * + * @param _setToken Instance of the SetToken + * @param _wrappedToken Address of the desired wrapped token + * @param _underlyingUnits Quantity of underlying units in Position units + * @param _integrationName Name of wrap module integration (mapping on integration registry) + */ + function wrapWithEther( + ISetToken _setToken, + address _wrappedToken, + uint256 _underlyingUnits, + string calldata _integrationName, + bytes memory _wrapData + ) + external + nonReentrant + onlyManagerAndValidSet(_setToken) + { + ( + uint256 notionalUnderlyingWrapped, + uint256 notionalWrapped + ) = _validateWrapAndUpdate( + _integrationName, + _setToken, + address(weth), + _wrappedToken, + _underlyingUnits, + _wrapData, + true // uses Ether + ); + + emit ComponentWrapped( + _setToken, + address(weth), + _wrappedToken, + notionalUnderlyingWrapped, + notionalWrapped, + _integrationName + ); + } + + /** + * MANAGER-ONLY: Instructs the SetToken to unwrap a wrapped asset into its underlying via a specified adapter. + * + * @param _setToken Instance of the SetToken + * @param _underlyingToken Address of the underlying asset + * @param _wrappedToken Address of the component to be unwrapped + * @param _wrappedUnits Quantity of wrapped tokens in Position units + * @param _integrationName ID of wrap module integration (mapping on integration registry) + */ + function unwrap( + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _wrappedUnits, + string calldata _integrationName, + bytes memory _unwrapData + ) + external + nonReentrant + onlyManagerAndValidSet(_setToken) + { + ( + uint256 notionalUnderlyingUnwrapped, + uint256 notionalUnwrapped + ) = _validateUnwrapAndUpdate( + _integrationName, + _setToken, + _underlyingToken, + _wrappedToken, + _wrappedUnits, + _unwrapData, + false // uses Ether + ); + + emit ComponentUnwrapped( + _setToken, + _underlyingToken, + _wrappedToken, + notionalUnderlyingUnwrapped, + notionalUnwrapped, + _integrationName + ); + } + + /** + * MANAGER-ONLY: Instructs the SetToken to unwrap a wrapped asset collateralized by Ether into Wrapped Ether. Since + * external protocol will send back Ether that Ether must be Wrapped into WETH in order to be accounted for by SetToken. + * + * @param _setToken Instance of the SetToken + * @param _wrappedToken Address of the component to be unwrapped + * @param _wrappedUnits Quantity of wrapped tokens in Position units + * @param _integrationName ID of wrap module integration (mapping on integration registry) + */ + function unwrapWithEther( + ISetToken _setToken, + address _wrappedToken, + uint256 _wrappedUnits, + string calldata _integrationName, + bytes memory _unwrapData + ) + external + nonReentrant + onlyManagerAndValidSet(_setToken) + { + ( + uint256 notionalUnderlyingUnwrapped, + uint256 notionalUnwrapped + ) = _validateUnwrapAndUpdate( + _integrationName, + _setToken, + address(weth), + _wrappedToken, + _wrappedUnits, + _unwrapData, + true // uses Ether + ); + + emit ComponentUnwrapped( + _setToken, + address(weth), + _wrappedToken, + notionalUnderlyingUnwrapped, + notionalUnwrapped, + _integrationName + ); + } + + /** + * Initializes this module to the SetToken. Only callable by the SetToken's manager. + * + * @param _setToken Instance of the SetToken to issue + */ + function initialize(ISetToken _setToken) external onlySetManager(_setToken, msg.sender) { + require(controller.isSet(address(_setToken)), "Must be controller-enabled SetToken"); + require(isSetPendingInitialization(_setToken), "Must be pending initialization"); + _setToken.initializeModule(); + } + + /** + * Removes this module from the SetToken, via call by the SetToken. + */ + function removeModule() external override {} + + + /* ============ Internal Functions ============ */ + + /** + * Validates the wrap operation is valid. In particular, the following checks are made: + * - The position is Default + * - The position has sufficient units given the transact quantity + * - The transact quantity > 0 + * + * It is expected that the adapter will check if wrappedToken/underlyingToken are a valid pair for the given + * integration. + */ + function _validateInputs( + ISetToken _setToken, + address _transactPosition, + uint256 _transactPositionUnits + ) + internal + view + { + require(_transactPositionUnits > 0, "Target position units must be > 0"); + require(_setToken.hasDefaultPosition(_transactPosition), "Target default position must be component"); + require( + _setToken.hasSufficientDefaultUnits(_transactPosition, _transactPositionUnits), + "Unit cant be greater than existing" + ); + } + + /** + * The WrapModule calculates the total notional underlying to wrap, approves the underlying to the 3rd party + * integration contract, then invokes the SetToken to call wrap by passing its calldata along. When raw ETH + * is being used (_usesEther = true) WETH position must first be unwrapped and underlyingAddress sent to + * adapter must be external protocol's ETH representative address. + * + * Returns notional amount of underlying tokens and wrapped tokens that were wrapped. + */ + function _validateWrapAndUpdate( + string calldata _integrationName, + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _underlyingUnits, + bytes memory _wrapData, + bool _usesEther + ) + internal + returns (uint256, uint256) + { + _validateInputs(_setToken, _underlyingToken, _underlyingUnits); + + // Snapshot pre wrap balances + ( + uint256 preActionUnderlyingNotional, + uint256 preActionWrapNotional + ) = _snapshotTargetAssetsBalance(_setToken, _underlyingToken, _wrappedToken); + + uint256 notionalUnderlying = _setToken.totalSupply().getDefaultTotalNotional(_underlyingUnits); + IWrapV2Adapter wrapAdapter = IWrapV2Adapter(getAndValidateAdapter(_integrationName)); + + // Execute any pre-wrap actions depending on if using raw ETH or not + if (_usesEther) { + _setToken.invokeUnwrapWETH(address(weth), notionalUnderlying); + } else { + _setToken.invokeApprove(_underlyingToken, wrapAdapter.getSpenderAddress(_underlyingToken, _wrappedToken), notionalUnderlying); + } + + // Get function call data and invoke on SetToken + _createWrapDataAndInvoke( + _setToken, + wrapAdapter, + _usesEther ? wrapAdapter.ETH_TOKEN_ADDRESS() : _underlyingToken, + _wrappedToken, + notionalUnderlying, + _wrapData + ); + + // Snapshot post wrap balances + ( + uint256 postActionUnderlyingNotional, + uint256 postActionWrapNotional + ) = _snapshotTargetAssetsBalance(_setToken, _underlyingToken, _wrappedToken); + + _updatePosition(_setToken, _underlyingToken, preActionUnderlyingNotional, postActionUnderlyingNotional); + _updatePosition(_setToken, _wrappedToken, preActionWrapNotional, postActionWrapNotional); + + return ( + preActionUnderlyingNotional.sub(postActionUnderlyingNotional), + postActionWrapNotional.sub(preActionWrapNotional) + ); + } + + /** + * The WrapModule calculates the total notional wrap token to unwrap, then invokes the SetToken to call + * unwrap by passing its calldata along. When raw ETH is being used (_usesEther = true) underlyingAddress + * sent to adapter must be set to external protocol's ETH representative address and ETH returned from + * external protocol is wrapped. + * + * Returns notional amount of underlying tokens and wrapped tokens unwrapped. + */ + function _validateUnwrapAndUpdate( + string calldata _integrationName, + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _wrappedTokenUnits, + bytes memory _unwrapData, + bool _usesEther + ) + internal + returns (uint256, uint256) + { + _validateInputs(_setToken, _wrappedToken, _wrappedTokenUnits); + + ( + uint256 preActionUnderlyingNotional, + uint256 preActionWrapNotional + ) = _snapshotTargetAssetsBalance(_setToken, _underlyingToken, _wrappedToken); + + uint256 notionalWrappedToken = _setToken.totalSupply().getDefaultTotalNotional(_wrappedTokenUnits); + IWrapV2Adapter wrapAdapter = IWrapV2Adapter(getAndValidateAdapter(_integrationName)); + + // Get function call data and invoke on SetToken + _createUnwrapDataAndInvoke( + _setToken, + wrapAdapter, + _usesEther ? wrapAdapter.ETH_TOKEN_ADDRESS() : _underlyingToken, + _wrappedToken, + notionalWrappedToken, + _unwrapData + ); + + if (_usesEther) { + _setToken.invokeWrapWETH(address(weth), address(_setToken).balance); + } + + ( + uint256 postActionUnderlyingNotional, + uint256 postActionWrapNotional + ) = _snapshotTargetAssetsBalance(_setToken, _underlyingToken, _wrappedToken); + + _updatePosition(_setToken, _underlyingToken, preActionUnderlyingNotional, postActionUnderlyingNotional); + _updatePosition(_setToken, _wrappedToken, preActionWrapNotional, postActionWrapNotional); + + return ( + postActionUnderlyingNotional.sub(preActionUnderlyingNotional), + preActionWrapNotional.sub(postActionWrapNotional) + ); + } + + /** + * Create the calldata for wrap and then invoke the call on the SetToken. + */ + function _createWrapDataAndInvoke( + ISetToken _setToken, + IWrapV2Adapter _wrapAdapter, + address _underlyingToken, + address _wrappedToken, + uint256 _notionalUnderlying, + bytes memory _wrapData + ) internal { + ( + address callTarget, + uint256 callValue, + bytes memory callByteData + ) = _wrapAdapter.getWrapCallData( + _underlyingToken, + _wrappedToken, + _notionalUnderlying, + address(_setToken), + _wrapData + ); + + _setToken.invoke(callTarget, callValue, callByteData); + } + + /** + * Create the calldata for unwrap and then invoke the call on the SetToken. + */ + function _createUnwrapDataAndInvoke( + ISetToken _setToken, + IWrapV2Adapter _wrapAdapter, + address _underlyingToken, + address _wrappedToken, + uint256 _notionalUnderlying, + bytes memory _unwrapData + ) internal { + ( + address callTarget, + uint256 callValue, + bytes memory callByteData + ) = _wrapAdapter.getUnwrapCallData( + _underlyingToken, + _wrappedToken, + _notionalUnderlying, + address(_setToken), + _unwrapData + ); + + _setToken.invoke(callTarget, callValue, callByteData); + } + + /** + * After a wrap/unwrap operation, check the underlying and wrap token quantities and recalculate + * the units ((total tokens - airdrop)/ total supply). Then update the position on the SetToken. + */ + function _updatePosition( + ISetToken _setToken, + address _token, + uint256 _preActionTokenBalance, + uint256 _postActionTokenBalance + ) internal { + uint256 newUnit = _setToken.totalSupply().calculateDefaultEditPositionUnit( + _preActionTokenBalance, + _postActionTokenBalance, + _setToken.getDefaultPositionRealUnit(_token).toUint256() + ); + + _setToken.editDefaultPosition(_token, newUnit); + } + + /** + * Take snapshot of SetToken's balance of underlying and wrapped tokens. + */ + function _snapshotTargetAssetsBalance( + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken + ) internal view returns(uint256, uint256) { + uint256 underlyingTokenBalance = IERC20(_underlyingToken).balanceOf(address(_setToken)); + uint256 wrapTokenBalance = IERC20(_wrappedToken).balanceOf(address(_setToken)); + + return ( + underlyingTokenBalance, + wrapTokenBalance + ); + } +} \ No newline at end of file From cb4a74c0b046c012184be56974cdff1177a78ee1 Mon Sep 17 00:00:00 2001 From: ncitron Date: Thu, 12 Aug 2021 17:52:56 -0400 Subject: [PATCH 2/8] add deploys --- utils/contracts/index.ts | 2 ++ utils/deploys/deployMocks.ts | 6 ++++++ utils/deploys/deployModules.ts | 8 +++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 29a646cde..dfc84e9b7 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -103,7 +103,9 @@ export { UniswapYieldHook } from "../../typechain/UniswapYieldHook"; export { UniswapV3ExchangeAdapter } from "../../typechain/UniswapV3ExchangeAdapter"; export { WETH9 } from "../../typechain/WETH9"; export { WrapAdapterMock } from "../../typechain/WrapAdapterMock"; +export { WrapV2AdapterMock } from "../../typechain/WrapV2AdapterMock"; export { WrapModule } from "../../typechain/WrapModule"; +export { WrapModuleV2 } from "../../typechain/WrapModuleV2"; export { YearnWrapAdapter } from "../../typechain/YearnWrapAdapter"; export { YearnStrategyMock } from "../../typechain/YearnStrategyMock"; export { ZeroExApiAdapter } from "../../typechain/ZeroExApiAdapter"; diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index b097cf874..37e819285 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -41,6 +41,7 @@ import { SynthetixExchangerMock, Uint256ArrayUtilsMock, WrapAdapterMock, + WrapV2AdapterMock, ZeroExMock, YearnStrategyMock, AaveV2Mock @@ -85,6 +86,7 @@ import { StandardTokenWithFeeMock__factory } from "../../typechain/factories/Sta import { TradeAdapterMock__factory } from "../../typechain/factories/TradeAdapterMock__factory"; import { Uint256ArrayUtilsMock__factory } from "../../typechain/factories/Uint256ArrayUtilsMock__factory"; import { WrapAdapterMock__factory } from "../../typechain/factories/WrapAdapterMock__factory"; +import { WrapV2AdapterMock__factory } from "../../typechain/factories/WrapV2AdapterMock__factory"; import { ZeroExMock__factory } from "../../typechain/factories/ZeroExMock__factory"; import { SynthMock__factory } from "../../typechain/factories/SynthMock__factory"; import { SynthetixExchangerMock__factory } from "../../typechain/factories/SynthetixExchangerMock__factory"; @@ -349,6 +351,10 @@ export default class DeployMocks { return await new ForceFunderMock__factory(this._deployerSigner).deploy(); } + public async deployWrapV2AdapterMock(): Promise { + return await new WrapV2AdapterMock__factory(this._deployerSigner).deploy(); + } + /************************************* * Instance getters ************************************/ diff --git a/utils/deploys/deployModules.ts b/utils/deploys/deployModules.ts index 3e1553aa0..825104576 100644 --- a/utils/deploys/deployModules.ts +++ b/utils/deploys/deployModules.ts @@ -18,7 +18,8 @@ import { StakingModule, StreamingFeeModule, TradeModule, - WrapModule + WrapModule, + WrapModuleV2 } from "../contracts"; import { Address } from "../types"; @@ -39,6 +40,7 @@ import { StakingModule__factory } from "../../typechain/factories/StakingModule_ import { StreamingFeeModule__factory } from "../../typechain/factories/StreamingFeeModule__factory"; import { TradeModule__factory } from "../../typechain/factories/TradeModule__factory"; import { WrapModule__factory } from "../../typechain/factories/WrapModule__factory"; +import { WrapModuleV2__factory } from "../../typechain/factories/WrapModuleV2__factory"; export default class DeployModules { private _deployerSigner: Signer; @@ -184,4 +186,8 @@ export default class DeployModules { protocolDataProvider ); } + + public async deployWrapModuleV2(controller: Address, weth: Address): Promise { + return await new WrapModuleV2__factory(this._deployerSigner).deploy(controller, weth); + } } \ No newline at end of file From 8deb71e86784e17e818917a1b96542db7bd2fbf3 Mon Sep 17 00:00:00 2001 From: ncitron Date: Thu, 12 Aug 2021 17:59:10 -0400 Subject: [PATCH 3/8] add integration tests for WrapModuleV2 --- test/protocol/modules/wrapModuleV2.spec.ts | 780 +++++++++++++++++++++ 1 file changed, 780 insertions(+) create mode 100644 test/protocol/modules/wrapModuleV2.spec.ts diff --git a/test/protocol/modules/wrapModuleV2.spec.ts b/test/protocol/modules/wrapModuleV2.spec.ts new file mode 100644 index 000000000..c3501d8db --- /dev/null +++ b/test/protocol/modules/wrapModuleV2.spec.ts @@ -0,0 +1,780 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ADDRESS_ZERO, ZERO, ZERO_BYTES } from "@utils/constants"; +import { SetToken, WrapV2AdapterMock, WrapModuleV2 } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + ether, + preciseMul, +} from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getProvider, + getRandomAccount, + getRandomAddress, + getSystemFixture, + getWaffleExpect, +} from "@utils/test/index"; +import { SystemFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("WrapModule", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + + let wrapModule: WrapModuleV2; + let wrapAdapterMock: WrapV2AdapterMock; + + const wrapAdapterMockIntegrationName: string = "MOCK_WRAPPER_V2"; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + wrapModule = await deployer.modules.deployWrapModuleV2(setup.controller.address, setup.weth.address); + await setup.controller.addModule(wrapModule.address); + + wrapAdapterMock = await deployer.mocks.deployWrapV2AdapterMock(); + await setup.integrationRegistry.addIntegration(wrapModule.address, wrapAdapterMockIntegrationName, wrapAdapterMock.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectController: Address; + let subjectWETH: Address; + + beforeEach(async () => { + subjectController = setup.controller.address; + subjectWETH = setup.weth.address; + }); + + async function subject(): Promise { + return deployer.modules.deployWrapModuleV2(subjectController, subjectWETH); + } + + it("should set the correct controller", async () => { + const wrapModule = await subject(); + + const controller = await wrapModule.controller(); + expect(controller).to.eq(subjectController); + }); + + it("should set the correct weth contract", async () => { + const wrapModule = await subject(); + + const weth = await wrapModule.weth(); + expect(weth).to.eq(subjectWETH); + }); + }); + + describe("#initialize", async () => { + let setToken: SetToken; + let subjectSetToken: Address; + let subjectCaller: Account; + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [wrapModule.address] + ); + subjectSetToken = setToken.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).initialize(subjectSetToken); + } + + it("should enable the Module on the SetToken", async () => { + await subject(); + const isModuleEnabled = await setToken.isInitializedModule(wrapModule.address); + expect(isModuleEnabled).to.eq(true); + }); + + describe("when the caller is not the SetToken manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when SetToken is not in pending state", async () => { + beforeEach(async () => { + const newModule = await getRandomAddress(); + await setup.controller.addModule(newModule); + + const wrapModuleNotPendingSetToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [newModule] + ); + + subjectSetToken = wrapModuleNotPendingSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the SetToken is not enabled on the controller", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [setup.weth.address], + [ether(1)], + [wrapModule.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be controller-enabled SetToken"); + }); + }); + }); + + describe("#removeModule", async () => { + let setToken: SetToken; + let subjectCaller: Account; + let subjectModule: Address; + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [wrapModule.address] + ); + await wrapModule.initialize(setToken.address); + + subjectModule = wrapModule.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return setToken.connect(subjectCaller.wallet).removeModule(subjectModule); + } + + it("should properly remove the module", async () => { + await subject(); + const isModuleEnabled = await setToken.isInitializedModule(subjectModule); + expect(isModuleEnabled).to.eq(false); + }); + }); + + context("when a SetToken has been deployed and issued", async () => { + let setToken: SetToken; + let setTokensIssued: BigNumber; + + before(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [setup.issuanceModule.address, wrapModule.address] + ); + + // Initialize modules + await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + await wrapModule.initialize(setToken.address); + + // Issue some Sets + setTokensIssued = ether(10); + const underlyingRequired = setTokensIssued; + await setup.weth.approve(setup.issuanceModule.address, underlyingRequired); + await setup.issuanceModule.issue(setToken.address, setTokensIssued, owner.address); + }); + + describe("#wrap", async () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectUnderlyingUnits: BigNumber; + let subjectIntegrationName: string; + let subjectCaller: Account; + let subjectWrapData: string; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = setup.weth.address; + subjectWrappedToken = wrapAdapterMock.address; + subjectUnderlyingUnits = ether(1); + subjectIntegrationName = wrapAdapterMockIntegrationName; + subjectCaller = owner; + subjectWrapData = ZERO_BYTES; + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectUnderlyingUnits, + subjectIntegrationName, + subjectWrapData + ); + } + + it("should mint the correct wrapped asset to the SetToken", async () => { + await subject(); + const wrappedBalance = await wrapAdapterMock.balanceOf(setToken.address); + const expectedTokenBalance = setTokensIssued; + expect(wrappedBalance).to.eq(expectedTokenBalance); + }); + + it("should reduce the correct quantity of the underlying quantity", async () => { + const previousUnderlyingBalance = await setup.weth.balanceOf(setToken.address); + + await subject(); + const underlyingTokenBalance = await setup.weth.balanceOf(setToken.address); + const expectedUnderlyingBalance = previousUnderlyingBalance.sub(setTokensIssued); + expect(underlyingTokenBalance).to.eq(expectedUnderlyingBalance); + }); + + it("remove the underlying position and replace with the wrapped token position", async () => { + await subject(); + + const positions = await setToken.getPositions(); + const receivedWrappedTokenPosition = positions[0]; + + expect(positions.length).to.eq(1); + expect(receivedWrappedTokenPosition.component).to.eq(subjectWrappedToken); + expect(receivedWrappedTokenPosition.unit).to.eq(subjectUnderlyingUnits); + }); + + it("emits the correct ComponentWrapped event", async () => { + await expect(subject()).to.emit(wrapModule, "ComponentWrapped").withArgs( + setToken.address, + subjectUnderlyingToken, + subjectWrappedToken, + preciseMul(subjectUnderlyingUnits, setTokensIssued), + setTokensIssued, + subjectIntegrationName + ); + }); + + describe("when the integration ID is invalid", async () => { + beforeEach(async () => { + subjectIntegrationName = "INVALID_NAME"; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid adapter"); + }); + }); + + describe("when the caller is not the manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when the SetToken has not initialized the module", async () => { + beforeEach(async () => { + const newSetToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [setup.issuanceModule.address, wrapModule.address] + ); + + subjectSetToken = newSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when the subjectComponent is not a Default Position", async () => { + beforeEach(async () => { + subjectUnderlyingToken = await getRandomAddress(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Target default position must be component"); + }); + }); + + describe("when the units is greater than on the position", async () => { + beforeEach(async () => { + subjectUnderlyingUnits = ether(100); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Unit cant be greater than existing"); + }); + }); + + describe("when the underlying units is 0", async () => { + beforeEach(async () => { + subjectUnderlyingUnits = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Target position units must be > 0"); + }); + }); + }); + + describe("#wrapWithEther", async () => { + let subjectSetToken: Address; + let subjectWrappedToken: Address; + let subjectUnderlyingUnits: BigNumber; + let subjectIntegrationName: string; + let subjectCaller: Account; + let subjectWrapData: string; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectWrappedToken = wrapAdapterMock.address; + subjectUnderlyingUnits = ether(1); + subjectIntegrationName = wrapAdapterMockIntegrationName; + subjectCaller = owner; + subjectWrapData = ZERO_BYTES; + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).wrapWithEther( + subjectSetToken, + subjectWrappedToken, + subjectUnderlyingUnits, + subjectIntegrationName, + subjectWrapData + ); + } + + it("should mint the correct wrapped asset to the SetToken", async () => { + await subject(); + const wrappedBalance = await wrapAdapterMock.balanceOf(setToken.address); + const expectedTokenBalance = setTokensIssued; + expect(wrappedBalance).to.eq(expectedTokenBalance); + }); + + it("should reduce the correct quantity of WETH", async () => { + const previousUnderlyingBalance = await setup.weth.balanceOf(setToken.address); + + await subject(); + const underlyingTokenBalance = await setup.weth.balanceOf(setToken.address); + const expectedUnderlyingBalance = previousUnderlyingBalance.sub(setTokensIssued); + expect(underlyingTokenBalance).to.eq(expectedUnderlyingBalance); + }); + + it("should send the correct quantity of ETH to the external protocol", async () => { + const provider = getProvider(); + const preEthBalance = await provider.getBalance(wrapAdapterMock.address); + + await subject(); + + const postEthBalance = await provider.getBalance(wrapAdapterMock.address); + expect(postEthBalance).to.eq(preEthBalance.add(preciseMul(subjectUnderlyingUnits, setTokensIssued))); + }); + + it("removes the underlying position and replace with the wrapped token position", async () => { + await subject(); + + const positions = await setToken.getPositions(); + const receivedWrappedTokenPosition = positions[0]; + + expect(positions.length).to.eq(1); + expect(receivedWrappedTokenPosition.component).to.eq(subjectWrappedToken); + expect(receivedWrappedTokenPosition.unit).to.eq(subjectUnderlyingUnits); + }); + + it("emits the correct ComponentWrapped event", async () => { + await expect(subject()).to.emit(wrapModule, "ComponentWrapped").withArgs( + setToken.address, + setup.weth.address, + subjectWrappedToken, + preciseMul(subjectUnderlyingUnits, setTokensIssued), + setTokensIssued, + subjectIntegrationName + ); + }); + + describe("when the integration ID is invalid", async () => { + beforeEach(async () => { + subjectIntegrationName = "INVALID_NAME"; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid adapter"); + }); + }); + + describe("when the caller is not the manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when the SetToken has not initialized the module", async () => { + beforeEach(async () => { + const newSetToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [setup.issuanceModule.address, wrapModule.address] + ); + + subjectSetToken = newSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when WETH is not a Default Position", async () => { + beforeEach(async () => { + const nonWethSetToken = await setup.createSetToken( + [setup.wbtc.address], + [ether(1)], + [setup.issuanceModule.address, wrapModule.address] + ); + + // Initialize modules + await setup.issuanceModule.initialize(nonWethSetToken.address, ADDRESS_ZERO); + await wrapModule.initialize(nonWethSetToken.address); + + subjectSetToken = nonWethSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Target default position must be component"); + }); + }); + + describe("when the units is greater than on the position", async () => { + beforeEach(async () => { + subjectUnderlyingUnits = ether(100); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Unit cant be greater than existing"); + }); + }); + + describe("when the underlying units is 0", async () => { + beforeEach(async () => { + subjectUnderlyingUnits = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Target position units must be > 0"); + }); + }); + }); + + describe("#unwrap", async () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectWrappedTokenUnits: BigNumber; + let subjectIntegrationName: string; + let subjectCaller: Account; + let subjectUnwrapData: string; + + let wrappedQuantity: BigNumber; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = setup.weth.address; + subjectWrappedToken = wrapAdapterMock.address; + subjectWrappedTokenUnits = ether(0.5); + subjectIntegrationName = wrapAdapterMockIntegrationName; + subjectCaller = owner; + subjectUnwrapData = ZERO_BYTES; + + wrappedQuantity = ether(1); + + await wrapModule.wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + wrappedQuantity, + subjectIntegrationName, + ZERO_BYTES + ); + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).unwrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectWrappedTokenUnits, + subjectIntegrationName, + subjectUnwrapData + ); + } + + it("should burn the correct wrapped asset to the SetToken", async () => { + await subject(); + const newWrappedBalance = await wrapAdapterMock.balanceOf(setToken.address); + const expectedTokenBalance = preciseMul(setTokensIssued, wrappedQuantity.sub(subjectWrappedTokenUnits)); + expect(newWrappedBalance).to.eq(expectedTokenBalance); + }); + + it("should properly update the underlying and wrapped token units", async () => { + await subject(); + + const positions = await setToken.getPositions(); + const [receivedWrappedPosition, receivedUnderlyingPosition] = positions; + + expect(positions.length).to.eq(2); + expect(receivedWrappedPosition.component).to.eq(subjectWrappedToken); + expect(receivedWrappedPosition.unit).to.eq(ether(0.5)); + + expect(receivedUnderlyingPosition.component).to.eq(subjectUnderlyingToken); + expect(receivedUnderlyingPosition.unit).to.eq(ether(0.5)); + }); + + it("emits the correct ComponentUnwrapped event", async () => { + await expect(subject()).to.emit(wrapModule, "ComponentUnwrapped").withArgs( + setToken.address, + subjectUnderlyingToken, + subjectWrappedToken, + preciseMul(subjectWrappedTokenUnits, setTokensIssued), + preciseMul(setTokensIssued, wrappedQuantity.sub(subjectWrappedTokenUnits)), + subjectIntegrationName + ); + }); + + describe("when the integration ID is invalid", async () => { + beforeEach(async () => { + subjectIntegrationName = "INVALID_NAME"; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid adapter"); + }); + }); + + describe("when the caller is not the manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when the SetToken has not initialized the module", async () => { + beforeEach(async () => { + const newSetToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [setup.issuanceModule.address, wrapModule.address] + ); + + subjectSetToken = newSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when the subjectComponent is not a Default Position", async () => { + beforeEach(async () => { + subjectWrappedToken = await getRandomAddress(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Target default position must be component"); + }); + }); + + describe("when the units is greater than on the position", async () => { + beforeEach(async () => { + subjectWrappedTokenUnits = ether(100); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Unit cant be greater than existing"); + }); + }); + + describe("when the underlying units is 0", async () => { + beforeEach(async () => { + subjectWrappedTokenUnits = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Target position units must be > 0"); + }); + }); + }); + + describe("#unwrapWithEther", async () => { + let subjectSetToken: Address; + let subjectWrappedToken: Address; + let subjectWrappedTokenUnits: BigNumber; + let subjectIntegrationName: string; + let subjectCaller: Account; + let subjectUnwrapData: string; + + let wrappedQuantity: BigNumber; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectWrappedToken = wrapAdapterMock.address; + subjectWrappedTokenUnits = ether(0.5); + subjectIntegrationName = wrapAdapterMockIntegrationName; + subjectCaller = owner; + subjectUnwrapData = ZERO_BYTES; + + wrappedQuantity = ether(1); + + await wrapModule.wrapWithEther( + subjectSetToken, + subjectWrappedToken, + wrappedQuantity, + subjectIntegrationName, + ZERO_BYTES + ); + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).unwrapWithEther( + subjectSetToken, + subjectWrappedToken, + subjectWrappedTokenUnits, + subjectIntegrationName, + subjectUnwrapData + ); + } + + it("should burn the correct wrapped asset to the SetToken", async () => { + await subject(); + const newWrappedBalance = await wrapAdapterMock.balanceOf(setToken.address); + const expectedTokenBalance = preciseMul(setTokensIssued, wrappedQuantity.sub(subjectWrappedTokenUnits)); + expect(newWrappedBalance).to.eq(expectedTokenBalance); + }); + + it("should properly update the underlying and wrapped token units", async () => { + await subject(); + + const positions = await setToken.getPositions(); + const [receivedWrappedPosition, receivedUnderlyingPosition] = positions; + + expect(positions.length).to.eq(2); + expect(receivedWrappedPosition.component).to.eq(subjectWrappedToken); + expect(receivedWrappedPosition.unit).to.eq(ether(0.5)); + + expect(receivedUnderlyingPosition.component).to.eq(setup.weth.address); + expect(receivedUnderlyingPosition.unit).to.eq(ether(0.5)); + }); + + it("should have sent the correct quantity of ETH to the SetToken", async () => { + const provider = getProvider(); + const preEthBalance = await provider.getBalance(wrapAdapterMock.address); + + await subject(); + + const postEthBalance = await provider.getBalance(wrapAdapterMock.address); + expect(postEthBalance).to.eq(preEthBalance.sub(preciseMul(subjectWrappedTokenUnits, setTokensIssued))); + }); + + it("emits the correct ComponentUnwrapped event", async () => { + await expect(subject()).to.emit(wrapModule, "ComponentUnwrapped").withArgs( + setToken.address, + setup.weth.address, + subjectWrappedToken, + preciseMul(subjectWrappedTokenUnits, setTokensIssued), + preciseMul(setTokensIssued, wrappedQuantity.sub(subjectWrappedTokenUnits)), + subjectIntegrationName + ); + }); + + describe("when the integration ID is invalid", async () => { + beforeEach(async () => { + subjectIntegrationName = "INVALID_NAME"; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid adapter"); + }); + }); + + describe("when the caller is not the manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when the SetToken has not initialized the module", async () => { + beforeEach(async () => { + const newSetToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [setup.issuanceModule.address, wrapModule.address] + ); + + subjectSetToken = newSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when the subjectComponent is not a Default Position", async () => { + beforeEach(async () => { + subjectWrappedToken = await getRandomAddress(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Target default position must be component"); + }); + }); + + describe("when the units is greater than on the position", async () => { + beforeEach(async () => { + subjectWrappedTokenUnits = ether(100); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Unit cant be greater than existing"); + }); + }); + + describe("when the underlying units is 0", async () => { + beforeEach(async () => { + subjectWrappedTokenUnits = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Target position units must be > 0"); + }); + }); + }); + }); +}); From 06924d0089c1c16fc5435724ca8f24c21f8eff80 Mon Sep 17 00:00:00 2001 From: ncitron Date: Thu, 12 Aug 2021 18:26:25 -0400 Subject: [PATCH 4/8] add compound wrap adapter --- .../wrap-v2/CompoundWrapV2Adapter.sol | 114 ++++++++++ .../wrap-v2/compoundWrapModuleV2.spec.ts | 212 ++++++++++++++++++ .../wrap-v2/compoundWrapV2Adapter.spec.ts | 174 ++++++++++++++ utils/contracts/index.ts | 1 + utils/deploys/deployAdapters.ts | 13 ++ 5 files changed, 514 insertions(+) create mode 100644 contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol create mode 100644 test/integration/wrap-v2/compoundWrapModuleV2.spec.ts create mode 100644 test/protocol/integration/wrap-v2/compoundWrapV2Adapter.spec.ts diff --git a/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol b/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol new file mode 100644 index 000000000..ed7206e0c --- /dev/null +++ b/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol @@ -0,0 +1,114 @@ +/* + Copyright 2020 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { ICErc20 } from "../../../interfaces/external/ICErc20.sol"; +import { IWrapV2Adapter } from "../../../interfaces/IWrapV2Adapter.sol"; +import { Compound } from "../lib/Compound.sol"; + +/** + * @title CompoundWrapV2Adapter + * @author Set Protocol, Ember Fund + * + * Wrap adapter for Compound that returns data for wraps/unwraps of tokens + */ +contract CompoundWrapV2Adapter is IWrapV2Adapter { + 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 override 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 + view + override + 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 + view + override + 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 view override returns(address) { + return address(_wrappedToken); + } + +} diff --git a/test/integration/wrap-v2/compoundWrapModuleV2.spec.ts b/test/integration/wrap-v2/compoundWrapModuleV2.spec.ts new file mode 100644 index 000000000..9dd04f54a --- /dev/null +++ b/test/integration/wrap-v2/compoundWrapModuleV2.spec.ts @@ -0,0 +1,212 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ADDRESS_ZERO, ZERO_BYTES } from "@utils/constants"; +import { SetToken, WrapModuleV2 } from "@utils/contracts"; +import { CERc20 } from "@utils/contracts/compound"; +import DeployHelper from "@utils/deploys"; +import { + ether, + preciseMul, + preciseDiv +} from "@utils/index"; +import { + getAccounts, + getWaffleExpect, + getSystemFixture, + getCompoundFixture, + addSnapshotBeforeRestoreAfterEach, +} from "@utils/test/index"; +import { CompoundFixture, SystemFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("compoundWrapModule", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + + let compoundSetup: CompoundFixture; + let cDai: CERc20; + let exchangeRate: BigNumber; + + let wrapModule: WrapModuleV2; + + const compoundWrapAdapterIntegrationName: string = "COMPOUND_WRAPPER"; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + // System setup + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + // Compound setup + compoundSetup = getCompoundFixture(owner.address); + await compoundSetup.initialize(); + + exchangeRate = ether(0.5); + cDai = await compoundSetup.createAndEnableCToken( + setup.dai.address, + exchangeRate, + compoundSetup.comptroller.address, + compoundSetup.interestRateModel.address, + "Compound DAI", + "cDAI", + 8, + ether(0.75), // 75% collateral factor + ether(1) + ); + + + // WrapModule setup + wrapModule = await deployer.modules.deployWrapModuleV2(setup.controller.address, setup.weth.address); + await setup.controller.addModule(wrapModule.address); + + // compoundWrapAdapter setup + const compoundLibrary = await deployer.libraries.deployCompound(); + const compoundWrapAdapter = await deployer.adapters.deployCompoundWrapV2Adapter( + "contracts/protocol/integration/lib/Compound.sol:Compound", + compoundLibrary.address + ); + await setup.integrationRegistry.addIntegration(wrapModule.address, compoundWrapAdapterIntegrationName, compoundWrapAdapter.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + context("when a SetToken has been deployed and issued", async () => { + let setToken: SetToken; + let setTokensIssued: BigNumber; + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.dai.address], + [ether(1)], + [setup.issuanceModule.address, wrapModule.address] + ); + + // Initialize modules + await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + await wrapModule.initialize(setToken.address); + + // Issue some Sets + setTokensIssued = ether(10); + const underlyingRequired = setTokensIssued; + await setup.dai.approve(setup.issuanceModule.address, underlyingRequired); + await setup.issuanceModule.issue(setToken.address, setTokensIssued, owner.address); + }); + + describe("#wrap", async () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectUnderlyingUnits: BigNumber; + let subjectIntegrationName: string; + let subjectWrapData: string; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = setup.dai.address; + subjectWrappedToken = cDai.address; + subjectUnderlyingUnits = ether(1); + subjectIntegrationName = compoundWrapAdapterIntegrationName; + subjectCaller = owner; + subjectWrapData = ZERO_BYTES; + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectUnderlyingUnits, + subjectIntegrationName, + subjectWrapData + ); + } + + it("should reduce the underlying quantity and mint the wrapped asset to the SetToken", async () => { + const previousUnderlyingBalance = await setup.dai.balanceOf(setToken.address); + + await subject(); + + const underlyingBalance = await setup.dai.balanceOf(setToken.address); + const wrappedBalance = await cDai.balanceOf(setToken.address); + const expectedUnderlyingBalance = previousUnderlyingBalance.sub(setTokensIssued); + + expect(underlyingBalance).to.eq(expectedUnderlyingBalance); + + const expectedWrappedBalance = preciseDiv(previousUnderlyingBalance, exchangeRate); + + expect(wrappedBalance).to.eq(expectedWrappedBalance); + }); + }); + + describe("#unwrap", () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectWrappedTokenUnits: BigNumber; + let subjectIntegrationName: string; + let subjectCaller: Account; + let subjectUnwrapData: string; + + let wrappedQuantity: BigNumber; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = setup.dai.address; + subjectWrappedToken = cDai.address; + subjectWrappedTokenUnits = BigNumber.from("5000000000"); // ctokens have 8 decimals + subjectIntegrationName = compoundWrapAdapterIntegrationName; + subjectUnwrapData = ZERO_BYTES; + subjectCaller = owner; + + wrappedQuantity = ether(1); + + await wrapModule.wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + wrappedQuantity, + subjectIntegrationName, + ZERO_BYTES + ); + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).unwrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectWrappedTokenUnits, + subjectIntegrationName, + subjectUnwrapData + ); + } + + it("should burn the wrapped asset to the SetToken and increase the underlying quantity", async () => { + const previousWrappedBalance = await cDai.balanceOf(setToken.address); + + await subject(); + + const underlyingBalance = await setup.dai.balanceOf(setToken.address); + const wrappedBalance = await cDai.balanceOf(setToken.address); + const delta = preciseMul(setTokensIssued, subjectWrappedTokenUnits); + const expectedUnderlyingBalance = preciseMul(delta, exchangeRate); + + expect(underlyingBalance).to.eq(expectedUnderlyingBalance); + + const expectedWrappedBalance = previousWrappedBalance.sub(delta); + + expect(wrappedBalance).to.eq(expectedWrappedBalance); + }); + }); + }); +}); diff --git a/test/protocol/integration/wrap-v2/compoundWrapV2Adapter.spec.ts b/test/protocol/integration/wrap-v2/compoundWrapV2Adapter.spec.ts new file mode 100644 index 000000000..1a41ba07a --- /dev/null +++ b/test/protocol/integration/wrap-v2/compoundWrapV2Adapter.spec.ts @@ -0,0 +1,174 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { CEther, CERc20 } from "@utils/contracts/compound"; +import { ETH_ADDRESS, ZERO, ZERO_BYTES } from "@utils/constants"; +import { CompoundFixture, SystemFixture } from "@utils/fixtures"; +import { CompoundWrapV2Adapter } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { ether } from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getSystemFixture, + getRandomAddress, + getCompoundFixture, + getWaffleExpect, +} from "@utils/test/index"; + +const expect = getWaffleExpect(); + +describe("CompoundWrapAdapter", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + let compoundSetup: CompoundFixture; + let compoundWrapAdapter: CompoundWrapV2Adapter; + let cEther: CEther; + let cDai: CERc20; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + compoundSetup = getCompoundFixture(owner.address); + await compoundSetup.initialize(); + + cEther = await compoundSetup.createAndEnableCEther( + ether(200000000), + compoundSetup.comptroller.address, + compoundSetup.interestRateModel.address, + "Compound ether", + "cETH", + 8, + ether(0.75), // 75% collateral factor + ether(1000) + ); + + cDai = await compoundSetup.createAndEnableCToken( + setup.dai.address, + ether(200000000), + compoundSetup.comptroller.address, + compoundSetup.interestRateModel.address, + "Compound Dai", + "cDAI", + 8, + ether(0.75), // 75% collateral factor + ether(1) + ); + + const compoundLibrary = await deployer.libraries.deployCompound(); + compoundWrapAdapter = await deployer.adapters.deployCompoundWrapV2Adapter( + "contracts/protocol/integration/lib/Compound.sol:Compound", + compoundLibrary.address + ); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#getSpenderAddress", async () => { + async function subject(): Promise { + return compoundWrapAdapter.getSpenderAddress(setup.dai.address, cDai.address); + } + + it("should return the correct spender address", async () => { + const spender = await subject(); + expect(spender).to.eq(cDai.address); + }); + }); + + describe("#getWrapCallData", async () => { + let subjectCToken: Address; + let subjectUnderlyingToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectWrapData: string; + + beforeEach(async () => { + subjectQuantity = ether(1); + subjectUnderlyingToken = setup.dai.address; + subjectCToken = cDai.address; + subjectTo = await getRandomAddress(); + subjectWrapData = ZERO_BYTES; + }); + + async function subject(): Promise { + return compoundWrapAdapter.getWrapCallData(subjectUnderlyingToken, subjectCToken, subjectQuantity, subjectTo, subjectWrapData); + } + + it("should return correct data)", async () => { + const [targetAddress, ethValue, callData] = await subject(); + + const expectedCalldata = cDai.interface.encodeFunctionData("mint", [subjectQuantity]); + + expect(targetAddress).to.eq(subjectCToken); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCalldata); + }); + + + describe("when underlying asset is ETH", () => { + let subjectCToken: Address; + let subjectQuantity: BigNumber; + + beforeEach(async () => { + subjectCToken = cEther.address; + subjectUnderlyingToken = ETH_ADDRESS; + subjectQuantity = ether(1); + }); + + async function subject(): Promise { + return compoundWrapAdapter.getWrapCallData(subjectUnderlyingToken, subjectCToken, subjectQuantity, subjectTo, subjectWrapData); + } + + it("should return correct data", async () => { + const [targetAddress, ethValue, callData] = await subject(); + + const expectedCallData = cEther.interface.encodeFunctionData("mint"); + + expect(targetAddress).to.eq(subjectCToken); + expect(ethValue).to.eq(subjectQuantity); + expect(callData).to.eq(expectedCallData); + }); + }); + + }); + + describe("#getUnwrapCallData", async () => { + let subjectCToken: Address; + let subjectUnderlyingToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectUnwrapData: string; + + beforeEach(async () => { + subjectCToken = cDai.address; + subjectUnderlyingToken = setup.dai.address; + subjectQuantity = ether(1); + subjectTo = await getRandomAddress(); + subjectUnwrapData = ZERO_BYTES; + }); + + async function subject(): Promise { + return compoundWrapAdapter.getUnwrapCallData(subjectUnderlyingToken, subjectCToken, subjectQuantity, subjectTo, subjectUnwrapData); + } + + it("should return correct data", async () => { + const [targetAddress, ethValue, callData] = await subject(); + + const expectedCallData = cDai.interface.encodeFunctionData("redeem", [subjectQuantity]); + + expect(targetAddress).to.eq(subjectCToken); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCallData); + }); + }); + +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index dfc84e9b7..10f3ba28d 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -24,6 +24,7 @@ export { CompoundLikeGovernanceAdapter } from "../../typechain/CompoundLikeGover export { CompoundLeverageModule } from "../../typechain/CompoundLeverageModule"; export { CompoundMock } from "../../typechain/CompoundMock"; export { CompoundWrapAdapter } from "../../typechain/CompoundWrapAdapter"; +export { CompoundWrapV2Adapter } from "../../typechain/CompoundWrapV2Adapter"; export { CompClaimAdapter } from "../../typechain/CompClaimAdapter"; export { ComptrollerMock } from "../../typechain/ComptrollerMock"; export { ContractCallerMock } from "../../typechain/ContractCallerMock"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index b5c7a112c..bee061f8e 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -15,6 +15,7 @@ import { AaveMigrationWrapAdapter, AaveWrapAdapter, CompoundWrapAdapter, + CompoundWrapV2Adapter, YearnWrapAdapter, UniswapPairPriceAdapter, UniswapV2ExchangeAdapter, @@ -46,6 +47,7 @@ import { ZeroExApiAdapter__factory } from "../../typechain/factories/ZeroExApiAd import { AaveMigrationWrapAdapter__factory } from "../../typechain/factories/AaveMigrationWrapAdapter__factory"; import { AaveWrapAdapter__factory } from "../../typechain/factories/AaveWrapAdapter__factory"; import { CompoundWrapAdapter__factory } from "../../typechain/factories/CompoundWrapAdapter__factory"; +import { CompoundWrapV2Adapter__factory } from "../../typechain/factories/CompoundWrapV2Adapter__factory"; import { YearnWrapAdapter__factory } from "../../typechain/factories/YearnWrapAdapter__factory"; import { UniswapPairPriceAdapter__factory } from "../../typechain/factories/UniswapPairPriceAdapter__factory"; import { UniswapV2ExchangeAdapter__factory } from "../../typechain/factories/UniswapV2ExchangeAdapter__factory"; @@ -214,4 +216,15 @@ export default class DeployAdapters { public async deployKyberV3IndexExchangeAdapter(dmmRouter: Address, dmmFactory: Address): Promise { return await new KyberV3IndexExchangeAdapter__factory(this._deployerSigner).deploy(dmmRouter, dmmFactory); } + + public async deployCompoundWrapV2Adapter(libraryName: string, libraryAddress: Address): Promise { + const linkId = convertLibraryNameToLinkId(libraryName); + return await new CompoundWrapV2Adapter__factory( + // @ts-ignore + { + [linkId]: libraryAddress, + }, + this._deployerSigner + ).deploy(); + } } From 74c191fc51aab54b9f55479f021740b03a54659f Mon Sep 17 00:00:00 2001 From: ncitron Date: Thu, 12 Aug 2021 20:23:31 -0400 Subject: [PATCH 5/8] add yearn wrap adapter --- .../wrap-v2/CompoundWrapV2Adapter.sol | 14 +- .../wrap-v2/YearnWrapV2Adapter.sol | 126 +++++++++++ .../wrap-v2/yearnWrapModuleV2.spec.ts | 214 ++++++++++++++++++ .../wrap-v2/compoundWrapV2Adapter.spec.ts | 2 +- .../wrap-v2/yearnWrapV2Adapter.spec.ts | 128 +++++++++++ utils/contracts/index.ts | 1 + utils/deploys/deployAdapters.ts | 6 + 7 files changed, 482 insertions(+), 9 deletions(-) create mode 100644 contracts/protocol/integration/wrap-v2/YearnWrapV2Adapter.sol create mode 100644 test/integration/wrap-v2/yearnWrapModuleV2.spec.ts create mode 100644 test/protocol/integration/wrap-v2/yearnWrapV2Adapter.spec.ts diff --git a/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol b/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol index ed7206e0c..2152aa388 100644 --- a/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol +++ b/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.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. @@ -29,14 +29,14 @@ import { Compound } from "../lib/Compound.sol"; * * Wrap adapter for Compound that returns data for wraps/unwraps of tokens */ -contract CompoundWrapV2Adapter is IWrapV2Adapter { +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 override ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address public constant ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /* ============ External Getter Functions ============ */ @@ -59,8 +59,7 @@ contract CompoundWrapV2Adapter is IWrapV2Adapter { bytes memory /* _wrapData */ ) external - view - override + pure returns (address, uint256, bytes memory) { uint256 value; @@ -94,8 +93,7 @@ contract CompoundWrapV2Adapter is IWrapV2Adapter { bytes memory /* _unwrapData */ ) external - view - override + pure returns (address, uint256, bytes memory) { ( , , bytes memory callData) = ICErc20(_wrappedToken).getRedeemCalldata(_wrappedTokenUnits); @@ -107,7 +105,7 @@ contract CompoundWrapV2Adapter is IWrapV2Adapter { * @param _wrappedToken Address of the wrapped token * @return address Address of the contract to approve tokens to */ - function getSpenderAddress(address /* _underlyingToken */, address _wrappedToken) external view override returns(address) { + function getSpenderAddress(address /* _underlyingToken */, address _wrappedToken) external pure returns(address) { return address(_wrappedToken); } diff --git a/contracts/protocol/integration/wrap-v2/YearnWrapV2Adapter.sol b/contracts/protocol/integration/wrap-v2/YearnWrapV2Adapter.sol new file mode 100644 index 000000000..9f6100fe4 --- /dev/null +++ b/contracts/protocol/integration/wrap-v2/YearnWrapV2Adapter.sol @@ -0,0 +1,126 @@ +/* + 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 { IWrapV2Adapter } from "../../../interfaces/IWrapV2Adapter.sol"; +import { IYearnVault } from "../../../interfaces/external/IYearnVault.sol"; + +/** + * @title YearnWrapV2Adapter + * @author Set Protocol, Ember Fund + * + * Wrap adapter for Yearn that returns data for wraps/unwraps of tokens + */ +contract YearnWrapV2Adapter { + + /* ============ Modifiers ============ */ + + /** + * Throws if the underlying/wrapped token pair is not valid + */ + modifier _onlyValidTokenPair(address _underlyingToken, address _wrappedToken) { + require(validTokenPair(_underlyingToken, _wrappedToken), "Must be a valid token pair"); + _; + } + + /* ============ Constructor ============ */ + + constructor() public { } + + /* ============ 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 + view + _onlyValidTokenPair(_underlyingToken, _wrappedToken) + returns (address, uint256, bytes memory) + { + bytes memory callData = abi.encodeWithSignature("deposit(uint256)", _underlyingUnits); + return (address(_wrappedToken), 0, callData); + } + + /** + * Generates the calldata to unwrap a wrapped asset into its underlying. + * + * @param _underlyingToken Address of the underlying asset + * @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 + view + _onlyValidTokenPair(_underlyingToken, _wrappedToken) + returns (address, uint256, bytes memory) + { + bytes memory callData = abi.encodeWithSignature("withdraw(uint256)", _wrappedTokenUnits); + return (address(_wrappedToken), 0, callData); + } + + /** + * Returns the address to approve source tokens for wrapping. + * + * @return address Address of the contract to approve tokens to + */ + function getSpenderAddress(address /* _underlyingToken */, address _wrappedToken) external pure returns(address) { + return address(_wrappedToken); + } + + /* ============ Internal Functions ============ */ + + /** + * Validates the underlying and wrapped token pair + * + * @param _underlyingToken Address of the underlying asset + * @param _wrappedToken Address of the wrapped asset + * + * @return bool Whether or not the wrapped token accepts the underlying token as collateral + */ + function validTokenPair(address _underlyingToken, address _wrappedToken) internal view returns(bool) { + address unwrappedToken = IYearnVault(_wrappedToken).token(); + return unwrappedToken == _underlyingToken; + } +} diff --git a/test/integration/wrap-v2/yearnWrapModuleV2.spec.ts b/test/integration/wrap-v2/yearnWrapModuleV2.spec.ts new file mode 100644 index 000000000..7a0e587e7 --- /dev/null +++ b/test/integration/wrap-v2/yearnWrapModuleV2.spec.ts @@ -0,0 +1,214 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ADDRESS_ZERO, ZERO_BYTES } from "@utils/constants"; +import { YearnWrapV2Adapter, SetToken, WrapModuleV2 } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + ether, + preciseMul, +} from "@utils/index"; +import { + getAccounts, + getWaffleExpect, + getSystemFixture, + getYearnFixture, + addSnapshotBeforeRestoreAfterEach, +} from "@utils/test/index"; +import { YearnFixture, SystemFixture } from "@utils/fixtures"; +import { Vault } from "@utils/contracts/yearn"; + + +const expect = getWaffleExpect(); + +describe("yearnWrapModule", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + + let yearnSetup: YearnFixture; + let daiVault: Vault; + + let wrapModule: WrapModuleV2; + let yearnWrapAdapter: YearnWrapV2Adapter; + + const yearnWrapAdapterIntegrationName: string = "YEARN_WRAPPER"; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + // System setup + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + // Yearn setup + yearnSetup = getYearnFixture(owner.address); + await yearnSetup.initialize(); + + daiVault = await yearnSetup.createAndEnableVaultWithStrategyMock( + setup.dai.address, owner.address, owner.address, owner.address, "daiMockStrategy", "yvDAI", ether(100) + ); + + // WrapModule setup + wrapModule = await deployer.modules.deployWrapModuleV2(setup.controller.address, setup.weth.address); + await setup.controller.addModule(wrapModule.address); + + // YearnWrapAdapter setup + yearnWrapAdapter = await deployer.adapters.deployYearnWrapV2Adapter(); + await setup.integrationRegistry.addIntegration(wrapModule.address, yearnWrapAdapterIntegrationName, yearnWrapAdapter.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + context("when a SetToken has been deployed and issued", async () => { + let setToken: SetToken; + let setTokensIssued: BigNumber; + + before(async () => { + setToken = await setup.createSetToken( + [setup.dai.address], + [ether(1)], + [setup.issuanceModule.address, wrapModule.address] + ); + + // Initialize modules + await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + await wrapModule.initialize(setToken.address); + + // Issue some Sets + setTokensIssued = ether(10); + const underlyingRequired = setTokensIssued; + await setup.dai.approve(setup.issuanceModule.address, underlyingRequired); + await setup.issuanceModule.issue(setToken.address, setTokensIssued, owner.address); + }); + + describe("#wrap", async () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectUnderlyingUnits: BigNumber; + let subjectIntegrationName: string; + let subjectWrapData: string; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = setup.dai.address; + subjectWrappedToken = daiVault.address; + subjectUnderlyingUnits = ether(1); + subjectIntegrationName = yearnWrapAdapterIntegrationName; + subjectWrapData = ZERO_BYTES; + subjectCaller = owner; + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectUnderlyingUnits, + subjectIntegrationName, + subjectWrapData + ); + } + + it("should reduce the underlying quantity and mint the wrapped asset to the SetToken", async () => { + const previousUnderlyingBalance = await setup.dai.balanceOf(setToken.address); + const previousWrappedBalance = await daiVault.balanceOf(setToken.address); + + await subject(); + + const underlyingBalance = await setup.dai.balanceOf(setToken.address); + const wrappedBalance = await daiVault.balanceOf(setToken.address); + + const expectedUnderlyingBalance = previousUnderlyingBalance.sub(setTokensIssued); + expect(underlyingBalance).to.eq(expectedUnderlyingBalance); + + const expectedWrappedBalance = previousWrappedBalance.add(setTokensIssued); + expect(wrappedBalance).to.eq(expectedWrappedBalance); + }); + + }); + + describe("#unwrap", () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectWrappedTokenUnits: BigNumber; + let subjectIntegrationName: string; + let subjectUnwrapData: string; + let subjectCaller: Account; + + let wrappedQuantity: BigNumber; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = setup.dai.address; + subjectWrappedToken = daiVault.address; + subjectWrappedTokenUnits = ether(0.5); + subjectIntegrationName = yearnWrapAdapterIntegrationName; + subjectUnwrapData = ZERO_BYTES; + subjectCaller = owner; + + wrappedQuantity = ether(1); + + await wrapModule.wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + wrappedQuantity, + subjectIntegrationName, + ZERO_BYTES + ); + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).unwrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectWrappedTokenUnits, + subjectIntegrationName, + subjectUnwrapData, + { + gasLimit: 5000000, + } + ); + } + + it("should burn the wrapped asset to the SetToken and increase the underlying quantity", async () => { + const previousUnderlyingBalance = await setup.dai.balanceOf(setToken.address); + const previousWrappedBalance = await daiVault.balanceOf(setToken.address); + + await subject(); + + const underlyingBalance = await setup.dai.balanceOf(setToken.address); + const wrappedBalance = await daiVault.balanceOf(setToken.address); + + const delta = preciseMul(setTokensIssued, wrappedQuantity.sub(subjectWrappedTokenUnits)); + + const expectedUnderlyingBalance = previousUnderlyingBalance.add(delta); + expect(underlyingBalance).to.eq(expectedUnderlyingBalance); + + const expectedWrappedBalance = previousWrappedBalance.sub(delta); + expect(wrappedBalance).to.eq(expectedWrappedBalance); + }); + + describe("when it is an invalid vault - underlying token", async () => { + beforeEach(async () => { + subjectUnderlyingToken = setup.usdc.address; + }); + + it("should revert as it the vault holds a different underlying token", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid token pair"); + }); + }); + + }); + }); +}); diff --git a/test/protocol/integration/wrap-v2/compoundWrapV2Adapter.spec.ts b/test/protocol/integration/wrap-v2/compoundWrapV2Adapter.spec.ts index 1a41ba07a..aa3cfb7d6 100644 --- a/test/protocol/integration/wrap-v2/compoundWrapV2Adapter.spec.ts +++ b/test/protocol/integration/wrap-v2/compoundWrapV2Adapter.spec.ts @@ -20,7 +20,7 @@ import { const expect = getWaffleExpect(); -describe("CompoundWrapAdapter", () => { +describe("CompoundWrapV2Adapter", () => { let owner: Account; let deployer: DeployHelper; let setup: SystemFixture; diff --git a/test/protocol/integration/wrap-v2/yearnWrapV2Adapter.spec.ts b/test/protocol/integration/wrap-v2/yearnWrapV2Adapter.spec.ts new file mode 100644 index 000000000..35f4608db --- /dev/null +++ b/test/protocol/integration/wrap-v2/yearnWrapV2Adapter.spec.ts @@ -0,0 +1,128 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ZERO, ZERO_BYTES } from "@utils/constants"; +import { YearnFixture, SystemFixture } from "@utils/fixtures"; +import { YearnWrapV2Adapter } from "@utils/contracts"; +import { Vault } from "@utils/contracts/yearn"; +import DeployHelper from "@utils/deploys"; +import { ether } from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getSystemFixture, + getRandomAddress, + getWaffleExpect, + getYearnFixture, +} from "@utils/test/index"; +import { defaultAbiCoder } from "@ethersproject/abi"; +import { solidityKeccak256 } from "ethers/lib/utils"; + +const expect = getWaffleExpect(); + +describe("YearnWrapV2Adapter", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + let yearnSetup: YearnFixture; + let daiVault: Vault; + let yearnWrapAdapter: YearnWrapV2Adapter; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + yearnSetup = getYearnFixture(owner.address); + await yearnSetup.initialize(); + + daiVault = await yearnSetup.createAndEnableVaultWithStrategyMock( + setup.dai.address, owner.address, owner.address, owner.address, "daiMockStrategy", "yvDAI", ether(100) + ); + + yearnWrapAdapter = await deployer.adapters.deployYearnWrapV2Adapter(); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#getSpenderAddress", async () => { + async function subject(): Promise { + return yearnWrapAdapter.getSpenderAddress(setup.dai.address, daiVault.address); + } + + it("should return the correct spender address", async () => { + const spender = await subject(); + expect(spender).to.eq(daiVault.address); + }); + }); + + describe("#getWrapCallData", async () => { + let subjectYToken: Address; + let subjectUnderlyingToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectWrapData: string; + + beforeEach(async () => { + subjectQuantity = ether(1); + subjectUnderlyingToken = setup.dai.address; + subjectYToken = daiVault.address; + subjectTo = await getRandomAddress(); + subjectWrapData = ZERO_BYTES; + }); + + async function subject(): Promise { + return yearnWrapAdapter.getWrapCallData(subjectUnderlyingToken, subjectYToken, subjectQuantity, subjectTo, subjectWrapData); + } + + it("should return correct data)", async () => { + const [targetAddress, ethValue, callData] = await subject(); + + const selector = solidityKeccak256(["string"], ["deposit(uint256)"]).slice(0, 10); + const data = defaultAbiCoder.encode(["uint256"], [subjectQuantity]).slice(2); + const expectedCalldata = selector + data; + + expect(targetAddress).to.eq(subjectYToken); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCalldata); + }); + }); + + describe("#getUnwrapCallData", async () => { + let subjectYToken: Address; + let subjectUnderlyingToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectUnwrapData: string; + + beforeEach(async () => { + subjectYToken = daiVault.address; + subjectUnderlyingToken = setup.dai.address; + subjectQuantity = ether(1); + subjectTo = await getRandomAddress(); + subjectUnwrapData = ZERO_BYTES; + }); + + async function subject(): Promise { + return yearnWrapAdapter.getUnwrapCallData(subjectUnderlyingToken, subjectYToken, subjectQuantity, subjectTo, subjectUnwrapData); + } + + it("should return correct data", async () => { + const [targetAddress, ethValue, callData] = await subject(); + + const selector = solidityKeccak256(["string"], ["withdraw(uint256)"]).slice(0, 10); + const data = defaultAbiCoder.encode(["uint256"], [subjectQuantity]).slice(2); + const expectedCalldata = selector + data; + + expect(targetAddress).to.eq(subjectYToken); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCalldata); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 10f3ba28d..5516d773d 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -108,6 +108,7 @@ export { WrapV2AdapterMock } from "../../typechain/WrapV2AdapterMock"; export { WrapModule } from "../../typechain/WrapModule"; export { WrapModuleV2 } from "../../typechain/WrapModuleV2"; export { YearnWrapAdapter } from "../../typechain/YearnWrapAdapter"; +export { YearnWrapV2Adapter } from "../../typechain/YearnWrapV2Adapter"; export { YearnStrategyMock } from "../../typechain/YearnStrategyMock"; export { ZeroExApiAdapter } from "../../typechain/ZeroExApiAdapter"; export { ZeroExMock } from "../../typechain/ZeroExMock"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index bee061f8e..d5f557c63 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -17,6 +17,7 @@ import { CompoundWrapAdapter, CompoundWrapV2Adapter, YearnWrapAdapter, + YearnWrapV2Adapter, UniswapPairPriceAdapter, UniswapV2ExchangeAdapter, UniswapV2ExchangeAdapterV2, @@ -49,6 +50,7 @@ import { AaveWrapAdapter__factory } from "../../typechain/factories/AaveWrapAdap import { CompoundWrapAdapter__factory } from "../../typechain/factories/CompoundWrapAdapter__factory"; import { CompoundWrapV2Adapter__factory } from "../../typechain/factories/CompoundWrapV2Adapter__factory"; import { YearnWrapAdapter__factory } from "../../typechain/factories/YearnWrapAdapter__factory"; +import { YearnWrapV2Adapter__factory } from "../../typechain/factories/YearnWrapV2Adapter__factory"; import { UniswapPairPriceAdapter__factory } from "../../typechain/factories/UniswapPairPriceAdapter__factory"; import { UniswapV2ExchangeAdapter__factory } from "../../typechain/factories/UniswapV2ExchangeAdapter__factory"; import { UniswapV2TransferFeeExchangeAdapter__factory } from "../../typechain/factories/UniswapV2TransferFeeExchangeAdapter__factory"; @@ -227,4 +229,8 @@ export default class DeployAdapters { this._deployerSigner ).deploy(); } + + public async deployYearnWrapV2Adapter(): Promise { + return await new YearnWrapV2Adapter__factory(this._deployerSigner).deploy(); + } } From d3060337427526be62dbe53e8fc50a62234182d9 Mon Sep 17 00:00:00 2001 From: ncitron Date: Thu, 12 Aug 2021 21:33:41 -0400 Subject: [PATCH 6/8] add approval on unwrap --- contracts/protocol/modules/WrapModuleV2.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/protocol/modules/WrapModuleV2.sol b/contracts/protocol/modules/WrapModuleV2.sol index 94c1c7484..3d6a4d2bf 100644 --- a/contracts/protocol/modules/WrapModuleV2.sol +++ b/contracts/protocol/modules/WrapModuleV2.sol @@ -403,6 +403,8 @@ contract WrapModuleV2 is ModuleBase, ReentrancyGuard { uint256 notionalWrappedToken = _setToken.totalSupply().getDefaultTotalNotional(_wrappedTokenUnits); IWrapV2Adapter wrapAdapter = IWrapV2Adapter(getAndValidateAdapter(_integrationName)); + _setToken.invokeApprove(_wrappedToken, wrapAdapter.getSpenderAddress(_underlyingToken, _wrappedToken), notionalWrappedToken); + // Get function call data and invoke on SetToken _createUnwrapDataAndInvoke( _setToken, From d8e007c6edbb5ab91c047fadd991637f01d1f65e Mon Sep 17 00:00:00 2001 From: ncitron Date: Fri, 13 Aug 2021 13:07:13 -0400 Subject: [PATCH 7/8] review fixes --- contracts/interfaces/IWrapV2Adapter.sol | 3 +-- .../mocks/integrations/WrapV2AdapterMock.sol | 26 +++++++++++++++++-- .../wrap-v2/CompoundWrapV2Adapter.sol | 3 +-- .../wrap-v2/YearnWrapV2Adapter.sol | 3 +-- contracts/protocol/modules/WrapModuleV2.sol | 6 ++++- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/contracts/interfaces/IWrapV2Adapter.sol b/contracts/interfaces/IWrapV2Adapter.sol index 67e0576a8..96506d186 100644 --- a/contracts/interfaces/IWrapV2Adapter.sol +++ b/contracts/interfaces/IWrapV2Adapter.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. @@ -21,7 +21,6 @@ pragma solidity 0.6.10; /** * @title IWrapV2Adapter * @author Set Protocol - * */ interface IWrapV2Adapter { diff --git a/contracts/mocks/integrations/WrapV2AdapterMock.sol b/contracts/mocks/integrations/WrapV2AdapterMock.sol index ddca3887b..8fa2cfdf6 100644 --- a/contracts/mocks/integrations/WrapV2AdapterMock.sol +++ b/contracts/mocks/integrations/WrapV2AdapterMock.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. @@ -65,7 +65,14 @@ contract WrapV2AdapterMock is ERC20 { } /** - * [x] + * Generates the calldata to wrap an underlying asset into a wrappedToken. + * + * @param _underlyingToken Address of the component to be wrapped + * @param _underlyingUnits Total quantity of underlying units to wrap + * + * @return _subject Target contract address + * @return _value Total quantity of underlying units (if underlying is ETH) + * @return _calldata Wrap calldata */ function getWrapCallData( address _underlyingToken, @@ -79,6 +86,16 @@ contract WrapV2AdapterMock is ERC20 { return (address(this), value, callData); } + /** + * Generates the calldata to unwrap a wrapped asset into its underlying. + * + * @param _underlyingToken Address of the underlying of the component to be unwrapped + * @param _wrappedTokenUnits Total quantity of wrapped token units to unwrap + * + * @return _subject Target contract address + * @return _value Total quantity of wrapped token units to unwrap. This will always be 0 for unwrapping + * @return _calldata Unwrap calldata + */ function getUnwrapCallData( address _underlyingToken, address /* _wrappedToken */, @@ -90,6 +107,11 @@ contract WrapV2AdapterMock is ERC20 { return (address(this), 0, callData); } + /** + * Returns the address to approve source tokens for wrapping. + * + * @return address Address of the contract to approve tokens to + */ function getSpenderAddress( address /* _underlyingToken */, address /* _wrappedToken */ diff --git a/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol b/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol index 2152aa388..e26bb7d1a 100644 --- a/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol +++ b/contracts/protocol/integration/wrap-v2/CompoundWrapV2Adapter.sol @@ -20,12 +20,11 @@ pragma solidity 0.6.10; pragma experimental "ABIEncoderV2"; import { ICErc20 } from "../../../interfaces/external/ICErc20.sol"; -import { IWrapV2Adapter } from "../../../interfaces/IWrapV2Adapter.sol"; import { Compound } from "../lib/Compound.sol"; /** * @title CompoundWrapV2Adapter - * @author Set Protocol, Ember Fund + * @author Set Protocol * * Wrap adapter for Compound that returns data for wraps/unwraps of tokens */ diff --git a/contracts/protocol/integration/wrap-v2/YearnWrapV2Adapter.sol b/contracts/protocol/integration/wrap-v2/YearnWrapV2Adapter.sol index 9f6100fe4..8bdc0d076 100644 --- a/contracts/protocol/integration/wrap-v2/YearnWrapV2Adapter.sol +++ b/contracts/protocol/integration/wrap-v2/YearnWrapV2Adapter.sol @@ -19,12 +19,11 @@ pragma solidity 0.6.10; pragma experimental "ABIEncoderV2"; -import { IWrapV2Adapter } from "../../../interfaces/IWrapV2Adapter.sol"; import { IYearnVault } from "../../../interfaces/external/IYearnVault.sol"; /** * @title YearnWrapV2Adapter - * @author Set Protocol, Ember Fund + * @author Set Protocol * * Wrap adapter for Yearn that returns data for wraps/unwraps of tokens */ diff --git a/contracts/protocol/modules/WrapModuleV2.sol b/contracts/protocol/modules/WrapModuleV2.sol index 3d6a4d2bf..4ba30e1ae 100644 --- a/contracts/protocol/modules/WrapModuleV2.sol +++ b/contracts/protocol/modules/WrapModuleV2.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. @@ -99,6 +99,7 @@ contract WrapModuleV2 is ModuleBase, ReentrancyGuard { * @param _wrappedToken Address of the desired wrapped token * @param _underlyingUnits Quantity of underlying units in Position units * @param _integrationName Name of wrap module integration (mapping on integration registry) + * @param _wrapData Arbitrary bytes to pass into the WrapV2Adapter */ function wrap( ISetToken _setToken, @@ -144,6 +145,7 @@ contract WrapModuleV2 is ModuleBase, ReentrancyGuard { * @param _wrappedToken Address of the desired wrapped token * @param _underlyingUnits Quantity of underlying units in Position units * @param _integrationName Name of wrap module integration (mapping on integration registry) + * @param _wrapData Arbitrary bytes to pass into the WrapV2Adapter */ function wrapWithEther( ISetToken _setToken, @@ -187,6 +189,7 @@ contract WrapModuleV2 is ModuleBase, ReentrancyGuard { * @param _wrappedToken Address of the component to be unwrapped * @param _wrappedUnits Quantity of wrapped tokens in Position units * @param _integrationName ID of wrap module integration (mapping on integration registry) + * @param _unwrapData Arbitrary bytes to pass into the WrapV2Adapter */ function unwrap( ISetToken _setToken, @@ -231,6 +234,7 @@ contract WrapModuleV2 is ModuleBase, ReentrancyGuard { * @param _wrappedToken Address of the component to be unwrapped * @param _wrappedUnits Quantity of wrapped tokens in Position units * @param _integrationName ID of wrap module integration (mapping on integration registry) + * @param _unwrapData Arbitrary bytes to pass into the WrapV2Adapter */ function unwrapWithEther( ISetToken _setToken, From f78d95b2fac4928948a444aad1f907ded228b4c5 Mon Sep 17 00:00:00 2001 From: ncitron Date: Fri, 13 Aug 2021 13:13:34 -0400 Subject: [PATCH 8/8] add comment about why we approve on unwrap --- contracts/protocol/modules/WrapModuleV2.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/protocol/modules/WrapModuleV2.sol b/contracts/protocol/modules/WrapModuleV2.sol index 4ba30e1ae..bfee1c525 100644 --- a/contracts/protocol/modules/WrapModuleV2.sol +++ b/contracts/protocol/modules/WrapModuleV2.sol @@ -407,6 +407,7 @@ contract WrapModuleV2 is ModuleBase, ReentrancyGuard { uint256 notionalWrappedToken = _setToken.totalSupply().getDefaultTotalNotional(_wrappedTokenUnits); IWrapV2Adapter wrapAdapter = IWrapV2Adapter(getAndValidateAdapter(_integrationName)); + // Approve wrapped token for spending in case protocols require approvals to transfer wrapped tokens _setToken.invokeApprove(_wrappedToken, wrapAdapter.getSpenderAddress(_underlyingToken, _wrappedToken), notionalWrappedToken); // Get function call data and invoke on SetToken