diff --git a/src/policies/cooler/CoolerV2Migrator.sol b/src/policies/cooler/CoolerV2Migrator.sol new file mode 100644 index 00000000..2ffa84e6 --- /dev/null +++ b/src/policies/cooler/CoolerV2Migrator.sol @@ -0,0 +1,439 @@ +// SPDX-License-Identifier: GLP-3.0 +pragma solidity ^0.8.15; + +// Interfaces +import {IERC3156FlashBorrower} from "src/interfaces/maker-dao/IERC3156FlashBorrower.sol"; +import {IERC3156FlashLender} from "src/interfaces/maker-dao/IERC3156FlashLender.sol"; +import {IDaiUsdsMigrator} from "src/interfaces/maker-dao/IDaiUsdsMigrator.sol"; +import {ICoolerV2Migrator} from "../interfaces/cooler/ICoolerV2Migrator.sol"; + +// Libraries +import {ReentrancyGuard} from "solmate/utils/ReentrancyGuard.sol"; +import {SafeCast} from "src/libraries/SafeCast.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; + +// Bophades +import {Kernel, Keycode, Permissions, Policy, toKeycode} from "src/Kernel.sol"; +import {CoolerFactory} from "src/external/cooler/CoolerFactory.sol"; +import {Clearinghouse} from "src/policies/Clearinghouse.sol"; +import {Cooler} from "src/external/cooler/Cooler.sol"; +import {CHREGv1} from "src/modules/CHREG/CHREG.v1.sol"; +import {RGSTYv1} from "src/modules/RGSTY/RGSTY.v1.sol"; +import {IMonoCooler} from "src/policies/interfaces/cooler/IMonoCooler.sol"; +import {IDLGTEv1} from "src/modules/DLGTE/IDLGTE.v1.sol"; +import {ROLESv1} from "src/modules/ROLES/ROLES.v1.sol"; +import {PolicyEnabler} from "src/policies/utils/PolicyEnabler.sol"; + +/// @title CoolerV2Migrator +/// @notice A contract that migrates debt from Olympus Cooler V1 facilities to Cooler V2. +/// This is compatible with all three versions of Cooler V1. +/// @dev This contract uses the `IERC3156FlashBorrower` interface to interact with Maker flashloans. +contract CoolerV2Migrator is + IERC3156FlashBorrower, + ICoolerV2Migrator, + ReentrancyGuard, + Policy, + PolicyEnabler +{ + using SafeCast for uint256; + using SafeTransferLib for ERC20; + + // ========= DATA STRUCTURES ========= // + + /// @notice Data structure used to store data about a Cooler + struct CoolerData { + Cooler cooler; + ERC20 debtToken; + uint256 numLoans; + } + + /// @notice Data structure used for flashloan parameters + struct FlashLoanData { + CoolerData[] coolers; + address currentOwner; + address newOwner; + uint256 usdsRequired; + IDLGTEv1.DelegationRequest[] delegationRequests; + } + + // ========= MODULES ========= // + + /// @notice The Clearinghouse registry module + /// @dev The value is set when the policy is activated + CHREGv1 internal CHREG; + + /// @notice The contract registry module + /// @dev The value is set when the policy is activated + RGSTYv1 internal RGSTY; + + // ========= STATE ========= // + + /// @notice The DAI token + /// @dev The value is set when the policy is activated + ERC20 internal DAI; + + /// @notice The USDS token + /// @dev The value is set when the policy is activated + ERC20 internal USDS; + + /// @notice The gOHM token + /// @dev The value is set when the policy is activated + ERC20 internal GOHM; + + /// @notice The DAI <> USDS Migrator + /// @dev The value is set when the policy is activated + IDaiUsdsMigrator internal MIGRATOR; + + /// @notice The ERC3156 flash loan provider + /// @dev The value is set when the policy is activated + IERC3156FlashLender internal FLASH; + + /// @notice The Cooler V2 contract + IMonoCooler internal immutable COOLERV2; + + // ========= CONSTRUCTOR ========= // + + constructor(address kernel_, address coolerV2_) Policy(Kernel(kernel_)) { + // Validate + if (coolerV2_ == address(0)) revert Params_InvalidAddress("coolerV2"); + + COOLERV2 = IMonoCooler(coolerV2_); + } + + /// @inheritdoc Policy + function configureDependencies() external override returns (Keycode[] memory dependencies) { + dependencies = new Keycode[](3); + dependencies[0] = toKeycode("CHREG"); + dependencies[1] = toKeycode("RGSTY"); + dependencies[2] = toKeycode("ROLES"); + + // Populate module dependencies + CHREG = CHREGv1(getModuleAddress(dependencies[0])); + RGSTY = RGSTYv1(getModuleAddress(dependencies[1])); + ROLES = ROLESv1(getModuleAddress(dependencies[2])); + + // Ensure Modules are using the expected major version. + // Modules should be sorted in alphabetical order. + bytes memory expected = abi.encode([1, 1, 1]); + (uint8 CHREG_MAJOR, ) = CHREG.VERSION(); + (uint8 RGSTY_MAJOR, ) = RGSTY.VERSION(); + (uint8 ROLES_MAJOR, ) = ROLES.VERSION(); + if (CHREG_MAJOR != 1 || RGSTY_MAJOR != 1 || ROLES_MAJOR != 1) + revert Policy_WrongModuleVersion(expected); + + // Populate variables + // This function will be called whenever a contract is registered or deregistered, which enables caching of the values + // Token contract addresses are immutable + DAI = ERC20(RGSTY.getImmutableContract("dai")); + USDS = ERC20(RGSTY.getImmutableContract("usds")); + GOHM = ERC20(RGSTY.getImmutableContract("gohm")); + // Utility contract addresses are mutable + FLASH = IERC3156FlashLender(RGSTY.getContract("flash")); + MIGRATOR = IDaiUsdsMigrator(RGSTY.getContract("dmgtr")); + + return dependencies; + } + + /// @inheritdoc Policy + /// @dev This policy does not require any permissions + function requestPermissions() external pure override returns (Permissions[] memory requests) { + requests = new Permissions[](0); + + return requests; + } + + // ========= OPERATION ========= // + + /// @inheritdoc ICoolerV2Migrator + function previewConsolidate( + address[] memory coolers_ + ) external view onlyEnabled returns (uint256 collateralAmount, uint256 borrowAmount) { + // Determine the totals + uint256 totalDebt; + for (uint256 i; i < coolers_.length; i++) { + Cooler cooler = Cooler(coolers_[i]); + + (uint256 principal, uint256 interest, uint256 collateral, , ) = _getDebtForCooler( + cooler + ); + + collateralAmount += collateral; + totalDebt += principal + interest; + } + + // Determine the lender fee + uint256 lenderFee = FLASH.flashFee(address(DAI), totalDebt); + borrowAmount = totalDebt + lenderFee; + + return (collateralAmount, borrowAmount); + } + + /// @inheritdoc ICoolerV2Migrator + /// @dev This function will revert if: + /// - The number of elements in `clearinghouses_` and `coolers_` are not the same. + /// - Any of the Coolers are not owned by the caller. + /// - Any of the Clearinghouses are not owned by the Olympus protocol. + /// - Any of the Coolers have not been created by the Clearinghouse's CoolerFactory. + /// - A duplicate Cooler is provided. + /// - The owner of the destination Cooler V2 has not provided authorization for this contract to manage their Cooler V2 position. + /// - The caller has not approved this contract to spend the collateral token, gOHM. + /// - The contract is not active. + /// - Re-entrancy is detected. + function consolidate( + address[] memory coolers_, + address[] memory clearinghouses_, + address newOwner_, + IMonoCooler.Authorization memory authorization_, + IMonoCooler.Signature calldata signature_, + IDLGTEv1.DelegationRequest[] calldata delegationRequests_ + ) external onlyEnabled nonReentrant { + // Validate that the number of clearinghouses and coolers are the same + if (clearinghouses_.length != coolers_.length) revert Params_InvalidArrays(); + + // Validate that the Clearinghouses and Coolers are protocol-owned + // Also calculate the principal and interest for each cooler + CoolerData[] memory coolerData = new CoolerData[](clearinghouses_.length); + + // Keep track of the total principal and interest for each debt token + uint256 daiRequired; + uint256 usdsRequired; + for (uint256 i; i < clearinghouses_.length; i++) { + // Check that the Clearinghouse is owned by the protocol + if (!_isValidClearinghouse(clearinghouses_[i])) revert Params_InvalidClearinghouse(); + + // Check that the Clearinghouse's CoolerFactory created the Cooler + if (!_isValidCooler(clearinghouses_[i], coolers_[i])) revert Params_InvalidCooler(); + + // Check that the Cooler is owned by the caller + Cooler cooler = Cooler(coolers_[i]); + if (cooler.owner() != msg.sender) revert Only_CoolerOwner(); + + // Check that the Cooler is not already in the array + for (uint256 j; j < coolerData.length; j++) { + if (address(coolerData[j].cooler) == coolers_[i]) revert Params_DuplicateCooler(); + } + + // Determine the total principal and interest for the cooler + ( + uint256 coolerPrincipal, + uint256 coolerInterest, + , + address debtToken, + uint256 numLoans + ) = _getDebtForCooler(cooler); + coolerData[i] = CoolerData({ + cooler: cooler, + debtToken: ERC20(debtToken), + numLoans: numLoans + }); + + if (debtToken == address(DAI)) { + daiRequired += coolerPrincipal; + daiRequired += coolerInterest; + } else if (debtToken == address(USDS)) { + usdsRequired += coolerPrincipal; + usdsRequired += coolerInterest; + } else { + // Unsupported debt token + revert Params_InvalidCooler(); + } + } + + // Set the Cooler V2 authorization signature, if provided + // If the new owner cannot provide a signature (e.g. multisig), they can call `IMonoCooler.setAuthorization()` instead + if (authorization_.account != address(0)) { + // Validate that authorization provider and new owner matches + if (authorization_.account != newOwner_) revert Params_InvalidNewOwner(); + + // Authorize this contract to manage user Cooler V2 position + COOLERV2.setAuthorizationWithSig(authorization_, signature_); + } + + // Take flashloan + // This will trigger the `onFlashLoan` function after the flashloan amount has been transferred to this contract + FLASH.flashLoan( + this, + address(DAI), + daiRequired + usdsRequired, + abi.encode( + FlashLoanData(coolerData, msg.sender, newOwner_, usdsRequired, delegationRequests_) + ) + ); + + // This shouldn't happen, but transfer any leftover funds back to the sender + uint256 usdsBalanceAfter = USDS.balanceOf(address(this)); + if (usdsBalanceAfter > 0) { + USDS.safeTransfer(msg.sender, usdsBalanceAfter); + } + uint256 daiBalanceAfter = DAI.balanceOf(address(this)); + if (daiBalanceAfter > 0) { + DAI.safeTransfer(msg.sender, daiBalanceAfter); + } + } + + /// @inheritdoc IERC3156FlashBorrower + /// @dev This function reverts if: + /// - The caller is not the flash loan provider + /// - The initiator is not this contract + function onFlashLoan( + address initiator_, + address, // flashloan token is only DAI + uint256 amount_, + uint256 lenderFee_, + bytes calldata params_ + ) external override returns (bytes32) { + // perform sanity checks + if (msg.sender != address(FLASH)) revert OnlyLender(); + if (initiator_ != address(this)) revert OnlyThis(); + + // Unpack param data + FlashLoanData memory flashLoanData = abi.decode(params_, (FlashLoanData)); + CoolerData[] memory coolers = flashLoanData.coolers; + + // If there are loans in USDS, convert the required amount to DAI + if (flashLoanData.usdsRequired > 0) { + DAI.safeApprove(address(MIGRATOR), flashLoanData.usdsRequired); + MIGRATOR.daiToUsds(address(this), flashLoanData.usdsRequired); + } + + // Keep track of debt tokens out and collateral in + uint256 totalRepaid; + uint256 totalCollateral; + + // Validate and repay loans for each cooler + for (uint256 i; i < coolers.length; ++i) { + CoolerData memory coolerData = coolers[i]; + + (uint256 repaid, uint256 collateral) = _handleRepayments( + coolerData.cooler, + coolerData.debtToken, + coolerData.numLoans + ); + totalRepaid += repaid; + totalCollateral += collateral; + } + + // Transfer the collateral from the cooler owner to this contract + GOHM.safeTransferFrom(flashLoanData.currentOwner, address(this), totalCollateral); + + // Approve the Cooler V2 to spend the collateral + GOHM.safeApprove(address(COOLERV2), totalCollateral); + + // Add collateral and borrow spent flash loan from Cooler V2 + COOLERV2.addCollateral( + totalCollateral.encodeUInt128(), + flashLoanData.newOwner, + flashLoanData.delegationRequests + ); + COOLERV2.borrow( + (totalRepaid + lenderFee_).encodeUInt128(), + flashLoanData.newOwner, + address(this) + ); + + // Convert the USDS to DAI + uint256 usdsBalance = USDS.balanceOf(address(this)); + if (usdsBalance > 0) MIGRATOR.usdsToDai(address(this), usdsBalance); + + // Approve the flash loan provider to collect the flashloan amount and fee + // The initiator will transfer any remaining DAI and USDS back to the caller + DAI.safeApprove(address(FLASH), amount_ + lenderFee_); + + return keccak256("ERC3156FlashBorrower.onFlashLoan"); + } + + function _handleRepayments( + Cooler cooler_, + ERC20 debtToken_, + uint256 numLoans_ + ) internal returns (uint256 repaid, uint256 collateral) { + // Provide upfront infinite approval to cooler + // The consolidate() function is gated by a nonReentrant modifier, so there cannot be a reentrancy attack during the approval and revocation + debtToken_.safeApprove(address(cooler_), type(uint256).max); + + // Iterate through and repay loans + for (uint256 i; i < numLoans_; i++) { + Cooler.Loan memory loan = cooler_.getLoan(i); + + // Only repay outstanding loans + if (loan.principal > 0) { + uint256 amount = loan.principal + loan.interestDue; + + repaid += amount; + collateral += loan.collateral; + + cooler_.repayLoan(i, amount); + } + } + + // Revoke approval + debtToken_.safeApprove(address(cooler_), 0); + + return (repaid, collateral); + } + + // ========= HELPER FUNCTIONS ========= // + + function _getDebtForCooler( + Cooler cooler_ + ) + internal + view + returns ( + uint256 coolerPrincipal, + uint256 coolerInterest, + uint256 coolerCollateral, + address debtToken, + uint256 numLoans + ) + { + // Determine the debt token + debtToken = address(cooler_.debt()); + + // The Cooler contract does not expose the number of loans, so we iterate until the call reverts + uint256 i; + while (true) { + try cooler_.getLoan(i) returns (Cooler.Loan memory loan) { + // Interest is paid down first, so if the principal is 0, the loan has been paid off + if (loan.principal > 0) { + coolerPrincipal += loan.principal; + coolerInterest += loan.interestDue; + coolerCollateral += loan.collateral; + } + i++; + } catch { + break; + } + } + + return (coolerPrincipal, coolerInterest, coolerCollateral, debtToken, i); + } + + /// @notice Check if a given cooler was created by the CoolerFactory for a Clearinghouse + /// @dev This function assumes that the authenticity of the Clearinghouse is already verified + /// + /// @param clearinghouse_ Clearinghouse contract + /// @param cooler_ Cooler contract + /// @return bool Whether the cooler was created by the CoolerFactory for the Clearinghouse + function _isValidCooler(address clearinghouse_, address cooler_) internal view returns (bool) { + Clearinghouse clearinghouse = Clearinghouse(clearinghouse_); + CoolerFactory coolerFactory = CoolerFactory(clearinghouse.factory()); + + return coolerFactory.created(cooler_); + } + + function _isValidClearinghouse(address clearinghouse_) internal view returns (bool) { + // We check against the registry (not just active), as repayments are still allowed when a Clearinghouse is deactivated + uint256 registryCount = CHREG.registryCount(); + bool found; + for (uint256 i; i < registryCount; i++) { + if (CHREG.registry(i) == clearinghouse_) { + found = true; + break; + } + } + + return found; + } +} diff --git a/src/policies/interfaces/cooler/ICoolerV2Migrator.sol b/src/policies/interfaces/cooler/ICoolerV2Migrator.sol new file mode 100644 index 00000000..ab4e67b8 --- /dev/null +++ b/src/policies/interfaces/cooler/ICoolerV2Migrator.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {IMonoCooler} from "./IMonoCooler.sol"; +import {IDLGTEv1} from "../../../modules/DLGTE/IDLGTE.v1.sol"; + +/// @title Cooler V2 Migrator +/// @notice Interface for contracts that migrate Cooler V1 loans to Cooler V2 +interface ICoolerV2Migrator { + // ========= ERRORS ========= // + + /// @notice Thrown when the caller is not the contract itself. + error OnlyThis(); + + /// @notice Thrown when the caller is not the flash lender. + error OnlyLender(); + + /// @notice Thrown when the Cooler is not owned by the caller + error Only_CoolerOwner(); + + /// @notice Thrown when the number of Clearinghouses does not equal the number of Coolers + error Params_InvalidArrays(); + + /// @notice Thrown when the Clearinghouse is not valid + error Params_InvalidClearinghouse(); + + /// @notice Thrown when the Cooler is not valid + error Params_InvalidCooler(); + + /// @notice Thrown when the new owner address provided does not match the authorization + error Params_InvalidNewOwner(); + + /// @notice Thrown when the Cooler is duplicated + error Params_DuplicateCooler(); + + /// @notice Thrown when the address is invalid + error Params_InvalidAddress(string reason_); + + // ========= FUNCTIONS ========= // + + /// @notice Preview the consolidation of a set of loans. + /// + /// @param coolers_ The Coolers to consolidate the loans from. + /// @return collateralAmount The amount of collateral that will be migrated into Cooler V2. + /// @return borrowAmount The amount of debt that will be borrowed from Cooler V2. + function previewConsolidate( + address[] calldata coolers_ + ) external view returns (uint256 collateralAmount, uint256 borrowAmount); + + /// @notice Consolidate Cooler V1 loans into Cooler V2 + /// + /// This function supports consolidation of loans from multiple Clearinghouses and Coolers, provided that the caller is the owner. + /// + /// As Cooler V2 has a higher LTV than Cooler V1, the additional funds required to pay for the interest and fees do not need to be provided by the caller, and will be borrowed from Cooler V2. + /// + /// It is expected that the caller will have already provided approval for this contract to spend the required tokens. See `requiredApprovals()` for more details. + /// + /// @dev The implementing function is expected to handle the following: + /// - Ensure that `clearinghouses_` and `coolers_` are valid + /// - Ensure that the caller is the owner of the Coolers + /// - Repay all loans in the Coolers + /// - Deposit the collateral into Cooler V2 + /// - Borrow the required amount from Cooler V2 to repay the Cooler V1 loans + /// + /// @param coolers_ The Coolers from which the loans will be migrated. + /// @param clearinghouses_ The respective Clearinghouses that created and issued the loans in `coolers_`. This array must be the same length as `coolers_`. + /// @param newOwner_ Address of the owner of the Cooler V2 position. This can be the same as the caller, or a different address. + /// @param authorization_ Authorization parameters for the new owner. Set the `account` field to the zero address to indicate that authorization has already been provided through `IMonoCooler.setAuthorization()`. + /// @param signature_ Authorization signature for the new owner. Ignored if `authorization_.account` is the zero address. + /// @param delegationRequests_ Delegation requests for the new owner. + function consolidate( + address[] memory coolers_, + address[] memory clearinghouses_, + address newOwner_, + IMonoCooler.Authorization memory authorization_, + IMonoCooler.Signature calldata signature_, + IDLGTEv1.DelegationRequest[] calldata delegationRequests_ + ) external; +} diff --git a/src/test/policies/cooler/CoolerV2Migrator.t.sol b/src/test/policies/cooler/CoolerV2Migrator.t.sol new file mode 100644 index 00000000..fe7ac903 --- /dev/null +++ b/src/test/policies/cooler/CoolerV2Migrator.t.sol @@ -0,0 +1,1479 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {MonoCoolerBaseTest} from "./MonoCoolerBase.t.sol"; + +import {Actions, Kernel} from "src/Kernel.sol"; +import {PolicyEnabler} from "src/policies/utils/PolicyEnabler.sol"; +import {CoolerV2Migrator} from "src/policies/cooler/CoolerV2Migrator.sol"; +import {Clearinghouse} from "src/policies/Clearinghouse.sol"; +import {Cooler} from "src/external/cooler/Cooler.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {MockERC4626} from "solmate/test/utils/mocks/MockERC4626.sol"; +import {MockDaiUsds} from "src/test/mocks/MockDaiUsds.sol"; +import {MockFlashloanLender} from "src/test/mocks/MockFlashloanLender.sol"; +import {OlympusClearinghouseRegistry} from "src/modules/CHREG/OlympusClearinghouseRegistry.sol"; +import {OlympusContractRegistry} from "src/modules/RGSTY/OlympusContractRegistry.sol"; +import {ContractRegistryAdmin} from "src/policies/ContractRegistryAdmin.sol"; +import {CoolerFactory} from "src/external/cooler/CoolerFactory.sol"; +import {IMonoCooler} from "src/policies/interfaces/cooler/IMonoCooler.sol"; +import {IDLGTEv1} from "src/modules/DLGTE/IDLGTE.v1.sol"; +import {ICoolerV2Migrator} from "src/policies/interfaces/cooler/ICoolerV2Migrator.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +contract CoolerV2MigratorTest is MonoCoolerBaseTest { + CoolerV2Migrator internal migrator; + CoolerFactory internal coolerFactory; + Clearinghouse internal clearinghouseUsds; + Clearinghouse internal clearinghouseDai; + + MockERC20 internal dai; + MockERC4626 internal sDai; + MockDaiUsds internal daiMigrator; + MockFlashloanLender internal flashLender; + + OlympusClearinghouseRegistry internal clearinghouseRegistry; + OlympusContractRegistry internal contractRegistry; + ContractRegistryAdmin internal contractRegistryAdmin; + + address internal USER; + address internal USER2; + uint256 internal USER_PK; + uint256 internal USER2_PK; + + IMonoCooler.Authorization internal authorization; + IMonoCooler.Signature internal signature; + IDLGTEv1.DelegationRequest[] internal delegationRequests; + + bytes32 private constant DOMAIN_TYPEHASH = + keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"); + bytes32 private constant AUTHORIZATION_TYPEHASH = + keccak256( + "Authorization(address account,address authorized,uint96 authorizationDeadline,uint256 nonce,uint256 signatureDeadline)" + ); + + function buildDomainSeparator() internal view returns (bytes32) { + return keccak256(abi.encode(DOMAIN_TYPEHASH, block.chainid, address(cooler))); + } + + mapping(address => address) internal clearinghouseToCooler; + + function setUp() public virtual override { + // MonoCooler setup + super.setUp(); + + (USER, USER_PK) = makeAddrAndKey("user"); + (USER2, USER2_PK) = makeAddrAndKey("user2"); + vm.label(USER, "USER"); + vm.label(USER2, "USER2"); + + // Tokens + dai = new MockERC20("DAI", "DAI", 18); + sDai = new MockERC4626(dai, "sDAI", "sDAI"); + vm.label(address(dai), "DAI"); + vm.label(address(sDai), "sDAI"); + + // Ensure the treasury has sDAI + dai.mint(address(this), 200000000e18); + dai.approve(address(sDai), 200000000e18); + sDai.mint(100000000e18, address(TRSRY)); + + // Set up a mock DAI-USDS migrator + daiMigrator = new MockDaiUsds(dai, usds); + vm.label(address(daiMigrator), "DAI-USDS Migrator"); + + // Set up a mock flash loan lender + flashLender = new MockFlashloanLender(0, address(dai)); + dai.mint(address(flashLender), 1000000e18); + vm.label(address(flashLender), "Flash Lender"); + + // Grant roles + rolesAdmin.grantRole("cooler_overseer", OVERSEER); + rolesAdmin.grantRole("emergency_shutdown", OVERSEER); + rolesAdmin.grantRole("contract_registry_admin", OVERSEER); + + // Install additional modules + clearinghouseRegistry = new OlympusClearinghouseRegistry( + kernel, + address(0), + new address[](0) + ); + contractRegistry = new OlympusContractRegistry(address(kernel)); + + vm.startPrank(EXECUTOR); + kernel.executeAction(Actions.InstallModule, address(clearinghouseRegistry)); + kernel.executeAction(Actions.InstallModule, address(contractRegistry)); + vm.stopPrank(); + + // Install ContractRegistryAdmin + contractRegistryAdmin = new ContractRegistryAdmin(address(kernel)); + + vm.startPrank(EXECUTOR); + kernel.executeAction(Actions.ActivatePolicy, address(contractRegistryAdmin)); + vm.stopPrank(); + + // Install Clearinghouses + coolerFactory = new CoolerFactory(); + clearinghouseDai = new Clearinghouse( + address(ohm), + address(gohm), + address(staking), + address(sDai), + address(coolerFactory), + address(kernel) + ); + clearinghouseUsds = new Clearinghouse( + address(ohm), + address(gohm), + address(staking), + address(susds), + address(coolerFactory), + address(kernel) + ); + vm.label(address(coolerFactory), "Cooler Factory"); + vm.label(address(clearinghouseDai), "DAI Clearinghouse"); + vm.label(address(clearinghouseUsds), "USDS Clearinghouse"); + + vm.startPrank(EXECUTOR); + kernel.executeAction(Actions.ActivatePolicy, address(clearinghouseDai)); + kernel.executeAction(Actions.ActivatePolicy, address(clearinghouseUsds)); + vm.stopPrank(); + + // Activate & deactivate DAI clearinghouse + vm.startPrank(OVERSEER); + clearinghouseDai.activate(); + clearinghouseDai.emergencyShutdown(); + vm.stopPrank(); + + // Activate USDS clearinghouse + vm.startPrank(OVERSEER); + clearinghouseUsds.activate(); + vm.stopPrank(); + + // Register contracts + vm.startPrank(OVERSEER); + contractRegistryAdmin.registerImmutableContract("dai", address(dai)); + contractRegistryAdmin.registerImmutableContract("usds", address(usds)); + contractRegistryAdmin.registerImmutableContract("gohm", address(gohm)); + contractRegistryAdmin.registerContract("flash", address(flashLender)); + contractRegistryAdmin.registerContract("dmgtr", address(daiMigrator)); + vm.stopPrank(); + + // CoolerV2Migrator setup + migrator = new CoolerV2Migrator(address(kernel), address(cooler)); + + // Install the policy + vm.prank(EXECUTOR); + kernel.executeAction(Actions.ActivatePolicy, address(migrator)); + + // Enable the policy + vm.startPrank(OVERSEER); + migrator.enable(abi.encode("")); + vm.stopPrank(); + } + + // ========= MODIFIERS ========= // + + modifier givenDisabled() { + vm.startPrank(OVERSEER); + migrator.disable(abi.encode("")); + vm.stopPrank(); + _; + } + + modifier givenEnabled() { + vm.startPrank(OVERSEER); + migrator.enable(abi.encode("")); + vm.stopPrank(); + _; + } + + modifier givenWalletHasCollateralToken(address wallet_, uint256 amount_) { + gohm.mint(wallet_, amount_); + _; + } + + modifier givenDaiClearinghouseIsEnabled() { + vm.startPrank(OVERSEER); + clearinghouseDai.activate(); + vm.stopPrank(); + _; + } + + function _getClearinghouse(bool isUsds_) internal view returns (Clearinghouse clearinghouse) { + if (isUsds_) { + return Clearinghouse(address(clearinghouseUsds)); + } + + return Clearinghouse(address(clearinghouseDai)); + } + + function _createCooler( + address wallet_, + bool isUsds_ + ) internal returns (address clearinghouse_, address cooler_) { + clearinghouse_ = address(_getClearinghouse(isUsds_)); + + // Create Cooler if needed + vm.startPrank(wallet_); + cooler_ = Clearinghouse(clearinghouse_).factory().generateCooler( + gohm, + isUsds_ ? usds : dai + ); + vm.stopPrank(); + + // Store the relationship + clearinghouseToCooler[clearinghouse_] = cooler_; + + return (clearinghouse_, cooler_); + } + + function _getCoolerArrays( + bool includeUsds_, + bool includeDai_ + ) internal view returns (address[] memory coolers, address[] memory clearinghouses) { + uint256 length; + if (includeUsds_) { + length++; + } + if (includeDai_) { + length++; + } + + coolers = new address[](length); + clearinghouses = new address[](length); + uint256 index; + + if (includeUsds_) { + coolers[index] = clearinghouseToCooler[address(clearinghouseUsds)]; + clearinghouses[index] = address(clearinghouseUsds); + index++; + } + if (includeDai_) { + coolers[index] = clearinghouseToCooler[address(clearinghouseDai)]; + clearinghouses[index] = address(clearinghouseDai); + index++; + } + + return (coolers, clearinghouses); + } + + function _takeLoan(address wallet_, bool isUsds_, uint256 collateralAmount_) internal { + (address clearinghouse_, address cooler_) = _createCooler(wallet_, isUsds_); + + // Approve spending of collateral + vm.prank(wallet_); + gohm.approve(clearinghouse_, collateralAmount_); + + // Determine the loan amount + (uint256 principal, ) = Clearinghouse(clearinghouse_).getLoanForCollateral( + collateralAmount_ + ); + + // Create loan + vm.startPrank(wallet_); + Clearinghouse(clearinghouse_).lendToCooler(Cooler(cooler_), principal); + vm.stopPrank(); + } + + modifier givenWalletHasLoan( + address wallet_, + bool isUsds_, + uint256 collateralAmount_ + ) { + _takeLoan(wallet_, isUsds_, collateralAmount_); + _; + } + + modifier givenWalletHasRepaidLoan( + address wallet_, + bool isUsds_, + uint256 loanId_ + ) { + Cooler walletCooler = _getCooler(isUsds_); + Cooler.Loan memory loan = walletCooler.getLoan(loanId_); + uint256 payableAmount = loan.principal + loan.interestDue; + + // Mint debt token to the wallet and approve spending + if (isUsds_) { + usds.mint(wallet_, payableAmount); + vm.prank(wallet_); + usds.approve(address(walletCooler), payableAmount); + } else { + dai.mint(wallet_, payableAmount); + vm.prank(wallet_); + dai.approve(address(walletCooler), payableAmount); + } + + // Repay the loan + vm.prank(wallet_); + walletCooler.repayLoan(loanId_, payableAmount); + _; + } + + function _approveMigratorSpendingCollateral(address wallet_, uint256 amount_) internal { + vm.prank(wallet_); + gohm.approve(address(migrator), amount_); + } + + modifier givenWalletHasApprovedMigratorSpendingCollateral(address wallet_, uint256 amount_) { + _approveMigratorSpendingCollateral(wallet_, amount_); + _; + } + + function _getCooler(bool isUsds_) internal view returns (Cooler cooler_) { + Clearinghouse clearinghouse = _getClearinghouse(isUsds_); + cooler_ = Cooler(clearinghouseToCooler[address(clearinghouse)]); + + return cooler_; + } + + function _getLoan( + bool isUsds_, + uint256 loanId_ + ) internal view returns (Cooler.Loan memory loan) { + Cooler existingCooler = _getCooler(isUsds_); + loan = existingCooler.getLoan(loanId_); + + return loan; + } + + modifier givenFlashFee(uint16 flashFee_) { + flashLender.setFeePercent(flashFee_); + _; + } + + function signedAuth( + address account, + uint256 accountPk, + address authorized, + uint96 authorizationDeadline, + uint256 signatureDeadline + ) + internal + view + returns (IMonoCooler.Authorization memory auth, IMonoCooler.Signature memory sig) + { + bytes32 domainSeparator = buildDomainSeparator(); + auth = IMonoCooler.Authorization({ + account: account, + authorized: authorized, + authorizationDeadline: authorizationDeadline, + nonce: cooler.authorizationNonces(account), + signatureDeadline: signatureDeadline + }); + bytes32 structHash = keccak256(abi.encode(AUTHORIZATION_TYPEHASH, auth)); + bytes32 typedDataHash = ECDSA.toTypedDataHash(domainSeparator, structHash); + (sig.v, sig.r, sig.s) = vm.sign(accountPk, typedDataHash); + } + + modifier givenAuthorization() { + vm.prank(USER); + cooler.setAuthorization(address(migrator), uint96(block.timestamp + 1)); + _; + } + + modifier givenAuthorizationCleared() { + vm.prank(USER); + cooler.setAuthorization(address(migrator), 0); + _; + } + + modifier givenAuthorizationSignatureSet(address owner_, uint256 ownerPk_) { + (authorization, signature) = signedAuth( + owner_, + ownerPk_, + address(migrator), + uint96(block.timestamp + 1), + uint96(block.timestamp + 1) + ); + _; + } + + modifier givenAuthorizationSignatureCleared() { + authorization = IMonoCooler.Authorization({ + account: address(0), + authorized: address(0), + authorizationDeadline: 0, + nonce: 0, + signatureDeadline: 0 + }); + signature = IMonoCooler.Signature({v: 0, r: bytes32(0), s: bytes32(0)}); + _; + } + + modifier givenDelegationRequest(int256 amount_) { + delegationRequests.push(IDLGTEv1.DelegationRequest({delegate: USER2, amount: amount_})); + _; + } + + // ========= ASSERTIONS ========= // + + function _expectRevert_disabled() internal { + vm.expectRevert(abi.encodeWithSelector(PolicyEnabler.NotEnabled.selector)); + } + + function _assertTokenBalances( + address wallet_, + uint256 collateralTokenBalance_, + uint256 usdsBalance_, + uint256 daiBalance_ + ) internal view { + assertEq( + gohm.balanceOf(wallet_), + collateralTokenBalance_, + string.concat(Strings.toHexString(wallet_), ": wallet collateral token balance") + ); + assertEq( + usds.balanceOf(wallet_), + usdsBalance_, + string.concat(Strings.toHexString(wallet_), ": wallet USDS balance") + ); + assertEq( + dai.balanceOf(wallet_), + daiBalance_, + string.concat(Strings.toHexString(wallet_), ": wallet DAI balance") + ); + } + + function _assertCoolerV2Loan( + address wallet_, + uint256 collateralBalance_, + uint256 debtBalance_ + ) internal view { + // MonoCooler should have a collateral token balance and debt token balance + IMonoCooler.AccountPosition memory position = cooler.accountPosition(wallet_); + assertEq(position.collateral, collateralBalance_, "account position collateral"); + assertEq(position.currentDebt, debtBalance_, "account position debt"); + } + + function _assertAuthorization( + address account_, + uint256 nonce_, + uint96 deadline_ + ) internal view { + assertEq(cooler.authorizationNonces(account_), nonce_, "authorization nonce"); + assertEq( + cooler.authorizations(account_, address(migrator)), + deadline_, + "authorization deadline" + ); + } + + function _assertCoolerV1Loans(bool isUsds_, uint256 numLoans_) internal view { + Cooler coolerV1 = _getCooler(isUsds_); + + for (uint256 i; i < numLoans_; i++) { + Cooler.Loan memory loan = coolerV1.getLoan(i); + assertEq(loan.principal, 0, "loan principal"); + assertEq(loan.interestDue, 0, "loan interest due"); + } + } + + // ========= TESTS ========= // + + // previewConsolidate + // given the contract is disabled + // [X] it reverts + // given there are no loans + // [X] it returns 0 collateral + // [X] it returns 0 borrowed + // given there is a repaid loan + // [X] it ignores the repaid loan + // given the flash fee is non-zero + // [X] it returns the total collateral returned + // [X] the total borrowed is the principal + interest + flash fee + // [X] it returns the total collateral returned + // [X] the total borrowed is the principal + interest + + function test_previewConsolidate_givenDisabled_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenDisabled + { + // Prepare input data + (address[] memory coolers, ) = _getCoolerArrays(true, false); + + // Expect revert + _expectRevert_disabled(); + + // Function + migrator.previewConsolidate(coolers); + } + + function test_previewConsolidate_givenNoLoans() public { + // Create a Cooler, but no loan + _createCooler(USER, true); + + // Prepare input data + (address[] memory coolers, ) = _getCoolerArrays(true, false); + + // Function + (uint256 collateralAmount, uint256 borrowedAmount) = migrator.previewConsolidate(coolers); + + // Assertions + assertEq(collateralAmount, 0, "collateralAmount"); + assertEq(borrowedAmount, 0, "borrowedAmount"); + } + + function test_previewConsolidate_givenRepaidLoan() + public + givenWalletHasCollateralToken(USER, 2e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasRepaidLoan(USER, true, 0) + { + // Prepare input data + (address[] memory coolers, ) = _getCoolerArrays(true, false); + + Cooler.Loan memory loanOne = _getLoan(true, 1); + uint256 loanOnePayable = loanOne.principal + loanOne.interestDue; + + // Function + (uint256 collateralAmount, uint256 borrowedAmount) = migrator.previewConsolidate(coolers); + + // Assertions + assertEq(collateralAmount, 1e18, "collateralAmount"); + assertEq(borrowedAmount, loanOnePayable, "borrowedAmount"); + } + + function test_previewConsolidate_givenFlashFeeNonZero() + public + givenWalletHasCollateralToken(USER, 3e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasLoan(USER, true, 2e18) + givenFlashFee(1e2) + { + // Prepare input data + (address[] memory coolers, ) = _getCoolerArrays(true, false); + + Cooler.Loan memory loanZero = _getLoan(true, 0); + uint256 loanZeroPayable = loanZero.principal + loanZero.interestDue; + + Cooler.Loan memory loanOne = _getLoan(true, 1); + uint256 loanOnePayable = loanOne.principal + loanOne.interestDue; + + uint256 flashFee = ((loanZeroPayable + loanOnePayable) * 1e2) / 100e2; + + // Function + (uint256 collateralAmount, uint256 borrowedAmount) = migrator.previewConsolidate(coolers); + + // Assertions + assertEq(collateralAmount, 3e18, "collateralAmount"); + assertEq(borrowedAmount, loanZeroPayable + loanOnePayable + flashFee, "borrowedAmount"); + } + + function test_previewConsolidate() + public + givenWalletHasCollateralToken(USER, 3e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasLoan(USER, true, 2e18) + { + // Prepare input data + (address[] memory coolers, ) = _getCoolerArrays(true, false); + + Cooler.Loan memory loanZero = _getLoan(true, 0); + uint256 loanZeroPayable = loanZero.principal + loanZero.interestDue; + + Cooler.Loan memory loanOne = _getLoan(true, 1); + uint256 loanOnePayable = loanOne.principal + loanOne.interestDue; + + // Function + (uint256 collateralAmount, uint256 borrowedAmount) = migrator.previewConsolidate(coolers); + + // Assertions + assertEq(collateralAmount, 3e18, "collateralAmount"); + assertEq(borrowedAmount, loanZeroPayable + loanOnePayable, "borrowedAmount"); + } + + // consolidate + // given the contract is disabled + // [X] it reverts + // given the number of clearinghouses and coolers are not the same + // [X] it reverts + // given any clearinghouse is not owned by the Olympus protocol + // [X] it reverts + // given any cooler is not created by the clearinghouse's CoolerFactory + // [X] it reverts + // given any cooler is not owned by the caller + // [X] it reverts + // given a cooler is a duplicate + // [X] it reverts + // given the Cooler debt token is not DAI or USDS + // [X] it reverts + // given the caller has not approved the CoolerV2Migrator to spend the collateral + // [X] it reverts + // given MonoCooler authorization has been provided + // [X] it does not set the authorization signature + // [X] it deposits the collateral into MonoCooler + // [X] it borrows the principal + interest from MonoCooler + // [X] the Cooler V1 loans are repaid + // [X] the migrator does not hold any tokens + // when a MonoCooler authorization signature is not provided + // [X] it reverts + // when the new owner is different to the existing owner + // when the authorization account does not match the new owner + // [X] it reverts + // [X] it sets the authorization signature + // [X] it deposits the collateral into MonoCooler + // [X] it borrows the principal + interest from MonoCooler + // [X] it sets the new owner as the owner of the Cooler V2 position + // [X] the Cooler V1 loans are repaid + // [X] the migrator does not hold any tokens + // given the flash fee is non-zero + // [X] it sets the authorization signature + // [X] it deposits the collateral into MonoCooler + // [X] it borrows the principal + interest + flash fee from MonoCooler + // [X] the Cooler V1 loans are repaid + // [X] the migrator does not hold any tokens + // when there are multiple loans + // [X] it sets the authorization signature + // [X] it deposits the collateral into MonoCooler + // [X] it borrows the principal + interest + flash fee from MonoCooler + // [X] the Cooler V1 loans are repaid + // [X] the migrator does not hold any tokens + // when there are loans from a DAI clearinghouse + // [X] it sets the authorization signature + // [X] it deposits the collateral into MonoCooler + // [X] it borrows the principal + interest from MonoCooler + // [X] the Cooler V1 loans are repaid + // [X] the migrator does not hold any tokens + // when there are loans from clearinghouses with different debt tokens + // [X] it sets the authorization signature + // [X] it deposits the collateral into MonoCooler + // [X] it borrows the principal + interest from MonoCooler + // [X] the Cooler V1 loans are repaid + // [X] the migrator does not hold any tokens + // when delegation requests are provided + // [X] it sets the authorization signature + // [X] it deposits the collateral into MonoCooler + // [X] it borrows the principal + interest from MonoCooler + // [X] the Cooler V1 loans are repaid + // [X] the migrator does not hold any tokens + // [X] the delegation requests are applied + // [X] it sets the authorization signature + // [X] it deposits the collateral into MonoCooler + // [X] it borrows the principal + interest from MonoCooler + // [X] it sets the existing owner as the owner of the Cooler V2 position + // [X] the Cooler V1 loans are repaid + // [X] the migrator does not hold any tokens + + function test_consolidate_givenDisabled_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 1e18) + givenDisabled + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Expect revert + _expectRevert_disabled(); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_givenClearinghouseCountGreater_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + address[] memory clearinghousesWithExtra = new address[](2); + clearinghousesWithExtra[0] = clearinghouses[0]; + clearinghousesWithExtra[1] = address(clearinghouseDai); + + // Expect revert + vm.expectRevert(abi.encodeWithSelector(ICoolerV2Migrator.Params_InvalidArrays.selector)); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghousesWithExtra, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_givenClearinghouseCountLess_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + { + // Create a DAI cooler, but no loan + _createCooler(USER, false); + + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, true); + + address[] memory clearinghousesWithLess = new address[](1); + clearinghousesWithLess[0] = clearinghouses[0]; + + // Expect revert + vm.expectRevert(abi.encodeWithSelector(ICoolerV2Migrator.Params_InvalidArrays.selector)); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghousesWithLess, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_givenClearinghouseCountZero_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + { + // Prepare input data + (address[] memory coolers, ) = _getCoolerArrays(true, false); + + address[] memory clearinghousesWithLess = new address[](0); + + // Expect revert + vm.expectRevert(abi.encodeWithSelector(ICoolerV2Migrator.Params_InvalidArrays.selector)); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghousesWithLess, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_givenClearinghouseNotOwned_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + { + // Create a new Clearinghouse, not owned by the Olympus protocol + Clearinghouse newClearinghouse = new Clearinghouse( + address(ohm), + address(gohm), + address(staking), + address(susds), + address(coolerFactory), + address(kernel) + ); + vm.startPrank(USER); + address newCooler = Clearinghouse(newClearinghouse).factory().generateCooler(gohm, usds); + vm.stopPrank(); + + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + address[] memory coolersWithNew = new address[](2); + coolersWithNew[0] = coolers[0]; + coolersWithNew[1] = newCooler; + + address[] memory clearinghousesWithNew = new address[](2); + clearinghousesWithNew[0] = clearinghouses[0]; + clearinghousesWithNew[1] = address(newClearinghouse); + + // Expect revert + vm.expectRevert( + abi.encodeWithSelector(ICoolerV2Migrator.Params_InvalidClearinghouse.selector) + ); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolersWithNew, + clearinghousesWithNew, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_givenCoolerNotOwned_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + { + // Create a new Cooler from a different CoolerFactory + CoolerFactory newCoolerFactory = new CoolerFactory(); + vm.startPrank(USER); + address newCooler = CoolerFactory(newCoolerFactory).generateCooler(gohm, usds); + vm.stopPrank(); + + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + address[] memory coolersWithNew = new address[](2); + coolersWithNew[0] = coolers[0]; + coolersWithNew[1] = newCooler; + + address[] memory clearinghousesWithNew = new address[](2); + clearinghousesWithNew[0] = clearinghouses[0]; + clearinghousesWithNew[1] = address(clearinghouseUsds); + + // Expect revert + vm.expectRevert(abi.encodeWithSelector(ICoolerV2Migrator.Params_InvalidCooler.selector)); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolersWithNew, + clearinghousesWithNew, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_givenDifferentOwner_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasCollateralToken(address(this), 1e18) + givenWalletHasLoan(address(this), true, 1e18) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Expect revert + vm.expectRevert(abi.encodeWithSelector(ICoolerV2Migrator.Only_CoolerOwner.selector)); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_whenDuplicateCooler_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Add duplicate entries + address[] memory coolersWithDuplicate = new address[](2); + coolersWithDuplicate[0] = coolers[0]; + coolersWithDuplicate[1] = coolers[0]; + + address[] memory clearinghousesWithDuplicate = new address[](2); + clearinghousesWithDuplicate[0] = clearinghouses[0]; + clearinghousesWithDuplicate[1] = clearinghouses[0]; + + // Expect revert + vm.expectRevert(abi.encodeWithSelector(ICoolerV2Migrator.Params_DuplicateCooler.selector)); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolersWithDuplicate, + clearinghousesWithDuplicate, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_givenDebtTokenDifferent_reverts() public { + // Create a new Clearinghouse with a different debt token + MockERC20 newDebtToken = new MockERC20("New Debt Token", "NDT", 18); + MockERC4626 newDebtTokenVault = new MockERC4626( + newDebtToken, + "New Debt Token Vault", + "NDTV" + ); + Clearinghouse newClearinghouse = new Clearinghouse( + address(ohm), + address(gohm), + address(staking), + address(newDebtTokenVault), + address(coolerFactory), + address(kernel) + ); + vm.startPrank(EXECUTOR); + kernel.executeAction(Actions.ActivatePolicy, address(newClearinghouse)); + vm.stopPrank(); + vm.startPrank(OVERSEER); + newClearinghouse.activate(); + vm.stopPrank(); + + // Create a Cooler in the new Clearinghouse + vm.startPrank(USER); + address newCooler = coolerFactory.generateCooler(gohm, newDebtToken); + vm.stopPrank(); + + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + address[] memory coolersWithNew = new address[](2); + coolersWithNew[0] = coolers[0]; + coolersWithNew[1] = newCooler; + + address[] memory clearinghousesWithNew = new address[](2); + clearinghousesWithNew[0] = clearinghouses[0]; + clearinghousesWithNew[1] = address(newClearinghouse); + + // Expect revert + vm.expectRevert(abi.encodeWithSelector(ICoolerV2Migrator.Params_InvalidCooler.selector)); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolersWithNew, + clearinghousesWithNew, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_givenSpendingCollateralNotApproved_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Expect revert + vm.expectRevert("TRANSFER_FROM_FAILED"); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_givenAuthorization() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 1e18) + givenAuthorization + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Get loan details + Cooler.Loan memory loan = _getLoan(true, 0); + uint256 totalPayable = loan.principal + loan.interestDue; + uint256 userUsdsBalance = usds.balanceOf(USER); + uint256 userDaiBalance = dai.balanceOf(USER); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + + // Assert token balances + _assertTokenBalances(USER, 0, userUsdsBalance, userDaiBalance); + _assertTokenBalances(address(migrator), 0, 0, 0); + + // Assert authorization via the contract call + _assertAuthorization(USER, 0, uint96(START_TIMESTAMP + 1)); + + // Assert cooler V1 loans are zeroed out + _assertCoolerV1Loans(true, 1); + + // Assert cooler V2 loans are created + _assertCoolerV2Loan(USER, 1e18, totalPayable); + _assertCoolerV2Loan(USER2, 0, 0); + } + + function test_consolidate_givenNoAuthorization_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 1e18) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Expect revert + vm.expectRevert(abi.encodeWithSelector(IMonoCooler.UnauthorizedOnBehalfOf.selector)); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_whenNewOwnerIsGiven_whenNewOwnerAccountDoesNotMatch_reverts() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 1e18) + givenAuthorizationSignatureSet(USER2, USER2_PK) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Expect revert + vm.expectRevert(abi.encodeWithSelector(ICoolerV2Migrator.Params_InvalidNewOwner.selector)); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, // does not match authorization.account + authorization, + signature, + delegationRequests + ); + } + + function test_consolidate_whenNewOwnerIsGiven() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 1e18) + givenAuthorizationSignatureSet(USER2, USER2_PK) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Get loan details + Cooler.Loan memory loan = _getLoan(true, 0); + uint256 totalPayable = loan.principal + loan.interestDue; + uint256 userUsdsBalance = usds.balanceOf(USER); + uint256 userDaiBalance = dai.balanceOf(USER); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER2, + authorization, + signature, + delegationRequests + ); + + // Assert token balances + _assertTokenBalances(USER, 0, userUsdsBalance, userDaiBalance); + _assertTokenBalances(USER2, 0, 0, 0); + _assertTokenBalances(address(migrator), 0, 0, 0); + + // Assert authorization via the signature + _assertAuthorization(USER, 0, 0); + _assertAuthorization(USER2, 1, uint96(START_TIMESTAMP + 1)); + + // Assert cooler V1 loans are zeroed out + _assertCoolerV1Loans(true, 1); + + // Assert cooler V2 loans are created + _assertCoolerV2Loan(USER, 0, 0); + _assertCoolerV2Loan(USER2, 1e18, totalPayable); + } + + function test_consolidate_givenFlashFeeNonZero() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 1e18) + givenAuthorizationSignatureSet(USER, USER_PK) + givenFlashFee(1e2) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Get loan details + Cooler.Loan memory loan = _getLoan(true, 0); + uint256 totalPayable = loan.principal + loan.interestDue; + uint256 userUsdsBalance = usds.balanceOf(USER); + uint256 userDaiBalance = dai.balanceOf(USER); + + // Calculate expected flash fee + uint256 flashFee = (totalPayable * 1e2) / 100e2; + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + + // Assert token balances + _assertTokenBalances(USER, 0, userUsdsBalance, userDaiBalance); + _assertTokenBalances(address(migrator), 0, 0, 0); + + // Assert authorization via the signature + _assertAuthorization(USER, 1, uint96(START_TIMESTAMP + 1)); + _assertAuthorization(USER2, 0, 0); + + // Assert cooler V1 loans are zeroed out + _assertCoolerV1Loans(true, 1); + + // Assert cooler V2 loans are created + _assertCoolerV2Loan(USER, 1e18, totalPayable + flashFee); + _assertCoolerV2Loan(USER2, 0, 0); + } + + function test_consolidate_givenMultipleLoans_givenFlashFeeNonZero() + public + givenWalletHasCollateralToken(USER, 2e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 2e18) + givenAuthorizationSignatureSet(USER, USER_PK) + givenFlashFee(1e2) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Get loan details + Cooler.Loan memory loanZero = _getLoan(true, 0); + uint256 loanZeroPayable = loanZero.principal + loanZero.interestDue; + + Cooler.Loan memory loanOne = _getLoan(true, 1); + uint256 loanOnePayable = loanOne.principal + loanOne.interestDue; + + uint256 userUsdsBalance = usds.balanceOf(USER); + uint256 userDaiBalance = dai.balanceOf(USER); + + uint256 flashFee = ((loanZeroPayable + loanOnePayable) * 1e2) / 100e2; + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + + // Assert token balances + _assertTokenBalances(USER, 0, userUsdsBalance, userDaiBalance); + _assertTokenBalances(address(migrator), 0, 0, 0); + + // Assert authorization via the signature + _assertAuthorization(USER, 1, uint96(START_TIMESTAMP + 1)); + _assertAuthorization(USER2, 0, 0); + + // Assert cooler V1 loans are zeroed out + _assertCoolerV1Loans(true, 2); + + // Assert cooler V2 loans are created + _assertCoolerV2Loan(USER, 2e18, loanZeroPayable + loanOnePayable + flashFee); + _assertCoolerV2Loan(USER2, 0, 0); + } + + function test_consolidate_givenDaiClearinghouse_givenFlashFeeNonZero() + public + givenWalletHasCollateralToken(USER, 2e18) + givenDaiClearinghouseIsEnabled + givenWalletHasLoan(USER, false, 1e18) + givenWalletHasLoan(USER, false, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 2e18) + givenAuthorizationSignatureSet(USER, USER_PK) + givenFlashFee(1e2) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(false, true); + + // Get loan details + Cooler.Loan memory loanZero = _getLoan(false, 0); + uint256 loanZeroPayable = loanZero.principal + loanZero.interestDue; + + Cooler.Loan memory loanOne = _getLoan(false, 1); + uint256 loanOnePayable = loanOne.principal + loanOne.interestDue; + + uint256 userUsdsBalance = usds.balanceOf(USER); + uint256 userDaiBalance = dai.balanceOf(USER); + + uint256 flashFee = ((loanZeroPayable + loanOnePayable) * 1e2) / 100e2; + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + + // Assert token balances + _assertTokenBalances(USER, 0, userUsdsBalance, userDaiBalance); + _assertTokenBalances(address(migrator), 0, 0, 0); + + // Assert authorization via the signature + _assertAuthorization(USER, 1, uint96(START_TIMESTAMP + 1)); + _assertAuthorization(USER2, 0, 0); + + // Assert cooler V1 loans are zeroed out + _assertCoolerV1Loans(true, 0); + _assertCoolerV1Loans(false, 2); + + // Assert cooler V2 loans are created + _assertCoolerV2Loan(USER, 2e18, loanZeroPayable + loanOnePayable + flashFee); + _assertCoolerV2Loan(USER2, 0, 0); + } + + function test_consolidate_givenMultipleClearinghouses_givenFlashFeeNonZero() + public + givenWalletHasCollateralToken(USER, 2e18) + givenDaiClearinghouseIsEnabled + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasLoan(USER, false, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 2e18) + givenAuthorizationSignatureSet(USER, USER_PK) + givenFlashFee(1e2) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, true); + + // Get loan details + Cooler.Loan memory loanZero = _getLoan(true, 0); + uint256 loanZeroPayable = loanZero.principal + loanZero.interestDue; + + Cooler.Loan memory loanOne = _getLoan(false, 0); + uint256 loanOnePayable = loanOne.principal + loanOne.interestDue; + + uint256 userUsdsBalance = usds.balanceOf(USER); + uint256 userDaiBalance = dai.balanceOf(USER); + + uint256 flashFee = ((loanZeroPayable + loanOnePayable) * 1e2) / 100e2; + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + + // Assert token balances + _assertTokenBalances(USER, 0, userUsdsBalance, userDaiBalance); + _assertTokenBalances(address(migrator), 0, 0, 0); + + // Assert authorization via the signature + _assertAuthorization(USER, 1, uint96(START_TIMESTAMP + 1)); + _assertAuthorization(USER2, 0, 0); + + // Assert cooler V1 loans are zeroed out + _assertCoolerV1Loans(true, 1); + _assertCoolerV1Loans(false, 1); + + // Assert cooler V2 loans are created + _assertCoolerV2Loan(USER, 2e18, loanZeroPayable + loanOnePayable + flashFee); + _assertCoolerV2Loan(USER2, 0, 0); + } + + function test_consolidate_whenDelegationRequestsAreGiven() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 1e18) + givenAuthorizationSignatureSet(USER, USER_PK) + givenDelegationRequest(1e18) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Get loan details + Cooler.Loan memory loan = _getLoan(true, 0); + uint256 totalPayable = loan.principal + loan.interestDue; + uint256 userUsdsBalance = usds.balanceOf(USER); + uint256 userDaiBalance = dai.balanceOf(USER); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + + // Assert token balances + _assertTokenBalances(USER, 0, userUsdsBalance, userDaiBalance); + _assertTokenBalances(address(migrator), 0, 0, 0); + + // Assert authorization via the signature + _assertAuthorization(USER, 1, uint96(START_TIMESTAMP + 1)); + _assertAuthorization(USER2, 0, 0); + + // Assert cooler V1 loans are zeroed out + _assertCoolerV1Loans(true, 1); + + // Assert cooler V2 loans are created + _assertCoolerV2Loan(USER, 1e18, totalPayable); + _assertCoolerV2Loan(USER2, 0, 0); + + // Assert delegation requests + expectOneDelegation(cooler, USER, USER2, 1e18); + } + + function test_consolidate_whenAuthorizationSignatureIsGiven() + public + givenWalletHasCollateralToken(USER, 1e18) + givenWalletHasLoan(USER, true, 1e18) + givenWalletHasApprovedMigratorSpendingCollateral(USER, 1e18) + givenAuthorizationSignatureSet(USER, USER_PK) + { + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Get loan details + Cooler.Loan memory loan = _getLoan(true, 0); + uint256 totalPayable = loan.principal + loan.interestDue; + uint256 userUsdsBalance = usds.balanceOf(USER); + uint256 userDaiBalance = dai.balanceOf(USER); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + + // Assert token balances + _assertTokenBalances(USER, 0, userUsdsBalance, userDaiBalance); + _assertTokenBalances(address(migrator), 0, 0, 0); + + // Assert authorization via the signature + _assertAuthorization(USER, 1, uint96(START_TIMESTAMP + 1)); + _assertAuthorization(USER2, 0, 0); + + // Assert cooler V1 loans are zeroed out + _assertCoolerV1Loans(true, 1); + + // Assert cooler V2 loans are created + _assertCoolerV2Loan(USER, 1e18, totalPayable); + _assertCoolerV2Loan(USER2, 0, 0); + } + + function test_consolidate_fuzz( + uint256 loanOneCollateral_, + uint256 loanTwoCollateral_ + ) public givenAuthorizationSignatureSet(USER, USER_PK) { + // 0.5-100 gOHM + uint256 loanOneCollateral = bound(loanOneCollateral_, 5e17, 100e18); + uint256 loanTwoCollateral = bound(loanTwoCollateral_, 5e17, 100e18); + + // Mint collateral + gohm.mint(USER, loanOneCollateral); + gohm.mint(USER, loanTwoCollateral); + + // Take loans + _takeLoan(USER, true, loanOneCollateral); + _takeLoan(USER, true, loanTwoCollateral); + + // Approve spending of collateral by the migrator + _approveMigratorSpendingCollateral(USER, loanOneCollateral + loanTwoCollateral); + + // Prepare input data + (address[] memory coolers, address[] memory clearinghouses) = _getCoolerArrays(true, false); + + // Get loan details + Cooler.Loan memory loanZero = _getLoan(true, 0); + uint256 loanZeroPayable = loanZero.principal + loanZero.interestDue; + + Cooler.Loan memory loanOne = _getLoan(true, 1); + uint256 loanOnePayable = loanOne.principal + loanOne.interestDue; + + uint256 userUsdsBalance = usds.balanceOf(USER); + uint256 userDaiBalance = dai.balanceOf(USER); + + // Call function + vm.prank(USER); + migrator.consolidate( + coolers, + clearinghouses, + USER, + authorization, + signature, + delegationRequests + ); + + // Assert token balances + // The user may have a gOHM balance + uint256 userGohmBalance = gohm.balanceOf(USER); + _assertTokenBalances(USER, userGohmBalance, userUsdsBalance, userDaiBalance); + _assertTokenBalances(address(migrator), 0, 0, 0); + + // Assert authorization via the signature + _assertAuthorization(USER, 1, uint96(START_TIMESTAMP + 1)); + _assertAuthorization(USER2, 0, 0); + + // Assert cooler V1 loans are zeroed out + _assertCoolerV1Loans(true, 1); + + // Assert cooler V2 loans are created + // In a fuzz test, the exact amount of collateral deposited may vary (due to rounding), but we have an invariant that the user gOHM balance + the collateral deposited should equal the loan principal + uint256 expectedDepositedCollateral = loanOneCollateral + + loanTwoCollateral - + userGohmBalance; + _assertCoolerV2Loan(USER, expectedDepositedCollateral, loanZeroPayable + loanOnePayable); + _assertCoolerV2Loan(USER2, 0, 0); + } +} diff --git a/src/test/policies/cooler/MonoCoolerBase.t.sol b/src/test/policies/cooler/MonoCoolerBase.t.sol index eef85d17..7f174b01 100644 --- a/src/test/policies/cooler/MonoCoolerBase.t.sol +++ b/src/test/policies/cooler/MonoCoolerBase.t.sol @@ -66,9 +66,14 @@ abstract contract MonoCoolerBaseTest is Test { ohm = new MockOhm("OHM", "OHM", 9); gohm = new MockGohm("gOHM", "gOHM", 18); staking = new MockStakingReal(address(ohm), address(gohm)); + vm.label(address(ohm), "OHM"); + vm.label(address(gohm), "gOHM"); + vm.label(address(staking), "Staking"); usds = new MockERC20("usds", "USDS", 18); susds = new MockERC4626(usds, "sUSDS", "sUSDS"); + vm.label(address(usds), "USDS"); + vm.label(address(susds), "sUSDS"); kernel = new Kernel(); // this contract will be the executor escrowFactory = new DelegateEscrowFactory(address(gohm));