diff --git a/contracts/interfaces/external/aave-v2/IAToken.sol b/contracts/interfaces/external/aave-v2/IAToken.sol index b93034f65..21ac91d8e 100644 --- a/contracts/interfaces/external/aave-v2/IAToken.sol +++ b/contracts/interfaces/external/aave-v2/IAToken.sol @@ -18,4 +18,6 @@ pragma solidity 0.6.10; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -interface IAToken is IERC20 {} +interface IAToken is IERC20 { + function UNDERLYING_ASSET_ADDRESS() external view returns (address); +} diff --git a/contracts/protocol/integration/wrap-v2/AaveV2WrapV2Adapter.sol b/contracts/protocol/integration/wrap-v2/AaveV2WrapV2Adapter.sol new file mode 100644 index 000000000..559366bae --- /dev/null +++ b/contracts/protocol/integration/wrap-v2/AaveV2WrapV2Adapter.sol @@ -0,0 +1,148 @@ +/* + 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 { IAToken } from "../../../interfaces/external/aave-v2/IAToken.sol"; +import { ILendingPool } from "../../../interfaces/external/aave-v2/ILendingPool.sol"; + +/** + * @title AaveV2WrapV2Adapter + * @author Set Protocol + * + * Wrap adapter for Aave V2 that returns data for wraps/unwraps of tokens + */ +contract AaveV2WrapV2Adapter { + + /* ============ 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"); + _; + } + + /* ========== State Variables ========= */ + + // Address of the Aave LendingPool contract + // Note: this address may change in the event of an upgrade + ILendingPool public lendingPool; + + /* ============ Constructor ============ */ + + constructor(ILendingPool _lendingPool) public { + lendingPool = _lendingPool; + } + + /* ============ 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 + * @param _to Address to send the wrapped tokens to + * + * @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(address,uint256,address,uint16)", + _underlyingToken, + _underlyingUnits, + _to, + 0 + ); + + return (address(lendingPool), 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 + * @param _to Address to send the unwrapped tokens to + * + * @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 /* _wrapData */ + ) + external + view + _onlyValidTokenPair(_underlyingToken, _wrappedToken) + returns (address, uint256, bytes memory) + { + bytes memory callData = abi.encodeWithSignature( + "withdraw(address,uint256,address)", + _underlyingToken, + _wrappedTokenUnits, + _to + ); + + return (address(lendingPool), 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 view returns(address) { + return address(lendingPool); + } + + /* ============ 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) { + return IAToken(_wrappedToken).UNDERLYING_ASSET_ADDRESS() == _underlyingToken; + } +} \ No newline at end of file diff --git a/test/integration/wrap-v2/aaveV2WrapModuleV2.spec.ts b/test/integration/wrap-v2/aaveV2WrapModuleV2.spec.ts new file mode 100644 index 000000000..023e10ae1 --- /dev/null +++ b/test/integration/wrap-v2/aaveV2WrapModuleV2.spec.ts @@ -0,0 +1,206 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ADDRESS_ZERO, MAX_UINT_256, ZERO_BYTES } from "@utils/constants"; +import { AaveV2WrapV2Adapter, SetToken, StandardTokenMock, WrapModuleV2 } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + ether, + preciseMul, +} from "@utils/index"; +import { + getAccounts, + getWaffleExpect, + getSystemFixture, + addSnapshotBeforeRestoreAfterEach, + getAaveV2Fixture, +} from "@utils/test/index"; +import { AaveV2Fixture, SystemFixture } from "@utils/fixtures"; +import { + AaveV2AToken +} from "@utils/contracts/aaveV2"; + +const expect = getWaffleExpect(); + +describe("AaveV2WrapModule", () => { + + let owner: Account; + let deployer: DeployHelper; + + let setV2Setup: SystemFixture; + let aaveV2Setup: AaveV2Fixture; + + let aaveV2WrapAdapter: AaveV2WrapV2Adapter; + let wrapModule: WrapModuleV2; + + let underlyingToken: StandardTokenMock; + let wrappedToken: AaveV2AToken; + + const aaveV2WrapAdapterIntegrationName: string = "AAVE_V2_WRAPPER"; + + before(async () => { + [ owner ] = await getAccounts(); + + // System setup + deployer = new DeployHelper(owner.wallet); + setV2Setup = getSystemFixture(owner.address); + await setV2Setup.initialize(); + + // Aave setup + aaveV2Setup = getAaveV2Fixture(owner.address); + await aaveV2Setup.initialize(setV2Setup.weth.address, setV2Setup.dai.address); + + underlyingToken = setV2Setup.dai; + wrappedToken = aaveV2Setup.daiReserveTokens.aToken; + + // WrapModule setup + wrapModule = await deployer.modules.deployWrapModuleV2(setV2Setup.controller.address, setV2Setup.weth.address); + await setV2Setup.controller.addModule(wrapModule.address); + + // AaveV2WrapAdapter setup + aaveV2WrapAdapter = await deployer.adapters.deployAaveV2WrapV2Adapter(aaveV2Setup.lendingPool.address); + await setV2Setup.integrationRegistry.addIntegration(wrapModule.address, aaveV2WrapAdapterIntegrationName, aaveV2WrapAdapter.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + context("when a SetToken has been deployed and issued", async () => { + let setToken: SetToken; + let setTokensIssued: BigNumber; + + before(async () => { + setToken = await setV2Setup.createSetToken( + [setV2Setup.dai.address], + [ether(1)], + [setV2Setup.issuanceModule.address, wrapModule.address] + ); + + // Initialize modules + await setV2Setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + await wrapModule.initialize(setToken.address); + + // Issue some Sets + setTokensIssued = ether(10); + const underlyingRequired = setTokensIssued; + await setV2Setup.dai.approve(setV2Setup.issuanceModule.address, underlyingRequired); + await setV2Setup.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 = underlyingToken.address; + subjectWrappedToken = wrappedToken.address; + subjectUnderlyingUnits = ether(1); + subjectIntegrationName = aaveV2WrapAdapterIntegrationName; + 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 underlyingToken.balanceOf(setToken.address); + const previousWrappedBalance = await wrappedToken.balanceOf(setToken.address); + + await subject(); + + const underlyingBalance = await underlyingToken.balanceOf(setToken.address); + const wrappedBalance = await wrappedToken.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 = underlyingToken.address; + subjectWrappedToken = wrappedToken.address; + subjectWrappedTokenUnits = ether(0.5); + subjectIntegrationName = aaveV2WrapAdapterIntegrationName; + subjectUnwrapData = ZERO_BYTES; + subjectCaller = owner; + + wrappedQuantity = ether(1); + + await wrapModule.wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + wrappedQuantity, + subjectIntegrationName, + ZERO_BYTES + ); + + await underlyingToken.approve(aaveV2Setup.lendingPool.address, MAX_UINT_256); + await aaveV2Setup.lendingPool.deposit(underlyingToken.address, ether(100000), owner.address, 0); + }); + + 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 underlyingToken.balanceOf(setToken.address); + const previousWrappedBalance = await wrappedToken.balanceOf(setToken.address); + + await subject(); + + const underlyingBalance = await underlyingToken.balanceOf(setToken.address); + const wrappedBalance = await wrappedToken.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); + }); + }); + }); +}); diff --git a/test/protocol/integration/wrap-v2/aaveV2WrapV2Adapter.spec.ts b/test/protocol/integration/wrap-v2/aaveV2WrapV2Adapter.spec.ts new file mode 100644 index 000000000..db14a753a --- /dev/null +++ b/test/protocol/integration/wrap-v2/aaveV2WrapV2Adapter.spec.ts @@ -0,0 +1,172 @@ +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 { AaveV2WrapV2Adapter, StandardTokenMock } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { ether } from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAaveV2Fixture, + getAccounts, + getRandomAddress, + getSystemFixture, + getWaffleExpect, +} from "@utils/test/index"; +import { AaveV2AToken } from "@utils/contracts/aaveV2"; +import { AaveV2Fixture, SystemFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("AaveV2WrapAdapter", () => { + + let owner: Account; + let deployer: DeployHelper; + + let setV2Setup: SystemFixture; + let aaveV2Setup: AaveV2Fixture; + + let aaveWrapAdapter: AaveV2WrapV2Adapter; + + let underlyingToken: StandardTokenMock; + let wrappedToken: AaveV2AToken; + + before(async () => { + [ owner ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSystemFixture(owner.address); + aaveV2Setup = getAaveV2Fixture(owner.address); + + await setV2Setup.initialize(); + await aaveV2Setup.initialize(setV2Setup.weth.address, setV2Setup.dai.address); + + underlyingToken = setV2Setup.dai; + wrappedToken = aaveV2Setup.daiReserveTokens.aToken; + + aaveWrapAdapter = await deployer.adapters.deployAaveV2WrapV2Adapter(aaveV2Setup.lendingPool.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectLendingPool: Address; + + beforeEach(async () => { + subjectLendingPool = aaveV2Setup.lendingPool.address; + }); + + async function subject(): Promise { + return deployer.adapters.deployAaveV2WrapV2Adapter(subjectLendingPool); + } + + it("should have the correct LendingPool addresses", async () => { + const deployedAaveV2WrapAdapter = await subject(); + + expect(await deployedAaveV2WrapAdapter.lendingPool()).to.eq(subjectLendingPool); + }); + }); + + describe("#getSpenderAddress", async () => { + async function subject(): Promise { + return aaveWrapAdapter.getSpenderAddress(underlyingToken.address, wrappedToken.address); + } + + it("should return the correct spender address", async () => { + const spender = await subject(); + + expect(spender).to.eq(aaveV2Setup.lendingPool.address); + }); + }); + + describe("#getWrapCallData", async () => { + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectUnderlyingUnits: BigNumber; + let subjectTo: Address; + let subjectWrapData: string; + + beforeEach(async () => { + subjectUnderlyingToken = underlyingToken.address; + subjectWrappedToken = wrappedToken.address; + subjectUnderlyingUnits = ether(2); + subjectTo = await getRandomAddress(); + subjectWrapData = ZERO_BYTES; + }); + + async function subject(): Promise<[string, BigNumber, string]> { + return aaveWrapAdapter.getWrapCallData(subjectUnderlyingToken, subjectWrappedToken, subjectUnderlyingUnits, subjectTo, subjectWrapData); + } + + it("should return correct data for valid pair", async () => { + const [targetAddress, ethValue, callData] = await subject(); + + const expectedCallData = aaveV2Setup.lendingPool.interface.encodeFunctionData( + "deposit", + [subjectUnderlyingToken, subjectUnderlyingUnits, subjectTo, 0] + ); + + expect(targetAddress).to.eq(aaveV2Setup.lendingPool.address); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCallData); + }); + + describe("when invalid wrapped token / underlying token pair", () => { + beforeEach(async () => { + subjectUnderlyingToken = underlyingToken.address; + subjectWrappedToken = aaveV2Setup.wethReserveTokens.aToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid token pair"); + }); + }); + }); + + describe("#getUnwrapCallData", async () => { + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectWrappedTokenUnits: BigNumber; + let subjectTo: Address; + let subjectUnwrapData: string; + + beforeEach(async () => { + subjectUnderlyingToken = underlyingToken.address; + subjectWrappedToken = wrappedToken.address; + subjectWrappedTokenUnits = ether(2); + subjectTo = await getRandomAddress(); + subjectUnwrapData = ZERO_BYTES; + }); + + async function subject(): Promise<[string, BigNumber, string]> { + return aaveWrapAdapter.getUnwrapCallData(subjectUnderlyingToken, subjectWrappedToken, subjectWrappedTokenUnits, subjectTo, subjectUnwrapData); + } + + it("should return correct data for valid pair", async () => { + const [targetAddress, ethValue, callData] = await subject(); + + const expectedCallData = aaveV2Setup.lendingPool.interface.encodeFunctionData( + "withdraw", + [subjectUnderlyingToken, subjectWrappedTokenUnits, subjectTo] + ); + + expect(targetAddress).to.eq(aaveV2Setup.lendingPool.address); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCallData); + }); + + describe("when invalid wrapped token / underlying token pair", () => { + beforeEach(async () => { + subjectUnderlyingToken = underlyingToken.address; + subjectWrappedToken = aaveV2Setup.wethReserveTokens.aToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid token pair"); + }); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 5516d773d..0162b8c1d 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -7,6 +7,7 @@ export { AaveMigrationWrapAdapter } from "../../typechain/AaveMigrationWrapAdapt export { AaveWrapAdapter } from "../../typechain/AaveWrapAdapter"; export { AaveV2 } from "../../typechain/AaveV2"; export { AaveV2Mock } from "../../typechain/AaveV2Mock"; +export { AaveV2WrapV2Adapter } from "../../typechain/AaveV2WrapV2Adapter"; export { AddressArrayUtilsMock } from "../../typechain/AddressArrayUtilsMock"; export { AirdropModule } from "../../typechain/AirdropModule"; export { AmmAdapterMock } from "../../typechain/AmmAdapterMock"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index d5f557c63..0ad70b64e 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -3,6 +3,7 @@ import { Signer } from "ethers"; import { AaveGovernanceAdapter, AaveGovernanceV2Adapter, + AaveV2WrapV2Adapter, AGIMigrationWrapAdapter, AxieInfinityMigrationWrapAdapter, BalancerV1IndexExchangeAdapter, @@ -36,6 +37,7 @@ import { Address, Bytes } from "./../types"; import { AaveGovernanceAdapter__factory } from "../../typechain/factories/AaveGovernanceAdapter__factory"; import { AaveGovernanceV2Adapter__factory } from "../../typechain/factories/AaveGovernanceV2Adapter__factory"; +import { AaveV2WrapV2Adapter__factory } from "../../typechain/factories/AaveV2WrapV2Adapter__factory"; import { AxieInfinityMigrationWrapAdapter__factory } from "../../typechain/factories/AxieInfinityMigrationWrapAdapter__factory"; import { BalancerV1IndexExchangeAdapter__factory } from "../../typechain/factories/BalancerV1IndexExchangeAdapter__factory"; import { CompoundLikeGovernanceAdapter__factory } from "../../typechain/factories/CompoundLikeGovernanceAdapter__factory"; @@ -233,4 +235,8 @@ export default class DeployAdapters { public async deployYearnWrapV2Adapter(): Promise { return await new YearnWrapV2Adapter__factory(this._deployerSigner).deploy(); } + + public async deployAaveV2WrapV2Adapter(lendingPool: Address): Promise { + return await new AaveV2WrapV2Adapter__factory(this._deployerSigner).deploy(lendingPool); + } }