From 4ac8dc380ecf53bfa56ddf3c318ff4c5e2f82766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Wed, 10 Apr 2024 17:28:27 +0100 Subject: [PATCH] feat: Add multicollateral registry and weakest collateral redemption --- contracts/src/BoldToken.sol | 82 ++--- contracts/src/CollateralRegistry.sol | 216 ++++++++++++++ contracts/src/Dependencies/LiquityBase.sol | 10 +- contracts/src/Interfaces/IBoldToken.sol | 7 + .../src/Interfaces/ICollateralRegistry.sol | 12 + contracts/src/Interfaces/ITroveManager.sol | 8 +- contracts/src/SortedTroves.sol | 2 +- contracts/src/TroveManager.sol | 107 ++++--- contracts/src/deployment.sol | 76 ++++- contracts/src/scripts/DeployLiquity2.s.sol | 2 +- contracts/src/test/TestContracts/BaseTest.sol | 5 +- .../src/test/TestContracts/DevTestSetup.sol | 23 +- contracts/src/test/basicOps.t.sol | 2 +- contracts/src/test/deployment.t.sol | 1 + contracts/src/test/multicollateral.t.sol | 279 ++++++++++++++++++ contracts/src/test/troveManager.t.sol | 4 +- contracts/test/OwnershipTest.js | 21 +- contracts/utils/deploymentHelpers.js | 3 +- 18 files changed, 730 insertions(+), 130 deletions(-) create mode 100644 contracts/src/CollateralRegistry.sol create mode 100644 contracts/src/Interfaces/ICollateralRegistry.sol create mode 100644 contracts/src/test/multicollateral.t.sol diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol index a5fbc348..6757ec93 100644 --- a/contracts/src/BoldToken.sol +++ b/contracts/src/BoldToken.sol @@ -8,16 +8,16 @@ import "./Dependencies/CheckContract.sol"; * * Based upon OpenZeppelin's ERC20 contract: * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol -* +* * and their EIP2612 (ERC20Permit / ERC712) functionality: * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/53516bc555a454862470e7860a9b5254db4d00f5/contracts/token/ERC20/ERC20Permit.sol -* +* * * --- Functionality added specific to the BoldToken --- -* -* 1) Transfer protection: blacklist of addresses that are invalid recipients (i.e. core Liquity contracts) in external -* transfer() and transferFrom() calls. The purpose is to protect users from losing tokens by mistakenly sending Bold directly to a Liquity -* core contract, when they should rather call the right function. +* +* 1) Transfer protection: blacklist of addresses that are invalid recipients (i.e. core Liquity contracts) in external +* transfer() and transferFrom() calls. The purpose is to protect users from losing tokens by mistakenly sending Bold directly to a Liquity +* core contract, when they should rather call the right function. * * 2) sendToPool() and returnFromPool(): functions callable only Liquity core contracts, which move Bold tokens between Liquity <-> user. */ @@ -53,47 +53,58 @@ contract BoldToken is CheckContract, IBoldToken { mapping(address => mapping(address => uint256)) private _allowances; // --- Addresses --- + /* address public immutable troveManagerAddress; address public immutable stabilityPoolAddress; address public immutable borrowerOperationsAddress; address public immutable activePoolAddress; + */ + // TODO: optimize to make them immutable + mapping(address => bool) troveManagerAddresses; + mapping(address => bool) stabilityPoolAddresses; + mapping(address => bool) borrowerOperationsAddresses; + mapping(address => bool) activePoolAddresses; // --- Events --- - event TroveManagerAddressChanged(address _troveManagerAddress); - event StabilityPoolAddressChanged(address _newStabilityPoolAddress); - event BorrowerOperationsAddressChanged(address _newBorrowerOperationsAddress); + event TroveManagerAddressAdded(address _newTroveManagerAddress); + event StabilityPoolAddressAdded(address _newStabilityPoolAddress); + event BorrowerOperationsAddressAdded(address _newBorrowerOperationsAddress); + event ActivePoolAddressAdded(address _newActivePoolAddress); + + constructor() { + bytes32 hashedName = keccak256(bytes(_NAME)); + bytes32 hashedVersion = keccak256(bytes(_VERSION)); + + _HASHED_NAME = hashedName; + _HASHED_VERSION = hashedVersion; + _CACHED_CHAIN_ID = _chainID(); + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, hashedName, hashedVersion); + + deploymentStartTime = block.timestamp; + } - constructor( + function setBranchAddresses( address _troveManagerAddress, address _stabilityPoolAddress, address _borrowerOperationsAddress, address _activePoolAddress - ) { + ) external { checkContract(_troveManagerAddress); checkContract(_stabilityPoolAddress); checkContract(_borrowerOperationsAddress); checkContract(_activePoolAddress); - troveManagerAddress = _troveManagerAddress; - emit TroveManagerAddressChanged(_troveManagerAddress); - - stabilityPoolAddress = _stabilityPoolAddress; - emit StabilityPoolAddressChanged(_stabilityPoolAddress); + troveManagerAddresses[_troveManagerAddress] = true; + emit TroveManagerAddressAdded(_troveManagerAddress); - borrowerOperationsAddress = _borrowerOperationsAddress; - emit BorrowerOperationsAddressChanged(_borrowerOperationsAddress); + stabilityPoolAddresses[_stabilityPoolAddress] = true; + emit StabilityPoolAddressAdded(_stabilityPoolAddress); - activePoolAddress = _activePoolAddress; + borrowerOperationsAddresses[_borrowerOperationsAddress] = true; + emit BorrowerOperationsAddressAdded(_borrowerOperationsAddress); - bytes32 hashedName = keccak256(bytes(_NAME)); - bytes32 hashedVersion = keccak256(bytes(_VERSION)); - - _HASHED_NAME = hashedName; - _HASHED_VERSION = hashedVersion; - _CACHED_CHAIN_ID = _chainID(); - _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, hashedName, hashedVersion); - - deploymentStartTime = block.timestamp; + activePoolAddresses[_activePoolAddress] = true; + emit ActivePoolAddressAdded(_activePoolAddress); } // --- Functions for intra-Liquity calls --- @@ -248,35 +259,30 @@ contract BoldToken is CheckContract, IBoldToken { _recipient != address(0) && _recipient != address(this), "Bold: Cannot transfer tokens directly to the Bold token contract or the zero address" ); - require( - _recipient != stabilityPoolAddress && _recipient != troveManagerAddress - && _recipient != borrowerOperationsAddress, - "Bold: Cannot transfer tokens directly to the StabilityPool, TroveManager or BorrowerOps" - ); } function _requireCallerIsBOorAP() internal view { require( - msg.sender == borrowerOperationsAddress || msg.sender == activePoolAddress, + borrowerOperationsAddresses[msg.sender] || activePoolAddresses[msg.sender], "BoldToken: Caller is not BO or AP" ); } function _requireCallerIsBOorTroveMorSP() internal view { require( - msg.sender == borrowerOperationsAddress || msg.sender == troveManagerAddress - || msg.sender == stabilityPoolAddress, + borrowerOperationsAddresses[msg.sender] || troveManagerAddresses[msg.sender] + || stabilityPoolAddresses[msg.sender], "Bold: Caller is neither BorrowerOperations nor TroveManager nor StabilityPool" ); } function _requireCallerIsStabilityPool() internal view { - require(msg.sender == stabilityPoolAddress, "Bold: Caller is not the StabilityPool"); + require(stabilityPoolAddresses[msg.sender], "Bold: Caller is not the StabilityPool"); } function _requireCallerIsTroveMorSP() internal view { require( - msg.sender == troveManagerAddress || msg.sender == stabilityPoolAddress, + troveManagerAddresses[msg.sender] || stabilityPoolAddresses[msg.sender], "Bold: Caller is neither TroveManager nor StabilityPool" ); } diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol new file mode 100644 index 00000000..8e8fa4a9 --- /dev/null +++ b/contracts/src/CollateralRegistry.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +import "./Interfaces/ITroveManager.sol"; +import "./Interfaces/IBoldToken.sol"; +import "./Dependencies/LiquityBase.sol"; + +import "./Interfaces/ICollateralRegistry.sol"; + +// import "forge-std/console.sol"; + +contract CollateralRegistry is LiquityBase, ICollateralRegistry { + // mapping from Collateral token address to the corresponding TroveManagers + //mapping(address => address) troveManagers; + // See: https://github.com/ethereum/solidity/issues/12587 + uint256 public immutable totalCollaterals; + + IERC20 internal immutable _token0; + IERC20 internal immutable _token1; + IERC20 internal immutable _token2; + IERC20 internal immutable _token3; + IERC20 internal immutable _token4; + IERC20 internal immutable _token5; + IERC20 internal immutable _token6; + IERC20 internal immutable _token7; + IERC20 internal immutable _token8; + IERC20 internal immutable _token9; + + ITroveManager internal immutable _troveManager0; + ITroveManager internal immutable _troveManager1; + ITroveManager internal immutable _troveManager2; + ITroveManager internal immutable _troveManager3; + ITroveManager internal immutable _troveManager4; + ITroveManager internal immutable _troveManager5; + ITroveManager internal immutable _troveManager6; + ITroveManager internal immutable _troveManager7; + ITroveManager internal immutable _troveManager8; + ITroveManager internal immutable _troveManager9; + + IBoldToken public immutable boldToken; + + constructor(IBoldToken _boldToken, IERC20[] memory _tokens, ITroveManager[] memory _troveManagers) { + //checkContract(address(_boldToken)); + + uint256 numTokens = _tokens.length; + require(numTokens > 0, "Collateral list cannot be empty"); + require(numTokens < 10, "Collateral list too long"); + require(numTokens == _troveManagers.length, "List sizes mismatch"); + totalCollaterals = numTokens; + + boldToken = _boldToken; + + _token0 = _tokens[0]; + _troveManager0 = _troveManagers[0]; + + _token1 = numTokens > 1 ? _tokens[1] : IERC20(address(0)); + _troveManager1 = numTokens > 1 ? _troveManagers[1] : ITroveManager(address(0)); + + _token2 = numTokens > 2 ? _tokens[2] : IERC20(address(0)); + _troveManager2 = numTokens > 2 ? _troveManagers[2] : ITroveManager(address(0)); + + _token3 = numTokens > 3 ? _tokens[3] : IERC20(address(0)); + _troveManager3 = numTokens > 3 ? _troveManagers[3] : ITroveManager(address(0)); + + _token4 = numTokens > 4 ? _tokens[4] : IERC20(address(0)); + _troveManager4 = numTokens > 4 ? _troveManagers[4] : ITroveManager(address(0)); + + _token5 = numTokens > 5 ? _tokens[5] : IERC20(address(0)); + _troveManager5 = numTokens > 5 ? _troveManagers[5] : ITroveManager(address(0)); + + _token6 = numTokens > 6 ? _tokens[6] : IERC20(address(0)); + _troveManager6 = numTokens > 6 ? _troveManagers[6] : ITroveManager(address(0)); + + _token7 = numTokens > 7 ? _tokens[7] : IERC20(address(0)); + _troveManager7 = numTokens > 7 ? _troveManagers[7] : ITroveManager(address(0)); + + _token8 = numTokens > 8 ? _tokens[8] : IERC20(address(0)); + _troveManager8 = numTokens > 8 ? _troveManagers[8] : ITroveManager(address(0)); + + _token9 = numTokens > 9 ? _tokens[9] : IERC20(address(0)); + _troveManager9 = numTokens > 9 ? _troveManagers[9] : ITroveManager(address(0)); + } + + function redeemCollateral(uint256 _boldAmount, uint256 _maxIterations, uint256 _maxFeePercentage) external { + _requireValidMaxFeePercentage(_maxFeePercentage); + _requireAmountGreaterThanZero(_boldAmount); + _requireBoldBalanceCoversRedemption(boldToken, msg.sender, _boldAmount); + + uint256 numCollaterals = totalCollaterals; + uint256[] memory unbackedPortions = new uint256[](numCollaterals); + uint256 totalUnbacked; + + // Gather and accumulate unbacked portions + for (uint256 index = 0; index < numCollaterals; index++) { + ITroveManager troveManager = getTroveManager(index); + uint256 unbackedPortion = troveManager.getUnbackedPortion(); + totalUnbacked += unbackedPortion; + unbackedPortions[index] = unbackedPortion; + } + + // The amount redeemed has to be outside SPs, and therefore unbacked + assert(totalUnbacked > _boldAmount); + + // Compute redemption amount for each collateral and redeem against the corresponding TroveManager + uint256 totalFeePercentage; + uint256 totalRedeemedAmount; // TODO: get rid of totalRedeemedAmount and just use _boldAmount + for (uint256 index = 0; index < numCollaterals; index++) { + uint256 unbackedPortion = unbackedPortions[index]; + if (unbackedPortion > 0) { + uint256 redeemAmount = _boldAmount * unbackedPortion / totalUnbacked; + if (redeemAmount > 0) { + ITroveManager troveManager = getTroveManager(index); + uint256 feePercentage = troveManager.redeemCollateral(msg.sender, redeemAmount, _maxIterations); + totalFeePercentage += feePercentage * redeemAmount; + totalRedeemedAmount += redeemAmount; + } + } + } + // TODO: get rid of totalRedeemedAmount and just use _boldAmount + assert(totalRedeemedAmount * DECIMAL_PRECISION / _boldAmount > 1e18 - 1e14); // 0.01% error + totalFeePercentage = totalFeePercentage / totalRedeemedAmount; + require(totalFeePercentage <= _maxFeePercentage, "Fee exceeded provided maximum"); + } + + function getTroveManager(uint256 _index) public view returns (ITroveManager) { + if (_index == 0) return _troveManager0; + else if (_index == 1) return _troveManager1; + else if (_index == 2) return _troveManager2; + else if (_index == 3) return _troveManager3; + else if (_index == 4) return _troveManager4; + else if (_index == 5) return _troveManager5; + else if (_index == 6) return _troveManager6; + else if (_index == 7) return _troveManager7; + else if (_index == 8) return _troveManager8; + else if (_index == 9) return _troveManager9; + else revert("Invalid index"); + } + + function getToken(uint256 _index) external view returns (IERC20) { + if (_index == 0) return _token0; + else if (_index == 1) return _token1; + else if (_index == 2) return _token2; + else if (_index == 3) return _token3; + else if (_index == 4) return _token4; + else if (_index == 5) return _token5; + else if (_index == 6) return _token6; + else if (_index == 7) return _token7; + else if (_index == 8) return _token8; + else if (_index == 9) return _token9; + else revert("Invalid index"); + } + + // require functions + + function _requireValidMaxFeePercentage(uint256 _maxFeePercentage) internal pure { + require( + _maxFeePercentage >= REDEMPTION_FEE_FLOOR && _maxFeePercentage <= DECIMAL_PRECISION, + "Max fee percentage must be between 0.5% and 100%" + ); + } + + function _requireAmountGreaterThanZero(uint256 _amount) internal pure { + require(_amount > 0, "TroveManager: Amount must be greater than zero"); + } + + function _requireBoldBalanceCoversRedemption(IBoldToken _boldToken, address _redeemer, uint256 _amount) + internal + view + { + uint256 boldBalance = _boldToken.balanceOf(_redeemer); + // Confirm redeemer's balance is less than total Bold supply + assert(boldBalance <= _boldToken.totalSupply()); + require( + boldBalance >= _amount, + "TroveManager: Requested redemption amount must be <= user's Bold token balance" + ); + } + + /* + TODO: do we need this? + function getTokenIndex(IERC20 token) external view returns (uint256) { + if (token == _token0) { return 0; } + else if (token == _token1) { return 1; } + else if (token == _token2) { return 2; } + else if (token == _token3) { return 3; } + else if (token == _token4) { return 4; } + else if (token == _token5) { return 5; } + else if (token == _token6) { return 6; } + else if (token == _token7) { return 7; } + else if (token == _token8) { return 8; } + else if (token == _token9) { return 9; } + else { + revert("Invalid token"); + } + } + + function getTroveManagerIndex(ITroveManager troveManager) external view returns (uint256) { + if (troveManager == _troveManager0) { return 0; } + else if (troveManager == _troveManager1) { return 1; } + else if (troveManager == _troveManager2) { return 2; } + else if (troveManager == _troveManager3) { return 3; } + else if (troveManager == _troveManager4) { return 4; } + else if (troveManager == _troveManager5) { return 5; } + else if (troveManager == _troveManager6) { return 6; } + else if (troveManager == _troveManager7) { return 7; } + else if (troveManager == _troveManager8) { return 8; } + else if (troveManager == _troveManager9) { return 9; } + else { + revert("Invalid troveManager"); + } + } + */ +} diff --git a/contracts/src/Dependencies/LiquityBase.sol b/contracts/src/Dependencies/LiquityBase.sol index 950b6d1a..54f22872 100644 --- a/contracts/src/Dependencies/LiquityBase.sol +++ b/contracts/src/Dependencies/LiquityBase.sol @@ -11,9 +11,9 @@ import "../Interfaces/ILiquityBase.sol"; //import "forge-std/console2.sol"; -/* +/* * Base contract for TroveManager, BorrowerOperations and StabilityPool. Contains global system constants and -* common functions. +* common functions. */ contract LiquityBase is BaseMath, ILiquityBase { // TODO: Pull all constants out into a separate base contract @@ -38,6 +38,7 @@ contract LiquityBase is BaseMath, ILiquityBase { uint256 public constant PERCENT_DIVISOR = 200; // dividing by 200 yields 0.5% uint256 public constant BORROWING_FEE_FLOOR = DECIMAL_PRECISION / 1000 * 5; // 0.5% + uint256 public constant REDEMPTION_FEE_FLOOR = DECIMAL_PRECISION / 1000 * 5; // 0.5% IActivePool public activePool; @@ -91,9 +92,4 @@ contract LiquityBase is BaseMath, ILiquityBase { return TCR < CCR; } - - function _requireUserAcceptsFee(uint256 _fee, uint256 _amount, uint256 _maxFeePercentage) internal pure { - uint256 feePercentage = _fee * DECIMAL_PRECISION / _amount; - require(feePercentage <= _maxFeePercentage, "Fee exceeded provided maximum"); - } } diff --git a/contracts/src/Interfaces/IBoldToken.sol b/contracts/src/Interfaces/IBoldToken.sol index 7b556beb..3436b195 100644 --- a/contracts/src/Interfaces/IBoldToken.sol +++ b/contracts/src/Interfaces/IBoldToken.sol @@ -7,6 +7,13 @@ import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.s import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; interface IBoldToken is IERC20, IERC20Metadata, IERC20Permit { + function setBranchAddresses( + address _troveManagerAddress, + address _stabilityPoolAddress, + address _borrowerOperationsAddress, + address _activePoolAddress + ) external; + function version() external pure returns (string memory); function deploymentStartTime() external view returns (uint256); diff --git a/contracts/src/Interfaces/ICollateralRegistry.sol b/contracts/src/Interfaces/ICollateralRegistry.sol new file mode 100644 index 00000000..239fac33 --- /dev/null +++ b/contracts/src/Interfaces/ICollateralRegistry.sol @@ -0,0 +1,12 @@ +pragma solidity 0.8.18; + +import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "./ITroveManager.sol"; + +interface ICollateralRegistry { + function redeemCollateral(uint256 _boldamount, uint256 _maxIterations, uint256 _maxFeePercentage) external; + // getters + function totalCollaterals() external view returns (uint256); + function getToken(uint256 _index) external view returns (IERC20); + function getTroveManager(uint256 _index) external view returns (ITroveManager); +} diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 6cc71935..00018f94 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -22,6 +22,7 @@ interface ITroveManager is IERC721, ILiquityBase { address _boldTokenAddress, address _sortedTrovesAddress ) external; + function setCollateralRegistry(address _collateralRegistryAddress) external; function stabilityPool() external view returns (IStabilityPool); function boldToken() external view returns (IBoldToken); @@ -42,7 +43,9 @@ interface ITroveManager is IERC721, ILiquityBase { function batchLiquidateTroves(uint256[] calldata _troveArray) external; - function redeemCollateral(uint256 _boldAmount, uint256 _maxIterations, uint256 _maxFee) external; + function redeemCollateral(address _sender, uint256 _boldAmount, uint256 _maxIterations) + external + returns (uint256 _feePercentage); function updateStakeAndTotalStakes(uint256 _troveId) external returns (uint256); @@ -79,6 +82,7 @@ interface ITroveManager is IERC721, ILiquityBase { function getRedemptionRateWithDecay() external view returns (uint256); function getRedemptionFeeWithDecay(uint256 _ETHDrawn) external view returns (uint256); + function getEffectiveRedemptionFee(uint256 _redeemAmount, uint256 _price) external view returns (uint256); function getTroveStatus(uint256 _troveId) external view returns (uint256); @@ -130,4 +134,6 @@ interface ITroveManager is IERC721, ILiquityBase { function checkRecoveryMode(uint256 _price) external view returns (bool); function checkTroveIsActive(uint256 _troveId) external view returns (bool); + + function getUnbackedPortion() external view returns (uint256); } diff --git a/contracts/src/SortedTroves.sol b/contracts/src/SortedTroves.sol index 663bfac5..3510d9e0 100644 --- a/contracts/src/SortedTroves.sol +++ b/contracts/src/SortedTroves.sol @@ -22,7 +22,7 @@ uint256 constant ROOT_NODE_ID = 0; * * The annual interest rate is stored on the Trove struct in TroveManager, not directly on the Node. * -* A node need only be re-inserted when the borrower adjusts their interest rate. Interest rate order is preserved +* A node need only be re-inserted when the borrower adjusts their interest rate. Interest rate order is preserved * under all other system operations. * * The list is a modification of the following audited SortedDoublyLinkedList: diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 192362a5..7b1efcd1 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -13,7 +13,7 @@ import "./Dependencies/LiquityBase.sol"; import "./Dependencies/Ownable.sol"; import "./Dependencies/CheckContract.sol"; -// import "forge-std/console2.sol"; +// import "forge-std/console.sol"; contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveManager { string public constant NAME = "TroveManager"; // TODO @@ -22,17 +22,13 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // --- Connected contract declarations --- address public borrowerOperationsAddress; - IStabilityPool public override stabilityPool; - address gasPoolAddress; - ICollSurplusPool collSurplusPool; - IBoldToken public override boldToken; - // A doubly linked list of Troves, sorted by their sorted by their collateral ratios ISortedTroves public sortedTroves; + address public collateralRegistryAddress; // --- Data structures --- @@ -45,7 +41,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana * (1/2) = d^720 => d = (1/2)^(1/720) */ uint256 public constant MINUTE_DECAY_FACTOR = 999037758833783000; - uint256 public constant REDEMPTION_FEE_FLOOR = DECIMAL_PRECISION / 1000 * 5; // 0.5% // To prevent redemptions unless Bold depegs below 0.95 and allow the system to take off uint256 public constant INITIAL_REDEMPTION_RATE = DECIMAL_PRECISION / 100 * 5; // 5% @@ -245,6 +240,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana event GasPoolAddressChanged(address _gasPoolAddress); event CollSurplusPoolAddressChanged(address _collSurplusPoolAddress); event SortedTrovesAddressChanged(address _sortedTrovesAddress); + event CollateralRegistryAddressChanged(address _collateralRegistryAddress); event Liquidation( uint256 _liquidatedDebt, uint256 _liquidatedColl, uint256 _collGasCompensation, uint256 _boldGasCompensation @@ -318,6 +314,12 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana emit PriceFeedAddressChanged(_priceFeedAddress); emit BoldTokenAddressChanged(_boldTokenAddress); emit SortedTrovesAddressChanged(_sortedTrovesAddress); + } + + function setCollateralRegistry(address _collateralRegistryAddress) external override onlyOwner { + checkContract(_collateralRegistryAddress); + collateralRegistryAddress = _collateralRegistryAddress; + emit CollateralRegistryAddressChanged(_collateralRegistryAddress); _renounceOwnership(); } @@ -946,23 +948,22 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana * redemption will stop after the last completely redeemed Trove and the sender will keep the remaining Bold amount, which they can attempt * to redeem later. */ - function redeemCollateral(uint256 _boldamount, uint256 _maxIterations, uint256 _maxFeePercentage) + function redeemCollateral(address _sender, uint256 _boldamount, uint256 _maxIterations) external override + returns (uint256 _feePercentage) { + _requireIsCollateralRegistry(); + ContractsCache memory contractsCache = ContractsCache(activePool, defaultPool, boldToken, sortedTroves, collSurplusPool, gasPoolAddress); RedemptionTotals memory totals; - _requireValidMaxFeePercentage(_maxFeePercentage); totals.price = priceFeed.fetchPrice(); + // TODO: remove. Return instead? _requireTCRoverMCR(totals.price); - _requireAmountGreaterThanZero(_boldamount); - _requireBoldBalanceCoversRedemption(contractsCache.boldToken, msg.sender, _boldamount); totals.totalBoldSupplyAtStart = getEntireSystemDebt(); - // Confirm redeemer's balance is less than total Bold supply - assert(contractsCache.boldToken.balanceOf(msg.sender) <= totals.totalBoldSupplyAtStart); totals.remainingBold = _boldamount; uint256 currentTroveId; @@ -1003,7 +1004,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana currentTroveId = nextUserToCheck; } - require(totals.totalETHDrawn > 0, "TroveManager: Unable to redeem any amount"); + // We are removing this condition to prevent blocking redemptions + //require(totals.totalETHDrawn > 0, "TroveManager: Unable to redeem any amount"); // Decay the baseRate due to time passed, and then increase it according to the size of this redemption. // Use the saved total Bold supply value, from before it was reduced by the redemption. @@ -1012,8 +1014,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // Calculate the ETH fee totals.ETHFee = _getRedemptionFee(totals.totalETHDrawn); - _requireUserAcceptsFee(totals.ETHFee, totals.totalETHDrawn, _maxFeePercentage); - // Do nothing with the fee - the funds remain in ActivePool. TODO: replace with new redemption fee scheme totals.ETHToSendToRedeemer = totals.totalETHDrawn - totals.ETHFee; @@ -1028,9 +1028,11 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana totals.totalOldWeightedRecordedTroveDebts ); - // Burn the total Bold that is cancelled with debt, and send the redeemed ETH to msg.sender - contractsCache.boldToken.burn(msg.sender, totals.totalBoldToRedeem); - contractsCache.activePool.sendETH(msg.sender, totals.ETHToSendToRedeemer); + // Burn the total Bold that is cancelled with debt, and send the redeemed ETH to sender + contractsCache.boldToken.burn(_sender, totals.totalBoldToRedeem); + contractsCache.activePool.sendETH(_sender, totals.ETHToSendToRedeemer); + + return totals.ETHFee * DECIMAL_PRECISION / totals.totalETHDrawn; } // --- Helper functions --- @@ -1376,23 +1378,34 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // --- Redemption fee functions --- /* - * This function has two impacts on the baseRate state variable: - * 1) decays the baseRate based on time passed since last redemption or Bold borrowing operation. - * then, - * 2) increases the baseRate based on the amount redeemed, as a proportion of total supply - */ - function _updateBaseRateFromRedemption(uint256 _ETHDrawn, uint256 _price, uint256 _totalBoldSupply) - internal + * This function has two impacts on the baseRate state variable: + * 1) decays the baseRate based on time passed since last redemption or Bold borrowing operation. + * then, + * 2) increases the baseRate based on the amount redeemed, as a proportion of total supply + */ + function _getUpdatedBaseRateFromRedemption(uint256 _redeemAmount, uint256 _totalBoldSupply) + internal view returns (uint256) { uint256 decayedBaseRate = _calcDecayedBaseRate(); /* Convert the drawn ETH back to Bold at face value rate (1 Bold:1 USD), in order to get - * the fraction of total supply that was redeemed at face value. */ - uint256 redeemedBoldFraction = _ETHDrawn * _price / _totalBoldSupply; + * the fraction of total supply that was redeemed at face value. */ + uint256 redeemedBoldFraction = _redeemAmount * DECIMAL_PRECISION / _totalBoldSupply; uint256 newBaseRate = decayedBaseRate + redeemedBoldFraction / BETA; newBaseRate = LiquityMath._min(newBaseRate, DECIMAL_PRECISION); // cap baseRate at a maximum of 100% + + return newBaseRate; + } + + // Updates the `baseRate` state with math from previous function + function _updateBaseRateFromRedemption(uint256 _ETHDrawn, uint256 _price, uint256 _totalBoldSupply) + internal + returns (uint256) + { + uint256 newBaseRate = _getUpdatedBaseRateFromRedemption(_ETHDrawn * _price / DECIMAL_PRECISION, _totalBoldSupply); + //assert(newBaseRate <= DECIMAL_PRECISION); // This is already enforced in the line above assert(newBaseRate > 0); // Base rate is always non-zero after redemption @@ -1428,6 +1441,12 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return _calcRedemptionFee(getRedemptionRateWithDecay(), _ETHDrawn); } + function getEffectiveRedemptionFee(uint256 _redeemAmount, uint256 _price) external view override returns (uint256) { + uint256 totalBoldSupply = getEntireSystemDebt(); + uint256 newBaseRate = _getUpdatedBaseRateFromRedemption(_redeemAmount, totalBoldSupply); + return _calcRedemptionFee(_calcRedemptionRate(newBaseRate), _redeemAmount * DECIMAL_PRECISION / _price); + } + function _calcRedemptionFee(uint256 _redemptionRate, uint256 _ETHDrawn) internal pure returns (uint256) { uint256 redemptionFee = _redemptionRate * _ETHDrawn / DECIMAL_PRECISION; require(redemptionFee < _ETHDrawn, "TroveManager: Fee would eat up all returned collateral"); @@ -1496,39 +1515,22 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana ); } - function _requireTroveIsActive(uint256 _troveId) internal view { - require(checkTroveIsActive(_troveId), "TroveManager: Trove does not exist or is closed"); + function _requireIsCollateralRegistry() internal view { + require(msg.sender == collateralRegistryAddress, "TroveManager: Caller is not the CollateralRegistry contract"); } - function _requireBoldBalanceCoversRedemption(IBoldToken _boldToken, address _redeemer, uint256 _amount) - internal - view - { - require( - _boldToken.balanceOf(_redeemer) >= _amount, - "TroveManager: Requested redemption amount must be <= user's Bold token balance" - ); + function _requireTroveIsActive(uint256 _troveId) internal view { + require(checkTroveIsActive(_troveId), "TroveManager: Trove does not exist or is closed"); } function _requireMoreThanOneTroveInSystem(uint256 TroveIdsArrayLength) internal view { require(TroveIdsArrayLength > 1 && sortedTroves.getSize() > 1, "TroveManager: Only one trove in the system"); } - function _requireAmountGreaterThanZero(uint256 _amount) internal pure { - require(_amount > 0, "TroveManager: Amount must be greater than zero"); - } - function _requireTCRoverMCR(uint256 _price) internal view { require(_getTCR(_price) >= MCR, "TroveManager: Cannot redeem when TCR < MCR"); } - function _requireValidMaxFeePercentage(uint256 _maxFeePercentage) internal pure { - require( - _maxFeePercentage >= REDEMPTION_FEE_FLOOR && _maxFeePercentage <= DECIMAL_PRECISION, - "Max fee percentage must be between 0.5% and 100%" - ); - } - // --- Trove property getters --- function getTroveStatus(uint256 _troveId) external view override returns (uint256) { @@ -1563,6 +1565,13 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return block.timestamp - Troves[_troveId].lastDebtUpdateTime > STALE_TROVE_DURATION; } + function getUnbackedPortion() external view returns (uint256) { + uint256 totalDebt = getEntireSystemDebt(); + uint256 spSize = stabilityPool.getTotalBoldDeposits(); + + return totalDebt - spSize; + } + // --- Trove property setters, called by BorrowerOperations --- function setTrovePropertiesOnOpen( diff --git a/contracts/src/deployment.sol b/contracts/src/deployment.sol index 441cc0f9..2bb48bea 100644 --- a/contracts/src/deployment.sol +++ b/contracts/src/deployment.sol @@ -13,10 +13,13 @@ import "./MultiTroveGetter.sol"; import "./SortedTroves.sol"; import "./StabilityPool.sol"; import "./TroveManager.sol"; +import "./CollateralRegistry.sol"; import "./MockInterestRouter.sol"; import "./test/TestContracts/PriceFeedTestnet.sol"; import {ERC20Faucet} from "./test/TestContracts/ERC20Faucet.sol"; +// import "forge-std/console.sol"; + struct LiquityContracts { IActivePool activePool; IBorrowerOperations borrowerOperations; @@ -25,36 +28,83 @@ struct LiquityContracts { ISortedTroves sortedTroves; IStabilityPool stabilityPool; ITroveManager troveManager; - IBoldToken boldToken; IPriceFeedTestnet priceFeed; GasPool gasPool; IInterestRouter interestRouter; IERC20 WETH; } -function _deployAndConnectContracts() returns (LiquityContracts memory contracts) { - contracts.WETH = new ERC20Faucet( +function _deployAndConnectContracts() + returns (LiquityContracts memory contracts, ICollateralRegistry collateralRegistry, IBoldToken boldToken) +{ + LiquityContracts[] memory contractsArray; + (contractsArray, collateralRegistry, boldToken) = _deployAndConnectContracts(1); + contracts = contractsArray[0]; +} + +function _deployAndConnectContracts(uint256 _numCollaterals) + returns (LiquityContracts[] memory contractsArray, ICollateralRegistry collateralRegistry, IBoldToken boldToken) +{ + boldToken = new BoldToken(); + + contractsArray = new LiquityContracts[](_numCollaterals); + IERC20[] memory collaterals = new IERC20[](_numCollaterals); + ITroveManager[] memory troveManagers = new ITroveManager[](_numCollaterals); + + LiquityContracts memory contracts; + IERC20 WETH = new ERC20Faucet( "Wrapped ETH", // _name "WETH", // _symbol 100 ether, // _tapAmount 1 days // _tapPeriod ); + contracts = _deployAndConnectCollateralContracts(WETH, boldToken); + contractsArray[0] = contracts; + collaterals[0] = contracts.WETH; + troveManagers[0] = contracts.troveManager; + // Multicollateral registry + for (uint256 i = 1; i < _numCollaterals; i++) { + IERC20 stETH = new ERC20Faucet( + string.concat("Staked ETH", string(abi.encode(i))), // _name + string.concat("stETH", string(abi.encode(i))), // _symbol + 100 ether, // _tapAmount + 1 days // _tapPeriod + ); + contracts = _deployAndConnectCollateralContracts(stETH, boldToken); + collaterals[i] = contracts.WETH; + troveManagers[i] = contracts.troveManager; + contractsArray[i] = contracts; + } + + collateralRegistry = new CollateralRegistry(boldToken, collaterals, troveManagers); + + // Set registry in TroveManagers + for (uint256 i = 0; i < _numCollaterals; i++) { + contractsArray[i].troveManager.setCollateralRegistry(address(collateralRegistry)); + } +} + +function _deployAndConnectCollateralContracts(IERC20 _collateralToken, IBoldToken _boldToken) + returns (LiquityContracts memory contracts) +{ // TODO: optimize deployment order & constructor args & connector functions + contracts.WETH = _collateralToken; + // Deploy all contracts - contracts.activePool = new ActivePool(address(contracts.WETH)); - contracts.borrowerOperations = new BorrowerOperations(address(contracts.WETH)); - contracts.collSurplusPool = new CollSurplusPool(address(contracts.WETH)); - contracts.defaultPool = new DefaultPool(address(contracts.WETH)); + contracts.activePool = new ActivePool(address(_collateralToken)); + contracts.borrowerOperations = new BorrowerOperations(address(_collateralToken)); + contracts.collSurplusPool = new CollSurplusPool(address(_collateralToken)); + contracts.defaultPool = new DefaultPool(address(_collateralToken)); contracts.gasPool = new GasPool(); contracts.priceFeed = new PriceFeedTestnet(); contracts.sortedTroves = new SortedTroves(); - contracts.stabilityPool = new StabilityPool(address(contracts.WETH)); + contracts.stabilityPool = new StabilityPool(address(_collateralToken)); contracts.troveManager = new TroveManager(); contracts.interestRouter = new MockInterestRouter(); - contracts.boldToken = new BoldToken( + _boldToken.setBranchAddresses( address(contracts.troveManager), address(contracts.stabilityPool), address(contracts.borrowerOperations), @@ -73,7 +123,7 @@ function _deployAndConnectContracts() returns (LiquityContracts memory contracts address(contracts.gasPool), address(contracts.collSurplusPool), address(contracts.priceFeed), - address(contracts.boldToken), + address(_boldToken), address(contracts.sortedTroves) ); @@ -87,7 +137,7 @@ function _deployAndConnectContracts() returns (LiquityContracts memory contracts address(contracts.collSurplusPool), address(contracts.priceFeed), address(contracts.sortedTroves), - address(contracts.boldToken) + address(_boldToken) ); // set contracts in the Pools @@ -95,7 +145,7 @@ function _deployAndConnectContracts() returns (LiquityContracts memory contracts address(contracts.borrowerOperations), address(contracts.troveManager), address(contracts.activePool), - address(contracts.boldToken), + address(_boldToken), address(contracts.sortedTroves), address(contracts.priceFeed) ); @@ -105,7 +155,7 @@ function _deployAndConnectContracts() returns (LiquityContracts memory contracts address(contracts.troveManager), address(contracts.stabilityPool), address(contracts.defaultPool), - address(contracts.boldToken), + address(_boldToken), address(contracts.interestRouter) ); diff --git a/contracts/src/scripts/DeployLiquity2.s.sol b/contracts/src/scripts/DeployLiquity2.s.sol index 0f37cd00..f414e82d 100644 --- a/contracts/src/scripts/DeployLiquity2.s.sol +++ b/contracts/src/scripts/DeployLiquity2.s.sol @@ -24,7 +24,7 @@ contract DeployLiquity2Script is Script, StdCheats { vm.startBroadcast(vm.envUint("DEPLOYER")); } - LiquityContracts memory contracts = _deployAndConnectContracts(); + (LiquityContracts memory contracts,,) = _deployAndConnectContracts(); vm.stopBroadcast(); if (vm.envOr("OPEN_DEMO_TROVES", false)) { diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index a99a98c8..fb8d1b79 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -11,6 +11,7 @@ import "../../Interfaces/IPriceFeed.sol"; import "../../Interfaces/ISortedTroves.sol"; import "../../Interfaces/IStabilityPool.sol"; import "../../Interfaces/ITroveManager.sol"; +import "../../Interfaces/ICollateralRegistry.sol"; import "./PriceFeedTestnet.sol"; import "../../Interfaces/IInterestRouter.sol"; import "../../GasPool.sol"; @@ -30,6 +31,7 @@ contract BaseTest is Test { address public F; address public G; + uint256 public constant DECIMAL_PRECISION = 1e18; uint256 public constant MAX_UINT256 = type(uint256).max; uint256 public constant SECONDS_IN_1_YEAR = 31536000; // 60*60*24*365 uint256 _100pct = 100e16; @@ -46,6 +48,7 @@ contract BaseTest is Test { IStabilityPool stabilityPool; ITroveManager troveManager; IBoldToken boldToken; + ICollateralRegistry collateralRegistry; IPriceFeedTestnet priceFeed; GasPool gasPool; @@ -231,7 +234,7 @@ contract BaseTest is Test { function redeem(address _from, uint256 _boldAmount) public { vm.startPrank(_from); - troveManager.redeemCollateral(_boldAmount, MAX_UINT256, 1e18); + collateralRegistry.redeemCollateral(_boldAmount, MAX_UINT256, 1e18); vm.stopPrank(); } diff --git a/contracts/src/test/TestContracts/DevTestSetup.sol b/contracts/src/test/TestContracts/DevTestSetup.sol index 59c5f686..8d2d40d6 100644 --- a/contracts/src/test/TestContracts/DevTestSetup.sol +++ b/contracts/src/test/TestContracts/DevTestSetup.sol @@ -8,19 +8,28 @@ contract DevTestSetup is BaseTest { IERC20 WETH; function giveAndApproveETH(address _account, uint256 _amount) public { - // Give some ETH to test accounts - deal(address(WETH), _account, _amount); + return giveAndApproveCollateral(WETH, _account, _amount, address(borrowerOperations)); + } + + function giveAndApproveCollateral( + IERC20 _token, + address _account, + uint256 _amount, + address _borrowerOperationsAddress + ) public { + // Give some Collateral to test accounts + deal(address(_token), _account, _amount); // Check accounts are funded - assertEq(WETH.balanceOf(_account), _amount); + assertEq(_token.balanceOf(_account), _amount); // Approve ETH to BorrowerOperations vm.startPrank(_account); - WETH.approve(address(borrowerOperations), _amount); + _token.approve(_borrowerOperationsAddress, _amount); vm.stopPrank(); // Check approvals - assertEq(WETH.allowance(_account, address(borrowerOperations)), _amount); + assertEq(_token.allowance(_account, _borrowerOperationsAddress), _amount); } function setUp() public virtual { @@ -40,7 +49,8 @@ contract DevTestSetup is BaseTest { accountsList[6] ); - LiquityContracts memory contracts = _deployAndConnectContracts(); + LiquityContracts memory contracts; + (contracts, collateralRegistry, boldToken) = _deployAndConnectContracts(); WETH = contracts.WETH; activePool = contracts.activePool; borrowerOperations = contracts.borrowerOperations; @@ -51,7 +61,6 @@ contract DevTestSetup is BaseTest { sortedTroves = contracts.sortedTroves; stabilityPool = contracts.stabilityPool; troveManager = contracts.troveManager; - boldToken = contracts.boldToken; mockInterestRouter = contracts.interestRouter; // Give some ETH to test accounts, and approve it to BorrowerOperations diff --git a/contracts/src/test/basicOps.t.sol b/contracts/src/test/basicOps.t.sol index 6ab5d309..bd8ed004 100644 --- a/contracts/src/test/basicOps.t.sol +++ b/contracts/src/test/basicOps.t.sol @@ -98,7 +98,7 @@ contract BasicOps is DevTestSetup { // A redeems 1k BOLD vm.startPrank(A); - troveManager.redeemCollateral(redemptionAmount, 10, 1e18); + collateralRegistry.redeemCollateral(redemptionAmount, 10, 1e18); // Check B's coll and debt reduced uint256 debt_2 = troveManager.getTroveDebt(B_Id); diff --git a/contracts/src/test/deployment.t.sol b/contracts/src/test/deployment.t.sol index 43910540..722de288 100644 --- a/contracts/src/test/deployment.t.sol +++ b/contracts/src/test/deployment.t.sol @@ -14,6 +14,7 @@ contract Deployment is DevTestSetup { assertNotEq(address(stabilityPool), address(0)); assertNotEq(address(troveManager), address(0)); assertNotEq(address(mockInterestRouter), address(0)); + assertNotEq(address(collateralRegistry), address(0)); logContractAddresses(); } diff --git a/contracts/src/test/multicollateral.t.sol b/contracts/src/test/multicollateral.t.sol new file mode 100644 index 00000000..efeeeeab --- /dev/null +++ b/contracts/src/test/multicollateral.t.sol @@ -0,0 +1,279 @@ +pragma solidity ^0.8.18; + +import "./TestContracts/DevTestSetup.sol"; + +contract MulticollateralTest is DevTestSetup { + uint256 constant NUM_COLLATERALS = 4; + LiquityContracts[] public contractsArray; + + function openMulticollateralTroveNoHints100pctMaxFeeWithIndex( + uint256 _collIndex, + address _account, + uint256 _index, + uint256 _coll, + uint256 _boldAmount, + uint256 _annualInterestRate + ) public returns (uint256) { + // TODO: remove when we switch to new gas compensation + if (_boldAmount >= 2000e18) _boldAmount -= 200e18; + + vm.startPrank(_account); + uint256 troveId = contractsArray[_collIndex].borrowerOperations.openTrove( + _account, _index, 1e18, _coll, _boldAmount, 0, 0, _annualInterestRate + ); + vm.stopPrank(); + return troveId; + } + + function makeMulticollateralSPDeposit(uint256 _collIndex, address _account, uint256 _amount) public { + vm.startPrank(_account); + contractsArray[_collIndex].stabilityPool.provideToSP(_amount); + vm.stopPrank(); + } + + function setUp() public override { + // Start tests at a non-zero timestamp + vm.warp(block.timestamp + 600); + + accounts = new Accounts(); + createAccounts(); + + (A, B, C, D, E, F, G) = ( + accountsList[0], + accountsList[1], + accountsList[2], + accountsList[3], + accountsList[4], + accountsList[5], + accountsList[6] + ); + + LiquityContracts[] memory _contractsArray; + (_contractsArray, collateralRegistry, boldToken) = _deployAndConnectContracts(NUM_COLLATERALS); + // Unimplemented feature (...):Copying of type struct LiquityContracts memory[] memory to storage not yet supported. + for (uint256 c = 0; c < NUM_COLLATERALS; c++) { + contractsArray.push(_contractsArray[c]); + } + // Set all price feeds to 2k + for (uint256 c = 0; c < NUM_COLLATERALS; c++) { + contractsArray[c].priceFeed.setPrice(2000e18); + } + + // Give some Collateral to test accounts, and approve it to BorrowerOperations + uint256 initialCollateralAmount = 10_000e18; + + for (uint256 c = 0; c < NUM_COLLATERALS; c++) { + for (uint256 i = 0; i < 6; i++) { + // A to F + giveAndApproveCollateral( + contractsArray[c].WETH, + accountsList[i], + initialCollateralAmount, + address(contractsArray[c].borrowerOperations) + ); + } + } + } + + function testMultiCollateralDeployment() public { + // check deployment + assertEq(collateralRegistry.totalCollaterals(), NUM_COLLATERALS, "Wrong number of branches"); + for (uint256 c = 0; c < NUM_COLLATERALS; c++) { + assertNotEq(address(collateralRegistry.getToken(c)), ZERO_ADDRESS, "Missing collateral token"); + assertNotEq(address(collateralRegistry.getTroveManager(c)), ZERO_ADDRESS, "Missing TroveManager"); + } + for (uint256 c = NUM_COLLATERALS; c < 10; c++) { + assertEq(address(collateralRegistry.getToken(c)), ZERO_ADDRESS, "Extra collateral token"); + assertEq(address(collateralRegistry.getTroveManager(c)), ZERO_ADDRESS, "Extra TroveManager"); + } + } + + function testMultiCollateralRedemption() public { + // All collaterals have the same price for this test + uint256 price = contractsArray[0].priceFeed.getPrice(); + + // First collateral unbacked Bold: 10k (SP empty) + openMulticollateralTroveNoHints100pctMaxFeeWithIndex(0, A, 0, 10e18, 10000e18, 5e16); + + // Second collateral unbacked Bold: 5k + openMulticollateralTroveNoHints100pctMaxFeeWithIndex(1, A, 0, 10e18, 10000e18, 5e16); + makeMulticollateralSPDeposit(1, A, 5000e18); + + // Third collateral unbacked Bold: 1k + openMulticollateralTroveNoHints100pctMaxFeeWithIndex(2, A, 0, 10e18, 10000e18, 5e16); + makeMulticollateralSPDeposit(2, A, 9000e18); + + // Fourth collateral unbacked Bold: 0 + openMulticollateralTroveNoHints100pctMaxFeeWithIndex(3, A, 0, 10e18, 10000e18, 5e16); + makeMulticollateralSPDeposit(3, A, 10000e18); + + // Check A’s final bal + // TODO: change when we switch to new gas compensation + //assertEq(boldToken.balanceOf(A), 16000e18, "Wrong Bold balance before redemption"); + assertEq(boldToken.balanceOf(A), 15200e18, "Wrong Bold balance before redemption"); + + // initial balances + uint256 coll1InitialBalance = contractsArray[0].WETH.balanceOf(A); + uint256 coll2InitialBalance = contractsArray[1].WETH.balanceOf(A); + uint256 coll3InitialBalance = contractsArray[2].WETH.balanceOf(A); + uint256 coll4InitialBalance = contractsArray[3].WETH.balanceOf(A); + + // fees + uint256 fee1 = contractsArray[0].troveManager.getEffectiveRedemptionFee(1000e18, price); + uint256 fee2 = contractsArray[1].troveManager.getEffectiveRedemptionFee(500e18, price); + uint256 fee3 = contractsArray[2].troveManager.getEffectiveRedemptionFee(100e18, price); + + // A redeems 1.6k + vm.startPrank(A); + collateralRegistry.redeemCollateral(1600e18, 0, 1e18); + vm.stopPrank(); + + // Check bold balance + // TODO: change when we switch to new gas compensation + //assertApproximatelyEqual(boldToken.balanceOf(A), 14400e18, 10, "Wrong Bold balance after redemption"); + assertApproximatelyEqual(boldToken.balanceOf(A), 13600e18, 10, "Wrong Bold balance after redemption"); + + // Check collateral balances + // final balances + uint256 coll1FinalBalance = contractsArray[0].WETH.balanceOf(A); + uint256 coll2FinalBalance = contractsArray[1].WETH.balanceOf(A); + uint256 coll3FinalBalance = contractsArray[2].WETH.balanceOf(A); + uint256 coll4FinalBalance = contractsArray[3].WETH.balanceOf(A); + + assertEq(coll1FinalBalance - coll1InitialBalance, 5e17 - fee1, "Wrong Collateral 1 balance"); + assertEq(coll2FinalBalance - coll2InitialBalance, 25e16 - fee2, "Wrong Collateral 2 balance"); + assertEq(coll3FinalBalance - coll3InitialBalance, 5e16 - fee3, "Wrong Collateral 3 balance"); + assertEq(coll4FinalBalance - coll4InitialBalance, 0, "Wrong Collateral 4 balance"); + } + + struct TestValues { + uint256 price; + uint256 unbackedPortion; + uint256 redeemAmount; + uint256 fee; + uint256 collInitialBalance; + uint256 collFinalBalance; + } + + function testMultiCollateralRedemptionFuzz( + uint256 _spBoldAmount1, + uint256 _spBoldAmount2, + uint256 _spBoldAmount3, + uint256 _spBoldAmount4, + uint256 _redemptionFraction + ) public { + TestValues memory testValues1; + TestValues memory testValues2; + TestValues memory testValues3; + TestValues memory testValues4; + + testValues1.price = contractsArray[0].priceFeed.getPrice(); + testValues2.price = contractsArray[1].priceFeed.getPrice(); + testValues3.price = contractsArray[2].priceFeed.getPrice(); + testValues4.price = contractsArray[3].priceFeed.getPrice(); + + uint256 boldAmount = 10000e18; + // TODO: remove gas compensation + _spBoldAmount1 = bound(_spBoldAmount1, 0, boldAmount - 200e18); + _spBoldAmount2 = bound(_spBoldAmount2, 0, boldAmount - 200e18); + _spBoldAmount3 = bound(_spBoldAmount3, 0, boldAmount - 200e18); + _spBoldAmount4 = bound(_spBoldAmount4, 0, boldAmount - 200e18); + // With too low redemption fractions, it reverts due to `newBaseRate` rounding down to zero, so we put a min of 0.01% + _redemptionFraction = bound(_redemptionFraction, 1e14, DECIMAL_PRECISION); + + // First collateral + openMulticollateralTroveNoHints100pctMaxFeeWithIndex(0, A, 0, 10e18, boldAmount, 5e16); + if (_spBoldAmount1 > 0) makeMulticollateralSPDeposit(0, A, _spBoldAmount1); + + // Second collateral + openMulticollateralTroveNoHints100pctMaxFeeWithIndex(1, A, 0, 10e18, boldAmount, 5e16); + if (_spBoldAmount2 > 0) makeMulticollateralSPDeposit(1, A, _spBoldAmount2); + + // Third collateral + openMulticollateralTroveNoHints100pctMaxFeeWithIndex(2, A, 0, 10e18, boldAmount, 5e16); + if (_spBoldAmount3 > 0) makeMulticollateralSPDeposit(2, A, _spBoldAmount3); + + // Fourth collateral + openMulticollateralTroveNoHints100pctMaxFeeWithIndex(3, A, 0, 10e18, boldAmount, 5e16); + if (_spBoldAmount4 > 0) makeMulticollateralSPDeposit(3, A, _spBoldAmount4); + + uint256 boldBalance = boldToken.balanceOf(A); + // Check A’s final bal + // TODO: change when we switch to new gas compensation + //assertEq(boldToken.balanceOf(A), boldAmount * 4 - _spBoldAmount1 - _spBoldAmount2 - _spBoldAmount3 - _spBoldAmount4, "Wrong Bold balance before redemption"); + // Stack too deep + //assertEq(boldBalance, boldAmount * 4 - _spBoldAmount1 - _spBoldAmount2 - _spBoldAmount3 - _spBoldAmount4 - 800e18, "Wrong Bold balance before redemption"); + + uint256 redeemAmount = boldBalance * _redemptionFraction / DECIMAL_PRECISION; + + // initial balances + testValues1.collInitialBalance = contractsArray[0].WETH.balanceOf(A); + testValues2.collInitialBalance = contractsArray[1].WETH.balanceOf(A); + testValues3.collInitialBalance = contractsArray[2].WETH.balanceOf(A); + testValues4.collInitialBalance = contractsArray[3].WETH.balanceOf(A); + + testValues1.unbackedPortion = boldAmount - _spBoldAmount1; + testValues2.unbackedPortion = boldAmount - _spBoldAmount2; + testValues3.unbackedPortion = boldAmount - _spBoldAmount3; + testValues4.unbackedPortion = boldAmount - _spBoldAmount4; + uint256 totalUnbacked = testValues1.unbackedPortion + testValues2.unbackedPortion + testValues3.unbackedPortion + + testValues4.unbackedPortion; + + testValues1.redeemAmount = redeemAmount * testValues1.unbackedPortion / totalUnbacked; + testValues2.redeemAmount = redeemAmount * testValues2.unbackedPortion / totalUnbacked; + testValues3.redeemAmount = redeemAmount * testValues3.unbackedPortion / totalUnbacked; + testValues4.redeemAmount = redeemAmount * testValues4.unbackedPortion / totalUnbacked; + + // fees + testValues1.fee = contractsArray[0].troveManager.getEffectiveRedemptionFee(testValues1.redeemAmount, testValues1.price); + testValues2.fee = contractsArray[1].troveManager.getEffectiveRedemptionFee(testValues2.redeemAmount, testValues2.price); + testValues3.fee = contractsArray[2].troveManager.getEffectiveRedemptionFee(testValues3.redeemAmount, testValues3.price); + testValues4.fee = contractsArray[3].troveManager.getEffectiveRedemptionFee(testValues4.redeemAmount, testValues4.price); + console.log(testValues1.fee, "fee1"); + console.log(testValues2.fee, "fee2"); + console.log(testValues3.fee, "fee3"); + console.log(testValues4.fee, "fee4"); + + // A redeems 1.6k + vm.startPrank(A); + collateralRegistry.redeemCollateral(redeemAmount, 0, 1e18); + vm.stopPrank(); + + // Check bold balance + assertApproximatelyEqual( + boldToken.balanceOf(A), boldBalance - redeemAmount, 10, "Wrong Bold balance after redemption" + ); + + // Check collateral balances + // final balances + testValues1.collFinalBalance = contractsArray[0].WETH.balanceOf(A); + testValues2.collFinalBalance = contractsArray[1].WETH.balanceOf(A); + testValues3.collFinalBalance = contractsArray[2].WETH.balanceOf(A); + testValues4.collFinalBalance = contractsArray[3].WETH.balanceOf(A); + + console.log(redeemAmount, "redeemAmount"); + console.log(testValues1.unbackedPortion, "testValues1.unbackedPortion"); + console.log(totalUnbacked, "totalUnbacked"); + console.log(testValues1.redeemAmount, "partial redeem amount 1"); + assertEq( + testValues1.collFinalBalance - testValues1.collInitialBalance, + testValues1.redeemAmount * DECIMAL_PRECISION / testValues1.price - testValues1.fee, + "Wrong Collateral 1 balance" + ); + assertEq( + testValues2.collFinalBalance - testValues2.collInitialBalance, + testValues2.redeemAmount * DECIMAL_PRECISION / testValues2.price - testValues2.fee, + "Wrong Collateral 2 balance" + ); + assertEq( + testValues3.collFinalBalance - testValues3.collInitialBalance, + testValues3.redeemAmount * DECIMAL_PRECISION / testValues3.price - testValues3.fee, + "Wrong Collateral 3 balance" + ); + assertEq( + testValues4.collFinalBalance - testValues4.collInitialBalance, + testValues4.redeemAmount * DECIMAL_PRECISION / testValues4.price - testValues4.fee, + "Wrong Collateral 4 balance" + ); + } +} diff --git a/contracts/src/test/troveManager.t.sol b/contracts/src/test/troveManager.t.sol index 0585f409..cdb57331 100644 --- a/contracts/src/test/troveManager.t.sol +++ b/contracts/src/test/troveManager.t.sol @@ -28,7 +28,7 @@ contract TroveManagerTest is DevTestSetup { // C redeems 1k BOLD vm.startPrank(C); - troveManager.redeemCollateral(redemptionAmount, 10, 1e18); + collateralRegistry.redeemCollateral(redemptionAmount, 10, 1e18); vm.stopPrank(); // Check A's coll and debt are the same @@ -58,7 +58,7 @@ contract TroveManagerTest is DevTestSetup { openTroveNoHints100pctMaxFee(A, 200 ether, 200000e18, 1e17); // A redeems 0.01 BOLD, base rate goes down to almost zero (it’s updated on redemption) vm.startPrank(A); - troveManager.redeemCollateral(1e16, 10, 1e18); + collateralRegistry.redeemCollateral(1e16, 10, 1e18); vm.stopPrank(); console.log(troveManager.baseRate(), "baseRate"); diff --git a/contracts/test/OwnershipTest.js b/contracts/test/OwnershipTest.js index 2639da5c..b28550ee 100644 --- a/contracts/test/OwnershipTest.js +++ b/contracts/test/OwnershipTest.js @@ -46,28 +46,33 @@ contract("All Liquity functions with onlyOwner modifier", async (accounts) => { } }; - const testSetAddresses = async (contract, numberOfAddresses) => { + const testSetAddresses = async (contract, numberOfAddresses, twice=true, method='setAddresses') => { const dumbContract = await GasPool.new(); const params = Array(numberOfAddresses).fill(dumbContract.address); // Attempt call from alice - await th.assertRevert(contract.setAddresses(...params, { from: alice })); + await th.assertRevert(contract[method](...params, { from: alice })); // Attempt to use zero address - await testZeroAddress(contract, params); + await testZeroAddress(contract, params, method); // Attempt to use non contract - await testNonContractAddress(contract, params); + await testNonContractAddress(contract, params, method); // Owner can successfully set any address - const txOwner = await contract.setAddresses(...params, { from: owner }); + const txOwner = await contract[method](...params, { from: owner }); assert.isTrue(txOwner.receipt.status); // fails if called twice - await th.assertRevert(contract.setAddresses(...params, { from: owner })); + if (twice) { + await th.assertRevert(contract[method](...params, { from: owner })); + } }; describe("TroveManager", async (accounts) => { - it("setAddresses(): reverts when called by non-owner, with wrong addresses, or twice", async () => { - await testSetAddresses(troveManager, 9); + it("setAddresses(): reverts when called by non-owner, with wrong addresses", async () => { + await testSetAddresses(troveManager, 9, false); + }); + it("setCollateralRegistry(): reverts when called by non-owner, with wrong address, or twice", async () => { + await testSetAddresses(troveManager, 1, true, 'setCollateralRegistry'); }); }); diff --git a/contracts/utils/deploymentHelpers.js b/contracts/utils/deploymentHelpers.js index 6510c39a..e43e53d5 100644 --- a/contracts/utils/deploymentHelpers.js +++ b/contracts/utils/deploymentHelpers.js @@ -120,7 +120,8 @@ class DeploymentHelper { } static async deployBoldToken(contracts, MockedBoldToken = BoldToken) { - contracts.boldToken = await MockedBoldToken.new( + contracts.boldToken = await MockedBoldToken.new(); + contracts.boldToken.setBranchAddresses( contracts.troveManager.address, contracts.stabilityPool.address, contracts.borrowerOperations.address,