From be3f636744a10ed7b06b1ae565d0b57817748e9b Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 17 Mar 2022 11:00:50 -0700 Subject: [PATCH 1/7] Remove NAVIssuanceModule (in favor of CustomOracleNAVIssuanceModule) --- test/product/uniswapYieldHook.spec.ts | 453 ++++++++++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 test/product/uniswapYieldHook.spec.ts diff --git a/test/product/uniswapYieldHook.spec.ts b/test/product/uniswapYieldHook.spec.ts new file mode 100644 index 000000000..dde31aebf --- /dev/null +++ b/test/product/uniswapYieldHook.spec.ts @@ -0,0 +1,453 @@ +import "module-alias/register"; + +import { BigNumber } from "ethers"; + +import { Address, CustomOracleNAVIssuanceSettings } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ZERO, ADDRESS_ZERO } from "@utils/constants"; +import { + NAVIssuanceCaller, + CustomOracleNavIssuanceModule, + SetToken, + UniswapYieldHook, + CustomSetValuerMock +} from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + bitcoin, + ether, + usdc, +} from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect, + getSystemFixture, + getRandomAccount, +} from "@utils/test/index"; + +import { SystemFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("UniswapYieldHook", () => { + let owner: Account; + let feeRecipient: Account; + let recipient: Account; + let deployer: DeployHelper; + + let setup: SystemFixture; + let navIssuanceModule: CustomOracleNavIssuanceModule; + let setToken: SetToken; + let setValuer: CustomSetValuerMock; + + let hook: UniswapYieldHook; + let navIssuanceCaller: NAVIssuanceCaller; + + const setRedeemLimit: BigNumber = ether(435); + const usdcIssueLimit: BigNumber = usdc(100000); + const ethIssueLimit: BigNumber = ether(435); + + before(async () => { + [ + owner, + feeRecipient, + recipient, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + navIssuanceModule = await deployer.modules.deployCustomOracleNavIssuanceModule( + setup.controller.address, + setup.weth.address + ); + await setup.controller.addModule(navIssuanceModule.address); + + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [navIssuanceModule.address, setup.issuanceModule.address] + ); + + hook = await deployer.product.deployUniswapYieldHook( + [setToken.address, setup.usdc.address, setup.weth.address], + [setRedeemLimit, usdcIssueLimit, ethIssueLimit], + ); + + navIssuanceCaller = await deployer.mocks.deployNAVIssuanceCaller(navIssuanceModule.address); + setValuer = await deployer.mocks.deployCustomSetValuerMock(); + + const navIssuanceSettings = { + managerIssuanceHook: hook.address, + managerRedemptionHook: hook.address, + setValuer: setValuer.address, + reserveAssets: [setup.usdc.address, setup.weth.address], + feeRecipient: feeRecipient.address, + managerFees: [ether(0.001), ether(0.002)], + maxManagerFee: ether(0.02), + premiumPercentage: ether(0.01), + maxPremiumPercentage: ether(0.1), + minSetTokenSupply: ether(100), + } as CustomOracleNAVIssuanceSettings; + + await navIssuanceModule.initialize( + setToken.address, + navIssuanceSettings + ); + + // Approve tokens to the controller + await setup.weth.approve(setup.controller.address, ether(100)); + await setup.usdc.approve(setup.controller.address, usdc(1000000)); + await setup.wbtc.approve(setup.controller.address, bitcoin(1000000)); + await setup.dai.approve(setup.controller.address, ether(1000000)); + + // Seed with 100 supply + await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + await setup.issuanceModule.issue(setToken.address, ether(100), owner.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectAssets: Address[]; + let subjectLimits: BigNumber[]; + + beforeEach(async () => { + subjectAssets = [setup.weth.address, setup.usdc.address]; + subjectLimits = [ether(400), usdc(100000)]; + }); + + async function subject(): Promise { + return await deployer.product.deployUniswapYieldHook(subjectAssets, subjectLimits); + } + + it("should set the correct limits", async () => { + const hook = await subject(); + + const wethLimit = await hook.assetLimits(subjectAssets[0]); + const usdcLimit = await hook.assetLimits(subjectAssets[1]); + expect(wethLimit).to.eq(subjectLimits[0]); + expect(usdcLimit).to.eq(subjectLimits[1]); + }); + + it("should set the correct assets", async () => { + const hook = await subject(); + + const assets = await hook.getAssets(); + expect(JSON.stringify(assets)).to.eq(JSON.stringify(subjectAssets)); + }); + + describe("when asset is duplicated", async () => { + beforeEach(async () => { + subjectAssets = [setup.weth.address, setup.weth.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Asset already added"); + }); + }); + + describe("when array lengths don't match", async () => { + beforeEach(async () => { + subjectAssets = [setup.weth.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Arrays must be equal"); + }); + }); + + describe("when arrays are empty", async () => { + beforeEach(async () => { + subjectAssets = []; + subjectLimits = []; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array must not be empty"); + }); + }); + }); + + describe("#issue", async () => { + let subjectSetToken: Address; + let subjectReserveAsset: Address; + let subjectReserveQuantity: BigNumber; + let subjectMinSetTokenReceived: BigNumber; + let subjectTo: Account; + let subjectCallFromContract: boolean; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectReserveAsset = setup.usdc.address; + subjectReserveQuantity = usdc(150000); + subjectMinSetTokenReceived = ZERO; + subjectTo = recipient; + subjectCallFromContract = false; + + await setup.usdc.approve(navIssuanceModule.address, subjectReserveQuantity); + }); + + async function subject(): Promise { + if (subjectCallFromContract) { + await setup.usdc.connect(owner.wallet).transfer(navIssuanceCaller.address, subjectReserveQuantity); + return navIssuanceCaller.issue( + subjectSetToken, + subjectReserveAsset, + subjectReserveQuantity, + subjectMinSetTokenReceived, + subjectTo.address + ); + } else { + return navIssuanceModule.issue( + subjectSetToken, + subjectReserveAsset, + subjectReserveQuantity, + subjectMinSetTokenReceived, + subjectTo.address + ); + } + } + + it("should not revert", async () => { + await expect(subject()).to.not.be.reverted; + }); + + describe("when call is from contract but less than redeem limit", async () => { + beforeEach(async () => { + subjectCallFromContract = true; + subjectReserveQuantity = usdc(90000); + }); + + it("should not revert", async () => { + await expect(subject()).to.not.be.reverted; + }); + }); + + describe("when call is from contract but greater than redeem limit", async () => { + beforeEach(async () => { + subjectCallFromContract = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Issue size too large for call from contract."); + }); + }); + }); + + describe("#redeem", async () => { + let subjectSetToken: Address; + let subjectReserveAsset: Address; + let subjectSetTokenQuantity: BigNumber; + let subjectMinReserveQuantityReceived: BigNumber; + let subjectTo: Account; + let subjectCallFromContract: boolean; + + beforeEach(async () => { + await setup.usdc.approve(navIssuanceModule.address, usdc(150000)); + await navIssuanceModule.issue( + setToken.address, + setup.usdc.address, + usdc(150000), + ZERO, + owner.address + ); + + subjectSetToken = setToken.address; + subjectReserveAsset = setup.usdc.address; + subjectSetTokenQuantity = ether(500); + subjectMinReserveQuantityReceived = ZERO; + subjectTo = recipient; + subjectCallFromContract = false; + }); + + async function subject(): Promise { + if (subjectCallFromContract) { + await setToken.connect(owner.wallet).transfer(navIssuanceCaller.address, subjectSetTokenQuantity); + return navIssuanceCaller.redeem( + subjectSetToken, + subjectReserveAsset, + subjectSetTokenQuantity, + subjectMinReserveQuantityReceived, + subjectTo.address + ); + } else { + return navIssuanceModule.redeem( + subjectSetToken, + subjectReserveAsset, + subjectSetTokenQuantity, + subjectMinReserveQuantityReceived, + subjectTo.address + ); + } + } + + it("should not revert", async () => { + await expect(subject()).to.not.be.reverted; + }); + + describe("when call is from contract but less than redeem limit", async () => { + beforeEach(async () => { + subjectCallFromContract = true; + subjectSetTokenQuantity = ether(400); + }); + + it("should not revert", async () => { + await expect(subject()).to.not.be.reverted; + }); + }); + + describe("when call is from contract but greater than redeem limit", async () => { + beforeEach(async () => { + subjectCallFromContract = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Redeem size too large for call from contract."); + }); + }); + }); + + describe("#addAssetLimit", async () => { + let subjectAsset: Address; + let subjectLimit: BigNumber; + let subjectCaller: Account; + + beforeEach(async () => { + subjectAsset = setup.wbtc.address; + subjectLimit = bitcoin(10); + subjectCaller = owner; + }); + + async function subject(): Promise { + return await hook.connect(subjectCaller.wallet).addAssetLimit(subjectAsset, subjectLimit); + } + + it("should set the correct limits", async () => { + await subject(); + + const wbtcLimit = await hook.assetLimits(subjectAsset); + expect(wbtcLimit).to.eq(subjectLimit); + }); + + it("should add wbtc to assets array", async () => { + await subject(); + + const assets = await hook.getAssets(); + expect(assets).to.contain(subjectAsset); + }); + + describe("when asset is duplicated", async () => { + beforeEach(async () => { + subjectAsset = setup.weth.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Asset already added"); + }); + }); + + describe("when caller is not owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#editAssetLimit", async () => { + let subjectAsset: Address; + let subjectLimit: BigNumber; + let subjectCaller: Account; + + beforeEach(async () => { + subjectAsset = setup.weth.address; + subjectLimit = ether(100); + subjectCaller = owner; + }); + + async function subject(): Promise { + return await hook.connect(subjectCaller.wallet).editAssetLimit(subjectAsset, subjectLimit); + } + + it("should set the correct limits", async () => { + await subject(); + + const wethLimit = await hook.assetLimits(subjectAsset); + expect(wethLimit).to.eq(subjectLimit); + }); + + describe("when asset is not already added", async () => { + beforeEach(async () => { + subjectAsset = setup.wbtc.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Asset not added"); + }); + }); + + describe("when caller is not owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#removeAssetLimit", async () => { + let subjectAsset: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectAsset = setup.weth.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return await hook.connect(subjectCaller.wallet).removeAssetLimit(subjectAsset); + } + + it("should set the correct limits", async () => { + await subject(); + + const wethLimit = await hook.assetLimits(subjectAsset); + expect(wethLimit).to.eq(ZERO); + }); + + it("should remove weth from assets array", async () => { + await subject(); + + const assets = await hook.getAssets(); + expect(assets).to.not.contain(subjectAsset); + }); + + describe("when asset is not already added", async () => { + beforeEach(async () => { + subjectAsset = setup.wbtc.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Asset not added"); + }); + }); + + describe("when caller is not owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); +}); From 37f00ddf4d089ee8253d41872b071cc92955fddf Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 17 Mar 2022 11:22:07 -0700 Subject: [PATCH 2/7] Rename DebtIssuanceModuleV2 to IssuanceModule --- .../protocol/modules/DebtIssuanceModuleV2.sol | 387 ------ contracts/protocol/modules/IssuanceModule.sol | 367 +++++- .../aaveUniswapLeverageDebtIssuance.spec.ts | 6 +- .../modules/debtIssuanceModuleV2.spec.ts | 1039 ----------------- test/protocol/modules/issuanceModule.spec.ts | 1017 +++++++++++++++- utils/contracts/index.ts | 1 - 6 files changed, 1366 insertions(+), 1451 deletions(-) delete mode 100644 contracts/protocol/modules/DebtIssuanceModuleV2.sol delete mode 100644 test/protocol/modules/debtIssuanceModuleV2.spec.ts diff --git a/contracts/protocol/modules/DebtIssuanceModuleV2.sol b/contracts/protocol/modules/DebtIssuanceModuleV2.sol deleted file mode 100644 index c857d4d52..000000000 --- a/contracts/protocol/modules/DebtIssuanceModuleV2.sol +++ /dev/null @@ -1,387 +0,0 @@ -/* - 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 { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; - -import { DebtIssuanceModule } from "./DebtIssuanceModule.sol"; -import { IController } from "../../interfaces/IController.sol"; -import { Invoke } from "../lib/Invoke.sol"; -import { ISetToken } from "../../interfaces/ISetToken.sol"; -import { IssuanceValidationUtils } from "../lib/IssuanceValidationUtils.sol"; -import { Position } from "../lib/Position.sol"; - -/** - * @title DebtIssuanceModuleV2 - * @author Set Protocol - * - * The DebtIssuanceModuleV2 is a module that enables users to issue and redeem SetTokens that contain default and all - * external positions, including debt positions. Module hooks are added to allow for syncing of positions, and component - * level hooks are added to ensure positions are replicated correctly. The manager can define arbitrary issuance logic - * in the manager hook, as well as specify issue and redeem fees. - * - * NOTE: - * DebtIssuanceModule contract confirms increase/decrease in balance of component held by the SetToken after every transfer in/out - * for each component during issuance/redemption. This contract replaces those strict checks with slightly looser checks which - * ensure that the SetToken remains collateralized after every transfer in/out for each component during issuance/redemption. - * This module should be used to issue/redeem SetToken whose one or more components return a balance value with +/-1 wei error. - * For example, this module can be used to issue/redeem SetTokens which has one or more aTokens as its components. - * The new checks do NOT apply to any transfers that are part of an external position. A token that has rounding issues may lead to - * reverts if it is included as an external position unless explicitly allowed in a module hook. - * - * The getRequiredComponentIssuanceUnits function on this module assumes that Default token balances will be synced on every issuance - * and redemption. If token balances are not being synced it will over-estimate the amount of tokens required to issue a Set. - */ -contract DebtIssuanceModuleV2 is DebtIssuanceModule { - using Position for uint256; - - /* ============ Constructor ============ */ - - constructor(IController _controller) public DebtIssuanceModule(_controller) {} - - /* ============ External Functions ============ */ - - /** - * Deposits components to the SetToken, replicates any external module component positions and mints - * the SetToken. If the token has a debt position all collateral will be transferred in first then debt - * will be returned to the minting address. If specified, a fee will be charged on issuance. - * - * NOTE: Overrides DebtIssuanceModule#issue external function and adds undercollateralization checks in place of the - * previous default strict balances checks. The undercollateralization checks are implemented in IssuanceValidationUtils library and they - * revert upon undercollateralization of the SetToken post component transfer. - * - * @param _setToken Instance of the SetToken to issue - * @param _quantity Quantity of SetToken to issue - * @param _to Address to mint SetToken to - */ - function issue( - ISetToken _setToken, - uint256 _quantity, - address _to - ) - external - override - nonReentrant - onlyValidAndInitializedSet(_setToken) - { - require(_quantity > 0, "Issue quantity must be > 0"); - - address hookContract = _callManagerPreIssueHooks(_setToken, _quantity, msg.sender, _to); - - _callModulePreIssueHooks(_setToken, _quantity); - - - uint256 initialSetSupply = _setToken.totalSupply(); - - ( - uint256 quantityWithFees, - uint256 managerFee, - uint256 protocolFee - ) = calculateTotalFees(_setToken, _quantity, true); - - // Prevent stack too deep - { - ( - address[] memory components, - uint256[] memory equityUnits, - uint256[] memory debtUnits - ) = _calculateRequiredComponentIssuanceUnits(_setToken, quantityWithFees, true); - - uint256 finalSetSupply = initialSetSupply.add(quantityWithFees); - - _resolveEquityPositions(_setToken, quantityWithFees, _to, true, components, equityUnits, initialSetSupply, finalSetSupply); - _resolveDebtPositions(_setToken, quantityWithFees, true, components, debtUnits, initialSetSupply, finalSetSupply); - _resolveFees(_setToken, managerFee, protocolFee); - } - - _setToken.mint(_to, _quantity); - - emit SetTokenIssued( - _setToken, - msg.sender, - _to, - hookContract, - _quantity, - managerFee, - protocolFee - ); - } - - /** - * Returns components from the SetToken, unwinds any external module component positions and burns the SetToken. - * If the token has debt positions, the module transfers in the required debt amounts from the caller and uses - * those funds to repay the debts on behalf of the SetToken. All debt will be paid down first then equity positions - * will be returned to the minting address. If specified, a fee will be charged on redeem. - * - * NOTE: Overrides DebtIssuanceModule#redeem internal function and adds undercollateralization checks in place of the - * previous default strict balances checks. The undercollateralization checks are implemented in IssuanceValidationUtils library - * and they revert upon undercollateralization of the SetToken post component transfer. - * - * @param _setToken Instance of the SetToken to redeem - * @param _quantity Quantity of SetToken to redeem - * @param _to Address to send collateral to - */ - function redeem( - ISetToken _setToken, - uint256 _quantity, - address _to - ) - external - override - nonReentrant - onlyValidAndInitializedSet(_setToken) - { - require(_quantity > 0, "Redeem quantity must be > 0"); - - _callModulePreRedeemHooks(_setToken, _quantity); - - uint256 initialSetSupply = _setToken.totalSupply(); - - // Place burn after pre-redeem hooks because burning tokens may lead to false accounting of synced positions - _setToken.burn(msg.sender, _quantity); - - ( - uint256 quantityNetFees, - uint256 managerFee, - uint256 protocolFee - ) = calculateTotalFees(_setToken, _quantity, false); - - // Prevent stack too deep - { - ( - address[] memory components, - uint256[] memory equityUnits, - uint256[] memory debtUnits - ) = _calculateRequiredComponentIssuanceUnits(_setToken, quantityNetFees, false); - - uint256 finalSetSupply = initialSetSupply.sub(quantityNetFees); - - _resolveDebtPositions(_setToken, quantityNetFees, false, components, debtUnits, initialSetSupply, finalSetSupply); - _resolveEquityPositions(_setToken, quantityNetFees, _to, false, components, equityUnits, initialSetSupply, finalSetSupply); - _resolveFees(_setToken, managerFee, protocolFee); - } - - emit SetTokenRedeemed( - _setToken, - msg.sender, - _to, - _quantity, - managerFee, - protocolFee - ); - } - - /* ============ External View Functions ============ */ - - /** - * Calculates the amount of each component needed to collateralize passed issue quantity plus fees of Sets as well as amount of debt - * that will be returned to caller. Default equity alues are calculated based on token balances and not position units in order to more - * closely track any accrued tokens that will be synced during issuance. External equity and debt positions will use the stored position - * units. IF TOKEN VALUES ARE NOT BEING SYNCED DURING ISSUANCE THIS FUNCTION WILL OVER ESTIMATE THE AMOUNT OF REQUIRED TOKENS. - * - * @param _setToken Instance of the SetToken to issue - * @param _quantity Amount of Sets to be issued - * - * @return address[] Array of component addresses making up the Set - * @return uint256[] Array of equity notional amounts of each component, respectively, represented as uint256 - * @return uint256[] Array of debt notional amounts of each component, respectively, represented as uint256 - */ - function getRequiredComponentIssuanceUnits( - ISetToken _setToken, - uint256 _quantity - ) - external - view - override - returns (address[] memory, uint256[] memory, uint256[] memory) - { - ( - uint256 totalQuantity,, - ) = calculateTotalFees(_setToken, _quantity, true); - - if(_setToken.totalSupply() == 0) { - return _calculateRequiredComponentIssuanceUnits(_setToken, totalQuantity, true); - } else { - ( - address[] memory components, - uint256[] memory equityUnits, - uint256[] memory debtUnits - ) = _getTotalIssuanceUnitsFromBalances(_setToken); - - uint256 componentsLength = components.length; - uint256[] memory totalEquityUnits = new uint256[](componentsLength); - uint256[] memory totalDebtUnits = new uint256[](componentsLength); - for (uint256 i = 0; i < components.length; i++) { - // Use preciseMulCeil to round up to ensure overcollateration of equity when small issue quantities are provided - // and use preciseMul to round debt calculations down to make sure we don't return too much debt to issuer - totalEquityUnits[i] = equityUnits[i].preciseMulCeil(totalQuantity); - totalDebtUnits[i] = debtUnits[i].preciseMul(totalQuantity); - } - - return (components, totalEquityUnits, totalDebtUnits); - } - } - - /* ============ Internal Functions ============ */ - - /** - * Resolve equity positions associated with SetToken. On issuance, the total equity position for an asset (including default and external - * positions) is transferred in. Then any external position hooks are called to transfer the external positions to their necessary place. - * On redemption all external positions are recalled by the external position hook, then those position plus any default position are - * transferred back to the _to address. - */ - function _resolveEquityPositions( - ISetToken _setToken, - uint256 _quantity, - address _to, - bool _isIssue, - address[] memory _components, - uint256[] memory _componentEquityQuantities, - uint256 _initialSetSupply, - uint256 _finalSetSupply - ) - internal - { - for (uint256 i = 0; i < _components.length; i++) { - address component = _components[i]; - uint256 componentQuantity = _componentEquityQuantities[i]; - if (componentQuantity > 0) { - if (_isIssue) { - // Call SafeERC20#safeTransferFrom instead of ExplicitERC20#transferFrom - SafeERC20.safeTransferFrom( - IERC20(component), - msg.sender, - address(_setToken), - componentQuantity - ); - - IssuanceValidationUtils.validateCollateralizationPostTransferInPreHook(_setToken, component, _initialSetSupply, componentQuantity); - - _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), true, true); - } else { - _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), false, true); - - // Call Invoke#invokeTransfer instead of Invoke#strictInvokeTransfer - _setToken.invokeTransfer(component, _to, componentQuantity); - - IssuanceValidationUtils.validateCollateralizationPostTransferOut(_setToken, component, _finalSetSupply); - } - } - } - } - - /** - * Resolve debt positions associated with SetToken. On issuance, debt positions are entered into by calling the external position hook. The - * resulting debt is then returned to the calling address. On redemption, the module transfers in the required debt amount from the caller - * and uses those funds to repay the debt on behalf of the SetToken. - */ - function _resolveDebtPositions( - ISetToken _setToken, - uint256 _quantity, - bool _isIssue, - address[] memory _components, - uint256[] memory _componentDebtQuantities, - uint256 _initialSetSupply, - uint256 _finalSetSupply - ) - internal - { - for (uint256 i = 0; i < _components.length; i++) { - address component = _components[i]; - uint256 componentQuantity = _componentDebtQuantities[i]; - if (componentQuantity > 0) { - if (_isIssue) { - _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), true, false); - - // Call Invoke#invokeTransfer instead of Invoke#strictInvokeTransfer - _setToken.invokeTransfer(component, msg.sender, componentQuantity); - - IssuanceValidationUtils.validateCollateralizationPostTransferOut(_setToken, component, _finalSetSupply); - } else { - // Call SafeERC20#safeTransferFrom instead of ExplicitERC20#transferFrom - SafeERC20.safeTransferFrom( - IERC20(component), - msg.sender, - address(_setToken), - componentQuantity - ); - - IssuanceValidationUtils.validateCollateralizationPostTransferInPreHook(_setToken, component, _initialSetSupply, componentQuantity); - - _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), false, false); - } - } - } - } - /** - * Reimplementation of _getTotalIssuanceUnits but instead derives Default equity positions from token balances on Set instead of from - * position units. This function is ONLY to be used in getRequiredComponentIssuanceUnits in order to return more accurate required - * token amounts to issuers when positions are being synced on issuance. - * - * @param _setToken Instance of the SetToken to issue - * - * @return address[] Array of component addresses making up the Set - * @return uint256[] Array of equity unit amounts of each component, respectively, represented as uint256 - * @return uint256[] Array of debt unit amounts of each component, respectively, represented as uint256 - */ - function _getTotalIssuanceUnitsFromBalances( - ISetToken _setToken - ) - internal - view - returns (address[] memory, uint256[] memory, uint256[] memory) - { - address[] memory components = _setToken.getComponents(); - uint256 componentsLength = components.length; - - uint256[] memory equityUnits = new uint256[](componentsLength); - uint256[] memory debtUnits = new uint256[](componentsLength); - - uint256 totalSupply = _setToken.totalSupply(); - - for (uint256 i = 0; i < components.length; i++) { - address component = components[i]; - int256 cumulativeEquity = totalSupply - .getDefaultPositionUnit(IERC20(component).balanceOf(address(_setToken))) - .toInt256(); - int256 cumulativeDebt = 0; - address[] memory externalPositions = _setToken.getExternalPositionModules(component); - - if (externalPositions.length > 0) { - for (uint256 j = 0; j < externalPositions.length; j++) { - int256 externalPositionUnit = _setToken.getExternalPositionRealUnit(component, externalPositions[j]); - - // If positionUnit <= 0 it will be "added" to debt position - if (externalPositionUnit > 0) { - cumulativeEquity = cumulativeEquity.add(externalPositionUnit); - } else { - cumulativeDebt = cumulativeDebt.add(externalPositionUnit); - } - } - } - - equityUnits[i] = cumulativeEquity.toUint256(); - debtUnits[i] = cumulativeDebt.mul(-1).toUint256(); - } - - return (components, equityUnits, debtUnits); - } -} \ No newline at end of file diff --git a/contracts/protocol/modules/IssuanceModule.sol b/contracts/protocol/modules/IssuanceModule.sol index 8875b5e44..bba2e0f56 100644 --- a/contracts/protocol/modules/IssuanceModule.sol +++ b/contracts/protocol/modules/IssuanceModule.sol @@ -1,5 +1,5 @@ /* - Copyright 2022 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. @@ -19,23 +19,372 @@ pragma solidity 0.6.10; pragma experimental "ABIEncoderV2"; -import { DebtIssuanceModuleV2 } from "./DebtIssuanceModuleV2.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +import { DebtIssuanceModule } from "./DebtIssuanceModule.sol"; import { IController } from "../../interfaces/IController.sol"; +import { Invoke } from "../lib/Invoke.sol"; +import { ISetToken } from "../../interfaces/ISetToken.sol"; +import { IssuanceValidationUtils } from "../lib/IssuanceValidationUtils.sol"; +import { Position } from "../lib/Position.sol"; /** - * @title IssuanceModule + * @title IssuanceModule (formerly DebtIssuanceModuleV2) * @author Set Protocol * * The IssuanceModule is a module that enables users to issue and redeem SetTokens that contain default and all - * external positions, including debt positions. The manager can define arbitrary issuance logic in the manager - * hook, as well as specify issue and redeem fees. The manager can remove the module. + * external positions, including debt positions. Module hooks are added to allow for syncing of positions, and component + * level hooks are added to ensure positions are replicated correctly. The manager can define arbitrary issuance logic + * in the manager hook, as well as specify issue and redeem fees. + * + * NOTE: + * DebtIssuanceModule contract confirms increase/decrease in balance of component held by the SetToken after every transfer in/out + * for each component during issuance/redemption. This contract replaces those strict checks with slightly looser checks which + * ensure that the SetToken remains collateralized after every transfer in/out for each component during issuance/redemption. + * This module should be used to issue/redeem SetToken whose one or more components return a balance value with +/-1 wei error. + * For example, this module can be used to issue/redeem SetTokens which has one or more aTokens as its components. + * The new checks do NOT apply to any transfers that are part of an external position. A token that has rounding issues may lead to + * reverts if it is included as an external position unless explicitly allowed in a module hook. + * + * The getRequiredComponentIssuanceUnits function on this module assumes that Default token balances will be synced on every issuance + * and redemption. If token balances are not being synced it will over-estimate the amount of tokens required to issue a Set. + * + * Changelog: + * + Renamed DebtIssuanceModuleV2 to IssuanceModule (March 17, 2022) */ -contract IssuanceModule is DebtIssuanceModuleV2 { +contract IssuanceModule is DebtIssuanceModule { + using Position for uint256; /* ============ Constructor ============ */ + constructor(IController _controller) public DebtIssuanceModule(_controller) {} + + /* ============ External Functions ============ */ + + /** + * Deposits components to the SetToken, replicates any external module component positions and mints + * the SetToken. If the token has a debt position all collateral will be transferred in first then debt + * will be returned to the minting address. If specified, a fee will be charged on issuance. + * + * NOTE: Overrides DebtIssuanceModule#issue external function and adds undercollateralization checks in place of the + * previous default strict balances checks. The undercollateralization checks are implemented in IssuanceValidationUtils library and they + * revert upon undercollateralization of the SetToken post component transfer. + * + * @param _setToken Instance of the SetToken to issue + * @param _quantity Quantity of SetToken to issue + * @param _to Address to mint SetToken to + */ + function issue( + ISetToken _setToken, + uint256 _quantity, + address _to + ) + external + override + nonReentrant + onlyValidAndInitializedSet(_setToken) + { + require(_quantity > 0, "Issue quantity must be > 0"); + + address hookContract = _callManagerPreIssueHooks(_setToken, _quantity, msg.sender, _to); + + _callModulePreIssueHooks(_setToken, _quantity); + + + uint256 initialSetSupply = _setToken.totalSupply(); + + ( + uint256 quantityWithFees, + uint256 managerFee, + uint256 protocolFee + ) = calculateTotalFees(_setToken, _quantity, true); + + // Prevent stack too deep + { + ( + address[] memory components, + uint256[] memory equityUnits, + uint256[] memory debtUnits + ) = _calculateRequiredComponentIssuanceUnits(_setToken, quantityWithFees, true); + + uint256 finalSetSupply = initialSetSupply.add(quantityWithFees); + + _resolveEquityPositions(_setToken, quantityWithFees, _to, true, components, equityUnits, initialSetSupply, finalSetSupply); + _resolveDebtPositions(_setToken, quantityWithFees, true, components, debtUnits, initialSetSupply, finalSetSupply); + _resolveFees(_setToken, managerFee, protocolFee); + } + + _setToken.mint(_to, _quantity); + + emit SetTokenIssued( + _setToken, + msg.sender, + _to, + hookContract, + _quantity, + managerFee, + protocolFee + ); + } + + /** + * Returns components from the SetToken, unwinds any external module component positions and burns the SetToken. + * If the token has debt positions, the module transfers in the required debt amounts from the caller and uses + * those funds to repay the debts on behalf of the SetToken. All debt will be paid down first then equity positions + * will be returned to the minting address. If specified, a fee will be charged on redeem. + * + * NOTE: Overrides DebtIssuanceModule#redeem internal function and adds undercollateralization checks in place of the + * previous default strict balances checks. The undercollateralization checks are implemented in IssuanceValidationUtils library + * and they revert upon undercollateralization of the SetToken post component transfer. + * + * @param _setToken Instance of the SetToken to redeem + * @param _quantity Quantity of SetToken to redeem + * @param _to Address to send collateral to + */ + function redeem( + ISetToken _setToken, + uint256 _quantity, + address _to + ) + external + override + nonReentrant + onlyValidAndInitializedSet(_setToken) + { + require(_quantity > 0, "Redeem quantity must be > 0"); + + _callModulePreRedeemHooks(_setToken, _quantity); + + uint256 initialSetSupply = _setToken.totalSupply(); + + // Place burn after pre-redeem hooks because burning tokens may lead to false accounting of synced positions + _setToken.burn(msg.sender, _quantity); + + ( + uint256 quantityNetFees, + uint256 managerFee, + uint256 protocolFee + ) = calculateTotalFees(_setToken, _quantity, false); + + // Prevent stack too deep + { + ( + address[] memory components, + uint256[] memory equityUnits, + uint256[] memory debtUnits + ) = _calculateRequiredComponentIssuanceUnits(_setToken, quantityNetFees, false); + + uint256 finalSetSupply = initialSetSupply.sub(quantityNetFees); + + _resolveDebtPositions(_setToken, quantityNetFees, false, components, debtUnits, initialSetSupply, finalSetSupply); + _resolveEquityPositions(_setToken, quantityNetFees, _to, false, components, equityUnits, initialSetSupply, finalSetSupply); + _resolveFees(_setToken, managerFee, protocolFee); + } + + emit SetTokenRedeemed( + _setToken, + msg.sender, + _to, + _quantity, + managerFee, + protocolFee + ); + } + + /* ============ External View Functions ============ */ + /** - * Set state controller state variable + * Calculates the amount of each component needed to collateralize passed issue quantity plus fees of Sets as well as amount of debt + * that will be returned to caller. Default equity alues are calculated based on token balances and not position units in order to more + * closely track any accrued tokens that will be synced during issuance. External equity and debt positions will use the stored position + * units. IF TOKEN VALUES ARE NOT BEING SYNCED DURING ISSUANCE THIS FUNCTION WILL OVER ESTIMATE THE AMOUNT OF REQUIRED TOKENS. + * + * @param _setToken Instance of the SetToken to issue + * @param _quantity Amount of Sets to be issued + * + * @return address[] Array of component addresses making up the Set + * @return uint256[] Array of equity notional amounts of each component, respectively, represented as uint256 + * @return uint256[] Array of debt notional amounts of each component, respectively, represented as uint256 */ - constructor(IController _controller) public DebtIssuanceModuleV2(_controller) {} -} + function getRequiredComponentIssuanceUnits( + ISetToken _setToken, + uint256 _quantity + ) + external + view + override + returns (address[] memory, uint256[] memory, uint256[] memory) + { + ( + uint256 totalQuantity,, + ) = calculateTotalFees(_setToken, _quantity, true); + + if(_setToken.totalSupply() == 0) { + return _calculateRequiredComponentIssuanceUnits(_setToken, totalQuantity, true); + } else { + ( + address[] memory components, + uint256[] memory equityUnits, + uint256[] memory debtUnits + ) = _getTotalIssuanceUnitsFromBalances(_setToken); + + uint256 componentsLength = components.length; + uint256[] memory totalEquityUnits = new uint256[](componentsLength); + uint256[] memory totalDebtUnits = new uint256[](componentsLength); + for (uint256 i = 0; i < components.length; i++) { + // Use preciseMulCeil to round up to ensure overcollateration of equity when small issue quantities are provided + // and use preciseMul to round debt calculations down to make sure we don't return too much debt to issuer + totalEquityUnits[i] = equityUnits[i].preciseMulCeil(totalQuantity); + totalDebtUnits[i] = debtUnits[i].preciseMul(totalQuantity); + } + + return (components, totalEquityUnits, totalDebtUnits); + } + } + + /* ============ Internal Functions ============ */ + + /** + * Resolve equity positions associated with SetToken. On issuance, the total equity position for an asset (including default and external + * positions) is transferred in. Then any external position hooks are called to transfer the external positions to their necessary place. + * On redemption all external positions are recalled by the external position hook, then those position plus any default position are + * transferred back to the _to address. + */ + function _resolveEquityPositions( + ISetToken _setToken, + uint256 _quantity, + address _to, + bool _isIssue, + address[] memory _components, + uint256[] memory _componentEquityQuantities, + uint256 _initialSetSupply, + uint256 _finalSetSupply + ) + internal + { + for (uint256 i = 0; i < _components.length; i++) { + address component = _components[i]; + uint256 componentQuantity = _componentEquityQuantities[i]; + if (componentQuantity > 0) { + if (_isIssue) { + // Call SafeERC20#safeTransferFrom instead of ExplicitERC20#transferFrom + SafeERC20.safeTransferFrom( + IERC20(component), + msg.sender, + address(_setToken), + componentQuantity + ); + + IssuanceValidationUtils.validateCollateralizationPostTransferInPreHook(_setToken, component, _initialSetSupply, componentQuantity); + + _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), true, true); + } else { + _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), false, true); + + // Call Invoke#invokeTransfer instead of Invoke#strictInvokeTransfer + _setToken.invokeTransfer(component, _to, componentQuantity); + + IssuanceValidationUtils.validateCollateralizationPostTransferOut(_setToken, component, _finalSetSupply); + } + } + } + } + + /** + * Resolve debt positions associated with SetToken. On issuance, debt positions are entered into by calling the external position hook. The + * resulting debt is then returned to the calling address. On redemption, the module transfers in the required debt amount from the caller + * and uses those funds to repay the debt on behalf of the SetToken. + */ + function _resolveDebtPositions( + ISetToken _setToken, + uint256 _quantity, + bool _isIssue, + address[] memory _components, + uint256[] memory _componentDebtQuantities, + uint256 _initialSetSupply, + uint256 _finalSetSupply + ) + internal + { + for (uint256 i = 0; i < _components.length; i++) { + address component = _components[i]; + uint256 componentQuantity = _componentDebtQuantities[i]; + if (componentQuantity > 0) { + if (_isIssue) { + _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), true, false); + + // Call Invoke#invokeTransfer instead of Invoke#strictInvokeTransfer + _setToken.invokeTransfer(component, msg.sender, componentQuantity); + + IssuanceValidationUtils.validateCollateralizationPostTransferOut(_setToken, component, _finalSetSupply); + } else { + // Call SafeERC20#safeTransferFrom instead of ExplicitERC20#transferFrom + SafeERC20.safeTransferFrom( + IERC20(component), + msg.sender, + address(_setToken), + componentQuantity + ); + + IssuanceValidationUtils.validateCollateralizationPostTransferInPreHook(_setToken, component, _initialSetSupply, componentQuantity); + + _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), false, false); + } + } + } + } + /** + * Reimplementation of _getTotalIssuanceUnits but instead derives Default equity positions from token balances on Set instead of from + * position units. This function is ONLY to be used in getRequiredComponentIssuanceUnits in order to return more accurate required + * token amounts to issuers when positions are being synced on issuance. + * + * @param _setToken Instance of the SetToken to issue + * + * @return address[] Array of component addresses making up the Set + * @return uint256[] Array of equity unit amounts of each component, respectively, represented as uint256 + * @return uint256[] Array of debt unit amounts of each component, respectively, represented as uint256 + */ + function _getTotalIssuanceUnitsFromBalances( + ISetToken _setToken + ) + internal + view + returns (address[] memory, uint256[] memory, uint256[] memory) + { + address[] memory components = _setToken.getComponents(); + uint256 componentsLength = components.length; + + uint256[] memory equityUnits = new uint256[](componentsLength); + uint256[] memory debtUnits = new uint256[](componentsLength); + + uint256 totalSupply = _setToken.totalSupply(); + + for (uint256 i = 0; i < components.length; i++) { + address component = components[i]; + int256 cumulativeEquity = totalSupply + .getDefaultPositionUnit(IERC20(component).balanceOf(address(_setToken))) + .toInt256(); + int256 cumulativeDebt = 0; + address[] memory externalPositions = _setToken.getExternalPositionModules(component); + + if (externalPositions.length > 0) { + for (uint256 j = 0; j < externalPositions.length; j++) { + int256 externalPositionUnit = _setToken.getExternalPositionRealUnit(component, externalPositions[j]); + + // If positionUnit <= 0 it will be "added" to debt position + if (externalPositionUnit > 0) { + cumulativeEquity = cumulativeEquity.add(externalPositionUnit); + } else { + cumulativeDebt = cumulativeDebt.add(externalPositionUnit); + } + } + } + + equityUnits[i] = cumulativeEquity.toUint256(); + debtUnits[i] = cumulativeDebt.mul(-1).toUint256(); + } + + return (components, equityUnits, debtUnits); + } +} \ No newline at end of file diff --git a/test/integration/aaveUniswapLeverageDebtIssuance.spec.ts b/test/integration/aaveUniswapLeverageDebtIssuance.spec.ts index 01d150502..d45d28a5c 100644 --- a/test/integration/aaveUniswapLeverageDebtIssuance.spec.ts +++ b/test/integration/aaveUniswapLeverageDebtIssuance.spec.ts @@ -5,7 +5,7 @@ import { Account } from "@utils/test/types"; import { AaveV2, AaveLeverageModule, - DebtIssuanceModuleV2, + IssuanceModule, SetToken, UniswapV2ExchangeAdapter, } from "@utils/contracts"; @@ -68,7 +68,7 @@ describe("AaveUniswapLeverageDebtIssuance", () => { let aaveV2Library: AaveV2; let aaveLeverageModule: AaveLeverageModule; - let debtIssuanceModule: DebtIssuanceModuleV2; + let debtIssuanceModule: IssuanceModule; let uniswapExchangeAdapter: UniswapV2ExchangeAdapter; let aWBTC: AaveV2AToken; @@ -176,7 +176,7 @@ describe("AaveUniswapLeverageDebtIssuance", () => { MAX_UINT_256 ); - debtIssuanceModule = await deployer.modules.deployDebtIssuanceModuleV2(setup.controller.address); + debtIssuanceModule = await deployer.modules.deployIssuanceModule(setup.controller.address); await setup.controller.addModule(debtIssuanceModule.address); aaveV2Library = await deployer.libraries.deployAaveV2(); diff --git a/test/protocol/modules/debtIssuanceModuleV2.spec.ts b/test/protocol/modules/debtIssuanceModuleV2.spec.ts deleted file mode 100644 index bd70f6fed..000000000 --- a/test/protocol/modules/debtIssuanceModuleV2.spec.ts +++ /dev/null @@ -1,1039 +0,0 @@ -import "module-alias/register"; - -import { BigNumber } from "ethers"; - -import { Address } from "@utils/types"; -import { Account } from "@utils/test/types"; -import { ZERO, ONE, ADDRESS_ZERO } from "@utils/constants"; -import { - DebtIssuanceModuleV2, - DebtModuleMock, - ManagerIssuanceHookMock, - ModuleIssuanceHookMock, - SetToken, - StandardTokenWithRoundingErrorMock -} from "@utils/contracts"; -import DeployHelper from "@utils/deploys"; -import { - ether, - preciseDiv, - preciseMul, - preciseMulCeil, -} from "@utils/index"; -import { - addSnapshotBeforeRestoreAfterEach, - getAccounts, - getSystemFixture, - getWaffleExpect, -} from "@utils/test/index"; -import { SystemFixture } from "@utils/fixtures"; -import { ContractTransaction } from "ethers"; - -const expect = getWaffleExpect(); - -describe("DebtIssuanceModuleV2", () => { - let owner: Account; - let manager: Account; - let feeRecipient: Account; - let dummyModule: Account; - let recipient: Account; - let deployer: DeployHelper; - let setup: SystemFixture; - - let debtModule: DebtModuleMock; - let externalPositionModule: ModuleIssuanceHookMock; - let debtIssuance: DebtIssuanceModuleV2; - let issuanceHook: ManagerIssuanceHookMock; - let setToken: SetToken; - let tokenWithRoundingError: StandardTokenWithRoundingErrorMock; - - before(async () => { - [ - owner, - manager, - feeRecipient, - dummyModule, // Set as protocol fee recipient - recipient, - ] = await getAccounts(); - - deployer = new DeployHelper(owner.wallet); - setup = getSystemFixture(owner.address); - - await setup.initialize(); - - tokenWithRoundingError = await deployer.mocks.deployTokenWithErrorMock(owner.address, ether(1000000), ZERO); - debtIssuance = await deployer.modules.deployDebtIssuanceModuleV2(setup.controller.address); - debtModule = await deployer.mocks.deployDebtModuleMock(setup.controller.address); - externalPositionModule = await deployer.mocks.deployModuleIssuanceHookMock(); - issuanceHook = await deployer.mocks.deployManagerIssuanceHookMock(); - - await setup.controller.addModule(debtIssuance.address); - await setup.controller.addModule(debtModule.address); - await setup.controller.addModule(externalPositionModule.address); - - setToken = await setup.createSetToken( - [tokenWithRoundingError.address], - [ether(1)], - [setup.issuanceModule.address, debtIssuance.address, debtModule.address, externalPositionModule.address], - manager.address, - "DebtToken", - "DBT" - ); - - await externalPositionModule.initialize(setToken.address); - }); - - addSnapshotBeforeRestoreAfterEach(); - - context("when DebtIssuanceModuleV2 is initialized", async () => { - let preIssueHook: Address; - let maxFee: BigNumber; - let issueFee: BigNumber; - let redeemFee: BigNumber; - - before(async () => { - await tokenWithRoundingError.setError(ZERO); - - preIssueHook = ADDRESS_ZERO; - maxFee = ether(0.02); - issueFee = ether(0.005); - redeemFee = ether(0.005); - }); - - beforeEach(async () => { - await debtIssuance.connect(manager.wallet).initialize( - setToken.address, - maxFee, - issueFee, - redeemFee, - feeRecipient.address, - preIssueHook - ); - await debtModule.connect(manager.wallet).initialize(setToken.address, debtIssuance.address); - }); - - - context("when SetToken components do not have any rounding error", async () => { - // Note: Tests below are an EXACT copy of the tests for DebtIssuanceModule. Only difference is this SetToken contains - // tokenWithRoundingError instead of weth as a default position. This is to ensure the DebtIssuanceModuleV2 behaves - // exactly similar to DebtIssuanceModule when there is no rounding error present in it's constituent components. - - describe("#issue", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - let subjectTo: Address; - let subjectCaller: Account; - - const debtUnits: BigNumber = ether(100); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); - - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - subjectTo = recipient.address; - subjectCaller = owner; - }); - - async function subject(): Promise { - return debtIssuance.connect(subjectCaller.wallet).issue( - subjectSetToken, - subjectQuantity, - subjectTo, - ); - } - - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - const managerBalance = await setToken.balanceOf(feeRecipient.address); - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - expect(managerBalance).to.eq(feeQuantity); - }); - - it("should have the correct token balances", async () => { - const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1)); - - const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethFlows)); - expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethFlows)); - expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); - }); - - it("should have called the module issue hook", async () => { - await subject(); - - const hookCalled = await debtModule.moduleIssueHookCalled(); - - expect(hookCalled).to.be.true; - }); - - it("should emit the correct SetTokenIssued event", async () => { - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - - await expect(subject()).to.emit(debtIssuance, "SetTokenIssued").withArgs( - setToken.address, - subjectCaller.address, - subjectTo, - preIssueHook, - subjectQuantity, - feeQuantity, - ZERO - ); - }); - - describe("when an external equity position is in place", async () => { - const externalUnits: BigNumber = ether(1); - - before(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); - }); - - after(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, ZERO); - }); - - it("should have the correct token balances", async () => { - const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); - const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethDefaultFlows = preciseMul(mintQuantity, ether(1)); - const wethExternalFlows = preciseMul(mintQuantity, externalUnits); - - const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); - const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethDefaultFlows.add(wethExternalFlows))); - expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethDefaultFlows)); - expect(postExternalWethBalance).to.eq(preExternalWethBalance.add(wethExternalFlows)); - expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); - }); - }); - - describe("when the manager issuance fee is 0", async () => { - before(async () => { - issueFee = ZERO; - }); - - after(async () => { - issueFee = ether(0.005); - }); - - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - }); - - it("should have the correct token balances", async () => { - const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethDefaultFlows = preciseMul(mintQuantity, ether(1)); - - const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethDefaultFlows)); - expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethDefaultFlows)); - expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); - }); - }); - - describe("when protocol fees are enabled", async () => { - const protocolFee: BigNumber = ether(.2); - - beforeEach(async () => { - await setup.controller.addFee(debtIssuance.address, ZERO, protocolFee); - }); - - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - const protocolSplit = preciseMul(feeQuantity, protocolFee); - - const managerBalance = await setToken.balanceOf(feeRecipient.address); - const protocolBalance = await setToken.balanceOf(dummyModule.address); // DummyModule is set as address in fixture setup - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - expect(managerBalance).to.eq(feeQuantity.sub(protocolSplit)); - expect(protocolBalance).to.eq(protocolSplit); - }); - }); - - describe("when manager issuance hook is defined", async () => { - before(async () => { - preIssueHook = issuanceHook.address; - }); - - after(async () => { - preIssueHook = ADDRESS_ZERO; - }); - - it("should call the issuance hook", async () => { - await subject(); - - const setToken = await issuanceHook.retrievedSetToken(); - - expect(setToken).to.eq(subjectSetToken); - }); - }); - - describe("when the issue quantity is 0", async () => { - beforeEach(async () => { - subjectQuantity = ZERO; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Issue quantity must be > 0"); - }); - }); - - describe("when the SetToken is not enabled on the controller", async () => { - beforeEach(async () => { - const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( - [tokenWithRoundingError.address], - [ether(1)], - [debtIssuance.address] - ); - - subjectSetToken = nonEnabledSetToken.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); - }); - }); - }); - - describe("#redeem", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - let subjectTo: Address; - let subjectCaller: Account; - - const debtUnits: BigNumber = ether(100); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - const [, equityFlows ] = await debtIssuance.getRequiredComponentRedemptionUnits(setToken.address, ether(1)); - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); - - await debtIssuance.issue(setToken.address, ether(1), owner.address); - - await setup.dai.approve(debtIssuance.address, ether(100.5)); - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - subjectTo = recipient.address; - subjectCaller = owner; - }); - - async function subject(): Promise { - return debtIssuance.connect(subjectCaller.wallet).redeem( - subjectSetToken, - subjectQuantity, - subjectTo, - ); - } - - it("should mint SetTokens to the correct addresses", async () => { - const preManagerBalance = await setToken.balanceOf(feeRecipient.address); - const preCallerBalance = await setToken.balanceOf(subjectCaller.address); - - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); - const postManagerBalance = await setToken.balanceOf(feeRecipient.address); - const postCallerBalance = await setToken.balanceOf(subjectCaller.address); - - expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); - expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); - }); - - it("should have the correct token balances", async () => { - const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); - const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); - const wethFlows = preciseMul(redeemQuantity, ether(1)); - - const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postToWethBalance).to.eq(preToWethBalance.add(wethFlows)); - expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethFlows)); - expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); - }); - - it("should have called the module issue hook", async () => { - await subject(); - - const hookCalled = await debtModule.moduleRedeemHookCalled(); - - expect(hookCalled).to.be.true; - }); - - it("should emit the correct SetTokenRedeemed event", async () => { - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - - await expect(subject()).to.emit(debtIssuance, "SetTokenRedeemed").withArgs( - setToken.address, - subjectCaller.address, - subjectTo, - subjectQuantity, - feeQuantity, - ZERO - ); - }); - - describe("when an external equity position is in place", async () => { - const externalUnits: BigNumber = ether(1); - - before(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); - }); - - after(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, ZERO); - }); - - it("should have the correct token balances", async () => { - const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); - const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); - const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); - const wethExternalFlows = preciseMul(redeemQuantity, externalUnits); - const wethDefaultFlows = preciseMul(redeemQuantity, ether(1)); - - const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); - const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postToWethBalance).to.eq(preToWethBalance.add(wethExternalFlows.add(wethDefaultFlows))); - expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethDefaultFlows)); - expect(postExternalWethBalance).to.eq(preExternalWethBalance.sub(wethExternalFlows)); - expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); - }); - }); - - describe("when the manager redemption fee is 0", async () => { - before(async () => { - redeemFee = ZERO; - }); - - after(async () => { - redeemFee = ether(0.005); - }); - - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(ZERO); - }); - - it("should have the correct token balances", async () => { - const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); - const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); - const wethFlows = preciseMul(redeemQuantity, ether(1)); - - const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postToWethBalance).to.eq(preToWethBalance.add(wethFlows)); - expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethFlows)); - expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); - }); - }); - - describe("when protocol fees are enabled", async () => { - const protocolFee: BigNumber = ether(.2); - - beforeEach(async () => { - await setup.controller.addFee(debtIssuance.address, ZERO, protocolFee); - }); - - it("should mint SetTokens to the correct addresses", async () => { - const preManagerBalance = await setToken.balanceOf(feeRecipient.address); - const preProtocolBalance = await setToken.balanceOf(dummyModule.address); - const preCallerBalance = await setToken.balanceOf(subjectCaller.address); - - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); - const protocolSplit = preciseMul(feeQuantity, protocolFee); - - const postManagerBalance = await setToken.balanceOf(feeRecipient.address); - const postProtocolBalance = await setToken.balanceOf(dummyModule.address); // DummyModule is set as address in fixture setup - const postCallerBalance = await setToken.balanceOf(subjectCaller.address); - - expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); - expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity.sub(protocolSplit))); - expect(postProtocolBalance).to.eq(preProtocolBalance.add(protocolSplit)); - }); - }); - - describe("when the issue quantity is 0", async () => { - beforeEach(async () => { - subjectQuantity = ZERO; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Redeem quantity must be > 0"); - }); - }); - - describe("when the SetToken is not enabled on the controller", async () => { - beforeEach(async () => { - const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( - [tokenWithRoundingError.address], - [ether(1)], - [debtIssuance.address] - ); - - subjectSetToken = nonEnabledSetToken.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); - }); - }); - }); - - describe("#getRequiredComponentIssuanceUnits", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - - const debtUnits: BigNumber = ether(100); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - - await debtIssuance.issue(subjectSetToken, subjectQuantity, owner.address); - }); - - async function subject(): Promise { - return debtIssuance.getRequiredComponentIssuanceUnits( - subjectSetToken, - subjectQuantity - ); - } - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil( mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1)); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - - describe("when an additive external equity position is in place", async () => { - const externalUnits: BigNumber = ether(1); - - beforeEach(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil( mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1).add(externalUnits)); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - }); - - describe("when a non-additive external equity position is in place", async () => { - const externalUnits: BigNumber = ether(50); - - beforeEach(async () => { - await externalPositionModule.addExternalPosition(setToken.address, setup.dai.address, externalUnits); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiDebtFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1)); - const daiEquityFlows = preciseMul(mintQuantity, externalUnits); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, daiEquityFlows]; - const expectedDebtFlows = [ZERO, daiDebtFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - }); - }); - }); - - context("when SetToken components do have rounding errors", async () => { - describe("#issue", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - let subjectTo: Address; - let subjectCaller: Account; - - const debtUnits: BigNumber = ether(100); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); - // send exact amount - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0]); - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - subjectTo = recipient.address; - subjectCaller = owner; - }); - - async function subject(): Promise { - return debtIssuance.connect(subjectCaller.wallet).issue( - subjectSetToken, - subjectQuantity, - subjectTo, - ); - } - - describe("when rounding error is negative one", async () => { - beforeEach(async () => { - await tokenWithRoundingError.setError(BigNumber.from(-1)); - }); - - describe("when set is exactly collateralized", async () => { - it("should revert", async () => { - await expect(subject()).to.be.revertedWith( - "Invalid transfer in. Results in undercollateralization" - ); - }); - }); - - describe("when set is over-collateralized by at least 1 wei", async () => { - beforeEach(async () => { - await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); - }); - - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - const managerBalance = await setToken.balanceOf(feeRecipient.address); - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - expect(managerBalance).to.eq(feeQuantity); - }); - }); - }); - - describe("when rounding error is positive one", async () => { - beforeEach(async () => { - await tokenWithRoundingError.setError(ONE); - }); - - describe("when set is exactly collateralized", async () => { - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - const managerBalance = await setToken.balanceOf(feeRecipient.address); - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - expect(managerBalance).to.eq(feeQuantity); - }); - }); - - describe("when set is over-collateralized by at least 1 wei", async () => { - beforeEach(async () => { - await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); - }); - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - const managerBalance = await setToken.balanceOf(feeRecipient.address); - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - expect(managerBalance).to.eq(feeQuantity); - }); - }); - }); - }); - - describe("#redeem", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - let subjectTo: Address; - let subjectCaller: Account; - - const debtUnits: BigNumber = ether(100); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); - // Send exact amount - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0]); - - await debtIssuance.issue(setToken.address, ether(1), owner.address); - - await setup.dai.approve(debtIssuance.address, ether(100.5)); - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - subjectTo = recipient.address; - subjectCaller = owner; - }); - - async function subject(): Promise { - return debtIssuance.connect(subjectCaller.wallet).redeem( - subjectSetToken, - subjectQuantity, - subjectTo, - ); - } - - describe("when rounding error is negative one", async () => { - beforeEach(async () => { - await tokenWithRoundingError.setError(BigNumber.from(-1)); - }); - - describe("when set is exactly collateralized", async () => { - it("should revert", async () => { - await expect(subject()).to.be.revertedWith( - "Invalid transfer out. Results in undercollateralization" - ); - }); - }); - - describe("when set is over-collateralized by at least 1 wei", async () => { - beforeEach(async () => { - await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); - }); - it("should mint SetTokens to the correct addresses", async () => { - const preManagerBalance = await setToken.balanceOf(feeRecipient.address); - const preCallerBalance = await setToken.balanceOf(subjectCaller.address); - - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); - const postManagerBalance = await setToken.balanceOf(feeRecipient.address); - const postCallerBalance = await setToken.balanceOf(subjectCaller.address); - - expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); - expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); - }); - }); - }); - - describe("when rounding error is positive one", async () => { - beforeEach(async () => { - await tokenWithRoundingError.setError(ONE); - }); - - describe("when set is exactly collateralized", async () => { - it("should mint SetTokens to the correct addresses", async () => { - const preManagerBalance = await setToken.balanceOf(feeRecipient.address); - const preCallerBalance = await setToken.balanceOf(subjectCaller.address); - - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); - const postManagerBalance = await setToken.balanceOf(feeRecipient.address); - const postCallerBalance = await setToken.balanceOf(subjectCaller.address); - - expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); - expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); - }); - }); - - describe("when set is over-collateralized by at least 1 wei", async () => { - beforeEach(async () => { - await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); - }); - - it("should mint SetTokens to the correct addresses", async () => { - const preManagerBalance = await setToken.balanceOf(feeRecipient.address); - const preCallerBalance = await setToken.balanceOf(subjectCaller.address); - - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); - const postManagerBalance = await setToken.balanceOf(feeRecipient.address); - const postCallerBalance = await setToken.balanceOf(subjectCaller.address); - - expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); - expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); - }); - }); - }); - }); - - describe("#getRequiredComponentIssuanceUnits", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - - const debtUnits: BigNumber = ether(100); - const accruedBalance = ether(.00001); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - - await tokenWithRoundingError.setError(accruedBalance); - }); - - async function subject(): Promise { - return debtIssuance.getRequiredComponentIssuanceUnits( - subjectSetToken, - subjectQuantity - ); - } - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1)); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - - describe("when an additive external equity position is in place", async () => { - const externalUnits: BigNumber = ether(1); - - beforeEach(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil( mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1).add(externalUnits)); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - }); - - describe("when a non-additive external equity position is in place", async () => { - const externalUnits: BigNumber = ether(50); - - beforeEach(async () => { - await externalPositionModule.addExternalPosition(setToken.address, setup.dai.address, externalUnits); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiDebtFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1)); - const daiEquityFlows = preciseMul(mintQuantity, externalUnits); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, daiEquityFlows]; - const expectedDebtFlows = [ZERO, daiDebtFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - }); - - describe("when tokens have been issued", async () => { - beforeEach(async () => { - const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); - - await debtIssuance.issue(subjectSetToken, subjectQuantity, owner.address); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMul(mintQuantity, debtUnits); - const wethFlows = preciseMulCeil(mintQuantity, preciseDiv(ether(1.005).add(accruedBalance), ether(1.005))); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - - describe("when an additive external equity position is in place", async () => { - const externalUnits: BigNumber = ether(1); - - beforeEach(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil( mintQuantity, debtUnits); - const wethFlows = preciseMulCeil(mintQuantity, preciseDiv(ether(1.005).add(accruedBalance), ether(1.005)).add(externalUnits)); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - }); - }); - }); - }); - }); -}); diff --git a/test/protocol/modules/issuanceModule.spec.ts b/test/protocol/modules/issuanceModule.spec.ts index 69704316e..f727a6ea7 100644 --- a/test/protocol/modules/issuanceModule.spec.ts +++ b/test/protocol/modules/issuanceModule.spec.ts @@ -1,46 +1,1039 @@ import "module-alias/register"; +import { BigNumber } from "ethers"; + +import { Address } from "@utils/types"; import { Account } from "@utils/test/types"; -import { IssuanceModule } from "@utils/contracts"; +import { ZERO, ONE, ADDRESS_ZERO } from "@utils/constants"; +import { + IssuanceModule, + DebtModuleMock, + ManagerIssuanceHookMock, + ModuleIssuanceHookMock, + SetToken, + StandardTokenWithRoundingErrorMock +} from "@utils/contracts"; import DeployHelper from "@utils/deploys"; +import { + ether, + preciseDiv, + preciseMul, + preciseMulCeil, +} from "@utils/index"; import { addSnapshotBeforeRestoreAfterEach, getAccounts, - getWaffleExpect, getSystemFixture, + getWaffleExpect, } from "@utils/test/index"; import { SystemFixture } from "@utils/fixtures"; +import { ContractTransaction } from "ethers"; const expect = getWaffleExpect(); describe("IssuanceModule", () => { let owner: Account; + let manager: Account; + let feeRecipient: Account; + let dummyModule: Account; + let recipient: Account; let deployer: DeployHelper; let setup: SystemFixture; + let debtModule: DebtModuleMock; + let externalPositionModule: ModuleIssuanceHookMock; + let debtIssuance: IssuanceModule; + let issuanceHook: ManagerIssuanceHookMock; + let setToken: SetToken; + let tokenWithRoundingError: StandardTokenWithRoundingErrorMock; + before(async () => { [ owner, + manager, + feeRecipient, + dummyModule, // Set as protocol fee recipient + recipient, ] = await getAccounts(); deployer = new DeployHelper(owner.wallet); setup = getSystemFixture(owner.address); + await setup.initialize(); + + tokenWithRoundingError = await deployer.mocks.deployTokenWithErrorMock(owner.address, ether(1000000), ZERO); + debtIssuance = await deployer.modules.deployIssuanceModule(setup.controller.address); + debtModule = await deployer.mocks.deployDebtModuleMock(setup.controller.address); + externalPositionModule = await deployer.mocks.deployModuleIssuanceHookMock(); + issuanceHook = await deployer.mocks.deployManagerIssuanceHookMock(); + + await setup.controller.addModule(debtIssuance.address); + await setup.controller.addModule(debtModule.address); + await setup.controller.addModule(externalPositionModule.address); + + setToken = await setup.createSetToken( + [tokenWithRoundingError.address], + [ether(1)], + [setup.issuanceModule.address, debtIssuance.address, debtModule.address, externalPositionModule.address], + manager.address, + "DebtToken", + "DBT" + ); + + await externalPositionModule.initialize(setToken.address); }); addSnapshotBeforeRestoreAfterEach(); - describe("#constructor", async () => { - let subjectIssuanceModule: IssuanceModule; + context("when IssuanceModule is initialized", async () => { + let preIssueHook: Address; + let maxFee: BigNumber; + let issueFee: BigNumber; + let redeemFee: BigNumber; + + before(async () => { + await tokenWithRoundingError.setError(ZERO); + + preIssueHook = ADDRESS_ZERO; + maxFee = ether(0.02); + issueFee = ether(0.005); + redeemFee = ether(0.005); + }); + + beforeEach(async () => { + await debtIssuance.connect(manager.wallet).initialize( + setToken.address, + maxFee, + issueFee, + redeemFee, + feeRecipient.address, + preIssueHook + ); + await debtModule.connect(manager.wallet).initialize(setToken.address, debtIssuance.address); + }); + + + context("when SetToken components do not have any rounding error", async () => { + // Note: Tests below are an EXACT copy of the tests for DebtIssuanceModule. Only difference is this SetToken contains + // tokenWithRoundingError instead of weth as a default position. This is to ensure the IssuanceModule behaves + // exactly similar to DebtIssuanceModule when there is no rounding error present in it's constituent components. + + describe("#issue", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectCaller: Account; + + const debtUnits: BigNumber = ether(100); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); + + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + subjectTo = recipient.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return debtIssuance.connect(subjectCaller.wallet).issue( + subjectSetToken, + subjectQuantity, + subjectTo, + ); + } + + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + const managerBalance = await setToken.balanceOf(feeRecipient.address); + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + expect(managerBalance).to.eq(feeQuantity); + }); + + it("should have the correct token balances", async () => { + const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1)); + + const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethFlows)); + expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethFlows)); + expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); + }); + + it("should have called the module issue hook", async () => { + await subject(); + + const hookCalled = await debtModule.moduleIssueHookCalled(); + + expect(hookCalled).to.be.true; + }); + + it("should emit the correct SetTokenIssued event", async () => { + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + + await expect(subject()).to.emit(debtIssuance, "SetTokenIssued").withArgs( + setToken.address, + subjectCaller.address, + subjectTo, + preIssueHook, + subjectQuantity, + feeQuantity, + ZERO + ); + }); + + describe("when an external equity position is in place", async () => { + const externalUnits: BigNumber = ether(1); + + before(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); + }); + + after(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, ZERO); + }); + + it("should have the correct token balances", async () => { + const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); + const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethDefaultFlows = preciseMul(mintQuantity, ether(1)); + const wethExternalFlows = preciseMul(mintQuantity, externalUnits); + + const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); + const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethDefaultFlows.add(wethExternalFlows))); + expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethDefaultFlows)); + expect(postExternalWethBalance).to.eq(preExternalWethBalance.add(wethExternalFlows)); + expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); + }); + }); + + describe("when the manager issuance fee is 0", async () => { + before(async () => { + issueFee = ZERO; + }); + + after(async () => { + issueFee = ether(0.005); + }); + + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + }); + + it("should have the correct token balances", async () => { + const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethDefaultFlows = preciseMul(mintQuantity, ether(1)); + + const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethDefaultFlows)); + expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethDefaultFlows)); + expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); + }); + }); + + describe("when protocol fees are enabled", async () => { + const protocolFee: BigNumber = ether(.2); + + beforeEach(async () => { + await setup.controller.addFee(debtIssuance.address, ZERO, protocolFee); + }); + + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + const protocolSplit = preciseMul(feeQuantity, protocolFee); + + const managerBalance = await setToken.balanceOf(feeRecipient.address); + const protocolBalance = await setToken.balanceOf(dummyModule.address); // DummyModule is set as address in fixture setup + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + expect(managerBalance).to.eq(feeQuantity.sub(protocolSplit)); + expect(protocolBalance).to.eq(protocolSplit); + }); + }); + + describe("when manager issuance hook is defined", async () => { + before(async () => { + preIssueHook = issuanceHook.address; + }); + + after(async () => { + preIssueHook = ADDRESS_ZERO; + }); + + it("should call the issuance hook", async () => { + await subject(); + + const setToken = await issuanceHook.retrievedSetToken(); + + expect(setToken).to.eq(subjectSetToken); + }); + }); + + describe("when the issue quantity is 0", async () => { + beforeEach(async () => { + subjectQuantity = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Issue quantity must be > 0"); + }); + }); + + describe("when the SetToken is not enabled on the controller", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [tokenWithRoundingError.address], + [ether(1)], + [debtIssuance.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#redeem", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectCaller: Account; + + const debtUnits: BigNumber = ether(100); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + const [, equityFlows ] = await debtIssuance.getRequiredComponentRedemptionUnits(setToken.address, ether(1)); + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); + + await debtIssuance.issue(setToken.address, ether(1), owner.address); + + await setup.dai.approve(debtIssuance.address, ether(100.5)); + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + subjectTo = recipient.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return debtIssuance.connect(subjectCaller.wallet).redeem( + subjectSetToken, + subjectQuantity, + subjectTo, + ); + } + + it("should mint SetTokens to the correct addresses", async () => { + const preManagerBalance = await setToken.balanceOf(feeRecipient.address); + const preCallerBalance = await setToken.balanceOf(subjectCaller.address); + + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); + const postManagerBalance = await setToken.balanceOf(feeRecipient.address); + const postCallerBalance = await setToken.balanceOf(subjectCaller.address); + + expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); + expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); + }); + + it("should have the correct token balances", async () => { + const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); + const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); + const wethFlows = preciseMul(redeemQuantity, ether(1)); + + const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postToWethBalance).to.eq(preToWethBalance.add(wethFlows)); + expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethFlows)); + expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); + }); + + it("should have called the module issue hook", async () => { + await subject(); + + const hookCalled = await debtModule.moduleRedeemHookCalled(); + + expect(hookCalled).to.be.true; + }); + + it("should emit the correct SetTokenRedeemed event", async () => { + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + + await expect(subject()).to.emit(debtIssuance, "SetTokenRedeemed").withArgs( + setToken.address, + subjectCaller.address, + subjectTo, + subjectQuantity, + feeQuantity, + ZERO + ); + }); + + describe("when an external equity position is in place", async () => { + const externalUnits: BigNumber = ether(1); + + before(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); + }); + + after(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, ZERO); + }); + + it("should have the correct token balances", async () => { + const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); + const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); + const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); + const wethExternalFlows = preciseMul(redeemQuantity, externalUnits); + const wethDefaultFlows = preciseMul(redeemQuantity, ether(1)); + + const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); + const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postToWethBalance).to.eq(preToWethBalance.add(wethExternalFlows.add(wethDefaultFlows))); + expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethDefaultFlows)); + expect(postExternalWethBalance).to.eq(preExternalWethBalance.sub(wethExternalFlows)); + expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); + }); + }); + + describe("when the manager redemption fee is 0", async () => { + before(async () => { + redeemFee = ZERO; + }); + + after(async () => { + redeemFee = ether(0.005); + }); + + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(ZERO); + }); + + it("should have the correct token balances", async () => { + const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); + const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); + const wethFlows = preciseMul(redeemQuantity, ether(1)); + + const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postToWethBalance).to.eq(preToWethBalance.add(wethFlows)); + expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethFlows)); + expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); + }); + }); + + describe("when protocol fees are enabled", async () => { + const protocolFee: BigNumber = ether(.2); + + beforeEach(async () => { + await setup.controller.addFee(debtIssuance.address, ZERO, protocolFee); + }); + + it("should mint SetTokens to the correct addresses", async () => { + const preManagerBalance = await setToken.balanceOf(feeRecipient.address); + const preProtocolBalance = await setToken.balanceOf(dummyModule.address); + const preCallerBalance = await setToken.balanceOf(subjectCaller.address); + + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); + const protocolSplit = preciseMul(feeQuantity, protocolFee); + + const postManagerBalance = await setToken.balanceOf(feeRecipient.address); + const postProtocolBalance = await setToken.balanceOf(dummyModule.address); // DummyModule is set as address in fixture setup + const postCallerBalance = await setToken.balanceOf(subjectCaller.address); + + expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); + expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity.sub(protocolSplit))); + expect(postProtocolBalance).to.eq(preProtocolBalance.add(protocolSplit)); + }); + }); + + describe("when the issue quantity is 0", async () => { + beforeEach(async () => { + subjectQuantity = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Redeem quantity must be > 0"); + }); + }); + + describe("when the SetToken is not enabled on the controller", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [tokenWithRoundingError.address], + [ether(1)], + [debtIssuance.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#getRequiredComponentIssuanceUnits", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + + const debtUnits: BigNumber = ether(100); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + + await debtIssuance.issue(subjectSetToken, subjectQuantity, owner.address); + }); + + async function subject(): Promise { + return debtIssuance.getRequiredComponentIssuanceUnits( + subjectSetToken, + subjectQuantity + ); + } + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil( mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1)); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + + describe("when an additive external equity position is in place", async () => { + const externalUnits: BigNumber = ether(1); + + beforeEach(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil( mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1).add(externalUnits)); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + }); + + describe("when a non-additive external equity position is in place", async () => { + const externalUnits: BigNumber = ether(50); + + beforeEach(async () => { + await externalPositionModule.addExternalPosition(setToken.address, setup.dai.address, externalUnits); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiDebtFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1)); + const daiEquityFlows = preciseMul(mintQuantity, externalUnits); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, daiEquityFlows]; + const expectedDebtFlows = [ZERO, daiDebtFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + }); + }); + }); + + context("when SetToken components do have rounding errors", async () => { + describe("#issue", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectCaller: Account; + + const debtUnits: BigNumber = ether(100); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); + // send exact amount + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0]); + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + subjectTo = recipient.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return debtIssuance.connect(subjectCaller.wallet).issue( + subjectSetToken, + subjectQuantity, + subjectTo, + ); + } + + describe("when rounding error is negative one", async () => { + beforeEach(async () => { + await tokenWithRoundingError.setError(BigNumber.from(-1)); + }); + + describe("when set is exactly collateralized", async () => { + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Invalid transfer in. Results in undercollateralization" + ); + }); + }); + + describe("when set is over-collateralized by at least 1 wei", async () => { + beforeEach(async () => { + await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); + }); + + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + const managerBalance = await setToken.balanceOf(feeRecipient.address); + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + expect(managerBalance).to.eq(feeQuantity); + }); + }); + }); + + describe("when rounding error is positive one", async () => { + beforeEach(async () => { + await tokenWithRoundingError.setError(ONE); + }); + + describe("when set is exactly collateralized", async () => { + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + const managerBalance = await setToken.balanceOf(feeRecipient.address); + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + expect(managerBalance).to.eq(feeQuantity); + }); + }); + + describe("when set is over-collateralized by at least 1 wei", async () => { + beforeEach(async () => { + await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); + }); + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + const managerBalance = await setToken.balanceOf(feeRecipient.address); + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + expect(managerBalance).to.eq(feeQuantity); + }); + }); + }); + }); + + describe("#redeem", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectCaller: Account; + + const debtUnits: BigNumber = ether(100); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); + // Send exact amount + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0]); + + await debtIssuance.issue(setToken.address, ether(1), owner.address); + + await setup.dai.approve(debtIssuance.address, ether(100.5)); + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + subjectTo = recipient.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return debtIssuance.connect(subjectCaller.wallet).redeem( + subjectSetToken, + subjectQuantity, + subjectTo, + ); + } + + describe("when rounding error is negative one", async () => { + beforeEach(async () => { + await tokenWithRoundingError.setError(BigNumber.from(-1)); + }); + + describe("when set is exactly collateralized", async () => { + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Invalid transfer out. Results in undercollateralization" + ); + }); + }); + + describe("when set is over-collateralized by at least 1 wei", async () => { + beforeEach(async () => { + await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); + }); + it("should mint SetTokens to the correct addresses", async () => { + const preManagerBalance = await setToken.balanceOf(feeRecipient.address); + const preCallerBalance = await setToken.balanceOf(subjectCaller.address); + + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); + const postManagerBalance = await setToken.balanceOf(feeRecipient.address); + const postCallerBalance = await setToken.balanceOf(subjectCaller.address); + + expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); + expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); + }); + }); + }); + + describe("when rounding error is positive one", async () => { + beforeEach(async () => { + await tokenWithRoundingError.setError(ONE); + }); + + describe("when set is exactly collateralized", async () => { + it("should mint SetTokens to the correct addresses", async () => { + const preManagerBalance = await setToken.balanceOf(feeRecipient.address); + const preCallerBalance = await setToken.balanceOf(subjectCaller.address); + + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); + const postManagerBalance = await setToken.balanceOf(feeRecipient.address); + const postCallerBalance = await setToken.balanceOf(subjectCaller.address); + + expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); + expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); + }); + }); + + describe("when set is over-collateralized by at least 1 wei", async () => { + beforeEach(async () => { + await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); + }); + + it("should mint SetTokens to the correct addresses", async () => { + const preManagerBalance = await setToken.balanceOf(feeRecipient.address); + const preCallerBalance = await setToken.balanceOf(subjectCaller.address); + + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); + const postManagerBalance = await setToken.balanceOf(feeRecipient.address); + const postCallerBalance = await setToken.balanceOf(subjectCaller.address); + + expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); + expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); + }); + }); + }); + }); + + describe("#getRequiredComponentIssuanceUnits", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + + const debtUnits: BigNumber = ether(100); + const accruedBalance = ether(.00001); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + + await tokenWithRoundingError.setError(accruedBalance); + }); + + async function subject(): Promise { + return debtIssuance.getRequiredComponentIssuanceUnits( + subjectSetToken, + subjectQuantity + ); + } + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1)); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + + describe("when an additive external equity position is in place", async () => { + const externalUnits: BigNumber = ether(1); + + beforeEach(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil( mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1).add(externalUnits)); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + }); + + describe("when a non-additive external equity position is in place", async () => { + const externalUnits: BigNumber = ether(50); + + beforeEach(async () => { + await externalPositionModule.addExternalPosition(setToken.address, setup.dai.address, externalUnits); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiDebtFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1)); + const daiEquityFlows = preciseMul(mintQuantity, externalUnits); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, daiEquityFlows]; + const expectedDebtFlows = [ZERO, daiDebtFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + }); + + describe("when tokens have been issued", async () => { + beforeEach(async () => { + const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); + + await debtIssuance.issue(subjectSetToken, subjectQuantity, owner.address); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMul(mintQuantity, debtUnits); + const wethFlows = preciseMulCeil(mintQuantity, preciseDiv(ether(1.005).add(accruedBalance), ether(1.005))); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + + describe("when an additive external equity position is in place", async () => { + const externalUnits: BigNumber = ether(1); + + beforeEach(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil( mintQuantity, debtUnits); + const wethFlows = preciseMulCeil(mintQuantity, preciseDiv(ether(1.005).add(accruedBalance), ether(1.005)).add(externalUnits)); - async function subject(): Promise { - return deployer.modules.deployIssuanceModule(setup.controller.address); - } + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; - it("should have the correct controller", async () => { - subjectIssuanceModule = await subject(); - const expectedController = await subjectIssuanceModule.controller(); - expect(expectedController).to.eq(setup.controller.address); + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + }); + }); + }); }); }); -}); \ No newline at end of file +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 7bb428b03..d24499f13 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -30,7 +30,6 @@ export { CustomOracleNavIssuanceModule } from "../../typechain/CustomOracleNavIs export { CustomSetValuerMock } from "../../typechain/CustomSetValuerMock"; export { DebtIssuanceMock } from "../../typechain/DebtIssuanceMock"; export { DebtIssuanceModule } from "../../typechain/DebtIssuanceModule"; -export { DebtIssuanceModuleV2 } from "../../typechain/DebtIssuanceModuleV2"; export { DebtModuleMock } from "../../typechain/DebtModuleMock"; export { DelegateRegistry } from "../../typechain/DelegateRegistry"; export { ExplicitERC20Mock } from "../../typechain/ExplicitERC20Mock"; From 1b3a6731471011dede6dd5426c970fadfe8a36f6 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 17 Mar 2022 11:47:04 -0700 Subject: [PATCH 3/7] Remove AaveGovernanceAdapter (in favor of AaveGovernanceV2Adapter) --- utils/deploys/deployModules.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/deploys/deployModules.ts b/utils/deploys/deployModules.ts index 6e10b64cc..4558e952e 100644 --- a/utils/deploys/deployModules.ts +++ b/utils/deploys/deployModules.ts @@ -33,7 +33,6 @@ import { ClaimModule__factory } from "../../typechain/factories/ClaimModule__fac import { CompoundLeverageModule__factory } from "../../typechain/factories/CompoundLeverageModule__factory"; import { CustomOracleNavIssuanceModule__factory } from "../../typechain/factories/CustomOracleNavIssuanceModule__factory"; import { DebtIssuanceModule__factory } from "../../typechain/factories/DebtIssuanceModule__factory"; -import { DebtIssuanceModuleV2__factory } from "../../typechain/factories/DebtIssuanceModuleV2__factory"; import { SlippageIssuanceModule__factory } from "../../typechain/factories/SlippageIssuanceModule__factory"; import { GeneralIndexModule__factory } from "../../typechain/factories/GeneralIndexModule__factory"; import { GovernanceModule__factory } from "../../typechain/factories/GovernanceModule__factory"; From 198b73e39c36a386a236158c29156a3a3358bef8 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Fri, 18 Mar 2022 19:21:20 -0700 Subject: [PATCH 4/7] fix(interfaces): add interfaces needed by set-v2-strategies --- contracts/interfaces/IIssuanceModule.sol | 41 +++++++++++++++++++ contracts/interfaces/ISetTokenCreator.sol | 32 +++++++++++++++ contracts/interfaces/IStreamingFeeModule.sol | 16 ++++++-- contracts/interfaces/ITradeModule.sol | 35 ++++++++++++++++ .../external/perp-v2/IAccountBalance.sol | 2 + .../StreamingFeeModuleViewer.sol | 2 +- 6 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 contracts/interfaces/IIssuanceModule.sol create mode 100644 contracts/interfaces/ISetTokenCreator.sol create mode 100644 contracts/interfaces/ITradeModule.sol diff --git a/contracts/interfaces/IIssuanceModule.sol b/contracts/interfaces/IIssuanceModule.sol new file mode 100644 index 000000000..a286c6627 --- /dev/null +++ b/contracts/interfaces/IIssuanceModule.sol @@ -0,0 +1,41 @@ +/* + Copyright 2022 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 { ISetToken } from "./ISetToken.sol"; + +/** + * @title IIssuanceModule + * @author Set Protocol + * + * Interface for interacting with Issuance module interface. + */ +interface IIssuanceModule { + function updateIssueFee(ISetToken _setToken, uint256 _newIssueFee) external; + function updateRedeemFee(ISetToken _setToken, uint256 _newRedeemFee) external; + function updateFeeRecipient(ISetToken _setToken, address _newRedeemFee) external; + + function initialize( + ISetToken _setToken, + uint256 _maxManagerFee, + uint256 _managerIssueFee, + uint256 _managerRedeemFee, + address _feeRecipient, + address _managerIssuanceHook + ) external; +} diff --git a/contracts/interfaces/ISetTokenCreator.sol b/contracts/interfaces/ISetTokenCreator.sol new file mode 100644 index 000000000..9465d1e74 --- /dev/null +++ b/contracts/interfaces/ISetTokenCreator.sol @@ -0,0 +1,32 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; + +interface ISetTokenCreator { + function create( + address[] memory _components, + int256[] memory _units, + address[] memory _modules, + address _manager, + string memory _name, + string memory _symbol + ) + external + returns (address); +} diff --git a/contracts/interfaces/IStreamingFeeModule.sol b/contracts/interfaces/IStreamingFeeModule.sol index e964243e3..45ab7d503 100644 --- a/contracts/interfaces/IStreamingFeeModule.sol +++ b/contracts/interfaces/IStreamingFeeModule.sol @@ -19,9 +19,19 @@ pragma solidity 0.6.10; pragma experimental "ABIEncoderV2"; import { ISetToken } from "./ISetToken.sol"; -import { StreamingFeeModule } from "../protocol/modules/StreamingFeeModule.sol"; interface IStreamingFeeModule { - function feeStates(ISetToken _setToken) external view returns (StreamingFeeModule.FeeState memory); + struct FeeState { + address feeRecipient; + uint256 maxStreamingFeePercentage; + uint256 streamingFeePercentage; + uint256 lastStreamingFeeTimestamp; + } + + function feeStates(ISetToken _setToken) external view returns (FeeState memory); function getFee(ISetToken _setToken) external view returns (uint256); -} \ No newline at end of file + function accrueFee(ISetToken _setToken) external; + function updateStreamingFee(ISetToken _setToken, uint256 _newFee) external; + function updateFeeRecipient(ISetToken _setToken, address _newFeeRecipient) external; + function initialize(ISetToken _setToken, FeeState memory _settings) external; +} diff --git a/contracts/interfaces/ITradeModule.sol b/contracts/interfaces/ITradeModule.sol new file mode 100644 index 000000000..5d0059995 --- /dev/null +++ b/contracts/interfaces/ITradeModule.sol @@ -0,0 +1,35 @@ +/* + Copyright 2022 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 { ISetToken } from "./ISetToken.sol"; + +interface ITradeModule { + function initialize(ISetToken _setToken) external; + + function trade( + ISetToken _setToken, + string memory _exchangeName, + address _sendToken, + uint256 _sendQuantity, + address _receiveToken, + uint256 _minReceiveQuantity, + bytes memory _data + ) external; +} diff --git a/contracts/interfaces/external/perp-v2/IAccountBalance.sol b/contracts/interfaces/external/perp-v2/IAccountBalance.sol index c2db0f302..47ec0993d 100644 --- a/contracts/interfaces/external/perp-v2/IAccountBalance.sol +++ b/contracts/interfaces/external/perp-v2/IAccountBalance.sol @@ -35,4 +35,6 @@ interface IAccountBalance { function getExchange() external view returns (address); function getOrderBook() external view returns (address); function getVault() external view returns (address); + function getTakerPositionSize(address trader, address baseToken) external view returns (int256); + function getTakerOpenNotional(address trader, address baseToken) external view returns (int256); } diff --git a/contracts/protocol-viewers/StreamingFeeModuleViewer.sol b/contracts/protocol-viewers/StreamingFeeModuleViewer.sol index a3c64e62b..b23a59553 100644 --- a/contracts/protocol-viewers/StreamingFeeModuleViewer.sol +++ b/contracts/protocol-viewers/StreamingFeeModuleViewer.sol @@ -49,7 +49,7 @@ contract StreamingFeeModuleViewer { StreamingFeeInfo[] memory feeInfo = new StreamingFeeInfo[](_setTokens.length); for (uint256 i = 0; i < _setTokens.length; i++) { - StreamingFeeModule.FeeState memory feeState = _streamingFeeModule.feeStates(_setTokens[i]); + IStreamingFeeModule.FeeState memory feeState = _streamingFeeModule.feeStates(_setTokens[i]); uint256 unaccruedFees = _streamingFeeModule.getFee(_setTokens[i]); feeInfo[i] = StreamingFeeInfo({ From a2757df5abd491ed9b30e6b28406a707e1b95ff0 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 24 Mar 2022 12:22:24 -0700 Subject: [PATCH 5/7] Fix rebase --- test/product/uniswapYieldHook.spec.ts | 453 -------------------------- utils/contracts/index.ts | 3 +- utils/deploys/deployModules.ts | 3 +- 3 files changed, 4 insertions(+), 455 deletions(-) delete mode 100644 test/product/uniswapYieldHook.spec.ts diff --git a/test/product/uniswapYieldHook.spec.ts b/test/product/uniswapYieldHook.spec.ts deleted file mode 100644 index dde31aebf..000000000 --- a/test/product/uniswapYieldHook.spec.ts +++ /dev/null @@ -1,453 +0,0 @@ -import "module-alias/register"; - -import { BigNumber } from "ethers"; - -import { Address, CustomOracleNAVIssuanceSettings } from "@utils/types"; -import { Account } from "@utils/test/types"; -import { ZERO, ADDRESS_ZERO } from "@utils/constants"; -import { - NAVIssuanceCaller, - CustomOracleNavIssuanceModule, - SetToken, - UniswapYieldHook, - CustomSetValuerMock -} from "@utils/contracts"; -import DeployHelper from "@utils/deploys"; -import { - bitcoin, - ether, - usdc, -} from "@utils/index"; -import { - addSnapshotBeforeRestoreAfterEach, - getAccounts, - getWaffleExpect, - getSystemFixture, - getRandomAccount, -} from "@utils/test/index"; - -import { SystemFixture } from "@utils/fixtures"; - -const expect = getWaffleExpect(); - -describe("UniswapYieldHook", () => { - let owner: Account; - let feeRecipient: Account; - let recipient: Account; - let deployer: DeployHelper; - - let setup: SystemFixture; - let navIssuanceModule: CustomOracleNavIssuanceModule; - let setToken: SetToken; - let setValuer: CustomSetValuerMock; - - let hook: UniswapYieldHook; - let navIssuanceCaller: NAVIssuanceCaller; - - const setRedeemLimit: BigNumber = ether(435); - const usdcIssueLimit: BigNumber = usdc(100000); - const ethIssueLimit: BigNumber = ether(435); - - before(async () => { - [ - owner, - feeRecipient, - recipient, - ] = await getAccounts(); - - deployer = new DeployHelper(owner.wallet); - setup = getSystemFixture(owner.address); - await setup.initialize(); - - navIssuanceModule = await deployer.modules.deployCustomOracleNavIssuanceModule( - setup.controller.address, - setup.weth.address - ); - await setup.controller.addModule(navIssuanceModule.address); - - setToken = await setup.createSetToken( - [setup.weth.address], - [ether(1)], - [navIssuanceModule.address, setup.issuanceModule.address] - ); - - hook = await deployer.product.deployUniswapYieldHook( - [setToken.address, setup.usdc.address, setup.weth.address], - [setRedeemLimit, usdcIssueLimit, ethIssueLimit], - ); - - navIssuanceCaller = await deployer.mocks.deployNAVIssuanceCaller(navIssuanceModule.address); - setValuer = await deployer.mocks.deployCustomSetValuerMock(); - - const navIssuanceSettings = { - managerIssuanceHook: hook.address, - managerRedemptionHook: hook.address, - setValuer: setValuer.address, - reserveAssets: [setup.usdc.address, setup.weth.address], - feeRecipient: feeRecipient.address, - managerFees: [ether(0.001), ether(0.002)], - maxManagerFee: ether(0.02), - premiumPercentage: ether(0.01), - maxPremiumPercentage: ether(0.1), - minSetTokenSupply: ether(100), - } as CustomOracleNAVIssuanceSettings; - - await navIssuanceModule.initialize( - setToken.address, - navIssuanceSettings - ); - - // Approve tokens to the controller - await setup.weth.approve(setup.controller.address, ether(100)); - await setup.usdc.approve(setup.controller.address, usdc(1000000)); - await setup.wbtc.approve(setup.controller.address, bitcoin(1000000)); - await setup.dai.approve(setup.controller.address, ether(1000000)); - - // Seed with 100 supply - await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); - await setup.issuanceModule.issue(setToken.address, ether(100), owner.address); - }); - - addSnapshotBeforeRestoreAfterEach(); - - describe("#constructor", async () => { - let subjectAssets: Address[]; - let subjectLimits: BigNumber[]; - - beforeEach(async () => { - subjectAssets = [setup.weth.address, setup.usdc.address]; - subjectLimits = [ether(400), usdc(100000)]; - }); - - async function subject(): Promise { - return await deployer.product.deployUniswapYieldHook(subjectAssets, subjectLimits); - } - - it("should set the correct limits", async () => { - const hook = await subject(); - - const wethLimit = await hook.assetLimits(subjectAssets[0]); - const usdcLimit = await hook.assetLimits(subjectAssets[1]); - expect(wethLimit).to.eq(subjectLimits[0]); - expect(usdcLimit).to.eq(subjectLimits[1]); - }); - - it("should set the correct assets", async () => { - const hook = await subject(); - - const assets = await hook.getAssets(); - expect(JSON.stringify(assets)).to.eq(JSON.stringify(subjectAssets)); - }); - - describe("when asset is duplicated", async () => { - beforeEach(async () => { - subjectAssets = [setup.weth.address, setup.weth.address]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Asset already added"); - }); - }); - - describe("when array lengths don't match", async () => { - beforeEach(async () => { - subjectAssets = [setup.weth.address]; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Arrays must be equal"); - }); - }); - - describe("when arrays are empty", async () => { - beforeEach(async () => { - subjectAssets = []; - subjectLimits = []; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Array must not be empty"); - }); - }); - }); - - describe("#issue", async () => { - let subjectSetToken: Address; - let subjectReserveAsset: Address; - let subjectReserveQuantity: BigNumber; - let subjectMinSetTokenReceived: BigNumber; - let subjectTo: Account; - let subjectCallFromContract: boolean; - - beforeEach(async () => { - subjectSetToken = setToken.address; - subjectReserveAsset = setup.usdc.address; - subjectReserveQuantity = usdc(150000); - subjectMinSetTokenReceived = ZERO; - subjectTo = recipient; - subjectCallFromContract = false; - - await setup.usdc.approve(navIssuanceModule.address, subjectReserveQuantity); - }); - - async function subject(): Promise { - if (subjectCallFromContract) { - await setup.usdc.connect(owner.wallet).transfer(navIssuanceCaller.address, subjectReserveQuantity); - return navIssuanceCaller.issue( - subjectSetToken, - subjectReserveAsset, - subjectReserveQuantity, - subjectMinSetTokenReceived, - subjectTo.address - ); - } else { - return navIssuanceModule.issue( - subjectSetToken, - subjectReserveAsset, - subjectReserveQuantity, - subjectMinSetTokenReceived, - subjectTo.address - ); - } - } - - it("should not revert", async () => { - await expect(subject()).to.not.be.reverted; - }); - - describe("when call is from contract but less than redeem limit", async () => { - beforeEach(async () => { - subjectCallFromContract = true; - subjectReserveQuantity = usdc(90000); - }); - - it("should not revert", async () => { - await expect(subject()).to.not.be.reverted; - }); - }); - - describe("when call is from contract but greater than redeem limit", async () => { - beforeEach(async () => { - subjectCallFromContract = true; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Issue size too large for call from contract."); - }); - }); - }); - - describe("#redeem", async () => { - let subjectSetToken: Address; - let subjectReserveAsset: Address; - let subjectSetTokenQuantity: BigNumber; - let subjectMinReserveQuantityReceived: BigNumber; - let subjectTo: Account; - let subjectCallFromContract: boolean; - - beforeEach(async () => { - await setup.usdc.approve(navIssuanceModule.address, usdc(150000)); - await navIssuanceModule.issue( - setToken.address, - setup.usdc.address, - usdc(150000), - ZERO, - owner.address - ); - - subjectSetToken = setToken.address; - subjectReserveAsset = setup.usdc.address; - subjectSetTokenQuantity = ether(500); - subjectMinReserveQuantityReceived = ZERO; - subjectTo = recipient; - subjectCallFromContract = false; - }); - - async function subject(): Promise { - if (subjectCallFromContract) { - await setToken.connect(owner.wallet).transfer(navIssuanceCaller.address, subjectSetTokenQuantity); - return navIssuanceCaller.redeem( - subjectSetToken, - subjectReserveAsset, - subjectSetTokenQuantity, - subjectMinReserveQuantityReceived, - subjectTo.address - ); - } else { - return navIssuanceModule.redeem( - subjectSetToken, - subjectReserveAsset, - subjectSetTokenQuantity, - subjectMinReserveQuantityReceived, - subjectTo.address - ); - } - } - - it("should not revert", async () => { - await expect(subject()).to.not.be.reverted; - }); - - describe("when call is from contract but less than redeem limit", async () => { - beforeEach(async () => { - subjectCallFromContract = true; - subjectSetTokenQuantity = ether(400); - }); - - it("should not revert", async () => { - await expect(subject()).to.not.be.reverted; - }); - }); - - describe("when call is from contract but greater than redeem limit", async () => { - beforeEach(async () => { - subjectCallFromContract = true; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Redeem size too large for call from contract."); - }); - }); - }); - - describe("#addAssetLimit", async () => { - let subjectAsset: Address; - let subjectLimit: BigNumber; - let subjectCaller: Account; - - beforeEach(async () => { - subjectAsset = setup.wbtc.address; - subjectLimit = bitcoin(10); - subjectCaller = owner; - }); - - async function subject(): Promise { - return await hook.connect(subjectCaller.wallet).addAssetLimit(subjectAsset, subjectLimit); - } - - it("should set the correct limits", async () => { - await subject(); - - const wbtcLimit = await hook.assetLimits(subjectAsset); - expect(wbtcLimit).to.eq(subjectLimit); - }); - - it("should add wbtc to assets array", async () => { - await subject(); - - const assets = await hook.getAssets(); - expect(assets).to.contain(subjectAsset); - }); - - describe("when asset is duplicated", async () => { - beforeEach(async () => { - subjectAsset = setup.weth.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Asset already added"); - }); - }); - - describe("when caller is not owner", async () => { - beforeEach(async () => { - subjectCaller = await getRandomAccount(); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - }); - - describe("#editAssetLimit", async () => { - let subjectAsset: Address; - let subjectLimit: BigNumber; - let subjectCaller: Account; - - beforeEach(async () => { - subjectAsset = setup.weth.address; - subjectLimit = ether(100); - subjectCaller = owner; - }); - - async function subject(): Promise { - return await hook.connect(subjectCaller.wallet).editAssetLimit(subjectAsset, subjectLimit); - } - - it("should set the correct limits", async () => { - await subject(); - - const wethLimit = await hook.assetLimits(subjectAsset); - expect(wethLimit).to.eq(subjectLimit); - }); - - describe("when asset is not already added", async () => { - beforeEach(async () => { - subjectAsset = setup.wbtc.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Asset not added"); - }); - }); - - describe("when caller is not owner", async () => { - beforeEach(async () => { - subjectCaller = await getRandomAccount(); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - }); - - describe("#removeAssetLimit", async () => { - let subjectAsset: Address; - let subjectCaller: Account; - - beforeEach(async () => { - subjectAsset = setup.weth.address; - subjectCaller = owner; - }); - - async function subject(): Promise { - return await hook.connect(subjectCaller.wallet).removeAssetLimit(subjectAsset); - } - - it("should set the correct limits", async () => { - await subject(); - - const wethLimit = await hook.assetLimits(subjectAsset); - expect(wethLimit).to.eq(ZERO); - }); - - it("should remove weth from assets array", async () => { - await subject(); - - const assets = await hook.getAssets(); - expect(assets).to.not.contain(subjectAsset); - }); - - describe("when asset is not already added", async () => { - beforeEach(async () => { - subjectAsset = setup.wbtc.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Asset not added"); - }); - }); - - describe("when caller is not owner", async () => { - beforeEach(async () => { - subjectCaller = await getRandomAccount(); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - }); -}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index d24499f13..ac2419d98 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -30,6 +30,7 @@ export { CustomOracleNavIssuanceModule } from "../../typechain/CustomOracleNavIs export { CustomSetValuerMock } from "../../typechain/CustomSetValuerMock"; export { DebtIssuanceMock } from "../../typechain/DebtIssuanceMock"; export { DebtIssuanceModule } from "../../typechain/DebtIssuanceModule"; +export { DebtIssuanceModuleV2 } from "../../typechain/DebtIssuanceModuleV2"; export { DebtModuleMock } from "../../typechain/DebtModuleMock"; export { DelegateRegistry } from "../../typechain/DelegateRegistry"; export { ExplicitERC20Mock } from "../../typechain/ExplicitERC20Mock"; @@ -118,4 +119,4 @@ export { WrapModuleV2 } from "../../typechain/WrapModuleV2"; export { YearnWrapV2Adapter } from "../../typechain/YearnWrapV2Adapter"; export { YearnStrategyMock } from "../../typechain/YearnStrategyMock"; export { ZeroExApiAdapter } from "../../typechain/ZeroExApiAdapter"; -export { ZeroExMock } from "../../typechain/ZeroExMock"; +export { ZeroExMock } from "../../typechain/ZeroExMock"; \ No newline at end of file diff --git a/utils/deploys/deployModules.ts b/utils/deploys/deployModules.ts index 4558e952e..40a9e5b96 100644 --- a/utils/deploys/deployModules.ts +++ b/utils/deploys/deployModules.ts @@ -33,6 +33,7 @@ import { ClaimModule__factory } from "../../typechain/factories/ClaimModule__fac import { CompoundLeverageModule__factory } from "../../typechain/factories/CompoundLeverageModule__factory"; import { CustomOracleNavIssuanceModule__factory } from "../../typechain/factories/CustomOracleNavIssuanceModule__factory"; import { DebtIssuanceModule__factory } from "../../typechain/factories/DebtIssuanceModule__factory"; +import { DebtIssuanceModuleV2__factory } from "../../typechain/factories/DebtIssuanceModuleV2__factory"; import { SlippageIssuanceModule__factory } from "../../typechain/factories/SlippageIssuanceModule__factory"; import { GeneralIndexModule__factory } from "../../typechain/factories/GeneralIndexModule__factory"; import { GovernanceModule__factory } from "../../typechain/factories/GovernanceModule__factory"; @@ -248,4 +249,4 @@ export default class DeployModules { maxPerpPositionsPerSet ); } -} \ No newline at end of file +} From 507872cbf51d5fa7f8632fadb20b1879417196b3 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 24 Mar 2022 12:37:48 -0700 Subject: [PATCH 6/7] Revert "Rename DebtIssuanceModuleV2 to IssuanceModule" This reverts commit 37f00ddf4d089ee8253d41872b071cc92955fddf. --- .../protocol/modules/DebtIssuanceModuleV2.sol | 387 ++++++ contracts/protocol/modules/IssuanceModule.sol | 367 +----- .../aaveUniswapLeverageDebtIssuance.spec.ts | 6 +- .../modules/debtIssuanceModuleV2.spec.ts | 1039 +++++++++++++++++ test/protocol/modules/issuanceModule.spec.ts | 1017 +--------------- 5 files changed, 1450 insertions(+), 1366 deletions(-) create mode 100644 contracts/protocol/modules/DebtIssuanceModuleV2.sol create mode 100644 test/protocol/modules/debtIssuanceModuleV2.spec.ts diff --git a/contracts/protocol/modules/DebtIssuanceModuleV2.sol b/contracts/protocol/modules/DebtIssuanceModuleV2.sol new file mode 100644 index 000000000..c857d4d52 --- /dev/null +++ b/contracts/protocol/modules/DebtIssuanceModuleV2.sol @@ -0,0 +1,387 @@ +/* + 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 { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +import { DebtIssuanceModule } from "./DebtIssuanceModule.sol"; +import { IController } from "../../interfaces/IController.sol"; +import { Invoke } from "../lib/Invoke.sol"; +import { ISetToken } from "../../interfaces/ISetToken.sol"; +import { IssuanceValidationUtils } from "../lib/IssuanceValidationUtils.sol"; +import { Position } from "../lib/Position.sol"; + +/** + * @title DebtIssuanceModuleV2 + * @author Set Protocol + * + * The DebtIssuanceModuleV2 is a module that enables users to issue and redeem SetTokens that contain default and all + * external positions, including debt positions. Module hooks are added to allow for syncing of positions, and component + * level hooks are added to ensure positions are replicated correctly. The manager can define arbitrary issuance logic + * in the manager hook, as well as specify issue and redeem fees. + * + * NOTE: + * DebtIssuanceModule contract confirms increase/decrease in balance of component held by the SetToken after every transfer in/out + * for each component during issuance/redemption. This contract replaces those strict checks with slightly looser checks which + * ensure that the SetToken remains collateralized after every transfer in/out for each component during issuance/redemption. + * This module should be used to issue/redeem SetToken whose one or more components return a balance value with +/-1 wei error. + * For example, this module can be used to issue/redeem SetTokens which has one or more aTokens as its components. + * The new checks do NOT apply to any transfers that are part of an external position. A token that has rounding issues may lead to + * reverts if it is included as an external position unless explicitly allowed in a module hook. + * + * The getRequiredComponentIssuanceUnits function on this module assumes that Default token balances will be synced on every issuance + * and redemption. If token balances are not being synced it will over-estimate the amount of tokens required to issue a Set. + */ +contract DebtIssuanceModuleV2 is DebtIssuanceModule { + using Position for uint256; + + /* ============ Constructor ============ */ + + constructor(IController _controller) public DebtIssuanceModule(_controller) {} + + /* ============ External Functions ============ */ + + /** + * Deposits components to the SetToken, replicates any external module component positions and mints + * the SetToken. If the token has a debt position all collateral will be transferred in first then debt + * will be returned to the minting address. If specified, a fee will be charged on issuance. + * + * NOTE: Overrides DebtIssuanceModule#issue external function and adds undercollateralization checks in place of the + * previous default strict balances checks. The undercollateralization checks are implemented in IssuanceValidationUtils library and they + * revert upon undercollateralization of the SetToken post component transfer. + * + * @param _setToken Instance of the SetToken to issue + * @param _quantity Quantity of SetToken to issue + * @param _to Address to mint SetToken to + */ + function issue( + ISetToken _setToken, + uint256 _quantity, + address _to + ) + external + override + nonReentrant + onlyValidAndInitializedSet(_setToken) + { + require(_quantity > 0, "Issue quantity must be > 0"); + + address hookContract = _callManagerPreIssueHooks(_setToken, _quantity, msg.sender, _to); + + _callModulePreIssueHooks(_setToken, _quantity); + + + uint256 initialSetSupply = _setToken.totalSupply(); + + ( + uint256 quantityWithFees, + uint256 managerFee, + uint256 protocolFee + ) = calculateTotalFees(_setToken, _quantity, true); + + // Prevent stack too deep + { + ( + address[] memory components, + uint256[] memory equityUnits, + uint256[] memory debtUnits + ) = _calculateRequiredComponentIssuanceUnits(_setToken, quantityWithFees, true); + + uint256 finalSetSupply = initialSetSupply.add(quantityWithFees); + + _resolveEquityPositions(_setToken, quantityWithFees, _to, true, components, equityUnits, initialSetSupply, finalSetSupply); + _resolveDebtPositions(_setToken, quantityWithFees, true, components, debtUnits, initialSetSupply, finalSetSupply); + _resolveFees(_setToken, managerFee, protocolFee); + } + + _setToken.mint(_to, _quantity); + + emit SetTokenIssued( + _setToken, + msg.sender, + _to, + hookContract, + _quantity, + managerFee, + protocolFee + ); + } + + /** + * Returns components from the SetToken, unwinds any external module component positions and burns the SetToken. + * If the token has debt positions, the module transfers in the required debt amounts from the caller and uses + * those funds to repay the debts on behalf of the SetToken. All debt will be paid down first then equity positions + * will be returned to the minting address. If specified, a fee will be charged on redeem. + * + * NOTE: Overrides DebtIssuanceModule#redeem internal function and adds undercollateralization checks in place of the + * previous default strict balances checks. The undercollateralization checks are implemented in IssuanceValidationUtils library + * and they revert upon undercollateralization of the SetToken post component transfer. + * + * @param _setToken Instance of the SetToken to redeem + * @param _quantity Quantity of SetToken to redeem + * @param _to Address to send collateral to + */ + function redeem( + ISetToken _setToken, + uint256 _quantity, + address _to + ) + external + override + nonReentrant + onlyValidAndInitializedSet(_setToken) + { + require(_quantity > 0, "Redeem quantity must be > 0"); + + _callModulePreRedeemHooks(_setToken, _quantity); + + uint256 initialSetSupply = _setToken.totalSupply(); + + // Place burn after pre-redeem hooks because burning tokens may lead to false accounting of synced positions + _setToken.burn(msg.sender, _quantity); + + ( + uint256 quantityNetFees, + uint256 managerFee, + uint256 protocolFee + ) = calculateTotalFees(_setToken, _quantity, false); + + // Prevent stack too deep + { + ( + address[] memory components, + uint256[] memory equityUnits, + uint256[] memory debtUnits + ) = _calculateRequiredComponentIssuanceUnits(_setToken, quantityNetFees, false); + + uint256 finalSetSupply = initialSetSupply.sub(quantityNetFees); + + _resolveDebtPositions(_setToken, quantityNetFees, false, components, debtUnits, initialSetSupply, finalSetSupply); + _resolveEquityPositions(_setToken, quantityNetFees, _to, false, components, equityUnits, initialSetSupply, finalSetSupply); + _resolveFees(_setToken, managerFee, protocolFee); + } + + emit SetTokenRedeemed( + _setToken, + msg.sender, + _to, + _quantity, + managerFee, + protocolFee + ); + } + + /* ============ External View Functions ============ */ + + /** + * Calculates the amount of each component needed to collateralize passed issue quantity plus fees of Sets as well as amount of debt + * that will be returned to caller. Default equity alues are calculated based on token balances and not position units in order to more + * closely track any accrued tokens that will be synced during issuance. External equity and debt positions will use the stored position + * units. IF TOKEN VALUES ARE NOT BEING SYNCED DURING ISSUANCE THIS FUNCTION WILL OVER ESTIMATE THE AMOUNT OF REQUIRED TOKENS. + * + * @param _setToken Instance of the SetToken to issue + * @param _quantity Amount of Sets to be issued + * + * @return address[] Array of component addresses making up the Set + * @return uint256[] Array of equity notional amounts of each component, respectively, represented as uint256 + * @return uint256[] Array of debt notional amounts of each component, respectively, represented as uint256 + */ + function getRequiredComponentIssuanceUnits( + ISetToken _setToken, + uint256 _quantity + ) + external + view + override + returns (address[] memory, uint256[] memory, uint256[] memory) + { + ( + uint256 totalQuantity,, + ) = calculateTotalFees(_setToken, _quantity, true); + + if(_setToken.totalSupply() == 0) { + return _calculateRequiredComponentIssuanceUnits(_setToken, totalQuantity, true); + } else { + ( + address[] memory components, + uint256[] memory equityUnits, + uint256[] memory debtUnits + ) = _getTotalIssuanceUnitsFromBalances(_setToken); + + uint256 componentsLength = components.length; + uint256[] memory totalEquityUnits = new uint256[](componentsLength); + uint256[] memory totalDebtUnits = new uint256[](componentsLength); + for (uint256 i = 0; i < components.length; i++) { + // Use preciseMulCeil to round up to ensure overcollateration of equity when small issue quantities are provided + // and use preciseMul to round debt calculations down to make sure we don't return too much debt to issuer + totalEquityUnits[i] = equityUnits[i].preciseMulCeil(totalQuantity); + totalDebtUnits[i] = debtUnits[i].preciseMul(totalQuantity); + } + + return (components, totalEquityUnits, totalDebtUnits); + } + } + + /* ============ Internal Functions ============ */ + + /** + * Resolve equity positions associated with SetToken. On issuance, the total equity position for an asset (including default and external + * positions) is transferred in. Then any external position hooks are called to transfer the external positions to their necessary place. + * On redemption all external positions are recalled by the external position hook, then those position plus any default position are + * transferred back to the _to address. + */ + function _resolveEquityPositions( + ISetToken _setToken, + uint256 _quantity, + address _to, + bool _isIssue, + address[] memory _components, + uint256[] memory _componentEquityQuantities, + uint256 _initialSetSupply, + uint256 _finalSetSupply + ) + internal + { + for (uint256 i = 0; i < _components.length; i++) { + address component = _components[i]; + uint256 componentQuantity = _componentEquityQuantities[i]; + if (componentQuantity > 0) { + if (_isIssue) { + // Call SafeERC20#safeTransferFrom instead of ExplicitERC20#transferFrom + SafeERC20.safeTransferFrom( + IERC20(component), + msg.sender, + address(_setToken), + componentQuantity + ); + + IssuanceValidationUtils.validateCollateralizationPostTransferInPreHook(_setToken, component, _initialSetSupply, componentQuantity); + + _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), true, true); + } else { + _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), false, true); + + // Call Invoke#invokeTransfer instead of Invoke#strictInvokeTransfer + _setToken.invokeTransfer(component, _to, componentQuantity); + + IssuanceValidationUtils.validateCollateralizationPostTransferOut(_setToken, component, _finalSetSupply); + } + } + } + } + + /** + * Resolve debt positions associated with SetToken. On issuance, debt positions are entered into by calling the external position hook. The + * resulting debt is then returned to the calling address. On redemption, the module transfers in the required debt amount from the caller + * and uses those funds to repay the debt on behalf of the SetToken. + */ + function _resolveDebtPositions( + ISetToken _setToken, + uint256 _quantity, + bool _isIssue, + address[] memory _components, + uint256[] memory _componentDebtQuantities, + uint256 _initialSetSupply, + uint256 _finalSetSupply + ) + internal + { + for (uint256 i = 0; i < _components.length; i++) { + address component = _components[i]; + uint256 componentQuantity = _componentDebtQuantities[i]; + if (componentQuantity > 0) { + if (_isIssue) { + _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), true, false); + + // Call Invoke#invokeTransfer instead of Invoke#strictInvokeTransfer + _setToken.invokeTransfer(component, msg.sender, componentQuantity); + + IssuanceValidationUtils.validateCollateralizationPostTransferOut(_setToken, component, _finalSetSupply); + } else { + // Call SafeERC20#safeTransferFrom instead of ExplicitERC20#transferFrom + SafeERC20.safeTransferFrom( + IERC20(component), + msg.sender, + address(_setToken), + componentQuantity + ); + + IssuanceValidationUtils.validateCollateralizationPostTransferInPreHook(_setToken, component, _initialSetSupply, componentQuantity); + + _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), false, false); + } + } + } + } + /** + * Reimplementation of _getTotalIssuanceUnits but instead derives Default equity positions from token balances on Set instead of from + * position units. This function is ONLY to be used in getRequiredComponentIssuanceUnits in order to return more accurate required + * token amounts to issuers when positions are being synced on issuance. + * + * @param _setToken Instance of the SetToken to issue + * + * @return address[] Array of component addresses making up the Set + * @return uint256[] Array of equity unit amounts of each component, respectively, represented as uint256 + * @return uint256[] Array of debt unit amounts of each component, respectively, represented as uint256 + */ + function _getTotalIssuanceUnitsFromBalances( + ISetToken _setToken + ) + internal + view + returns (address[] memory, uint256[] memory, uint256[] memory) + { + address[] memory components = _setToken.getComponents(); + uint256 componentsLength = components.length; + + uint256[] memory equityUnits = new uint256[](componentsLength); + uint256[] memory debtUnits = new uint256[](componentsLength); + + uint256 totalSupply = _setToken.totalSupply(); + + for (uint256 i = 0; i < components.length; i++) { + address component = components[i]; + int256 cumulativeEquity = totalSupply + .getDefaultPositionUnit(IERC20(component).balanceOf(address(_setToken))) + .toInt256(); + int256 cumulativeDebt = 0; + address[] memory externalPositions = _setToken.getExternalPositionModules(component); + + if (externalPositions.length > 0) { + for (uint256 j = 0; j < externalPositions.length; j++) { + int256 externalPositionUnit = _setToken.getExternalPositionRealUnit(component, externalPositions[j]); + + // If positionUnit <= 0 it will be "added" to debt position + if (externalPositionUnit > 0) { + cumulativeEquity = cumulativeEquity.add(externalPositionUnit); + } else { + cumulativeDebt = cumulativeDebt.add(externalPositionUnit); + } + } + } + + equityUnits[i] = cumulativeEquity.toUint256(); + debtUnits[i] = cumulativeDebt.mul(-1).toUint256(); + } + + return (components, equityUnits, debtUnits); + } +} \ No newline at end of file diff --git a/contracts/protocol/modules/IssuanceModule.sol b/contracts/protocol/modules/IssuanceModule.sol index bba2e0f56..8875b5e44 100644 --- a/contracts/protocol/modules/IssuanceModule.sol +++ b/contracts/protocol/modules/IssuanceModule.sol @@ -1,5 +1,5 @@ /* - Copyright 2021 Set Labs Inc. + Copyright 2022 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. @@ -19,372 +19,23 @@ pragma solidity 0.6.10; pragma experimental "ABIEncoderV2"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; - -import { DebtIssuanceModule } from "./DebtIssuanceModule.sol"; +import { DebtIssuanceModuleV2 } from "./DebtIssuanceModuleV2.sol"; import { IController } from "../../interfaces/IController.sol"; -import { Invoke } from "../lib/Invoke.sol"; -import { ISetToken } from "../../interfaces/ISetToken.sol"; -import { IssuanceValidationUtils } from "../lib/IssuanceValidationUtils.sol"; -import { Position } from "../lib/Position.sol"; /** - * @title IssuanceModule (formerly DebtIssuanceModuleV2) + * @title IssuanceModule * @author Set Protocol * * The IssuanceModule is a module that enables users to issue and redeem SetTokens that contain default and all - * external positions, including debt positions. Module hooks are added to allow for syncing of positions, and component - * level hooks are added to ensure positions are replicated correctly. The manager can define arbitrary issuance logic - * in the manager hook, as well as specify issue and redeem fees. - * - * NOTE: - * DebtIssuanceModule contract confirms increase/decrease in balance of component held by the SetToken after every transfer in/out - * for each component during issuance/redemption. This contract replaces those strict checks with slightly looser checks which - * ensure that the SetToken remains collateralized after every transfer in/out for each component during issuance/redemption. - * This module should be used to issue/redeem SetToken whose one or more components return a balance value with +/-1 wei error. - * For example, this module can be used to issue/redeem SetTokens which has one or more aTokens as its components. - * The new checks do NOT apply to any transfers that are part of an external position. A token that has rounding issues may lead to - * reverts if it is included as an external position unless explicitly allowed in a module hook. - * - * The getRequiredComponentIssuanceUnits function on this module assumes that Default token balances will be synced on every issuance - * and redemption. If token balances are not being synced it will over-estimate the amount of tokens required to issue a Set. - * - * Changelog: - * + Renamed DebtIssuanceModuleV2 to IssuanceModule (March 17, 2022) + * external positions, including debt positions. The manager can define arbitrary issuance logic in the manager + * hook, as well as specify issue and redeem fees. The manager can remove the module. */ -contract IssuanceModule is DebtIssuanceModule { - using Position for uint256; +contract IssuanceModule is DebtIssuanceModuleV2 { /* ============ Constructor ============ */ - constructor(IController _controller) public DebtIssuanceModule(_controller) {} - - /* ============ External Functions ============ */ - - /** - * Deposits components to the SetToken, replicates any external module component positions and mints - * the SetToken. If the token has a debt position all collateral will be transferred in first then debt - * will be returned to the minting address. If specified, a fee will be charged on issuance. - * - * NOTE: Overrides DebtIssuanceModule#issue external function and adds undercollateralization checks in place of the - * previous default strict balances checks. The undercollateralization checks are implemented in IssuanceValidationUtils library and they - * revert upon undercollateralization of the SetToken post component transfer. - * - * @param _setToken Instance of the SetToken to issue - * @param _quantity Quantity of SetToken to issue - * @param _to Address to mint SetToken to - */ - function issue( - ISetToken _setToken, - uint256 _quantity, - address _to - ) - external - override - nonReentrant - onlyValidAndInitializedSet(_setToken) - { - require(_quantity > 0, "Issue quantity must be > 0"); - - address hookContract = _callManagerPreIssueHooks(_setToken, _quantity, msg.sender, _to); - - _callModulePreIssueHooks(_setToken, _quantity); - - - uint256 initialSetSupply = _setToken.totalSupply(); - - ( - uint256 quantityWithFees, - uint256 managerFee, - uint256 protocolFee - ) = calculateTotalFees(_setToken, _quantity, true); - - // Prevent stack too deep - { - ( - address[] memory components, - uint256[] memory equityUnits, - uint256[] memory debtUnits - ) = _calculateRequiredComponentIssuanceUnits(_setToken, quantityWithFees, true); - - uint256 finalSetSupply = initialSetSupply.add(quantityWithFees); - - _resolveEquityPositions(_setToken, quantityWithFees, _to, true, components, equityUnits, initialSetSupply, finalSetSupply); - _resolveDebtPositions(_setToken, quantityWithFees, true, components, debtUnits, initialSetSupply, finalSetSupply); - _resolveFees(_setToken, managerFee, protocolFee); - } - - _setToken.mint(_to, _quantity); - - emit SetTokenIssued( - _setToken, - msg.sender, - _to, - hookContract, - _quantity, - managerFee, - protocolFee - ); - } - - /** - * Returns components from the SetToken, unwinds any external module component positions and burns the SetToken. - * If the token has debt positions, the module transfers in the required debt amounts from the caller and uses - * those funds to repay the debts on behalf of the SetToken. All debt will be paid down first then equity positions - * will be returned to the minting address. If specified, a fee will be charged on redeem. - * - * NOTE: Overrides DebtIssuanceModule#redeem internal function and adds undercollateralization checks in place of the - * previous default strict balances checks. The undercollateralization checks are implemented in IssuanceValidationUtils library - * and they revert upon undercollateralization of the SetToken post component transfer. - * - * @param _setToken Instance of the SetToken to redeem - * @param _quantity Quantity of SetToken to redeem - * @param _to Address to send collateral to - */ - function redeem( - ISetToken _setToken, - uint256 _quantity, - address _to - ) - external - override - nonReentrant - onlyValidAndInitializedSet(_setToken) - { - require(_quantity > 0, "Redeem quantity must be > 0"); - - _callModulePreRedeemHooks(_setToken, _quantity); - - uint256 initialSetSupply = _setToken.totalSupply(); - - // Place burn after pre-redeem hooks because burning tokens may lead to false accounting of synced positions - _setToken.burn(msg.sender, _quantity); - - ( - uint256 quantityNetFees, - uint256 managerFee, - uint256 protocolFee - ) = calculateTotalFees(_setToken, _quantity, false); - - // Prevent stack too deep - { - ( - address[] memory components, - uint256[] memory equityUnits, - uint256[] memory debtUnits - ) = _calculateRequiredComponentIssuanceUnits(_setToken, quantityNetFees, false); - - uint256 finalSetSupply = initialSetSupply.sub(quantityNetFees); - - _resolveDebtPositions(_setToken, quantityNetFees, false, components, debtUnits, initialSetSupply, finalSetSupply); - _resolveEquityPositions(_setToken, quantityNetFees, _to, false, components, equityUnits, initialSetSupply, finalSetSupply); - _resolveFees(_setToken, managerFee, protocolFee); - } - - emit SetTokenRedeemed( - _setToken, - msg.sender, - _to, - _quantity, - managerFee, - protocolFee - ); - } - - /* ============ External View Functions ============ */ - /** - * Calculates the amount of each component needed to collateralize passed issue quantity plus fees of Sets as well as amount of debt - * that will be returned to caller. Default equity alues are calculated based on token balances and not position units in order to more - * closely track any accrued tokens that will be synced during issuance. External equity and debt positions will use the stored position - * units. IF TOKEN VALUES ARE NOT BEING SYNCED DURING ISSUANCE THIS FUNCTION WILL OVER ESTIMATE THE AMOUNT OF REQUIRED TOKENS. - * - * @param _setToken Instance of the SetToken to issue - * @param _quantity Amount of Sets to be issued - * - * @return address[] Array of component addresses making up the Set - * @return uint256[] Array of equity notional amounts of each component, respectively, represented as uint256 - * @return uint256[] Array of debt notional amounts of each component, respectively, represented as uint256 + * Set state controller state variable */ - function getRequiredComponentIssuanceUnits( - ISetToken _setToken, - uint256 _quantity - ) - external - view - override - returns (address[] memory, uint256[] memory, uint256[] memory) - { - ( - uint256 totalQuantity,, - ) = calculateTotalFees(_setToken, _quantity, true); - - if(_setToken.totalSupply() == 0) { - return _calculateRequiredComponentIssuanceUnits(_setToken, totalQuantity, true); - } else { - ( - address[] memory components, - uint256[] memory equityUnits, - uint256[] memory debtUnits - ) = _getTotalIssuanceUnitsFromBalances(_setToken); - - uint256 componentsLength = components.length; - uint256[] memory totalEquityUnits = new uint256[](componentsLength); - uint256[] memory totalDebtUnits = new uint256[](componentsLength); - for (uint256 i = 0; i < components.length; i++) { - // Use preciseMulCeil to round up to ensure overcollateration of equity when small issue quantities are provided - // and use preciseMul to round debt calculations down to make sure we don't return too much debt to issuer - totalEquityUnits[i] = equityUnits[i].preciseMulCeil(totalQuantity); - totalDebtUnits[i] = debtUnits[i].preciseMul(totalQuantity); - } - - return (components, totalEquityUnits, totalDebtUnits); - } - } - - /* ============ Internal Functions ============ */ - - /** - * Resolve equity positions associated with SetToken. On issuance, the total equity position for an asset (including default and external - * positions) is transferred in. Then any external position hooks are called to transfer the external positions to their necessary place. - * On redemption all external positions are recalled by the external position hook, then those position plus any default position are - * transferred back to the _to address. - */ - function _resolveEquityPositions( - ISetToken _setToken, - uint256 _quantity, - address _to, - bool _isIssue, - address[] memory _components, - uint256[] memory _componentEquityQuantities, - uint256 _initialSetSupply, - uint256 _finalSetSupply - ) - internal - { - for (uint256 i = 0; i < _components.length; i++) { - address component = _components[i]; - uint256 componentQuantity = _componentEquityQuantities[i]; - if (componentQuantity > 0) { - if (_isIssue) { - // Call SafeERC20#safeTransferFrom instead of ExplicitERC20#transferFrom - SafeERC20.safeTransferFrom( - IERC20(component), - msg.sender, - address(_setToken), - componentQuantity - ); - - IssuanceValidationUtils.validateCollateralizationPostTransferInPreHook(_setToken, component, _initialSetSupply, componentQuantity); - - _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), true, true); - } else { - _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), false, true); - - // Call Invoke#invokeTransfer instead of Invoke#strictInvokeTransfer - _setToken.invokeTransfer(component, _to, componentQuantity); - - IssuanceValidationUtils.validateCollateralizationPostTransferOut(_setToken, component, _finalSetSupply); - } - } - } - } - - /** - * Resolve debt positions associated with SetToken. On issuance, debt positions are entered into by calling the external position hook. The - * resulting debt is then returned to the calling address. On redemption, the module transfers in the required debt amount from the caller - * and uses those funds to repay the debt on behalf of the SetToken. - */ - function _resolveDebtPositions( - ISetToken _setToken, - uint256 _quantity, - bool _isIssue, - address[] memory _components, - uint256[] memory _componentDebtQuantities, - uint256 _initialSetSupply, - uint256 _finalSetSupply - ) - internal - { - for (uint256 i = 0; i < _components.length; i++) { - address component = _components[i]; - uint256 componentQuantity = _componentDebtQuantities[i]; - if (componentQuantity > 0) { - if (_isIssue) { - _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), true, false); - - // Call Invoke#invokeTransfer instead of Invoke#strictInvokeTransfer - _setToken.invokeTransfer(component, msg.sender, componentQuantity); - - IssuanceValidationUtils.validateCollateralizationPostTransferOut(_setToken, component, _finalSetSupply); - } else { - // Call SafeERC20#safeTransferFrom instead of ExplicitERC20#transferFrom - SafeERC20.safeTransferFrom( - IERC20(component), - msg.sender, - address(_setToken), - componentQuantity - ); - - IssuanceValidationUtils.validateCollateralizationPostTransferInPreHook(_setToken, component, _initialSetSupply, componentQuantity); - - _executeExternalPositionHooks(_setToken, _quantity, IERC20(component), false, false); - } - } - } - } - /** - * Reimplementation of _getTotalIssuanceUnits but instead derives Default equity positions from token balances on Set instead of from - * position units. This function is ONLY to be used in getRequiredComponentIssuanceUnits in order to return more accurate required - * token amounts to issuers when positions are being synced on issuance. - * - * @param _setToken Instance of the SetToken to issue - * - * @return address[] Array of component addresses making up the Set - * @return uint256[] Array of equity unit amounts of each component, respectively, represented as uint256 - * @return uint256[] Array of debt unit amounts of each component, respectively, represented as uint256 - */ - function _getTotalIssuanceUnitsFromBalances( - ISetToken _setToken - ) - internal - view - returns (address[] memory, uint256[] memory, uint256[] memory) - { - address[] memory components = _setToken.getComponents(); - uint256 componentsLength = components.length; - - uint256[] memory equityUnits = new uint256[](componentsLength); - uint256[] memory debtUnits = new uint256[](componentsLength); - - uint256 totalSupply = _setToken.totalSupply(); - - for (uint256 i = 0; i < components.length; i++) { - address component = components[i]; - int256 cumulativeEquity = totalSupply - .getDefaultPositionUnit(IERC20(component).balanceOf(address(_setToken))) - .toInt256(); - int256 cumulativeDebt = 0; - address[] memory externalPositions = _setToken.getExternalPositionModules(component); - - if (externalPositions.length > 0) { - for (uint256 j = 0; j < externalPositions.length; j++) { - int256 externalPositionUnit = _setToken.getExternalPositionRealUnit(component, externalPositions[j]); - - // If positionUnit <= 0 it will be "added" to debt position - if (externalPositionUnit > 0) { - cumulativeEquity = cumulativeEquity.add(externalPositionUnit); - } else { - cumulativeDebt = cumulativeDebt.add(externalPositionUnit); - } - } - } - - equityUnits[i] = cumulativeEquity.toUint256(); - debtUnits[i] = cumulativeDebt.mul(-1).toUint256(); - } - - return (components, equityUnits, debtUnits); - } -} \ No newline at end of file + constructor(IController _controller) public DebtIssuanceModuleV2(_controller) {} +} diff --git a/test/integration/aaveUniswapLeverageDebtIssuance.spec.ts b/test/integration/aaveUniswapLeverageDebtIssuance.spec.ts index d45d28a5c..01d150502 100644 --- a/test/integration/aaveUniswapLeverageDebtIssuance.spec.ts +++ b/test/integration/aaveUniswapLeverageDebtIssuance.spec.ts @@ -5,7 +5,7 @@ import { Account } from "@utils/test/types"; import { AaveV2, AaveLeverageModule, - IssuanceModule, + DebtIssuanceModuleV2, SetToken, UniswapV2ExchangeAdapter, } from "@utils/contracts"; @@ -68,7 +68,7 @@ describe("AaveUniswapLeverageDebtIssuance", () => { let aaveV2Library: AaveV2; let aaveLeverageModule: AaveLeverageModule; - let debtIssuanceModule: IssuanceModule; + let debtIssuanceModule: DebtIssuanceModuleV2; let uniswapExchangeAdapter: UniswapV2ExchangeAdapter; let aWBTC: AaveV2AToken; @@ -176,7 +176,7 @@ describe("AaveUniswapLeverageDebtIssuance", () => { MAX_UINT_256 ); - debtIssuanceModule = await deployer.modules.deployIssuanceModule(setup.controller.address); + debtIssuanceModule = await deployer.modules.deployDebtIssuanceModuleV2(setup.controller.address); await setup.controller.addModule(debtIssuanceModule.address); aaveV2Library = await deployer.libraries.deployAaveV2(); diff --git a/test/protocol/modules/debtIssuanceModuleV2.spec.ts b/test/protocol/modules/debtIssuanceModuleV2.spec.ts new file mode 100644 index 000000000..bd70f6fed --- /dev/null +++ b/test/protocol/modules/debtIssuanceModuleV2.spec.ts @@ -0,0 +1,1039 @@ +import "module-alias/register"; + +import { BigNumber } from "ethers"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ZERO, ONE, ADDRESS_ZERO } from "@utils/constants"; +import { + DebtIssuanceModuleV2, + DebtModuleMock, + ManagerIssuanceHookMock, + ModuleIssuanceHookMock, + SetToken, + StandardTokenWithRoundingErrorMock +} from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + ether, + preciseDiv, + preciseMul, + preciseMulCeil, +} from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getSystemFixture, + getWaffleExpect, +} from "@utils/test/index"; +import { SystemFixture } from "@utils/fixtures"; +import { ContractTransaction } from "ethers"; + +const expect = getWaffleExpect(); + +describe("DebtIssuanceModuleV2", () => { + let owner: Account; + let manager: Account; + let feeRecipient: Account; + let dummyModule: Account; + let recipient: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + + let debtModule: DebtModuleMock; + let externalPositionModule: ModuleIssuanceHookMock; + let debtIssuance: DebtIssuanceModuleV2; + let issuanceHook: ManagerIssuanceHookMock; + let setToken: SetToken; + let tokenWithRoundingError: StandardTokenWithRoundingErrorMock; + + before(async () => { + [ + owner, + manager, + feeRecipient, + dummyModule, // Set as protocol fee recipient + recipient, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + + await setup.initialize(); + + tokenWithRoundingError = await deployer.mocks.deployTokenWithErrorMock(owner.address, ether(1000000), ZERO); + debtIssuance = await deployer.modules.deployDebtIssuanceModuleV2(setup.controller.address); + debtModule = await deployer.mocks.deployDebtModuleMock(setup.controller.address); + externalPositionModule = await deployer.mocks.deployModuleIssuanceHookMock(); + issuanceHook = await deployer.mocks.deployManagerIssuanceHookMock(); + + await setup.controller.addModule(debtIssuance.address); + await setup.controller.addModule(debtModule.address); + await setup.controller.addModule(externalPositionModule.address); + + setToken = await setup.createSetToken( + [tokenWithRoundingError.address], + [ether(1)], + [setup.issuanceModule.address, debtIssuance.address, debtModule.address, externalPositionModule.address], + manager.address, + "DebtToken", + "DBT" + ); + + await externalPositionModule.initialize(setToken.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + context("when DebtIssuanceModuleV2 is initialized", async () => { + let preIssueHook: Address; + let maxFee: BigNumber; + let issueFee: BigNumber; + let redeemFee: BigNumber; + + before(async () => { + await tokenWithRoundingError.setError(ZERO); + + preIssueHook = ADDRESS_ZERO; + maxFee = ether(0.02); + issueFee = ether(0.005); + redeemFee = ether(0.005); + }); + + beforeEach(async () => { + await debtIssuance.connect(manager.wallet).initialize( + setToken.address, + maxFee, + issueFee, + redeemFee, + feeRecipient.address, + preIssueHook + ); + await debtModule.connect(manager.wallet).initialize(setToken.address, debtIssuance.address); + }); + + + context("when SetToken components do not have any rounding error", async () => { + // Note: Tests below are an EXACT copy of the tests for DebtIssuanceModule. Only difference is this SetToken contains + // tokenWithRoundingError instead of weth as a default position. This is to ensure the DebtIssuanceModuleV2 behaves + // exactly similar to DebtIssuanceModule when there is no rounding error present in it's constituent components. + + describe("#issue", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectCaller: Account; + + const debtUnits: BigNumber = ether(100); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); + + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + subjectTo = recipient.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return debtIssuance.connect(subjectCaller.wallet).issue( + subjectSetToken, + subjectQuantity, + subjectTo, + ); + } + + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + const managerBalance = await setToken.balanceOf(feeRecipient.address); + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + expect(managerBalance).to.eq(feeQuantity); + }); + + it("should have the correct token balances", async () => { + const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1)); + + const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethFlows)); + expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethFlows)); + expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); + }); + + it("should have called the module issue hook", async () => { + await subject(); + + const hookCalled = await debtModule.moduleIssueHookCalled(); + + expect(hookCalled).to.be.true; + }); + + it("should emit the correct SetTokenIssued event", async () => { + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + + await expect(subject()).to.emit(debtIssuance, "SetTokenIssued").withArgs( + setToken.address, + subjectCaller.address, + subjectTo, + preIssueHook, + subjectQuantity, + feeQuantity, + ZERO + ); + }); + + describe("when an external equity position is in place", async () => { + const externalUnits: BigNumber = ether(1); + + before(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); + }); + + after(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, ZERO); + }); + + it("should have the correct token balances", async () => { + const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); + const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethDefaultFlows = preciseMul(mintQuantity, ether(1)); + const wethExternalFlows = preciseMul(mintQuantity, externalUnits); + + const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); + const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethDefaultFlows.add(wethExternalFlows))); + expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethDefaultFlows)); + expect(postExternalWethBalance).to.eq(preExternalWethBalance.add(wethExternalFlows)); + expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); + }); + }); + + describe("when the manager issuance fee is 0", async () => { + before(async () => { + issueFee = ZERO; + }); + + after(async () => { + issueFee = ether(0.005); + }); + + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + }); + + it("should have the correct token balances", async () => { + const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethDefaultFlows = preciseMul(mintQuantity, ether(1)); + + const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethDefaultFlows)); + expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethDefaultFlows)); + expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); + }); + }); + + describe("when protocol fees are enabled", async () => { + const protocolFee: BigNumber = ether(.2); + + beforeEach(async () => { + await setup.controller.addFee(debtIssuance.address, ZERO, protocolFee); + }); + + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + const protocolSplit = preciseMul(feeQuantity, protocolFee); + + const managerBalance = await setToken.balanceOf(feeRecipient.address); + const protocolBalance = await setToken.balanceOf(dummyModule.address); // DummyModule is set as address in fixture setup + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + expect(managerBalance).to.eq(feeQuantity.sub(protocolSplit)); + expect(protocolBalance).to.eq(protocolSplit); + }); + }); + + describe("when manager issuance hook is defined", async () => { + before(async () => { + preIssueHook = issuanceHook.address; + }); + + after(async () => { + preIssueHook = ADDRESS_ZERO; + }); + + it("should call the issuance hook", async () => { + await subject(); + + const setToken = await issuanceHook.retrievedSetToken(); + + expect(setToken).to.eq(subjectSetToken); + }); + }); + + describe("when the issue quantity is 0", async () => { + beforeEach(async () => { + subjectQuantity = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Issue quantity must be > 0"); + }); + }); + + describe("when the SetToken is not enabled on the controller", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [tokenWithRoundingError.address], + [ether(1)], + [debtIssuance.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#redeem", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectCaller: Account; + + const debtUnits: BigNumber = ether(100); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + const [, equityFlows ] = await debtIssuance.getRequiredComponentRedemptionUnits(setToken.address, ether(1)); + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); + + await debtIssuance.issue(setToken.address, ether(1), owner.address); + + await setup.dai.approve(debtIssuance.address, ether(100.5)); + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + subjectTo = recipient.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return debtIssuance.connect(subjectCaller.wallet).redeem( + subjectSetToken, + subjectQuantity, + subjectTo, + ); + } + + it("should mint SetTokens to the correct addresses", async () => { + const preManagerBalance = await setToken.balanceOf(feeRecipient.address); + const preCallerBalance = await setToken.balanceOf(subjectCaller.address); + + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); + const postManagerBalance = await setToken.balanceOf(feeRecipient.address); + const postCallerBalance = await setToken.balanceOf(subjectCaller.address); + + expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); + expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); + }); + + it("should have the correct token balances", async () => { + const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); + const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); + const wethFlows = preciseMul(redeemQuantity, ether(1)); + + const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postToWethBalance).to.eq(preToWethBalance.add(wethFlows)); + expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethFlows)); + expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); + }); + + it("should have called the module issue hook", async () => { + await subject(); + + const hookCalled = await debtModule.moduleRedeemHookCalled(); + + expect(hookCalled).to.be.true; + }); + + it("should emit the correct SetTokenRedeemed event", async () => { + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + + await expect(subject()).to.emit(debtIssuance, "SetTokenRedeemed").withArgs( + setToken.address, + subjectCaller.address, + subjectTo, + subjectQuantity, + feeQuantity, + ZERO + ); + }); + + describe("when an external equity position is in place", async () => { + const externalUnits: BigNumber = ether(1); + + before(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); + }); + + after(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, ZERO); + }); + + it("should have the correct token balances", async () => { + const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); + const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); + const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); + const wethExternalFlows = preciseMul(redeemQuantity, externalUnits); + const wethDefaultFlows = preciseMul(redeemQuantity, ether(1)); + + const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); + const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postToWethBalance).to.eq(preToWethBalance.add(wethExternalFlows.add(wethDefaultFlows))); + expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethDefaultFlows)); + expect(postExternalWethBalance).to.eq(preExternalWethBalance.sub(wethExternalFlows)); + expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); + }); + }); + + describe("when the manager redemption fee is 0", async () => { + before(async () => { + redeemFee = ZERO; + }); + + after(async () => { + redeemFee = ether(0.005); + }); + + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(ZERO); + }); + + it("should have the correct token balances", async () => { + const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + await subject(); + + const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); + const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); + const wethFlows = preciseMul(redeemQuantity, ether(1)); + + const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); + const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); + const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); + const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); + const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); + + expect(postToWethBalance).to.eq(preToWethBalance.add(wethFlows)); + expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethFlows)); + expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); + expect(postSetDaiBalance).to.eq(preSetDaiBalance); + expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); + }); + }); + + describe("when protocol fees are enabled", async () => { + const protocolFee: BigNumber = ether(.2); + + beforeEach(async () => { + await setup.controller.addFee(debtIssuance.address, ZERO, protocolFee); + }); + + it("should mint SetTokens to the correct addresses", async () => { + const preManagerBalance = await setToken.balanceOf(feeRecipient.address); + const preProtocolBalance = await setToken.balanceOf(dummyModule.address); + const preCallerBalance = await setToken.balanceOf(subjectCaller.address); + + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); + const protocolSplit = preciseMul(feeQuantity, protocolFee); + + const postManagerBalance = await setToken.balanceOf(feeRecipient.address); + const postProtocolBalance = await setToken.balanceOf(dummyModule.address); // DummyModule is set as address in fixture setup + const postCallerBalance = await setToken.balanceOf(subjectCaller.address); + + expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); + expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity.sub(protocolSplit))); + expect(postProtocolBalance).to.eq(preProtocolBalance.add(protocolSplit)); + }); + }); + + describe("when the issue quantity is 0", async () => { + beforeEach(async () => { + subjectQuantity = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Redeem quantity must be > 0"); + }); + }); + + describe("when the SetToken is not enabled on the controller", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [tokenWithRoundingError.address], + [ether(1)], + [debtIssuance.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#getRequiredComponentIssuanceUnits", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + + const debtUnits: BigNumber = ether(100); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + + await debtIssuance.issue(subjectSetToken, subjectQuantity, owner.address); + }); + + async function subject(): Promise { + return debtIssuance.getRequiredComponentIssuanceUnits( + subjectSetToken, + subjectQuantity + ); + } + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil( mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1)); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + + describe("when an additive external equity position is in place", async () => { + const externalUnits: BigNumber = ether(1); + + beforeEach(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil( mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1).add(externalUnits)); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + }); + + describe("when a non-additive external equity position is in place", async () => { + const externalUnits: BigNumber = ether(50); + + beforeEach(async () => { + await externalPositionModule.addExternalPosition(setToken.address, setup.dai.address, externalUnits); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiDebtFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1)); + const daiEquityFlows = preciseMul(mintQuantity, externalUnits); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, daiEquityFlows]; + const expectedDebtFlows = [ZERO, daiDebtFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + }); + }); + }); + + context("when SetToken components do have rounding errors", async () => { + describe("#issue", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectCaller: Account; + + const debtUnits: BigNumber = ether(100); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); + // send exact amount + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0]); + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + subjectTo = recipient.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return debtIssuance.connect(subjectCaller.wallet).issue( + subjectSetToken, + subjectQuantity, + subjectTo, + ); + } + + describe("when rounding error is negative one", async () => { + beforeEach(async () => { + await tokenWithRoundingError.setError(BigNumber.from(-1)); + }); + + describe("when set is exactly collateralized", async () => { + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Invalid transfer in. Results in undercollateralization" + ); + }); + }); + + describe("when set is over-collateralized by at least 1 wei", async () => { + beforeEach(async () => { + await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); + }); + + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + const managerBalance = await setToken.balanceOf(feeRecipient.address); + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + expect(managerBalance).to.eq(feeQuantity); + }); + }); + }); + + describe("when rounding error is positive one", async () => { + beforeEach(async () => { + await tokenWithRoundingError.setError(ONE); + }); + + describe("when set is exactly collateralized", async () => { + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + const managerBalance = await setToken.balanceOf(feeRecipient.address); + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + expect(managerBalance).to.eq(feeQuantity); + }); + }); + + describe("when set is over-collateralized by at least 1 wei", async () => { + beforeEach(async () => { + await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); + }); + it("should mint SetTokens to the correct addresses", async () => { + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); + const managerBalance = await setToken.balanceOf(feeRecipient.address); + const toBalance = await setToken.balanceOf(subjectTo); + + expect(toBalance).to.eq(subjectQuantity); + expect(managerBalance).to.eq(feeQuantity); + }); + }); + }); + }); + + describe("#redeem", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + let subjectTo: Address; + let subjectCaller: Account; + + const debtUnits: BigNumber = ether(100); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); + // Send exact amount + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0]); + + await debtIssuance.issue(setToken.address, ether(1), owner.address); + + await setup.dai.approve(debtIssuance.address, ether(100.5)); + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + subjectTo = recipient.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return debtIssuance.connect(subjectCaller.wallet).redeem( + subjectSetToken, + subjectQuantity, + subjectTo, + ); + } + + describe("when rounding error is negative one", async () => { + beforeEach(async () => { + await tokenWithRoundingError.setError(BigNumber.from(-1)); + }); + + describe("when set is exactly collateralized", async () => { + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Invalid transfer out. Results in undercollateralization" + ); + }); + }); + + describe("when set is over-collateralized by at least 1 wei", async () => { + beforeEach(async () => { + await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); + }); + it("should mint SetTokens to the correct addresses", async () => { + const preManagerBalance = await setToken.balanceOf(feeRecipient.address); + const preCallerBalance = await setToken.balanceOf(subjectCaller.address); + + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); + const postManagerBalance = await setToken.balanceOf(feeRecipient.address); + const postCallerBalance = await setToken.balanceOf(subjectCaller.address); + + expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); + expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); + }); + }); + }); + + describe("when rounding error is positive one", async () => { + beforeEach(async () => { + await tokenWithRoundingError.setError(ONE); + }); + + describe("when set is exactly collateralized", async () => { + it("should mint SetTokens to the correct addresses", async () => { + const preManagerBalance = await setToken.balanceOf(feeRecipient.address); + const preCallerBalance = await setToken.balanceOf(subjectCaller.address); + + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); + const postManagerBalance = await setToken.balanceOf(feeRecipient.address); + const postCallerBalance = await setToken.balanceOf(subjectCaller.address); + + expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); + expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); + }); + }); + + describe("when set is over-collateralized by at least 1 wei", async () => { + beforeEach(async () => { + await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); + }); + + it("should mint SetTokens to the correct addresses", async () => { + const preManagerBalance = await setToken.balanceOf(feeRecipient.address); + const preCallerBalance = await setToken.balanceOf(subjectCaller.address); + + await subject(); + + const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); + const postManagerBalance = await setToken.balanceOf(feeRecipient.address); + const postCallerBalance = await setToken.balanceOf(subjectCaller.address); + + expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); + expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); + }); + }); + }); + }); + + describe("#getRequiredComponentIssuanceUnits", async () => { + let subjectSetToken: Address; + let subjectQuantity: BigNumber; + + const debtUnits: BigNumber = ether(100); + const accruedBalance = ether(.00001); + + beforeEach(async () => { + await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); + await setup.dai.transfer(debtModule.address, ether(100.5)); + + subjectSetToken = setToken.address; + subjectQuantity = ether(1); + + await tokenWithRoundingError.setError(accruedBalance); + }); + + async function subject(): Promise { + return debtIssuance.getRequiredComponentIssuanceUnits( + subjectSetToken, + subjectQuantity + ); + } + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1)); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + + describe("when an additive external equity position is in place", async () => { + const externalUnits: BigNumber = ether(1); + + beforeEach(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil( mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1).add(externalUnits)); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + }); + + describe("when a non-additive external equity position is in place", async () => { + const externalUnits: BigNumber = ether(50); + + beforeEach(async () => { + await externalPositionModule.addExternalPosition(setToken.address, setup.dai.address, externalUnits); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiDebtFlows = preciseMulCeil(mintQuantity, debtUnits); + const wethFlows = preciseMul(mintQuantity, ether(1)); + const daiEquityFlows = preciseMul(mintQuantity, externalUnits); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, daiEquityFlows]; + const expectedDebtFlows = [ZERO, daiDebtFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + }); + + describe("when tokens have been issued", async () => { + beforeEach(async () => { + const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); + await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); + + await debtIssuance.issue(subjectSetToken, subjectQuantity, owner.address); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMul(mintQuantity, debtUnits); + const wethFlows = preciseMulCeil(mintQuantity, preciseDiv(ether(1.005).add(accruedBalance), ether(1.005))); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + + describe("when an additive external equity position is in place", async () => { + const externalUnits: BigNumber = ether(1); + + beforeEach(async () => { + await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); + }); + + it("should return the correct issue token amounts", async () => { + const [components, equityFlows, debtFlows] = await subject(); + + const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); + const daiFlows = preciseMulCeil( mintQuantity, debtUnits); + const wethFlows = preciseMulCeil(mintQuantity, preciseDiv(ether(1.005).add(accruedBalance), ether(1.005)).add(externalUnits)); + + const expectedComponents = await setToken.getComponents(); + const expectedEquityFlows = [wethFlows, ZERO]; + const expectedDebtFlows = [ZERO, daiFlows]; + + expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); + expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); + expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/protocol/modules/issuanceModule.spec.ts b/test/protocol/modules/issuanceModule.spec.ts index f727a6ea7..69704316e 100644 --- a/test/protocol/modules/issuanceModule.spec.ts +++ b/test/protocol/modules/issuanceModule.spec.ts @@ -1,1039 +1,46 @@ import "module-alias/register"; -import { BigNumber } from "ethers"; - -import { Address } from "@utils/types"; import { Account } from "@utils/test/types"; -import { ZERO, ONE, ADDRESS_ZERO } from "@utils/constants"; -import { - IssuanceModule, - DebtModuleMock, - ManagerIssuanceHookMock, - ModuleIssuanceHookMock, - SetToken, - StandardTokenWithRoundingErrorMock -} from "@utils/contracts"; +import { IssuanceModule } from "@utils/contracts"; import DeployHelper from "@utils/deploys"; -import { - ether, - preciseDiv, - preciseMul, - preciseMulCeil, -} from "@utils/index"; import { addSnapshotBeforeRestoreAfterEach, getAccounts, - getSystemFixture, getWaffleExpect, + getSystemFixture, } from "@utils/test/index"; import { SystemFixture } from "@utils/fixtures"; -import { ContractTransaction } from "ethers"; const expect = getWaffleExpect(); describe("IssuanceModule", () => { let owner: Account; - let manager: Account; - let feeRecipient: Account; - let dummyModule: Account; - let recipient: Account; let deployer: DeployHelper; let setup: SystemFixture; - let debtModule: DebtModuleMock; - let externalPositionModule: ModuleIssuanceHookMock; - let debtIssuance: IssuanceModule; - let issuanceHook: ManagerIssuanceHookMock; - let setToken: SetToken; - let tokenWithRoundingError: StandardTokenWithRoundingErrorMock; - before(async () => { [ owner, - manager, - feeRecipient, - dummyModule, // Set as protocol fee recipient - recipient, ] = await getAccounts(); deployer = new DeployHelper(owner.wallet); setup = getSystemFixture(owner.address); - await setup.initialize(); - - tokenWithRoundingError = await deployer.mocks.deployTokenWithErrorMock(owner.address, ether(1000000), ZERO); - debtIssuance = await deployer.modules.deployIssuanceModule(setup.controller.address); - debtModule = await deployer.mocks.deployDebtModuleMock(setup.controller.address); - externalPositionModule = await deployer.mocks.deployModuleIssuanceHookMock(); - issuanceHook = await deployer.mocks.deployManagerIssuanceHookMock(); - - await setup.controller.addModule(debtIssuance.address); - await setup.controller.addModule(debtModule.address); - await setup.controller.addModule(externalPositionModule.address); - - setToken = await setup.createSetToken( - [tokenWithRoundingError.address], - [ether(1)], - [setup.issuanceModule.address, debtIssuance.address, debtModule.address, externalPositionModule.address], - manager.address, - "DebtToken", - "DBT" - ); - - await externalPositionModule.initialize(setToken.address); }); addSnapshotBeforeRestoreAfterEach(); - context("when IssuanceModule is initialized", async () => { - let preIssueHook: Address; - let maxFee: BigNumber; - let issueFee: BigNumber; - let redeemFee: BigNumber; - - before(async () => { - await tokenWithRoundingError.setError(ZERO); - - preIssueHook = ADDRESS_ZERO; - maxFee = ether(0.02); - issueFee = ether(0.005); - redeemFee = ether(0.005); - }); - - beforeEach(async () => { - await debtIssuance.connect(manager.wallet).initialize( - setToken.address, - maxFee, - issueFee, - redeemFee, - feeRecipient.address, - preIssueHook - ); - await debtModule.connect(manager.wallet).initialize(setToken.address, debtIssuance.address); - }); - - - context("when SetToken components do not have any rounding error", async () => { - // Note: Tests below are an EXACT copy of the tests for DebtIssuanceModule. Only difference is this SetToken contains - // tokenWithRoundingError instead of weth as a default position. This is to ensure the IssuanceModule behaves - // exactly similar to DebtIssuanceModule when there is no rounding error present in it's constituent components. - - describe("#issue", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - let subjectTo: Address; - let subjectCaller: Account; - - const debtUnits: BigNumber = ether(100); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); - - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - subjectTo = recipient.address; - subjectCaller = owner; - }); - - async function subject(): Promise { - return debtIssuance.connect(subjectCaller.wallet).issue( - subjectSetToken, - subjectQuantity, - subjectTo, - ); - } - - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - const managerBalance = await setToken.balanceOf(feeRecipient.address); - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - expect(managerBalance).to.eq(feeQuantity); - }); - - it("should have the correct token balances", async () => { - const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1)); - - const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethFlows)); - expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethFlows)); - expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); - }); - - it("should have called the module issue hook", async () => { - await subject(); - - const hookCalled = await debtModule.moduleIssueHookCalled(); - - expect(hookCalled).to.be.true; - }); - - it("should emit the correct SetTokenIssued event", async () => { - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - - await expect(subject()).to.emit(debtIssuance, "SetTokenIssued").withArgs( - setToken.address, - subjectCaller.address, - subjectTo, - preIssueHook, - subjectQuantity, - feeQuantity, - ZERO - ); - }); - - describe("when an external equity position is in place", async () => { - const externalUnits: BigNumber = ether(1); - - before(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); - }); - - after(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, ZERO); - }); - - it("should have the correct token balances", async () => { - const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); - const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethDefaultFlows = preciseMul(mintQuantity, ether(1)); - const wethExternalFlows = preciseMul(mintQuantity, externalUnits); - - const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); - const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethDefaultFlows.add(wethExternalFlows))); - expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethDefaultFlows)); - expect(postExternalWethBalance).to.eq(preExternalWethBalance.add(wethExternalFlows)); - expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); - }); - }); - - describe("when the manager issuance fee is 0", async () => { - before(async () => { - issueFee = ZERO; - }); - - after(async () => { - issueFee = ether(0.005); - }); - - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - }); - - it("should have the correct token balances", async () => { - const preMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethDefaultFlows = preciseMul(mintQuantity, ether(1)); - - const postMinterWethBalance = await tokenWithRoundingError.balanceOf(subjectCaller.address); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postMinterDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postMinterWethBalance).to.eq(preMinterWethBalance.sub(wethDefaultFlows)); - expect(postSetWethBalance).to.eq(preSetWethBalance.add(wethDefaultFlows)); - expect(postMinterDaiBalance).to.eq(preMinterDaiBalance.add(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.sub(daiFlows)); - }); - }); - - describe("when protocol fees are enabled", async () => { - const protocolFee: BigNumber = ether(.2); - - beforeEach(async () => { - await setup.controller.addFee(debtIssuance.address, ZERO, protocolFee); - }); - - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - const protocolSplit = preciseMul(feeQuantity, protocolFee); - - const managerBalance = await setToken.balanceOf(feeRecipient.address); - const protocolBalance = await setToken.balanceOf(dummyModule.address); // DummyModule is set as address in fixture setup - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - expect(managerBalance).to.eq(feeQuantity.sub(protocolSplit)); - expect(protocolBalance).to.eq(protocolSplit); - }); - }); - - describe("when manager issuance hook is defined", async () => { - before(async () => { - preIssueHook = issuanceHook.address; - }); - - after(async () => { - preIssueHook = ADDRESS_ZERO; - }); - - it("should call the issuance hook", async () => { - await subject(); - - const setToken = await issuanceHook.retrievedSetToken(); - - expect(setToken).to.eq(subjectSetToken); - }); - }); - - describe("when the issue quantity is 0", async () => { - beforeEach(async () => { - subjectQuantity = ZERO; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Issue quantity must be > 0"); - }); - }); - - describe("when the SetToken is not enabled on the controller", async () => { - beforeEach(async () => { - const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( - [tokenWithRoundingError.address], - [ether(1)], - [debtIssuance.address] - ); - - subjectSetToken = nonEnabledSetToken.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); - }); - }); - }); - - describe("#redeem", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - let subjectTo: Address; - let subjectCaller: Account; - - const debtUnits: BigNumber = ether(100); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - const [, equityFlows ] = await debtIssuance.getRequiredComponentRedemptionUnits(setToken.address, ether(1)); - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); - - await debtIssuance.issue(setToken.address, ether(1), owner.address); - - await setup.dai.approve(debtIssuance.address, ether(100.5)); - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - subjectTo = recipient.address; - subjectCaller = owner; - }); - - async function subject(): Promise { - return debtIssuance.connect(subjectCaller.wallet).redeem( - subjectSetToken, - subjectQuantity, - subjectTo, - ); - } - - it("should mint SetTokens to the correct addresses", async () => { - const preManagerBalance = await setToken.balanceOf(feeRecipient.address); - const preCallerBalance = await setToken.balanceOf(subjectCaller.address); - - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); - const postManagerBalance = await setToken.balanceOf(feeRecipient.address); - const postCallerBalance = await setToken.balanceOf(subjectCaller.address); - - expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); - expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); - }); - - it("should have the correct token balances", async () => { - const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); - const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); - const wethFlows = preciseMul(redeemQuantity, ether(1)); - - const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postToWethBalance).to.eq(preToWethBalance.add(wethFlows)); - expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethFlows)); - expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); - }); - - it("should have called the module issue hook", async () => { - await subject(); - - const hookCalled = await debtModule.moduleRedeemHookCalled(); - - expect(hookCalled).to.be.true; - }); - - it("should emit the correct SetTokenRedeemed event", async () => { - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - - await expect(subject()).to.emit(debtIssuance, "SetTokenRedeemed").withArgs( - setToken.address, - subjectCaller.address, - subjectTo, - subjectQuantity, - feeQuantity, - ZERO - ); - }); - - describe("when an external equity position is in place", async () => { - const externalUnits: BigNumber = ether(1); - - before(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); - }); - - after(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, ZERO); - }); - - it("should have the correct token balances", async () => { - const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); - const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); - const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); - const wethExternalFlows = preciseMul(redeemQuantity, externalUnits); - const wethDefaultFlows = preciseMul(redeemQuantity, ether(1)); - - const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postExternalWethBalance = await tokenWithRoundingError.balanceOf(externalPositionModule.address); - const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postToWethBalance).to.eq(preToWethBalance.add(wethExternalFlows.add(wethDefaultFlows))); - expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethDefaultFlows)); - expect(postExternalWethBalance).to.eq(preExternalWethBalance.sub(wethExternalFlows)); - expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); - }); - }); - - describe("when the manager redemption fee is 0", async () => { - before(async () => { - redeemFee = ZERO; - }); - - after(async () => { - redeemFee = ether(0.005); - }); - - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(ZERO); - }); - - it("should have the correct token balances", async () => { - const preToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const preSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const preRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const preSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const preExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - await subject(); - - const redeemQuantity = preciseMul(subjectQuantity, ether(1).sub(redeemFee)); - const daiFlows = preciseMulCeil(redeemQuantity, debtUnits); - const wethFlows = preciseMul(redeemQuantity, ether(1)); - - const postToWethBalance = await tokenWithRoundingError.balanceOf(subjectTo); - const postSetWethBalance = await tokenWithRoundingError.balanceOf(subjectSetToken); - const postRedeemerDaiBalance = await setup.dai.balanceOf(subjectCaller.address); - const postSetDaiBalance = await setup.dai.balanceOf(subjectSetToken); - const postExternalDaiBalance = await setup.dai.balanceOf(debtModule.address); - - expect(postToWethBalance).to.eq(preToWethBalance.add(wethFlows)); - expect(postSetWethBalance).to.eq(preSetWethBalance.sub(wethFlows)); - expect(postRedeemerDaiBalance).to.eq(preRedeemerDaiBalance.sub(daiFlows)); - expect(postSetDaiBalance).to.eq(preSetDaiBalance); - expect(postExternalDaiBalance).to.eq(preExternalDaiBalance.add(daiFlows)); - }); - }); - - describe("when protocol fees are enabled", async () => { - const protocolFee: BigNumber = ether(.2); - - beforeEach(async () => { - await setup.controller.addFee(debtIssuance.address, ZERO, protocolFee); - }); - - it("should mint SetTokens to the correct addresses", async () => { - const preManagerBalance = await setToken.balanceOf(feeRecipient.address); - const preProtocolBalance = await setToken.balanceOf(dummyModule.address); - const preCallerBalance = await setToken.balanceOf(subjectCaller.address); - - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); - const protocolSplit = preciseMul(feeQuantity, protocolFee); - - const postManagerBalance = await setToken.balanceOf(feeRecipient.address); - const postProtocolBalance = await setToken.balanceOf(dummyModule.address); // DummyModule is set as address in fixture setup - const postCallerBalance = await setToken.balanceOf(subjectCaller.address); - - expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); - expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity.sub(protocolSplit))); - expect(postProtocolBalance).to.eq(preProtocolBalance.add(protocolSplit)); - }); - }); - - describe("when the issue quantity is 0", async () => { - beforeEach(async () => { - subjectQuantity = ZERO; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Redeem quantity must be > 0"); - }); - }); - - describe("when the SetToken is not enabled on the controller", async () => { - beforeEach(async () => { - const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( - [tokenWithRoundingError.address], - [ether(1)], - [debtIssuance.address] - ); - - subjectSetToken = nonEnabledSetToken.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); - }); - }); - }); - - describe("#getRequiredComponentIssuanceUnits", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - - const debtUnits: BigNumber = ether(100); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - - await debtIssuance.issue(subjectSetToken, subjectQuantity, owner.address); - }); - - async function subject(): Promise { - return debtIssuance.getRequiredComponentIssuanceUnits( - subjectSetToken, - subjectQuantity - ); - } - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil( mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1)); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - - describe("when an additive external equity position is in place", async () => { - const externalUnits: BigNumber = ether(1); - - beforeEach(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil( mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1).add(externalUnits)); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - }); - - describe("when a non-additive external equity position is in place", async () => { - const externalUnits: BigNumber = ether(50); - - beforeEach(async () => { - await externalPositionModule.addExternalPosition(setToken.address, setup.dai.address, externalUnits); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiDebtFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1)); - const daiEquityFlows = preciseMul(mintQuantity, externalUnits); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, daiEquityFlows]; - const expectedDebtFlows = [ZERO, daiDebtFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - }); - }); - }); - - context("when SetToken components do have rounding errors", async () => { - describe("#issue", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - let subjectTo: Address; - let subjectCaller: Account; - - const debtUnits: BigNumber = ether(100); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); - // send exact amount - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0]); - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - subjectTo = recipient.address; - subjectCaller = owner; - }); - - async function subject(): Promise { - return debtIssuance.connect(subjectCaller.wallet).issue( - subjectSetToken, - subjectQuantity, - subjectTo, - ); - } - - describe("when rounding error is negative one", async () => { - beforeEach(async () => { - await tokenWithRoundingError.setError(BigNumber.from(-1)); - }); - - describe("when set is exactly collateralized", async () => { - it("should revert", async () => { - await expect(subject()).to.be.revertedWith( - "Invalid transfer in. Results in undercollateralization" - ); - }); - }); - - describe("when set is over-collateralized by at least 1 wei", async () => { - beforeEach(async () => { - await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); - }); - - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - const managerBalance = await setToken.balanceOf(feeRecipient.address); - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - expect(managerBalance).to.eq(feeQuantity); - }); - }); - }); - - describe("when rounding error is positive one", async () => { - beforeEach(async () => { - await tokenWithRoundingError.setError(ONE); - }); - - describe("when set is exactly collateralized", async () => { - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - const managerBalance = await setToken.balanceOf(feeRecipient.address); - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - expect(managerBalance).to.eq(feeQuantity); - }); - }); - - describe("when set is over-collateralized by at least 1 wei", async () => { - beforeEach(async () => { - await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); - }); - it("should mint SetTokens to the correct addresses", async () => { - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, issueFee); - const managerBalance = await setToken.balanceOf(feeRecipient.address); - const toBalance = await setToken.balanceOf(subjectTo); - - expect(toBalance).to.eq(subjectQuantity); - expect(managerBalance).to.eq(feeQuantity); - }); - }); - }); - }); - - describe("#redeem", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - let subjectTo: Address; - let subjectCaller: Account; - - const debtUnits: BigNumber = ether(100); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); - // Send exact amount - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0]); - - await debtIssuance.issue(setToken.address, ether(1), owner.address); - - await setup.dai.approve(debtIssuance.address, ether(100.5)); - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - subjectTo = recipient.address; - subjectCaller = owner; - }); - - async function subject(): Promise { - return debtIssuance.connect(subjectCaller.wallet).redeem( - subjectSetToken, - subjectQuantity, - subjectTo, - ); - } - - describe("when rounding error is negative one", async () => { - beforeEach(async () => { - await tokenWithRoundingError.setError(BigNumber.from(-1)); - }); - - describe("when set is exactly collateralized", async () => { - it("should revert", async () => { - await expect(subject()).to.be.revertedWith( - "Invalid transfer out. Results in undercollateralization" - ); - }); - }); - - describe("when set is over-collateralized by at least 1 wei", async () => { - beforeEach(async () => { - await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); - }); - it("should mint SetTokens to the correct addresses", async () => { - const preManagerBalance = await setToken.balanceOf(feeRecipient.address); - const preCallerBalance = await setToken.balanceOf(subjectCaller.address); - - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); - const postManagerBalance = await setToken.balanceOf(feeRecipient.address); - const postCallerBalance = await setToken.balanceOf(subjectCaller.address); - - expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); - expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); - }); - }); - }); - - describe("when rounding error is positive one", async () => { - beforeEach(async () => { - await tokenWithRoundingError.setError(ONE); - }); - - describe("when set is exactly collateralized", async () => { - it("should mint SetTokens to the correct addresses", async () => { - const preManagerBalance = await setToken.balanceOf(feeRecipient.address); - const preCallerBalance = await setToken.balanceOf(subjectCaller.address); - - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); - const postManagerBalance = await setToken.balanceOf(feeRecipient.address); - const postCallerBalance = await setToken.balanceOf(subjectCaller.address); - - expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); - expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); - }); - }); - - describe("when set is over-collateralized by at least 1 wei", async () => { - beforeEach(async () => { - await tokenWithRoundingError.connect(owner.wallet).transfer(setToken.address, ONE); - }); - - it("should mint SetTokens to the correct addresses", async () => { - const preManagerBalance = await setToken.balanceOf(feeRecipient.address); - const preCallerBalance = await setToken.balanceOf(subjectCaller.address); - - await subject(); - - const feeQuantity = preciseMulCeil(subjectQuantity, redeemFee); - const postManagerBalance = await setToken.balanceOf(feeRecipient.address); - const postCallerBalance = await setToken.balanceOf(subjectCaller.address); - - expect(postManagerBalance).to.eq(preManagerBalance.add(feeQuantity)); - expect(postCallerBalance).to.eq(preCallerBalance.sub(subjectQuantity)); - }); - }); - }); - }); - - describe("#getRequiredComponentIssuanceUnits", async () => { - let subjectSetToken: Address; - let subjectQuantity: BigNumber; - - const debtUnits: BigNumber = ether(100); - const accruedBalance = ether(.00001); - - beforeEach(async () => { - await debtModule.addDebt(setToken.address, setup.dai.address, debtUnits); - await setup.dai.transfer(debtModule.address, ether(100.5)); - - subjectSetToken = setToken.address; - subjectQuantity = ether(1); - - await tokenWithRoundingError.setError(accruedBalance); - }); - - async function subject(): Promise { - return debtIssuance.getRequiredComponentIssuanceUnits( - subjectSetToken, - subjectQuantity - ); - } - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1)); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - - describe("when an additive external equity position is in place", async () => { - const externalUnits: BigNumber = ether(1); - - beforeEach(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil( mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1).add(externalUnits)); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - }); - - describe("when a non-additive external equity position is in place", async () => { - const externalUnits: BigNumber = ether(50); - - beforeEach(async () => { - await externalPositionModule.addExternalPosition(setToken.address, setup.dai.address, externalUnits); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiDebtFlows = preciseMulCeil(mintQuantity, debtUnits); - const wethFlows = preciseMul(mintQuantity, ether(1)); - const daiEquityFlows = preciseMul(mintQuantity, externalUnits); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, daiEquityFlows]; - const expectedDebtFlows = [ZERO, daiDebtFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - }); - - describe("when tokens have been issued", async () => { - beforeEach(async () => { - const [, equityFlows ] = await debtIssuance.getRequiredComponentIssuanceUnits(setToken.address, ether(1)); - await tokenWithRoundingError.approve(debtIssuance.address, equityFlows[0].mul(ether(1.005))); - - await debtIssuance.issue(subjectSetToken, subjectQuantity, owner.address); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMul(mintQuantity, debtUnits); - const wethFlows = preciseMulCeil(mintQuantity, preciseDiv(ether(1.005).add(accruedBalance), ether(1.005))); - - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; - - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - - describe("when an additive external equity position is in place", async () => { - const externalUnits: BigNumber = ether(1); - - beforeEach(async () => { - await externalPositionModule.addExternalPosition(setToken.address, tokenWithRoundingError.address, externalUnits); - }); - - it("should return the correct issue token amounts", async () => { - const [components, equityFlows, debtFlows] = await subject(); - - const mintQuantity = preciseMul(subjectQuantity, ether(1).add(issueFee)); - const daiFlows = preciseMulCeil( mintQuantity, debtUnits); - const wethFlows = preciseMulCeil(mintQuantity, preciseDiv(ether(1.005).add(accruedBalance), ether(1.005)).add(externalUnits)); + describe("#constructor", async () => { + let subjectIssuanceModule: IssuanceModule; - const expectedComponents = await setToken.getComponents(); - const expectedEquityFlows = [wethFlows, ZERO]; - const expectedDebtFlows = [ZERO, daiFlows]; + async function subject(): Promise { + return deployer.modules.deployIssuanceModule(setup.controller.address); + } - expect(JSON.stringify(expectedComponents)).to.eq(JSON.stringify(components)); - expect(JSON.stringify(expectedEquityFlows)).to.eq(JSON.stringify(equityFlows)); - expect(JSON.stringify(expectedDebtFlows)).to.eq(JSON.stringify(debtFlows)); - }); - }); - }); - }); + it("should have the correct controller", async () => { + subjectIssuanceModule = await subject(); + const expectedController = await subjectIssuanceModule.controller(); + expect(expectedController).to.eq(setup.controller.address); }); }); -}); +}); \ No newline at end of file From a787d939e4dace4e3d4426204d8c241b1e41f8a5 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 24 Mar 2022 12:41:46 -0700 Subject: [PATCH 7/7] Revert EOF newline changes --- utils/contracts/index.ts | 2 +- utils/deploys/deployModules.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index ac2419d98..7bb428b03 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -119,4 +119,4 @@ export { WrapModuleV2 } from "../../typechain/WrapModuleV2"; export { YearnWrapV2Adapter } from "../../typechain/YearnWrapV2Adapter"; export { YearnStrategyMock } from "../../typechain/YearnStrategyMock"; export { ZeroExApiAdapter } from "../../typechain/ZeroExApiAdapter"; -export { ZeroExMock } from "../../typechain/ZeroExMock"; \ No newline at end of file +export { ZeroExMock } from "../../typechain/ZeroExMock"; diff --git a/utils/deploys/deployModules.ts b/utils/deploys/deployModules.ts index 40a9e5b96..6e10b64cc 100644 --- a/utils/deploys/deployModules.ts +++ b/utils/deploys/deployModules.ts @@ -249,4 +249,4 @@ export default class DeployModules { maxPerpPositionsPerSet ); } -} +} \ No newline at end of file