diff --git a/contracts/protocol/integration/wrap/SushiBarWrapAdapter.sol b/contracts/protocol/integration/wrap/SushiBarWrapAdapter.sol new file mode 100644 index 000000000..709590b73 --- /dev/null +++ b/contracts/protocol/integration/wrap/SushiBarWrapAdapter.sol @@ -0,0 +1,130 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; + +/** + * @title SushiBarWrapAdapter + * @author Yam Finance, Set Protocol + * + * 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 ============ */ + + /** + * Set state variables + * + * @param _sushiToken Address of SUSHI token + * @param _xSushiToken Address of xSUSHI token + */ + constructor(address _sushiToken, address _xSushiToken) public { + sushiToken = _sushiToken; + xSushiToken = _xSushiToken; + } + + /* ============ External Functions ============ */ + + /** + * Generates the calldata to wrap Sushi into xSushi. + * + * @param _underlyingToken Address of SUSHI token + * @param _wrappedToken Address of xSUSHI token + * @param _underlyingUnits Total quantity of SUSHI 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) + { + _validateWrapInputs(_underlyingToken, _wrappedToken); + + // Signature for wrapping in xSUSHI is 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 SUSHI token + * @param _wrappedToken Address of xSUSHI token + * @param _wrappedTokenUnits Total quantity of xSUSHI units to unwrap + * + * @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) + { + _validateWrapInputs(_underlyingToken, _wrappedToken); + + // Signature for unwrapping in xSUSHI is 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); + } + + /* ============ Internal Functions ============ */ + + /** + * Validate inputs prior to getting wrap and unwrap calldata. + * + * @param _underlyingToken Address of SUSHI token + * @param _wrappedToken Address of xSUSHI token + */ + function _validateWrapInputs(address _underlyingToken, address _wrappedToken) internal view { + require(_underlyingToken == sushiToken, "Underlying token must be SUSHI"); + require(_wrappedToken == xSushiToken, "Wrapped token must be xSUSHI"); + } +} \ No newline at end of file diff --git a/external/abi/sushiswap/SushiBar.json b/external/abi/sushiswap/SushiBar.json new file mode 100644 index 000000000..db997d246 --- /dev/null +++ b/external/abi/sushiswap/SushiBar.json @@ -0,0 +1,8 @@ +{ + "contractName": "SushiBar", + "abi": [{"inputs":[{"internalType":"contract IERC20","name":"_sushi","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"enter","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_share","type":"uint256"}],"name":"leave","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"sushi","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}], + "bytecode": "608060405234801561001057600080fd5b5060405162001217380380620012178339818101604052602081101561003557600080fd5b5051604080518082018252600881526729bab9b434a130b960c11b60208281019182528351808501909452600684526578535553484960d01b908401528151919291610083916003916100d0565b5080516100979060049060208401906100d0565b5050600580546001600160a01b0390931661010002610100600160a81b031960ff19909416601217939093169290921790915550610163565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061011157805160ff191683800117855561013e565b8280016001018555821561013e579182015b8281111561013e578251825591602001919060010190610123565b5061014a92915061014e565b5090565b5b8082111561014a576000815560010161014f565b6110a480620001736000396000f3fe608060405234801561001057600080fd5b50600436106100ea5760003560e01c806367dfd4c91161008c578063a457c2d711610066578063a457c2d7146102b7578063a59f3e0c146102e3578063a9059cbb14610300578063dd62ed3e1461032c576100ea565b806367dfd4c91461026a57806370a082311461028957806395d89b41146102af576100ea565b806318160ddd116100c857806318160ddd146101d057806323b872dd146101ea578063313ce56714610220578063395093511461023e576100ea565b806306fdde03146100ef578063095ea7b31461016c5780630a087903146101ac575b600080fd5b6100f761035a565b6040805160208082528351818301528351919283929083019185019080838360005b83811015610131578181015183820152602001610119565b50505050905090810190601f16801561015e5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6101986004803603604081101561018257600080fd5b506001600160a01b0381351690602001356103f0565b604080519115158252519081900360200190f35b6101b461040e565b604080516001600160a01b039092168252519081900360200190f35b6101d8610422565b60408051918252519081900360200190f35b6101986004803603606081101561020057600080fd5b506001600160a01b03813581169160208101359091169060400135610428565b6102286104af565b6040805160ff9092168252519081900360200190f35b6101986004803603604081101561025457600080fd5b506001600160a01b0381351690602001356104b8565b6102876004803603602081101561028057600080fd5b5035610506565b005b6101d86004803603602081101561029f57600080fd5b50356001600160a01b031661064b565b6100f7610666565b610198600480360360408110156102cd57600080fd5b506001600160a01b0381351690602001356106c7565b610287600480360360208110156102f957600080fd5b503561072f565b6101986004803603604081101561031657600080fd5b506001600160a01b038135169060200135610854565b6101d86004803603604081101561034257600080fd5b506001600160a01b0381358116916020013516610868565b60038054604080516020601f60026000196101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156103e65780601f106103bb576101008083540402835291602001916103e6565b820191906000526020600020905b8154815290600101906020018083116103c957829003601f168201915b5050505050905090565b60006104046103fd610893565b8484610897565b5060015b92915050565b60055461010090046001600160a01b031681565b60025490565b6000610435848484610983565b6104a584610441610893565b6104a085604051806060016040528060288152602001610fb8602891396001600160a01b038a1660009081526001602052604081209061047f610893565b6001600160a01b031681526020810191909152604001600020549190610ade565b610897565b5060019392505050565b60055460ff1690565b60006104046104c5610893565b846104a085600160006104d6610893565b6001600160a01b03908116825260208083019390935260409182016000908120918c168152925290205490610b75565b6000610510610422565b905060006105b6826105b0600560019054906101000a90046001600160a01b03166001600160a01b03166370a08231306040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b15801561057d57600080fd5b505afa158015610591573d6000803e3d6000fd5b505050506040513d60208110156105a757600080fd5b50518690610bd6565b90610c2f565b90506105c23384610c71565b6005546040805163a9059cbb60e01b81523360048201526024810184905290516101009092046001600160a01b03169163a9059cbb916044808201926020929091908290030181600087803b15801561061a57600080fd5b505af115801561062e573d6000803e3d6000fd5b505050506040513d602081101561064457600080fd5b5050505050565b6001600160a01b031660009081526020819052604090205490565b60048054604080516020601f60026000196101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156103e65780601f106103bb576101008083540402835291602001916103e6565b60006104046106d4610893565b846104a08560405180606001604052806025815260200161104a60259139600160006106fe610893565b6001600160a01b03908116825260208083019390935260409182016000908120918d16815292529020549190610ade565b600554604080516370a0823160e01b8152306004820152905160009261010090046001600160a01b0316916370a08231916024808301926020929190829003018186803b15801561077f57600080fd5b505afa158015610793573d6000803e3d6000fd5b505050506040513d60208110156107a957600080fd5b5051905060006107b7610422565b90508015806107c4575081155b156107d8576107d33384610d6d565b6107f6565b60006107e8836105b08685610bd6565b90506107f43382610d6d565b505b600554604080516323b872dd60e01b81523360048201523060248201526044810186905290516101009092046001600160a01b0316916323b872dd916064808201926020929091908290030181600087803b15801561061a57600080fd5b6000610404610861610893565b8484610983565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b3390565b6001600160a01b0383166108dc5760405162461bcd60e51b81526004018080602001828103825260248152602001806110266024913960400191505060405180910390fd5b6001600160a01b0382166109215760405162461bcd60e51b8152600401808060200182810382526022815260200180610f4f6022913960400191505060405180910390fd5b6001600160a01b03808416600081815260016020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6001600160a01b0383166109c85760405162461bcd60e51b81526004018080602001828103825260258152602001806110016025913960400191505060405180910390fd5b6001600160a01b038216610a0d5760405162461bcd60e51b8152600401808060200182810382526023815260200180610f0a6023913960400191505060405180910390fd5b610a18838383610e5d565b610a5581604051806060016040528060268152602001610f71602691396001600160a01b0386166000908152602081905260409020549190610ade565b6001600160a01b038085166000908152602081905260408082209390935590841681522054610a849082610b75565b6001600160a01b038084166000818152602081815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b60008184841115610b6d5760405162461bcd60e51b81526004018080602001828103825283818151815260200191508051906020019080838360005b83811015610b32578181015183820152602001610b1a565b50505050905090810190601f168015610b5f5780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b600082820183811015610bcf576040805162461bcd60e51b815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b9392505050565b600082610be557506000610408565b82820282848281610bf257fe5b0414610bcf5760405162461bcd60e51b8152600401808060200182810382526021815260200180610f976021913960400191505060405180910390fd5b6000610bcf83836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f000000000000815250610e62565b6001600160a01b038216610cb65760405162461bcd60e51b8152600401808060200182810382526021815260200180610fe06021913960400191505060405180910390fd5b610cc282600083610e5d565b610cff81604051806060016040528060228152602001610f2d602291396001600160a01b0385166000908152602081905260409020549190610ade565b6001600160a01b038316600090815260208190526040902055600254610d259082610ec7565b6002556040805182815290516000916001600160a01b038516917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a35050565b6001600160a01b038216610dc8576040805162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f206164647265737300604482015290519081900360640190fd5b610dd460008383610e5d565b600254610de19082610b75565b6002556001600160a01b038216600090815260208190526040902054610e079082610b75565b6001600160a01b0383166000818152602081815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35050565b505050565b60008183610eb15760405162461bcd60e51b8152602060048201818152835160248401528351909283926044909101919085019080838360008315610b32578181015183820152602001610b1a565b506000838581610ebd57fe5b0495945050505050565b6000610bcf83836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250610ade56fe45524332303a207472616e7366657220746f20746865207a65726f206164647265737345524332303a206275726e20616d6f756e7420657863656564732062616c616e636545524332303a20617070726f766520746f20746865207a65726f206164647265737345524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e6365536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f7745524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636545524332303a206275726e2066726f6d20746865207a65726f206164647265737345524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737345524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa264697066735822122078a474ef0187246289e8ad301097c4a953d44a78c1198df89577d09214f91b9664736f6c634300060c0033", + "deployedBytecode": "", + "linkReferences": {}, + "deployedLinkReferences": {} + } \ No newline at end of file diff --git a/external/contracts/sushiswap/SushiBar.sol b/external/contracts/sushiswap/SushiBar.sol new file mode 100644 index 000000000..d0fe22e11 --- /dev/null +++ b/external/contracts/sushiswap/SushiBar.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; + +// SushiBar is the coolest bar in town. You come in with some Sushi, and leave with more! The longer you stay, the more Sushi you get. +// +// This contract handles swapping to and from xSushi, SushiSwap's staking token. +contract SushiBar is ERC20("SushiBar", "xSUSHI") { + using SafeMath for uint256; + IERC20 public sushi; + + // Define the Sushi token contract + constructor(IERC20 _sushi) public { + sushi = _sushi; + } + + // Enter the bar. Pay some SUSHIs. Earn some shares. + // Locks Sushi and mints xSushi + function enter(uint256 _amount) public { + // Gets the amount of Sushi locked in the contract + uint256 totalSushi = sushi.balanceOf(address(this)); + // Gets the amount of xSushi in existence + uint256 totalShares = totalSupply(); + // If no xSushi exists, mint it 1:1 to the amount put in + if (totalShares == 0 || totalSushi == 0) { + _mint(msg.sender, _amount); + } + // Calculate and mint the amount of xSushi the Sushi is worth. The ratio will change overtime, as xSushi is burned/minted and Sushi deposited + gained from fees / withdrawn. + else { + uint256 what = _amount.mul(totalShares).div(totalSushi); + _mint(msg.sender, what); + } + // Lock the Sushi in the contract + sushi.transferFrom(msg.sender, address(this), _amount); + } + + // Leave the bar. Claim back your SUSHIs. + // Unlocks the staked + gained Sushi and burns xSushi + function leave(uint256 _share) public { + // Gets the amount of xSushi in existence + uint256 totalShares = totalSupply(); + // Calculates the amount of Sushi the xSushi is worth + uint256 what = _share.mul(sushi.balanceOf(address(this))).div(totalShares); + _burn(msg.sender, _share); + sushi.transfer(msg.sender, what); + } +} \ No newline at end of file diff --git a/test/integration/sushiBarWrapModule.spec.ts b/test/integration/sushiBarWrapModule.spec.ts new file mode 100644 index 000000000..8383d5b6d --- /dev/null +++ b/test/integration/sushiBarWrapModule.spec.ts @@ -0,0 +1,204 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ADDRESS_ZERO } from "@utils/constants"; +import { + SushiBar, + SushiBarWrapAdapter, + SetToken, + StandardTokenMock, + WrapModule +} from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + ether, + preciseMul +} from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect, + getSystemFixture, +} from "@utils/test/index"; +import { SystemFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("SushiBarWrapModule", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + + let wrapModule: WrapModule; + let sushiBarWrapAdapter: SushiBarWrapAdapter; + let sushiToken: StandardTokenMock; + let xSushiToken: SushiBar; + + const sushiBarWrapAdapterIntegrationName: string = "SUSHI_BAR_WRAP_ADAPTER"; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + // System setup + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + // WrapModule setup + wrapModule = await deployer.modules.deployWrapModule(setup.controller.address, setup.weth.address); + await setup.controller.addModule(wrapModule.address); + + // Deploy SUSHI and xSUSHI tokens + sushiToken = await deployer.mocks.deployTokenMock(owner.address); + xSushiToken = await deployer.external.deploySushiBar(sushiToken.address); + + // AaveMigrationWrapAdapter setup + sushiBarWrapAdapter = await deployer.adapters.deploySushiBarWrapAdapter( + sushiToken.address, + xSushiToken.address + ); + + await setup.integrationRegistry.addIntegration(wrapModule.address, sushiBarWrapAdapterIntegrationName, sushiBarWrapAdapter.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + context("when a SetToken has been deployed and issued", async () => { + let setToken: SetToken; + let setTokensIssued: BigNumber; + + before(async () => { + setToken = await setup.createSetToken( + [sushiToken.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); + await sushiToken.approve(setup.issuanceModule.address, setTokensIssued); + + await setup.issuanceModule.issue(setToken.address, setTokensIssued, owner.address); + + // Mint initial xSUSHI and send extra + await sushiToken.approve(xSushiToken.address, ether(10000)); + await xSushiToken.enter(ether(1)); + await sushiToken.transfer(xSushiToken.address, ether(1)); + }); + + describe("#wrap", async () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectUnderlyingUnits: BigNumber; + let subjectIntegrationName: string; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = sushiToken.address; + subjectWrappedToken = xSushiToken.address; + subjectUnderlyingUnits = ether(0.4); + subjectIntegrationName = sushiBarWrapAdapterIntegrationName; + subjectCaller = owner; + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectUnderlyingUnits, + subjectIntegrationName, + ); + } + + it("should wrap SUSHI into xSUSHI", async () => { + const previousUnderlyingBalance = await sushiToken.balanceOf(setToken.address); + const previousSushiTokenUnit = await setToken.getDefaultPositionRealUnit(sushiToken.address); + const totalXSushiSupply = await xSushiToken.totalSupply(); + const totalSushiBalance = await sushiToken.balanceOf(xSushiToken.address); + + await subject(); + + const underlyingBalance = await sushiToken.balanceOf(setToken.address); + const currentSushiTokenUnit = await setToken.getDefaultPositionRealUnit(sushiToken.address); + const currentXSushiTokenUnit = await setToken.getDefaultPositionRealUnit(xSushiToken.address); + + const expectedUnderlyingBalance = previousUnderlyingBalance.sub(preciseMul(setTokensIssued, subjectUnderlyingUnits)); + expect(underlyingBalance).to.eq(expectedUnderlyingBalance); + expect(currentSushiTokenUnit).to.eq(previousSushiTokenUnit.sub(subjectUnderlyingUnits)); + expect(currentXSushiTokenUnit).to.eq(subjectUnderlyingUnits.mul(totalXSushiSupply).div(totalSushiBalance)); + }); + }); + + describe("#unwrap", () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectWrappedTokenUnits: BigNumber; + let subjectIntegrationName: string; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = sushiToken.address; + subjectWrappedToken = xSushiToken.address; + subjectWrappedTokenUnits = ether(0.1); + subjectIntegrationName = sushiBarWrapAdapterIntegrationName; + subjectCaller = owner; + + // Mint initial xSUSHI and send extra + await sushiToken.approve(xSushiToken.address, ether(10000)); + await xSushiToken.enter(ether(1)); + await sushiToken.transfer(xSushiToken.address, ether(1)); + + // Wrap xSUSHI + await wrapModule.wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + ether(1), + subjectIntegrationName, + ); + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).unwrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectWrappedTokenUnits, + subjectIntegrationName + ); + } + + it("should unwrap xSUSHI into SUSHI", async () => { + const previousXSushiBalance = await xSushiToken.balanceOf(setToken.address); + const previousXSushiTokenUnit = await setToken.getDefaultPositionRealUnit(xSushiToken.address); + const totalXSushiSupply = await xSushiToken.totalSupply(); + const totalSushiBalance = await sushiToken.balanceOf(xSushiToken.address); + + await subject(); + + const xSushiBalance = await xSushiToken.balanceOf(setToken.address); + const currentSushiTokenUnit = await setToken.getDefaultPositionRealUnit(sushiToken.address); + const currentXSushiTokenUnit = await setToken.getDefaultPositionRealUnit(xSushiToken.address); + + const expectedXSushiBalance = previousXSushiBalance.sub(preciseMul(setTokensIssued, subjectWrappedTokenUnits)); + + expect(xSushiBalance).to.eq(expectedXSushiBalance); + expect(currentXSushiTokenUnit).to.eq(previousXSushiTokenUnit.sub(subjectWrappedTokenUnits)); + expect(currentSushiTokenUnit).to.eq(subjectWrappedTokenUnits.mul(totalSushiBalance).div(totalXSushiSupply)); + }); + }); + }); +}); diff --git a/test/protocol/integration/wrap/sushiBarWrapAdapter.spec.ts b/test/protocol/integration/wrap/sushiBarWrapAdapter.spec.ts new file mode 100644 index 000000000..555d249d1 --- /dev/null +++ b/test/protocol/integration/wrap/sushiBarWrapAdapter.spec.ts @@ -0,0 +1,188 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ZERO } from "@utils/constants"; +import DeployHelper from "@utils/deploys"; +import { + cacheBeforeEach, + getAccounts, + getWaffleExpect +} from "@utils/test"; +import { + ether, + bigNumberToData +} from "@utils/common"; +import { SushiBarWrapAdapter } from "@utils/contracts"; + +const expect = getWaffleExpect(); + +describe("SushiBarWrapAdapter", () => { + let owner: Account; + let deployer: DeployHelper; + let sushiWrapAdapter: SushiBarWrapAdapter; + let underlyingToken: Account; + let wrappedToken: Account; + let ethWrappedToken: Account; + let otherUnderlyingToken: Account; + + cacheBeforeEach(async () => { + [ + owner, + underlyingToken, + wrappedToken, + ethWrappedToken, + otherUnderlyingToken, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + sushiWrapAdapter = await deployer.adapters.deploySushiBarWrapAdapter( + underlyingToken.address, + wrappedToken.address + ); + }); + + 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("Underlying token must be SUSHI"); + }); + }); + + describe("when invalid wrapped token", () => { + beforeEach(async () => { + subjectUnderlyingToken = underlyingToken.address; + subjectWrappedToken = ethWrappedToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Wrapped token must be xSUSHI"); + }); + }); + }); + + 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("Underlying token must be SUSHI"); + }); + }); + + describe("when invalid wrapped token", () => { + beforeEach(async () => { + subjectUnderlyingToken = underlyingToken.address; + subjectWrappedToken = ethWrappedToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Wrapped token must be xSUSHI"); + }); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index f50f7f8c8..d970f6139 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -78,6 +78,8 @@ export { StakingRewards } from "../../typechain/StakingRewards"; export { StandardTokenMock } from "../../typechain/StandardTokenMock"; export { StandardTokenWithFeeMock } from "../../typechain/StandardTokenWithFeeMock"; export { StreamingFeeModule } from "../../typechain/StreamingFeeModule"; +export { SushiBar } from "../../typechain/SushiBar"; +export { SushiBarWrapAdapter } from "../../typechain/SushiBarWrapAdapter"; export { SynthetixExchangeAdapter } from "../../typechain/SynthetixExchangeAdapter"; export { SynthetixExchangerMock } from "../../typechain/SynthetixExchangerMock"; export { SynthMock } from "../../typechain/SynthMock"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index b5c7a112c..d3373ac11 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -26,6 +26,7 @@ import { ZeroExApiAdapter, SnapshotGovernanceAdapter, SynthetixExchangeAdapter, + SushiBarWrapAdapter, CompoundBravoGovernanceAdapter, CompClaimAdapter, } from "../contracts"; @@ -56,6 +57,7 @@ import { UniswapV3IndexExchangeAdapter__factory } from "../../typechain/factorie import { UniswapV3ExchangeAdapter__factory } from "../../typechain/factories/UniswapV3ExchangeAdapter__factory"; import { SnapshotGovernanceAdapter__factory } from "../../typechain/factories/SnapshotGovernanceAdapter__factory"; import { SynthetixExchangeAdapter__factory } from "../../typechain/factories/SynthetixExchangeAdapter__factory"; +import { SushiBarWrapAdapter__factory } from "../../typechain/factories/SushiBarWrapAdapter__factory"; import { CompoundBravoGovernanceAdapter__factory } from "../../typechain/factories/CompoundBravoGovernanceAdapter__factory"; import { CompClaimAdapter__factory, AGIMigrationWrapAdapter__factory } from "../../typechain"; @@ -183,6 +185,13 @@ export default class DeployAdapters { return await new UniswapPairPriceAdapter__factory(this._deployerSigner).deploy(controller, uniswapFactory, uniswapPools); } + public async deploySushiBarWrapAdapter( + sushi: Address, + sushiBar: Address + ): Promise { + return await new SushiBarWrapAdapter__factory(this._deployerSigner).deploy(sushi, sushiBar); + } + public async getUniswapPairPriceAdapter(uniswapAdapterAddress: Address): Promise { return await new UniswapPairPriceAdapter__factory(this._deployerSigner).attach(uniswapAdapterAddress); } diff --git a/utils/deploys/deployExternal.ts b/utils/deploys/deployExternal.ts index 0ae51ef91..3af4878c3 100644 --- a/utils/deploys/deployExternal.ts +++ b/utils/deploys/deployExternal.ts @@ -154,6 +154,11 @@ import { } from "../contracts/index"; import { SingularityNetToken__factory } from "../../typechain/factories/SingularityNetToken__factory"; +import { + SushiBar +} from "../contracts/index"; +import { SushiBar__factory } from "../../typechain/factories/SushiBar__factory"; + import { SwapRouter, UniswapV3Factory, @@ -607,6 +612,11 @@ export default class DeployExternalContracts { return await new SingularityNetToken__factory(this._deployerSigner).deploy(); } + // Sushi + public async deploySushiBar(sushiToken: Address): Promise { + return await new SushiBar__factory(this._deployerSigner).deploy(sushiToken); + } + // Uniswap V3 public async deployUniswapV3Factory(): Promise { return await new UniswapV3Factory__factory(this._deployerSigner).deploy();