From ec7ce837d2306dfbd6b71b2f8975e7262637ec7b Mon Sep 17 00:00:00 2001 From: SnakePoison <1284031+snake-poison@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:17:38 -0400 Subject: [PATCH] feat: GlobalAuctionRebalanceExtension contract, tests & utils (#153) * feat: GlobalAuctionRebalanceExtension contract, tests & utils * test: adds cases that increase coverage to 100% * feat: prevent intialization when not ready. * style: update constructor docs. --- .../GlobalAuctionRebalanceExtension.sol | 271 ++++++++ .../globalAuctionRebalanceExtension.spec.ts | 614 ++++++++++++++++++ utils/contracts/index.ts | 1 + utils/deploys/deployGlobalExtensions.ts | 14 +- 4 files changed, 899 insertions(+), 1 deletion(-) create mode 100644 contracts/global-extensions/GlobalAuctionRebalanceExtension.sol create mode 100644 test/global-extensions/globalAuctionRebalanceExtension.spec.ts diff --git a/contracts/global-extensions/GlobalAuctionRebalanceExtension.sol b/contracts/global-extensions/GlobalAuctionRebalanceExtension.sol new file mode 100644 index 00000000..ba2e02ee --- /dev/null +++ b/contracts/global-extensions/GlobalAuctionRebalanceExtension.sol @@ -0,0 +1,271 @@ +/* + Copyright 2023 Index Coop + + 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 { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { AddressArrayUtils } from "../lib/AddressArrayUtils.sol"; +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IAuctionRebalanceModuleV1 } from "../interfaces/IAuctionRebalanceModuleV1.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; + + +/** + * @title GlobalAuctionRebalanceExtension + * @author Index Coop + * + * @dev Extension contract for interacting with the AuctionRebalanceModuleV1. This contract acts as a pass-through and functions + * are only callable by operator. + */ +contract GlobalAuctionRebalanceExtension is BaseGlobalExtension { + using AddressArrayUtils for address[]; + using SafeMath for uint256; + + /* ============ Events ============ */ + + event AuctionRebalanceExtensionInitialized( + address indexed _setToken, + address indexed _delegatedManager + ); + + + /* ============ Structs ============ */ + + struct AuctionExecutionParams { + uint256 targetUnit; // Target quantity of the component in Set, in precise units (10 ** 18). + string priceAdapterName; // Identifier for the price adapter to be used. + bytes priceAdapterConfigData; // Encoded data for configuring the chosen price adapter. + } + + /* ============ State Variables ============ */ + + IAuctionRebalanceModuleV1 public immutable auctionModule; // AuctionRebalanceModuleV1 + + + /* ============ Constructor ============ */ + /* + * Instantiate with ManagerCore address and WrapModuleV2 address. + * + * @param _managerCore Address of ManagerCore contract + * @param _auctionModule Address of AuctionRebalanceModuleV1 contract + */ + constructor(IManagerCore _managerCore, IAuctionRebalanceModuleV1 _auctionModule) public BaseGlobalExtension(_managerCore) { + auctionModule = _auctionModule; + } + + /* ============ External Functions ============ */ + + + /** + * @dev ONLY OWNER: Initializes AuctionRebalanceModuleV1 on the SetToken associated with the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize the AuctionRebalanceModuleV1 for + */ + function initializeModule(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) { + require(_delegatedManager.isInitializedExtension(address(this)), "Extension must be initialized"); + _initializeModule(_delegatedManager.setToken(), _delegatedManager); + } + + /** + * @dev ONLY OWNER: Initializes AuctionRebalanceExtension to the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) { + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + + emit AuctionRebalanceExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * @dev ONLY OWNER: Initializes AuctionRebalanceExtension to the DelegatedManager and AuctionRebalanceModuleV1 to the SetToken. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeModuleAndExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager){ + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + _initializeModule(setToken, _delegatedManager); + + emit AuctionRebalanceExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + + /** + * @dev ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the AuctionRebalanceExtension + * @dev _removeExtension implements the only manager assertion. + */ + function removeExtension() external override { + IDelegatedManager delegatedManager = IDelegatedManager(msg.sender); + ISetToken setToken = delegatedManager.setToken(); + + _removeExtension(setToken, delegatedManager); + } + + /** + * @dev OPERATOR ONLY: Checks that the old components array matches the current components array and then invokes the + * AuctionRebalanceModuleV1 startRebalance function. + * + * Refer to AuctionRebalanceModuleV1 for function specific restrictions. + * + * @param _quoteAsset ERC20 token used as the quote asset in auctions. + * @param _oldComponents Addresses of existing components in the SetToken. + * @param _newComponents Addresses of new components to be added. + * @param _newComponentsAuctionParams AuctionExecutionParams for new components, indexed corresponding to _newComponents. + * @param _oldComponentsAuctionParams AuctionExecutionParams for existing components, indexed corresponding to + * the current component positions. Set to 0 for components being removed. + * @param _shouldLockSetToken Indicates if the rebalance should lock the SetToken. + * @param _rebalanceDuration Duration of the rebalance in seconds. + * @param _positionMultiplier Position multiplier at the time target units were calculated. + */ + function startRebalance( + ISetToken _setToken, + IERC20 _quoteAsset, + address[] memory _oldComponents, + address[] memory _newComponents, + AuctionExecutionParams[] memory _newComponentsAuctionParams, + AuctionExecutionParams[] memory _oldComponentsAuctionParams, + bool _shouldLockSetToken, + uint256 _rebalanceDuration, + uint256 _positionMultiplier + ) + external + onlyOperator(_setToken) + { + address[] memory currentComponents = _setToken.getComponents(); + + require(currentComponents.length == _oldComponents.length, "Mismatch: old and current components length"); + + for (uint256 i = 0; i < _oldComponents.length; i++) { + require(currentComponents[i] == _oldComponents[i], "Mismatch: old and current components"); + } + + bytes memory callData = abi.encodeWithSelector( + IAuctionRebalanceModuleV1.startRebalance.selector, + _setToken, + _quoteAsset, + _newComponents, + _newComponentsAuctionParams, + _oldComponentsAuctionParams, + _shouldLockSetToken, + _rebalanceDuration, + _positionMultiplier + ); + + _invokeManager(_manager((_setToken)), address(auctionModule), callData); + } + + /** + * @dev OPERATOR ONLY: Unlocks SetToken via AuctionRebalanceModuleV1. + * Refer to AuctionRebalanceModuleV1 for function specific restrictions. + * + * @param _setToken Address of the SetToken to unlock. + */ + function unlock(ISetToken _setToken) external onlyOperator(_setToken) { + bytes memory callData = abi.encodeWithSelector( + IAuctionRebalanceModuleV1.unlock.selector, + _setToken + ); + + _invokeManager(_manager((_setToken)), address(auctionModule), callData); + } + + /** + * @dev OPERATOR ONLY: Sets the target raise percentage for all components on AuctionRebalanceModuleV1. + * Refer to AuctionRebalanceModuleV1 for function specific restrictions. + * + * @param _setToken Address of the SetToken to update unit targets of. + * @param _raiseTargetPercentage Amount to raise all component's unit targets by (in precise units) + */ + function setRaiseTargetPercentage(ISetToken _setToken, uint256 _raiseTargetPercentage) external onlyOperator(_setToken) { + bytes memory callData = abi.encodeWithSelector( + IAuctionRebalanceModuleV1.setRaiseTargetPercentage.selector, + _setToken, + _raiseTargetPercentage + ); + + _invokeManager(_manager((_setToken)), address(auctionModule), callData); + } + + /** + * @dev OPERATOR ONLY: Updates the bidding permission status for a list of addresses on AuctionRebalanceModuleV1. + * Refer to AuctionRebalanceModuleV1 for function specific restrictions. + * + * @param _setToken Address of the SetToken to rebalance bidder status of. + * @param _bidders An array of addresses whose bidding permission status is to be toggled. + * @param _statuses An array of booleans indicating the new bidding permission status for each corresponding address in `_bidders`. + */ + function setBidderStatus( + ISetToken _setToken, + address[] memory _bidders, + bool[] memory _statuses + ) + external + onlyOperator(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IAuctionRebalanceModuleV1.setBidderStatus.selector, + _setToken, + _bidders, + _statuses + ); + + _invokeManager(_manager((_setToken)), address(auctionModule), callData); + } + + /** + * @dev OPERATOR ONLY: Sets whether anyone can bid on the AuctionRebalanceModuleV1. + * Refer to AuctionRebalanceModuleV1 for function specific restrictions. + * + * @param _setToken Address of the SetToken to update anyone bid status of. + * @param _status A boolean indicating if anyone can bid. + */ + function setAnyoneBid( ISetToken _setToken, bool _status) external onlyOperator(_setToken) { + bytes memory callData = abi.encodeWithSelector( + IAuctionRebalanceModuleV1.setAnyoneBid.selector, + _setToken, + _status + ); + + _invokeManager(_manager((_setToken)), address(auctionModule), callData); + } + + /* ============ Internal Functions ============ */ + + /** + * Internal function to initialize AuctionRebalanceModuleV1 on the SetToken associated with the DelegatedManager. + * + * @param _setToken Instance of the SetToken corresponding to the DelegatedManager + * @param _delegatedManager Instance of the DelegatedManager to initialize the AuctionRebalanceModuleV1 for + */ + function _initializeModule(ISetToken _setToken, IDelegatedManager _delegatedManager) internal { + bytes memory callData = abi.encodeWithSelector(IAuctionRebalanceModuleV1.initialize.selector, _setToken); + _invokeManager(_delegatedManager, address(auctionModule), callData); + } + +} diff --git a/test/global-extensions/globalAuctionRebalanceExtension.spec.ts b/test/global-extensions/globalAuctionRebalanceExtension.spec.ts new file mode 100644 index 00000000..facdbc80 --- /dev/null +++ b/test/global-extensions/globalAuctionRebalanceExtension.spec.ts @@ -0,0 +1,614 @@ +import "module-alias/register"; + +import { Address, Account } from "@utils/types"; +import { ADDRESS_ZERO, ZERO } from "@utils/constants"; +import { GlobalAuctionRebalanceExtension, DelegatedManager, ManagerCore, ConstantPriceAdapter } from "@utils/contracts/index"; +import { SetToken } from "@utils/contracts/setV2"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getSetFixture, + getWaffleExpect, + bitcoin, + usdc, + getTransactionTimestamp, + increaseTimeAsync, + getRandomAccount, +} from "@utils/index"; +import { SetFixture } from "@utils/fixtures"; +import { BigNumber, ContractTransaction } from "ethers"; + +const expect = getWaffleExpect(); + +describe("GlobalAuctionRebalanceExtension", () => { + let owner: Account; + let methodologist: Account; + let operator: Account; + let factory: Account; + + + let setV2Setup: SetFixture; + + let deployer: DeployHelper; + let setToken: SetToken; + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + + let auctionRebalanceExtension: GlobalAuctionRebalanceExtension; + + let priceAdapter: ConstantPriceAdapter; + + before(async () => { + [ + owner, + methodologist, + operator, + factory, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSetFixture(owner.address); + await setV2Setup.initialize(); + + priceAdapter = await deployer.setV2.deployConstantPriceAdapter(); + + await setV2Setup.integrationRegistry.addIntegration( + setV2Setup.auctionModule.address, + "ConstantPriceAdapter", + priceAdapter.address + ); + + managerCore = await deployer.managerCore.deployManagerCore(); + auctionRebalanceExtension = await deployer.globalExtensions.deployGlobalAuctionRebalanceExtension( + managerCore.address, + setV2Setup.auctionModule.address + ); + + + setToken = await setV2Setup.createSetToken( + [setV2Setup.dai.address, setV2Setup.wbtc.address, setV2Setup.weth.address], + [ether(100), bitcoin(.01), ether(.1)], + [setV2Setup.auctionModule.address, setV2Setup.issuanceModule.address] + ); + + await setV2Setup.issuanceModule.initialize( + setToken.address, + ADDRESS_ZERO + ); + + delegatedManager = await deployer.manager.deployDelegatedManager( + setToken.address, + factory.address, + methodologist.address, + [auctionRebalanceExtension.address], + [operator.address], + [setV2Setup.dai.address, setV2Setup.weth.address], + true + ); + await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([auctionRebalanceExtension.address], [factory.address]); + await managerCore.connect(factory.wallet).addManager(delegatedManager.address); + + + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", () => { + let subjectManagerCore: Address; + let subjectAuctionRebalanceModule: Address; + + beforeEach(async () => { + subjectManagerCore = managerCore.address; + subjectAuctionRebalanceModule = setV2Setup.auctionModule.address; + }); + + async function subject(): Promise { + const extension = await deployer.globalExtensions.deployGlobalAuctionRebalanceExtension( + subjectManagerCore, + subjectAuctionRebalanceModule + ); + await delegatedManager.addExtensions([extension.address]); + return extension; + } + + it("should set the correct manager core", async () => { + const auctionExtension = await subject(); + + const actualManagerCore = await auctionExtension.managerCore(); + expect(actualManagerCore).to.eq(subjectManagerCore); + }); + + it("should set the correct auction rebalance module address", async () => { + const auctionExtension = await subject(); + + const actualAuctionRebalanceModule = await auctionExtension.auctionModule(); + expect(actualAuctionRebalanceModule).to.eq(subjectAuctionRebalanceModule); + }); + it("should be able to initialize extension and module at the same time", async () => { + const auctionExtension = await subject(); + await expect(auctionExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address)).to.not.be.reverted; + }); + + it("should revert if module is initialized and extension is not", async () => { + const extension = await deployer.globalExtensions.deployGlobalAuctionRebalanceExtension( + subjectManagerCore, + subjectAuctionRebalanceModule + ); + await expect(extension.connect(owner.wallet).initializeModule(delegatedManager.address)).to.be.revertedWith("Extension must be initialized"); + }); + + it("should revert if module is initialized without being added", async () => { + const extension = await deployer.globalExtensions.deployGlobalAuctionRebalanceExtension( + subjectManagerCore, + subjectAuctionRebalanceModule + ); + await expect(extension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address)).to.be.revertedWith("Extension must be pending"); + }); + + it("should revert if extension is initialized without being added", async () => { + const extension = await deployer.globalExtensions.deployGlobalAuctionRebalanceExtension( + subjectManagerCore, + subjectAuctionRebalanceModule + ); + await expect(extension.connect(owner.wallet).initializeExtension(delegatedManager.address)).to.be.revertedWith("Extension must be pending"); + }); + + }); + + context("when auction rebalance extension is deployed and module needs to be initialized", () => { + let subjectCaller: Account; + let subjectDelegatedManager: Address; + + beforeEach(async () => { + subjectCaller = owner; + subjectDelegatedManager = delegatedManager.address; + await auctionRebalanceExtension.connect(subjectCaller.wallet).initializeExtension(delegatedManager.address); + }); + + describe("#initializeModule", () => { + + async function subject() { + return await auctionRebalanceExtension.connect(subjectCaller.wallet).initializeModule(subjectDelegatedManager); + } + + it("should initialize AuctionRebalanceModule", async () => { + await subject(); + const isInitialized = await setToken.isInitializedModule(setV2Setup.auctionModule.address); + expect(isInitialized).to.be.true; + }); + + it("should set the correct delegated manager for the given setToken", async () => { + await subject(); + const actualManager = await auctionRebalanceExtension.setManagers(setToken.address); + expect(actualManager).to.eq(delegatedManager.address); + }); + + describe("when the initializer is not the owner", () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + }); + + context("when auction rebalance extension is deployed and system fully set up", () => { + beforeEach(async () => { + await auctionRebalanceExtension.connect(owner.wallet).initializeModule(delegatedManager.address); + }); + + describe("#startRebalance", () => { + let subjectQuoteAsset: Address; + let subjectOldComponents: Address[]; + let subjectNewComponents: Address[]; + let subjectNewComponentsAuctionParams: any[]; + let subjectOldComponentsAuctionParams: any[]; + let subjectShouldLockSetToken: boolean; + let subjectRebalanceDuration: BigNumber; + let subjectPositionMultiplier: BigNumber; + let subjectCaller: Account; + let subjectSetToken: Address; + + beforeEach(async () => { + subjectQuoteAsset = setV2Setup.weth.address; + + subjectOldComponents = [setV2Setup.dai.address, setV2Setup.wbtc.address, setV2Setup.weth.address]; + subjectNewComponents = [setV2Setup.usdc.address]; + + subjectNewComponentsAuctionParams = [ + { + targetUnit: usdc(100), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), + }, + ]; + + subjectOldComponentsAuctionParams = [ + { + targetUnit: ether(50), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), + }, + { + targetUnit: bitcoin(.01), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), + }, + { + targetUnit: ether(.1), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), + }, + ]; + + subjectShouldLockSetToken = true; + subjectRebalanceDuration = BigNumber.from(86400); + subjectPositionMultiplier = ether(.999); + subjectCaller = operator; + subjectSetToken = setToken.address; + }); + + async function subject(): Promise { + return await auctionRebalanceExtension.connect(subjectCaller.wallet).startRebalance( + subjectSetToken, + subjectQuoteAsset, + subjectOldComponents, + subjectNewComponents, + subjectNewComponentsAuctionParams, + subjectOldComponentsAuctionParams, + subjectShouldLockSetToken, + subjectRebalanceDuration, + subjectPositionMultiplier + ); + } + + it("should set the auction execution params correctly", async () => { + await subject(); + expect(1).to.eq(1); + + const aggregateComponents = [...subjectOldComponents, ...subjectNewComponents]; + const aggregateAuctionParams = [...subjectOldComponentsAuctionParams, ...subjectNewComponentsAuctionParams]; + + for (let i = 0; i < aggregateAuctionParams.length; i++) { + const executionInfo = await setV2Setup.auctionModule.executionInfo(setToken.address, aggregateComponents[i]); + expect(executionInfo.targetUnit).to.eq(aggregateAuctionParams[i].targetUnit); + expect(executionInfo.priceAdapterName).to.eq(aggregateAuctionParams[i].priceAdapterName); + expect(executionInfo.priceAdapterConfigData).to.eq(aggregateAuctionParams[i].priceAdapterConfigData); + } + }); + + it("should set the rebalance info correctly", async () => { + const txnTimestamp = await getTransactionTimestamp(subject()); + + const rebalanceInfo = await setV2Setup.auctionModule.rebalanceInfo(setToken.address); + + expect(rebalanceInfo.quoteAsset).to.eq(subjectQuoteAsset); + expect(rebalanceInfo.rebalanceStartTime).to.eq(txnTimestamp); + expect(rebalanceInfo.rebalanceDuration).to.eq(subjectRebalanceDuration); + expect(rebalanceInfo.positionMultiplier).to.eq(subjectPositionMultiplier); + expect(rebalanceInfo.raiseTargetPercentage).to.eq(ZERO); + + const rebalanceComponents = await setV2Setup.auctionModule.getRebalanceComponents(setToken.address); + const aggregateComponents = [...subjectOldComponents, ...subjectNewComponents]; + + for (let i = 0; i < rebalanceComponents.length; i++) { + expect(rebalanceComponents[i]).to.eq(aggregateComponents[i]); + } + }); + + describe("when there are no new components", () => { + beforeEach(async () => { + subjectNewComponents = []; + subjectNewComponentsAuctionParams = []; + }); + + it("should set the auction execution params correctly", async () => { + await subject(); + + for (let i = 0; i < subjectOldComponents.length; i++) { + const executionInfo = await setV2Setup.auctionModule.executionInfo(setToken.address, subjectOldComponents[i]); + expect(executionInfo.targetUnit).to.eq(subjectOldComponentsAuctionParams[i].targetUnit); + expect(executionInfo.priceAdapterName).to.eq(subjectOldComponentsAuctionParams[i].priceAdapterName); + expect(executionInfo.priceAdapterConfigData).to.eq(subjectOldComponentsAuctionParams[i].priceAdapterConfigData); + } + }); + + it("should set the rebalance info correctly", async () => { + const txnTimestamp = await getTransactionTimestamp(subject()); + + const rebalanceInfo = await setV2Setup.auctionModule.rebalanceInfo(setToken.address); + + expect(rebalanceInfo.quoteAsset).to.eq(subjectQuoteAsset); + expect(rebalanceInfo.rebalanceStartTime).to.eq(txnTimestamp); + expect(rebalanceInfo.rebalanceDuration).to.eq(subjectRebalanceDuration); + expect(rebalanceInfo.positionMultiplier).to.eq(subjectPositionMultiplier); + expect(rebalanceInfo.raiseTargetPercentage).to.eq(ZERO); + + const rebalanceComponents = await setV2Setup.auctionModule.getRebalanceComponents(setToken.address); + for (let i = 0; i < rebalanceComponents.length; i++) { + expect(rebalanceComponents[i]).to.eq(subjectOldComponents[i]); + } + }); + }); + + describe("when old components are passed in different order", () => { + beforeEach(async () => { + subjectOldComponents = [setV2Setup.dai.address, setV2Setup.weth.address, setV2Setup.wbtc.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Mismatch: old and current components"); + }); + }); + + describe("when old components array is shorter than current components array", () => { + beforeEach(async () => { + subjectOldComponents = [setV2Setup.dai.address, setV2Setup.wbtc.address]; + subjectOldComponentsAuctionParams = [ + { + targetUnit: ether(50), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), + }, + { + targetUnit: bitcoin(.01), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), + }, + ]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Mismatch: old and current components length"); + }); + }); + + describe("when old components array is longer than current components array", () => { + beforeEach(async () => { + const price = await priceAdapter.getEncodedData(ether(1)); + subjectOldComponents = [setV2Setup.dai.address, setV2Setup.wbtc.address, setV2Setup.weth.address, setV2Setup.usdc.address]; + subjectOldComponentsAuctionParams = [ + { + targetUnit: ether(50), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: price, + }, + { + targetUnit: bitcoin(.01), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: price, + }, + { + targetUnit: ether(.1), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: price, + }, + { + targetUnit: usdc(100), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: price, + }, + ]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Mismatch: old and current components length"); + }); + }); + + describe("when not all old components have an entry", () => { + beforeEach(async () => { + subjectOldComponents = [setV2Setup.dai.address, setV2Setup.wbtc.address, setV2Setup.usdc.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Mismatch: old and current components"); + }); + }); + + describe("when the caller is not the operator", () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be approved operator"); + }); + }); + }); + + describe("#unlock", () => { + let subjectCaller: Account; + + beforeEach(async () => { + const oldComponents = [setV2Setup.dai.address, setV2Setup.wbtc.address, setV2Setup.weth.address]; + + const oldComponentsAuctionParams = [ + { + targetUnit: ether(100), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), + }, + { + targetUnit: bitcoin(.01), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), + }, + { + targetUnit: ether(.1), + priceAdapterName: "ConstantPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), + }, + ]; + + await auctionRebalanceExtension.connect(operator.wallet).startRebalance( + setToken.address, + setV2Setup.weth.address, + oldComponents, + [], + [], + oldComponentsAuctionParams, + true, + BigNumber.from(5), + ether(.999) + ); + + subjectCaller = operator; + }); + + async function subject(): Promise { + await increaseTimeAsync(BigNumber.from(6)); + return await auctionRebalanceExtension.connect(subjectCaller.wallet).unlock(setToken.address); + } + + it("should unlock the SetToken", async () => { + const isLockedBefore = await setToken.isLocked(); + expect(isLockedBefore).to.be.true; + + await subject(); + + const isLockedAfter = await setToken.isLocked(); + expect(isLockedAfter).to.be.false; + }); + + describe("when the caller is not the operator", () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be approved operator"); + }); + }); + }); + + describe("#setRaiseTargetPercentage", () => { + let subjectRaiseTargetPercentage: BigNumber; + let subjectCaller: Account; + + beforeEach(async () => { + subjectRaiseTargetPercentage = ether(.001); + subjectCaller = operator; + }); + + async function subject(): Promise { + return await auctionRebalanceExtension.connect(subjectCaller.wallet).setRaiseTargetPercentage( + setToken.address, + subjectRaiseTargetPercentage, + ); + } + + it("should correctly set the raiseTargetPercentage", async () => { + await subject(); + + const actualRaiseTargetPercentage = (await setV2Setup.auctionModule.rebalanceInfo(setToken.address)).raiseTargetPercentage; + + expect(actualRaiseTargetPercentage).to.eq(subjectRaiseTargetPercentage); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be approved operator"); + }); + }); + }); + + describe("#setBidderStatus", () => { + let subjectBidders: Address[]; + let subjectStatuses: boolean[]; + let subjectCaller: Account; + + beforeEach(async () => { + subjectBidders = [methodologist.address]; + subjectStatuses = [true]; + subjectCaller = operator; + }); + + async function subject(): Promise { + return await auctionRebalanceExtension.connect(subjectCaller.wallet).setBidderStatus( + setToken.address, + subjectBidders, + subjectStatuses + ); + } + + it("should correctly set the bidder status", async () => { + await subject(); + + const isCaller = await setV2Setup.auctionModule.isAllowedBidder(setToken.address, subjectBidders[0]); + + expect(isCaller).to.be.true; + }); + + describe("when the caller is not the operator", () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be approved operator"); + }); + }); + }); + + describe("#setAnyoneBid", () => { + let subjectStatus: boolean; + let subjectCaller: Account; + + beforeEach(async () => { + subjectStatus = true; + subjectCaller = operator; + }); + + async function subject(): Promise { + return await auctionRebalanceExtension.connect(subjectCaller.wallet).setAnyoneBid( + setToken.address, + subjectStatus + ); + } + + it("should correctly set anyone bid", async () => { + await subject(); + + const anyoneBid = await setV2Setup.auctionModule.permissionInfo(setToken.address); + + expect(anyoneBid).to.be.true; + }); + + describe("when the caller is not the operator", () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be approved operator"); + }); + }); + }); + + describe("#removeExtension", () => { + async function subject() { + return await delegatedManager.connect(owner.wallet).removeExtensions([auctionRebalanceExtension.address]); + } + it("should remove the extension", async () => { + const setManagerBeforeRemove = auctionRebalanceExtension.setManagers(setToken.address); + await subject(); + const setManagerAfterRemove = auctionRebalanceExtension.setManagers(setToken.address); + + expect(setManagerBeforeRemove).to.not.eq(setManagerAfterRemove); + }); + }) + ; }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 94082263..54edd46d 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -63,3 +63,4 @@ export { GlobalStreamingFeeSplitExtension } from "../../typechain/GlobalStreamin export { GlobalBatchTradeExtension } from "../../typechain/GlobalBatchTradeExtension"; export { GlobalWrapExtension } from "../../typechain/GlobalWrapExtension"; export { GlobalClaimExtension } from "../../typechain/GlobalClaimExtension"; +export { GlobalAuctionRebalanceExtension } from "../../typechain/GlobalAuctionRebalanceExtension"; \ No newline at end of file diff --git a/utils/deploys/deployGlobalExtensions.ts b/utils/deploys/deployGlobalExtensions.ts index b148e42e..657e8aaf 100644 --- a/utils/deploys/deployGlobalExtensions.ts +++ b/utils/deploys/deployGlobalExtensions.ts @@ -6,7 +6,8 @@ import { GlobalIssuanceExtension, GlobalStreamingFeeSplitExtension, GlobalTradeExtension, - GlobalWrapExtension + GlobalWrapExtension, + GlobalAuctionRebalanceExtension } from "../contracts/index"; import { GlobalBatchTradeExtension__factory } from "../../typechain/factories/GlobalBatchTradeExtension__factory"; @@ -15,6 +16,7 @@ import { GlobalIssuanceExtension__factory } from "../../typechain/factories/Glob import { GlobalStreamingFeeSplitExtension__factory } from "../../typechain/factories/GlobalStreamingFeeSplitExtension__factory"; import { GlobalTradeExtension__factory } from "../../typechain/factories/GlobalTradeExtension__factory"; import { GlobalWrapExtension__factory } from "../../typechain/factories/GlobalWrapExtension__factory"; +import { GlobalAuctionRebalanceExtension__factory } from "../../typechain/factories/GlobalAuctionRebalanceExtension__factory"; export default class DeployGlobalExtensions { private _deployerSigner: Signer; @@ -88,4 +90,14 @@ export default class DeployGlobalExtensions { wrapModule, ); } + + public async deployGlobalAuctionRebalanceExtension( + managerCore: Address, + auctionModule: Address + ): Promise { + return await new GlobalAuctionRebalanceExtension__factory(this._deployerSigner).deploy( + managerCore, + auctionModule, + ); + } }