diff --git a/contracts/protocol/modules/IssuanceModule.sol b/contracts/protocol/modules/IssuanceModule.sol index bc14560e6..8875b5e44 100644 --- a/contracts/protocol/modules/IssuanceModule.sol +++ b/contracts/protocol/modules/IssuanceModule.sol @@ -1,5 +1,5 @@ /* - Copyright 2020 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,278 +19,23 @@ pragma solidity 0.6.10; pragma experimental "ABIEncoderV2"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol"; -import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import { SignedSafeMath } from "@openzeppelin/contracts/math/SignedSafeMath.sol"; - +import { DebtIssuanceModuleV2 } from "./DebtIssuanceModuleV2.sol"; import { IController } from "../../interfaces/IController.sol"; -import { IManagerIssuanceHook } from "../../interfaces/IManagerIssuanceHook.sol"; -import { IModuleIssuanceHook } from "../../interfaces/IModuleIssuanceHook.sol"; -import { Invoke } from "../lib/Invoke.sol"; -import { ISetToken } from "../../interfaces/ISetToken.sol"; -import { ModuleBase } from "../lib/ModuleBase.sol"; -import { Position } from "../lib/Position.sol"; -import { PreciseUnitMath } from "../../lib/PreciseUnitMath.sol"; /** * @title IssuanceModule * @author Set Protocol * - * The IssuanceModule is a module that enables users to issue and redeem SetTokens that contain default and - * non-debt external Positions. Managers are able to set an external contract hook that is called before an - * issuance is called. + * 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. */ -contract IssuanceModule is ModuleBase, ReentrancyGuard { - using Invoke for ISetToken; - using Position for ISetToken; - using PreciseUnitMath for uint256; - using SafeMath for uint256; - using SafeCast for int256; - using SignedSafeMath for int256; - - /* ============ Events ============ */ - - event SetTokenIssued(address indexed _setToken, address _issuer, address _to, address _hookContract, uint256 _quantity); - event SetTokenRedeemed(address indexed _setToken, address _redeemer, address _to, uint256 _quantity); - - /* ============ State Variables ============ */ - - // Mapping of SetToken to Issuance hook configurations - mapping(ISetToken => IManagerIssuanceHook) public managerIssuanceHook; +contract IssuanceModule is DebtIssuanceModuleV2 { /* ============ Constructor ============ */ /** * Set state controller state variable */ - constructor(IController _controller) public ModuleBase(_controller) {} - - /* ============ External Functions ============ */ - - /** - * Deposits components to the SetToken and replicates any external module component positions and mints - * the SetToken. Any issuances with SetTokens that have external positions with negative unit will revert. - * - * @param _setToken Instance of the SetToken contract - * @param _quantity Quantity of the SetToken to mint - * @param _to Address to mint SetToken to - */ - function issue( - ISetToken _setToken, - uint256 _quantity, - address _to - ) - external - nonReentrant - onlyValidAndInitializedSet(_setToken) - { - require(_quantity > 0, "Issue quantity must be > 0"); - - address hookContract = _callPreIssueHooks(_setToken, _quantity, msg.sender, _to); - - ( - address[] memory components, - uint256[] memory componentQuantities - ) = getRequiredComponentIssuanceUnits(_setToken, _quantity, true); - - // For each position, transfer the required underlying to the SetToken and call external module hooks - for (uint256 i = 0; i < components.length; i++) { - transferFrom( - IERC20(components[i]), - msg.sender, - address(_setToken), - componentQuantities[i] - ); - - _executeExternalPositionHooks(_setToken, _quantity, IERC20(components[i]), true); - } - - _setToken.mint(_to, _quantity); - - emit SetTokenIssued(address(_setToken), msg.sender, _to, hookContract, _quantity); - } - - /** - * Burns a user's SetToken of specified quantity, unwinds external positions, and returns components - * to the specified address. Does not work for debt/negative external positions. - * - * @param _setToken Instance of the SetToken contract - * @param _quantity Quantity of the SetToken to redeem - * @param _to Address to send component assets to - */ - function redeem( - ISetToken _setToken, - uint256 _quantity, - address _to - ) - external - nonReentrant - onlyValidAndInitializedSet(_setToken) - { - require(_quantity > 0, "Redeem quantity must be > 0"); - - _setToken.burn(msg.sender, _quantity); - - ( - address[] memory components, - uint256[] memory componentQuantities - ) = getRequiredComponentIssuanceUnits(_setToken, _quantity, false); - - for (uint256 i = 0; i < components.length; i++) { - _executeExternalPositionHooks(_setToken, _quantity, IERC20(components[i]), false); - - _setToken.strictInvokeTransfer( - components[i], - _to, - componentQuantities[i] - ); - } - - emit SetTokenRedeemed(address(_setToken), msg.sender, _to, _quantity); - } - - /** - * Initializes this module to the SetToken with issuance-related hooks. Only callable by the SetToken's manager. - * Hook addresses are optional. Address(0) means that no hook will be called - * - * @param _setToken Instance of the SetToken to issue - * @param _preIssueHook Instance of the Manager Contract with the Pre-Issuance Hook function - */ - function initialize( - ISetToken _setToken, - IManagerIssuanceHook _preIssueHook - ) - external - onlySetManager(_setToken, msg.sender) - onlyValidAndPendingSet(_setToken) - { - managerIssuanceHook[_setToken] = _preIssueHook; - - _setToken.initializeModule(); - } - - /** - * Reverts as this module should not be removable after added. Users should always - * have a way to redeem their Sets - */ - function removeModule() external override { - revert("The IssuanceModule module cannot be removed"); - } - - /* ============ External Getter Functions ============ */ - - /** - * Retrieves the addresses and units required to issue/redeem a particular quantity of SetToken. - * - * @param _setToken Instance of the SetToken to issue - * @param _quantity Quantity of SetToken to issue - * @param _isIssue Boolean whether the quantity is issuance or redemption - * @return address[] List of component addresses - * @return uint256[] List of component units required for a given SetToken quantity - */ - function getRequiredComponentIssuanceUnits( - ISetToken _setToken, - uint256 _quantity, - bool _isIssue - ) - public - view - returns (address[] memory, uint256[] memory) - { - ( - address[] memory components, - uint256[] memory issuanceUnits - ) = _getTotalIssuanceUnits(_setToken); - - uint256[] memory notionalUnits = new uint256[](components.length); - for (uint256 i = 0; i < issuanceUnits.length; i++) { - // Use preciseMulCeil to round up to ensure overcollateration when small issue quantities are provided - // and preciseMul to round down to ensure overcollateration when small redeem quantities are provided - notionalUnits[i] = _isIssue ? - issuanceUnits[i].preciseMulCeil(_quantity) : - issuanceUnits[i].preciseMul(_quantity); - } - - return (components, notionalUnits); - } - - /* ============ Internal Functions ============ */ - - /** - * Retrieves the component addresses and list of total units for components. This will revert if the external unit - * is ever equal or less than 0 . - */ - function _getTotalIssuanceUnits(ISetToken _setToken) internal view returns (address[] memory, uint256[] memory) { - address[] memory components = _setToken.getComponents(); - uint256[] memory totalUnits = new uint256[](components.length); - - for (uint256 i = 0; i < components.length; i++) { - address component = components[i]; - int256 cumulativeUnits = _setToken.getDefaultPositionRealUnit(component); - - address[] memory externalModules = _setToken.getExternalPositionModules(component); - if (externalModules.length > 0) { - for (uint256 j = 0; j < externalModules.length; j++) { - int256 externalPositionUnit = _setToken.getExternalPositionRealUnit(component, externalModules[j]); - - require(externalPositionUnit > 0, "Only positive external unit positions are supported"); - - cumulativeUnits = cumulativeUnits.add(externalPositionUnit); - } - } - - totalUnits[i] = cumulativeUnits.toUint256(); - } - - return (components, totalUnits); - } - - /** - * If a pre-issue hook has been configured, call the external-protocol contract. Pre-issue hook logic - * can contain arbitrary logic including validations, external function calls, etc. - * Note: All modules with external positions must implement ExternalPositionIssueHooks - */ - function _callPreIssueHooks( - ISetToken _setToken, - uint256 _quantity, - address _caller, - address _to - ) - internal - returns(address) - { - IManagerIssuanceHook preIssueHook = managerIssuanceHook[_setToken]; - if (address(preIssueHook) != address(0)) { - preIssueHook.invokePreIssueHook(_setToken, _quantity, _caller, _to); - return address(preIssueHook); - } - - return address(0); - } - - /** - * For each component's external module positions, calculate the total notional quantity, and - * call the module's issue hook or redeem hook. - * Note: It is possible that these hooks can cause the states of other modules to change. - * It can be problematic if the a hook called an external function that called back into a module, resulting in state inconsistencies. - */ - function _executeExternalPositionHooks( - ISetToken _setToken, - uint256 _setTokenQuantity, - IERC20 _component, - bool isIssue - ) - internal - { - address[] memory externalPositionModules = _setToken.getExternalPositionModules(address(_component)); - for (uint256 i = 0; i < externalPositionModules.length; i++) { - if (isIssue) { - IModuleIssuanceHook(externalPositionModules[i]).componentIssueHook(_setToken, _setTokenQuantity, _component, true); - } else { - IModuleIssuanceHook(externalPositionModules[i]).componentRedeemHook(_setToken, _setTokenQuantity, _component, true); - } - } - } + constructor(IController _controller) public DebtIssuanceModuleV2(_controller) {} } diff --git a/package.json b/package.json index f55c35b2d..acc2eedda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@setprotocol/set-protocol-v2", - "version": "0.1.13", + "version": "0.1.14", "description": "", "main": "dist", "types": "dist/types", diff --git a/test/protocol/modules/issuanceModule.spec.ts b/test/protocol/modules/issuanceModule.spec.ts index 2a52f54ed..69704316e 100644 --- a/test/protocol/modules/issuanceModule.spec.ts +++ b/test/protocol/modules/issuanceModule.spec.ts @@ -1,21 +1,11 @@ import "module-alias/register"; -import { BigNumber } from "ethers"; -import { Address } from "@utils/types"; import { Account } from "@utils/test/types"; -import { ADDRESS_ZERO, ZERO, ONE } from "@utils/constants"; -import { IssuanceModule, ManagerIssuanceHookMock, ModuleIssuanceHookMock, SetToken } from "@utils/contracts"; +import { IssuanceModule } from "@utils/contracts"; import DeployHelper from "@utils/deploys"; -import { - bitcoin, - ether, - preciseMul, -} from "@utils/index"; import { addSnapshotBeforeRestoreAfterEach, getAccounts, - getRandomAccount, - getRandomAddress, getWaffleExpect, getSystemFixture, } from "@utils/test/index"; @@ -25,475 +15,32 @@ const expect = getWaffleExpect(); describe("IssuanceModule", () => { let owner: Account; - let recipient: Account; let deployer: DeployHelper; let setup: SystemFixture; - let issuanceModule: IssuanceModule; - let moduleIssuanceHook: ModuleIssuanceHookMock; - before(async () => { [ owner, - recipient, ] = await getAccounts(); deployer = new DeployHelper(owner.wallet); setup = getSystemFixture(owner.address); await setup.initialize(); - - issuanceModule = await deployer.modules.deployIssuanceModule(setup.controller.address); - moduleIssuanceHook = await deployer.mocks.deployModuleIssuanceHookMock(); - await setup.controller.addModule(issuanceModule.address); - await setup.controller.addModule(moduleIssuanceHook.address); - await setup.controller.addModule(owner.address); }); addSnapshotBeforeRestoreAfterEach(); - describe("#initialize", async () => { - let setToken: SetToken; - let subjectSetToken: Address; - let subjectPreIssuanceHook: Address; - let subjectCaller: Account; - - beforeEach(async () => { - setToken = await setup.createSetToken( - [setup.weth.address], - [ether(1)], - [issuanceModule.address] - ); - subjectSetToken = setToken.address; - subjectPreIssuanceHook = await getRandomAddress(); - subjectCaller = owner; - }); - - async function subject(): Promise { - return issuanceModule.connect(subjectCaller.wallet).initialize( - subjectSetToken, - subjectPreIssuanceHook, - ); - } - - it("should enable the Module on the SetToken", async () => { - await subject(); - const isModuleEnabled = await setToken.isInitializedModule(issuanceModule.address); - expect(isModuleEnabled).to.eq(true); - }); - - it("should properly set the issuance hooks", async () => { - await subject(); - const preIssuanceHooks = await issuanceModule.managerIssuanceHook(subjectSetToken); - expect(preIssuanceHooks).to.eq(subjectPreIssuanceHook); - }); - - describe("when the caller is not the SetToken manager", async () => { - beforeEach(async () => { - subjectCaller = await getRandomAccount(); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); - }); - }); - - describe("when SetToken is not in pending state", async () => { - beforeEach(async () => { - const newModule = await getRandomAddress(); - await setup.controller.addModule(newModule); - - const issuanceModuleNotPendingSetToken = await setup.createSetToken( - [setup.weth.address], - [ether(1)], - [newModule] - ); - - subjectSetToken = issuanceModuleNotPendingSetToken.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Must be pending initialization"); - }); - }); - - describe("when the SetToken is not enabled on the controller", async () => { - beforeEach(async () => { - const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( - [setup.weth.address], - [ether(1)], - [issuanceModule.address] - ); - - subjectSetToken = nonEnabledSetToken.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Must be controller-enabled SetToken"); - }); - }); - }); - - describe("#removeModule", async () => { - let subjectCaller: Account; + describe("#constructor", async () => { + let subjectIssuanceModule: IssuanceModule; - beforeEach(async () => { - subjectCaller = owner; - }); - - async function subject(): Promise { - return issuanceModule.connect(subjectCaller.wallet).removeModule(); + async function subject(): Promise { + return deployer.modules.deployIssuanceModule(setup.controller.address); } - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("The IssuanceModule module cannot be removed"); - }); - }); - - describe("#issue", async () => { - let setToken: SetToken; - - let subjectSetToken: Address; - let subjectIssueQuantity: BigNumber; - let subjectTo: Account; - let subjectCaller: Account; - - let preIssueHook: Address; - - context("when the components are default WBTC, WETH, and external DAI", async () => { - beforeEach(async () => { - setToken = await setup.createSetToken( - [setup.weth.address, setup.wbtc.address], - [ether(1), bitcoin(2)], - [issuanceModule.address, moduleIssuanceHook.address, owner.address] - ); - await issuanceModule.initialize(setToken.address, preIssueHook); - await moduleIssuanceHook.initialize(setToken.address); - await setToken.initializeModule(); - - // Add a DAI position held by an external mock - await setToken.addComponent(setup.dai.address); - await setToken.addExternalPositionModule(setup.dai.address, moduleIssuanceHook.address); - await setToken.editExternalPositionUnit(setup.dai.address, moduleIssuanceHook.address, ether(3)); - - // Approve tokens to the module - await setup.weth.approve(issuanceModule.address, ether(5)); - await setup.wbtc.approve(issuanceModule.address, bitcoin(10)); - await setup.dai.approve(issuanceModule.address, ether(6)); - - subjectSetToken = setToken.address; - subjectIssueQuantity = ether(2); - subjectTo = recipient; - subjectCaller = owner; - }); - - context("when there are no hooks", async () => { - before(async () => { - preIssueHook = ADDRESS_ZERO; - }); - - async function subject(): Promise { - return issuanceModule.connect(subjectCaller.wallet).issue( - subjectSetToken, - subjectIssueQuantity, - subjectTo.address - ); - } - - it("should issue the Set to the recipient", async () => { - await subject(); - const issuedBalance = await setToken.balanceOf(recipient.address); - expect(issuedBalance).to.eq(subjectIssueQuantity); - }); - - it("should have deposited the eth and wbtc into the SetToken", async () => { - await subject(); - const depositedWETHBalance = await setup.weth.balanceOf(setToken.address); - const expectedBTCBalance = subjectIssueQuantity; - expect(depositedWETHBalance).to.eq(expectedBTCBalance); - - const depositedBTCBalance = await setup.wbtc.balanceOf(setToken.address); - const expectedBalance = preciseMul(subjectIssueQuantity, bitcoin(2)); - expect(depositedBTCBalance).to.eq(expectedBalance); - }); - - it("should have deposited DAI into the module hook contract", async () => { - await subject(); - const depositedDAIBalance = await setup.dai.balanceOf(moduleIssuanceHook.address); - const expectedDAIBalance = preciseMul(ether(3), subjectIssueQuantity); - expect(depositedDAIBalance).to.eq(expectedDAIBalance); - }); - - it("should emit the SetTokenIssued event", async () => { - await expect(subject()).to.emit(issuanceModule, "SetTokenIssued").withArgs( - subjectSetToken, - subjectCaller.address, - subjectTo.address, - ADDRESS_ZERO, - subjectIssueQuantity, - ); - }); - - describe("when the issue quantity is extremely small", async () => { - beforeEach(async () => { - subjectIssueQuantity = ONE; - }); - - it("should transfer the minimal units of components to the SetToken", async () => { - await subject(); - const depositedWETHBalance = await setup.weth.balanceOf(setToken.address); - const expectedWETHBalance = ONE; - expect(depositedWETHBalance).to.eq(expectedWETHBalance); - - const depositedBTCBalance = await setup.wbtc.balanceOf(setToken.address); - const expectedBTCBalance = ONE; - expect(depositedBTCBalance).to.eq(expectedBTCBalance); - }); - }); - - describe("when an external position is a negative value", async () => { - beforeEach(async () => { - await setToken.editExternalPositionUnit(setup.dai.address, moduleIssuanceHook.address, ether(-1)); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Only positive external unit positions are supported"); - }); - }); - - describe("when the issue quantity is 0", async () => { - beforeEach(async () => { - subjectIssueQuantity = 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( - [setup.weth.address], - [ether(1)], - [issuanceModule.address] - ); - - subjectSetToken = nonEnabledSetToken.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); - }); - }); - }); - - context("when a preIssueHook has been set", async () => { - let issuanceHookContract: ManagerIssuanceHookMock; - - before(async () => { - issuanceHookContract = await deployer.mocks.deployManagerIssuanceHookMock(); - - preIssueHook = issuanceHookContract.address; - }); - - async function subject(): Promise { - return issuanceModule.issue(subjectSetToken, subjectIssueQuantity, subjectTo.address); - } - - it("should properly call the pre-issue hooks", async () => { - await subject(); - const retrievedSetToken = await issuanceHookContract.retrievedSetToken(); - const retrievedIssueQuantity = await issuanceHookContract.retrievedIssueQuantity(); - const retrievedSender = await issuanceHookContract.retrievedSender(); - const retrievedTo = await issuanceHookContract.retrievedTo(); - - expect(retrievedSetToken).to.eq(subjectSetToken); - expect(retrievedIssueQuantity).to.eq(subjectIssueQuantity); - expect(retrievedSender).to.eq(owner.address); - expect(retrievedTo).to.eq(subjectTo.address); - }); - }); - }); - }); - - describe("#redeem", async () => { - let setToken: SetToken; - - let subjectSetToken: Address; - let subjectRedeemQuantity: BigNumber; - let subjectTo: Address; - let subjectCaller: Account; - - let preIssueHook: Address; - - context("when the components are WBTC and WETH", async () => { - beforeEach(async () => { - preIssueHook = ADDRESS_ZERO; - - setToken = await setup.createSetToken( - [setup.weth.address, setup.wbtc.address], - [ether(1), bitcoin(2)], - [issuanceModule.address, moduleIssuanceHook.address, owner.address] - ); - await issuanceModule.initialize(setToken.address, preIssueHook); - await moduleIssuanceHook.initialize(setToken.address); - await setToken.initializeModule(); - - await setToken.addComponent(setup.dai.address); - await setToken.addExternalPositionModule(setup.dai.address, moduleIssuanceHook.address); - await setToken.editExternalPositionUnit(setup.dai.address, moduleIssuanceHook.address, ether(3)); - - // Approve tokens to the controller - await setup.weth.approve(issuanceModule.address, ether(5)); - await setup.wbtc.approve(issuanceModule.address, bitcoin(10)); - await setup.dai.approve(issuanceModule.address, ether(15)); - - subjectSetToken = setToken.address; - subjectRedeemQuantity = ether(1); - subjectTo = recipient.address; - subjectCaller = owner; - - const issueQuantity = ether(2); - await issuanceModule.issue(subjectSetToken, issueQuantity, subjectCaller.address); - }); - - async function subject(): Promise { - return issuanceModule.connect(subjectCaller.wallet).redeem(subjectSetToken, subjectRedeemQuantity, subjectTo); - } - - it("should redeem the Set", async () => { - await subject(); - const redeemBalance = await setToken.balanceOf(owner.address); - expect(redeemBalance).to.eq(ether(1)); - }); - - it("should have deposited the components to the recipients account", async () => { - const beforeWETHBalance = await setup.weth.balanceOf(recipient.address); - const beforeBTCBalance = await setup.wbtc.balanceOf(recipient.address); - const beforeDAIBalance = await setup.dai.balanceOf(recipient.address); - - await subject(); - const afterWETHBalance = await setup.weth.balanceOf(recipient.address); - const expectedWETHBalance = beforeWETHBalance.add(subjectRedeemQuantity); - expect(afterWETHBalance).to.eq(expectedWETHBalance); - - const afterBTCBalance = await setup.wbtc.balanceOf(recipient.address); - const expectedBalance = beforeBTCBalance.add(preciseMul(subjectRedeemQuantity, bitcoin(2))); - expect(afterBTCBalance).to.eq(expectedBalance); - - const afterDAIBalance = await setup.dai.balanceOf(recipient.address); - const expectedDAIBalance = beforeDAIBalance.add(preciseMul(subjectRedeemQuantity, ether(3))); - expect(afterDAIBalance).to.eq(expectedDAIBalance); - }); - - it("should have subtracted from the components from the SetToken", async () => { - const beforeWETHBalance = await setup.weth.balanceOf(setToken.address); - const beforeBTCBalance = await setup.wbtc.balanceOf(setToken.address); - - await subject(); - const afterWETHBalance = await setup.weth.balanceOf(setToken.address); - const expectedBTCBalance = beforeWETHBalance.sub(subjectRedeemQuantity); - expect(afterWETHBalance).to.eq(expectedBTCBalance); - - const afterBTCBalance = await setup.wbtc.balanceOf(setToken.address); - const expectedBalance = beforeBTCBalance.sub(subjectRedeemQuantity.mul(bitcoin(2)).div(ether(1))); - expect(afterBTCBalance).to.eq(expectedBalance); - }); - - it("should have subtracted from the components from the Module", async () => { - const beforeDAIBalance = await setup.dai.balanceOf(moduleIssuanceHook.address); - - await subject(); - - const afterDAIBalance = await setup.dai.balanceOf(moduleIssuanceHook.address); - const expectedBalance = beforeDAIBalance.sub(preciseMul(subjectRedeemQuantity, ether(3))); - expect(afterDAIBalance).to.eq(expectedBalance); - }); - - it("should emit the SetTokenRedeemed event", async () => { - await expect(subject()).to.emit(issuanceModule, "SetTokenRedeemed").withArgs( - subjectSetToken, - subjectCaller.address, - subjectTo, - subjectRedeemQuantity - ); - }); - - describe("when the issue quantity is extremely small", async () => { - beforeEach(async () => { - subjectRedeemQuantity = ONE; - }); - - it("should transfer the minimal units of components to the SetToken", async () => { - const previousCallerBTCBalance = await setup.wbtc.balanceOf(subjectCaller.address); - - await subject(); - - const afterCallerBTCBalance = await setup.wbtc.balanceOf(subjectCaller.address); - expect(previousCallerBTCBalance).to.eq(afterCallerBTCBalance); - }); - }); - - describe("when an external position is a negative value", async () => { - beforeEach(async () => { - await setToken.editExternalPositionUnit(setup.dai.address, moduleIssuanceHook.address, ether(-1)); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Only positive external unit positions are supported"); - }); - }); - - describe("when the issue quantity is greater than the callers balance", async () => { - beforeEach(async () => { - subjectRedeemQuantity = ether(4); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("ERC20: burn amount exceeds balance"); - }); - }); - - describe("when one of the components has a recipient-related fee", async () => { - beforeEach(async () => { - const tokenWithFee = await deployer.mocks.deployTokenWithFeeMock(setToken.address, ether(20), ether(0.1)); - - const retrievedPosition = (await setToken.getPositions())[0]; - - await setToken.addComponent(tokenWithFee.address); - await setToken.editDefaultPositionUnit(tokenWithFee.address, retrievedPosition.unit); - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Invalid post transfer balance"); - }); - }); - - describe("when the issue quantity is 0", async () => { - beforeEach(async () => { - subjectRedeemQuantity = 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( - [setup.weth.address], - [ether(1)], - [issuanceModule.address] - ); - - subjectSetToken = nonEnabledSetToken.address; - }); - - it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); - }); - }); + 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