From 5436a6b33e8a8a9dd82dc8d89c7a1d9c068c21c2 Mon Sep 17 00:00:00 2001 From: nate welch Date: Mon, 28 Dec 2020 20:39:21 -0500 Subject: [PATCH] Added SushiBarWrapAdapter. --- .../integration/SushiBarWrapAdapter.sol | 109 +++++++++++ .../integration/sushiWrapAdapter.spec.ts | 173 ++++++++++++++++++ utils/contracts/index.ts | 1 + utils/deploys/deployAdapters.ts | 12 +- 4 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 contracts/protocol/integration/SushiBarWrapAdapter.sol create mode 100644 test/protocol/integration/sushiWrapAdapter.spec.ts diff --git a/contracts/protocol/integration/SushiBarWrapAdapter.sol b/contracts/protocol/integration/SushiBarWrapAdapter.sol new file mode 100644 index 0000000..b57d595 --- /dev/null +++ b/contracts/protocol/integration/SushiBarWrapAdapter.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.6.10; + +/** + * @title SushiBarWrapAdapter + * @author Yam Finance + * + * Wrap adapter for depositing/withdrawing sushi to/from SushiBar (xSushi) + */ +contract SushiBarWrapAdapter { + + /* ============ State Variables ============ */ + + + // Address of SUSHI token + address public immutable sushiToken; + + // Address of xSUSHI token + address public immutable xsushiToken; + + /* ============ Constructor ============ */ + + /** + * State variables + * + * @param _sushiToken Address of SUSHI token + * @param _xsushiToken Address of XSUSHI token (also the SushiBar) + */ + constructor( + address _sushiToken, + address _xsushiToken + ) + public + { + sushiToken = _sushiToken; + xsushiToken = _xsushiToken; + } + + /* ============ External Getter Functions ============ */ + + /** + * Generates the calldata to wrap sushi into xsushi. + * + * @param _underlyingToken Address of the component to be wrapped + * @param _wrappedToken Address of the wrapped component + * @param _underlyingUnits Total quantity of underlying units to wrap + * + * @return address Target contract address + * @return uint256 Unused, always 0 + * @return bytes Wrap calldata + */ + function getWrapCallData( + address _underlyingToken, + address _wrappedToken, + uint256 _underlyingUnits + ) + external + view + returns (address, uint256, bytes memory) + { + require(_underlyingToken == sushiToken, "Must be a valid token pair"); + require(_wrappedToken == xsushiToken, "Must be a valid token pair"); + + // enter(uint256 _amount) + bytes memory callData = abi.encodeWithSignature("enter(uint256)", _underlyingUnits); + + return (xsushiToken, 0, callData); + } + + /** + * Generates the calldata to unwrap xsushi to sushi + * + * @param _underlyingToken Address of the component to be unwrapped to + * @param _wrappedToken Address of the wrapped component + * @param _wrappedTokenUnits Total quantity of wrapped units to wrap + * + * @return address Target contract address + * @return uint256 Unused, always 0 + * @return bytes Unwrap calldata + */ + function getUnwrapCallData( + address _underlyingToken, + address _wrappedToken, + uint256 _wrappedTokenUnits + ) + external + view + returns (address, uint256, bytes memory) + { + require(_underlyingToken == sushiToken, "Must be a valid token pair"); + require(_wrappedToken == xsushiToken, "Must be a valid token pair"); + + // leave(uint256 _amount) + bytes memory callData = abi.encodeWithSignature("leave(uint256)", _wrappedTokenUnits); + + return (xsushiToken, 0, callData); + + } + + /** + * Returns the address to approve source tokens for wrapping. + * + * @return address Address of the contract to approve tokens to. This is the SushiBar (xSushi) contract. + */ + function getSpenderAddress(address /*_underlyingToken*/, address /*_wrappedToken*/) external view returns(address) { + return address(xsushiToken); + } +} \ No newline at end of file diff --git a/test/protocol/integration/sushiWrapAdapter.spec.ts b/test/protocol/integration/sushiWrapAdapter.spec.ts new file mode 100644 index 0000000..2fc7715 --- /dev/null +++ b/test/protocol/integration/sushiWrapAdapter.spec.ts @@ -0,0 +1,173 @@ +import "module-alias/register"; +import { BigNumber } from "ethers/utils"; + +import { Address, Account } from "@utils/types"; +import { ZERO } from "@utils/constants"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect, + bigNumberToData +} from "@utils/index"; +import { SushiBarWrapAdapter } from "@utils/contracts"; + +const expect = getWaffleExpect(); + +describe.only("SushiWrapAdapter", () => { + let owner: Account; + let deployer: DeployHelper; + let sushiWrapAdapter: SushiBarWrapAdapter; + let underlyingToken: Account; + let wrappedToken: Account; + let ethWrappedToken: Account; + let otherUnderlyingToken: Account; + + before(async () => { + [ + owner, + underlyingToken, + wrappedToken, + ethWrappedToken, + otherUnderlyingToken, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + sushiWrapAdapter = await deployer.adapters.deploySushiBarWrapAdapter(underlyingToken.address, wrappedToken.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + + async function subject(): Promise { + return deployer.adapters.deploySushiBarWrapAdapter(underlyingToken.address, wrappedToken.address); + } + + it("should have the correct sushi and sushiBar addresses", async () => { + const deployedSushiBarWrapAdapter = await subject(); + + const actualSushi = await deployedSushiBarWrapAdapter.sushiToken(); + const actualSushiBar = await deployedSushiBarWrapAdapter.xsushiToken(); + expect(actualSushi).to.eq(underlyingToken.address); + expect(actualSushiBar).to.eq(wrappedToken.address); + }); + }); + + describe("#getSpenderAddress", async () => { + async function subject(): Promise { + return sushiWrapAdapter.getSpenderAddress(underlyingToken.address, wrappedToken.address); + } + + it("should return the correct spender address", async () => { + const spender = await subject(); + + expect(spender).to.eq(wrappedToken.address); + }); + }); + + describe("#getWrapCallData", async () => { + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectUnderlyingUnits: BigNumber; + const depositSignature = "0xa59f3e0c"; // enter(uint256) + const generateCallData = (token: Address, units: BigNumber) => + depositSignature + + bigNumberToData(units); + + beforeEach(async () => { + subjectUnderlyingToken = underlyingToken.address; + subjectWrappedToken = wrappedToken.address; + subjectUnderlyingUnits = ether(2); + }); + + async function subject(): Promise { + return sushiWrapAdapter.getWrapCallData(subjectUnderlyingToken, subjectWrappedToken, subjectUnderlyingUnits); + } + + it("should return correct data for valid pair", async () => { + const [targetAddress, ethValue, callData] = await subject(); + + const expectedCallData = generateCallData(subjectUnderlyingToken, subjectUnderlyingUnits); + + expect(targetAddress).to.eq(wrappedToken.address); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCallData); + }); + + + + describe("when invalid underlying token", () => { + beforeEach(async () => { + subjectUnderlyingToken = otherUnderlyingToken.address; + subjectWrappedToken = wrappedToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid token pair"); + }); + }); + + describe("when invalid wrapped token", () => { + beforeEach(async () => { + subjectUnderlyingToken = underlyingToken.address; + subjectWrappedToken = ethWrappedToken.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; + const redeemSignature = "0x67dfd4c9"; // leave(uint256) + const generateCallData = (units: BigNumber) => redeemSignature + bigNumberToData(units); + + beforeEach(async () => { + subjectUnderlyingToken = underlyingToken.address; + subjectWrappedToken = wrappedToken.address; + subjectWrappedTokenUnits = ether(2); + }); + + async function subject(): Promise { + return sushiWrapAdapter.getUnwrapCallData(subjectUnderlyingToken, subjectWrappedToken, subjectWrappedTokenUnits); + } + + it("should return correct data for valid pair", async () => { + const [targetAddress, ethValue, callData] = await subject(); + + const expectedCallData = generateCallData(subjectWrappedTokenUnits); + + expect(targetAddress).to.eq(subjectWrappedToken); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCallData); + }); + + describe("when invalid underlying token", () => { + beforeEach(async () => { + subjectUnderlyingToken = otherUnderlyingToken.address; + subjectWrappedToken = wrappedToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid token pair"); + }); + }); + + describe("when invalid wrapped token", () => { + beforeEach(async () => { + subjectUnderlyingToken = underlyingToken.address; + subjectWrappedToken = ethWrappedToken.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 1cafc93..b1422b9 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -63,3 +63,4 @@ export { UniswapYieldStrategy } from "../../typechain/UniswapYieldStrategy"; export { Weth9 } from "../../typechain/Weth9"; export { WrapAdapterMock } from "../../typechain/WrapAdapterMock"; export { WrapModule } from "../../typechain/WrapModule"; +export { SushiBarWrapAdapter } from "../../typechain/SushiBarWrapAdapter"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index b84f0c0..1c0c62d 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -8,7 +8,8 @@ import { OneInchExchangeAdapter, AaveMigrationWrapAdapter, AaveWrapAdapter, - UniswapPairPriceAdapter + UniswapPairPriceAdapter, + SushiBarWrapAdapter } from "../contracts"; import { Address, Bytes } from "./../types"; @@ -21,6 +22,7 @@ import { OneInchExchangeAdapterFactory } from "../../typechain/OneInchExchangeAd import { AaveMigrationWrapAdapterFactory } from "../../typechain/AaveMigrationWrapAdapterFactory"; import { AaveWrapAdapterFactory } from "../../typechain/AaveWrapAdapterFactory"; import { UniswapPairPriceAdapterFactory } from "../../typechain/UniswapPairPriceAdapterFactory"; +import { SushiBarWrapAdapterFactory } from "@typechain/SushiBarWrapAdapterFactory"; export default class DeployAdapters { private _deployerSigner: Signer; @@ -77,7 +79,15 @@ export default class DeployAdapters { return await new UniswapPairPriceAdapterFactory(this._deployerSigner).deploy(controller, uniswapFactory, uniswapPools); } + public async deploySushiBarWrapAdapter( + sushi: Address, + sushiBar: Address + ): Promise { + return await new SushiBarWrapAdapterFactory(this._deployerSigner).deploy(sushi, sushiBar); + } + public async getUniswapPairPriceAdapter(uniswapAdapterAddress: Address): Promise { return await new UniswapPairPriceAdapterFactory(this._deployerSigner).attach(uniswapAdapterAddress); } + }