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 1/9] 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, From 594f424dfefb4c5b03ab3c298fbde88f201f1c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Mon, 29 Apr 2024 17:23:46 +0100 Subject: [PATCH 2/9] fix: Check branch TCR on redemption Address PR #127 comments. --- contracts/src/BoldToken.sol | 27 ++++++---- contracts/src/CollateralRegistry.sol | 52 +++++++++++------- contracts/src/Interfaces/IBoldToken.sol | 2 + contracts/src/Interfaces/ITroveManager.sol | 6 +-- contracts/src/TroveManager.sol | 61 +++++++++------------- contracts/src/deployment.sol | 2 +- contracts/src/test/multicollateral.t.sol | 20 +++---- contracts/test/OwnershipTest.js | 25 ++++++--- 8 files changed, 110 insertions(+), 85 deletions(-) diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol index 6757ec93..c054a04a 100644 --- a/contracts/src/BoldToken.sol +++ b/contracts/src/BoldToken.sol @@ -2,8 +2,9 @@ pragma solidity 0.8.18; +import "./Dependencies/Ownable.sol"; + import "./Interfaces/IBoldToken.sol"; -import "./Dependencies/CheckContract.sol"; /* * * Based upon OpenZeppelin's ERC20 contract: @@ -22,7 +23,7 @@ import "./Dependencies/CheckContract.sol"; * 2) sendToPool() and returnFromPool(): functions callable only Liquity core contracts, which move Bold tokens between Liquity <-> user. */ -contract BoldToken is CheckContract, IBoldToken { +contract BoldToken is Ownable, IBoldToken { uint256 private _totalSupply; string internal constant _NAME = "Bold Stablecoin"; string internal constant _SYMBOL = "Bold"; @@ -60,12 +61,14 @@ contract BoldToken is CheckContract, IBoldToken { address public immutable activePoolAddress; */ // TODO: optimize to make them immutable + address public collateralRegistryAddress; mapping(address => bool) troveManagerAddresses; mapping(address => bool) stabilityPoolAddresses; mapping(address => bool) borrowerOperationsAddresses; mapping(address => bool) activePoolAddresses; // --- Events --- + event CollateralRegistryAddressChanged(address _newCollateralRegistryAddress); event TroveManagerAddressAdded(address _newTroveManagerAddress); event StabilityPoolAddressAdded(address _newStabilityPoolAddress); event BorrowerOperationsAddressAdded(address _newBorrowerOperationsAddress); @@ -88,12 +91,7 @@ contract BoldToken is CheckContract, IBoldToken { address _stabilityPoolAddress, address _borrowerOperationsAddress, address _activePoolAddress - ) external { - checkContract(_troveManagerAddress); - checkContract(_stabilityPoolAddress); - checkContract(_borrowerOperationsAddress); - checkContract(_activePoolAddress); - + ) external override onlyOwner { troveManagerAddresses[_troveManagerAddress] = true; emit TroveManagerAddressAdded(_troveManagerAddress); @@ -107,6 +105,13 @@ contract BoldToken is CheckContract, IBoldToken { emit ActivePoolAddressAdded(_activePoolAddress); } + function setCollateralRegistry(address _collateralRegistryAddress) external override onlyOwner { + collateralRegistryAddress = _collateralRegistryAddress; + emit CollateralRegistryAddressChanged(_collateralRegistryAddress); + + _renounceOwnership(); + } + // --- Functions for intra-Liquity calls --- function mint(address _account, uint256 _amount) external override { @@ -115,7 +120,7 @@ contract BoldToken is CheckContract, IBoldToken { } function burn(address _account, uint256 _amount) external override { - _requireCallerIsBOorTroveMorSP(); + _requireCallerIsCRorBOorSP(); _burn(_account, _amount); } @@ -268,9 +273,9 @@ contract BoldToken is CheckContract, IBoldToken { ); } - function _requireCallerIsBOorTroveMorSP() internal view { + function _requireCallerIsCRorBOorSP() internal view { require( - borrowerOperationsAddresses[msg.sender] || troveManagerAddresses[msg.sender] + msg.sender == collateralRegistryAddress || borrowerOperationsAddresses[msg.sender] || stabilityPoolAddresses[msg.sender], "Bold: Caller is neither BorrowerOperations nor TroveManager nor StabilityPool" ); diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index 8e8fa4a9..6e20024b 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -84,6 +84,12 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { _troveManager9 = numTokens > 9 ? _troveManagers[9] : ITroveManager(address(0)); } + struct RedemptionTotals { + uint256 totalUnbacked; + uint256 totalFeePercentage; + uint256 totalRedeemedAmount; + } + function redeemCollateral(uint256 _boldAmount, uint256 _maxIterations, uint256 _maxFeePercentage) external { _requireValidMaxFeePercentage(_maxFeePercentage); _requireAmountGreaterThanZero(_boldAmount); @@ -91,38 +97,47 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { uint256 numCollaterals = totalCollaterals; uint256[] memory unbackedPortions = new uint256[](numCollaterals); - uint256 totalUnbacked; + uint256[] memory prices = new uint256[](numCollaterals); + + RedemptionTotals memory totals; // 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; + (uint256 unbackedPortion, uint256 price, bool redeemable) = + troveManager.getUnbackedPortionPriceAndRedeemability(); + if (redeemable) { + totals.totalUnbacked += unbackedPortion; + unbackedPortions[index] = unbackedPortion; + prices[index] = price; + } } // The amount redeemed has to be outside SPs, and therefore unbacked - assert(totalUnbacked > _boldAmount); + assert(totals.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; + //uint256 unbackedPortion = unbackedPortions[index]; + if (unbackedPortions[index] > 0) { + uint256 redeemAmount = _boldAmount * unbackedPortions[index] / totals.totalUnbacked; if (redeemAmount > 0) { ITroveManager troveManager = getTroveManager(index); - uint256 feePercentage = troveManager.redeemCollateral(msg.sender, redeemAmount, _maxIterations); - totalFeePercentage += feePercentage * redeemAmount; - totalRedeemedAmount += redeemAmount; + (uint256 redeemedAmount, uint256 feePercentage) = + troveManager.redeemCollateral(msg.sender, redeemAmount, prices[index], _maxIterations); + totals.totalFeePercentage += feePercentage * redeemedAmount; + totals.totalRedeemedAmount += redeemedAmount; } } } - // 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"); + + // Burn the total Bold that is cancelled with debt + if (totals.totalRedeemedAmount > 0) { + boldToken.burn(msg.sender, totals.totalRedeemedAmount); + } + assert(totals.totalRedeemedAmount * DECIMAL_PRECISION / _boldAmount > 1e18 - 1e14); // 0.01% error + totals.totalFeePercentage = totals.totalFeePercentage / totals.totalRedeemedAmount; + require(totals.totalFeePercentage <= _maxFeePercentage, "Fee exceeded provided maximum"); } function getTroveManager(uint256 _index) public view returns (ITroveManager) { @@ -174,8 +189,7 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { // 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" + boldBalance >= _amount, "TroveManager: Requested redemption amount must be <= user's Bold token balance" ); } diff --git a/contracts/src/Interfaces/IBoldToken.sol b/contracts/src/Interfaces/IBoldToken.sol index 3436b195..28d397dd 100644 --- a/contracts/src/Interfaces/IBoldToken.sol +++ b/contracts/src/Interfaces/IBoldToken.sol @@ -14,6 +14,8 @@ interface IBoldToken is IERC20, IERC20Metadata, IERC20Permit { address _activePoolAddress ) external; + function setCollateralRegistry(address _collateralRegistryAddress) external; + function version() external pure returns (string memory); function deploymentStartTime() external view returns (uint256); diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 00018f94..3ef9139b 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -43,9 +43,9 @@ interface ITroveManager is IERC721, ILiquityBase { function batchLiquidateTroves(uint256[] calldata _troveArray) external; - function redeemCollateral(address _sender, uint256 _boldAmount, uint256 _maxIterations) + function redeemCollateral(address _sender, uint256 _boldAmount, uint256 _price, uint256 _maxIterations) external - returns (uint256 _feePercentage); + returns (uint256 _redemeedAmount, uint256 _feePercentage); function updateStakeAndTotalStakes(uint256 _troveId) external returns (uint256); @@ -135,5 +135,5 @@ interface ITroveManager is IERC721, ILiquityBase { function checkTroveIsActive(uint256 _troveId) external view returns (bool); - function getUnbackedPortion() external view returns (uint256); + function getUnbackedPortionPriceAndRedeemability() external returns (uint256, uint256, bool); } diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 7b1efcd1..0fbe1e3e 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -11,11 +11,10 @@ import "./Interfaces/IBoldToken.sol"; import "./Interfaces/ISortedTroves.sol"; import "./Dependencies/LiquityBase.sol"; import "./Dependencies/Ownable.sol"; -import "./Dependencies/CheckContract.sol"; // import "forge-std/console.sol"; -contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveManager { +contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { string public constant NAME = "TroveManager"; // TODO string public constant SYMBOL = "Lv2T"; // TODO @@ -285,16 +284,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana address _boldTokenAddress, address _sortedTrovesAddress ) external override onlyOwner { - checkContract(_borrowerOperationsAddress); - checkContract(_activePoolAddress); - checkContract(_defaultPoolAddress); - checkContract(_stabilityPoolAddress); - checkContract(_gasPoolAddress); - checkContract(_collSurplusPoolAddress); - checkContract(_priceFeedAddress); - checkContract(_boldTokenAddress); - checkContract(_sortedTrovesAddress); - borrowerOperationsAddress = _borrowerOperationsAddress; activePool = IActivePool(_activePoolAddress); defaultPool = IDefaultPool(_defaultPoolAddress); @@ -317,7 +306,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana } function setCollateralRegistry(address _collateralRegistryAddress) external override onlyOwner { - checkContract(_collateralRegistryAddress); collateralRegistryAddress = _collateralRegistryAddress; emit CollateralRegistryAddressChanged(_collateralRegistryAddress); @@ -948,10 +936,10 @@ 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(address _sender, uint256 _boldamount, uint256 _maxIterations) + function redeemCollateral(address _sender, uint256 _boldamount, uint256 _price, uint256 _maxIterations) external override - returns (uint256 _feePercentage) + returns (uint256 _redemeedAmount, uint256 _feePercentage) { _requireIsCollateralRegistry(); @@ -959,10 +947,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana ContractsCache(activePool, defaultPool, boldToken, sortedTroves, collSurplusPool, gasPoolAddress); RedemptionTotals memory totals; - totals.price = priceFeed.fetchPrice(); - // TODO: remove. Return instead? - _requireTCRoverMCR(totals.price); - totals.totalBoldSupplyAtStart = getEntireSystemDebt(); totals.remainingBold = _boldamount; @@ -977,13 +961,13 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // Save the uint256 of the Trove preceding the current one uint256 nextUserToCheck = contractsCache.sortedTroves.getPrev(currentTroveId); // Skip if ICR < 100%, to make sure that redemptions always improve the CR of hit Troves - if (getCurrentICR(currentTroveId, totals.price) < _100pct) { + if (getCurrentICR(currentTroveId, _price) < _100pct) { currentTroveId = nextUserToCheck; continue; } SingleRedemptionValues memory singleRedemption = - _redeemCollateralFromTrove(contractsCache, currentTroveId, totals.remainingBold, totals.price); + _redeemCollateralFromTrove(contractsCache, currentTroveId, totals.remainingBold, _price); totals.totalBoldToRedeem = totals.totalBoldToRedeem + singleRedemption.BoldLot; totals.totalRedistDebtGains = totals.totalRedistDebtGains + singleRedemption.redistDebtGain; @@ -1009,7 +993,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana // 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. - _updateBaseRateFromRedemption(totals.totalETHDrawn, totals.price, totals.totalBoldSupplyAtStart); + _updateBaseRateFromRedemption(totals.totalETHDrawn, _price, totals.totalBoldSupplyAtStart); // Calculate the ETH fee totals.ETHFee = _getRedemptionFee(totals.totalETHDrawn); @@ -1028,11 +1012,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 sender - contractsCache.boldToken.burn(_sender, totals.totalBoldToRedeem); + // Send the redeemed ETH to sender contractsCache.activePool.sendETH(_sender, totals.ETHToSendToRedeemer); + // We’ll burn all the Bold together out in the CollateralRegistry, to save gas - return totals.ETHFee * DECIMAL_PRECISION / totals.totalETHDrawn; + return (totals.totalBoldToRedeem, totals.ETHFee * DECIMAL_PRECISION / totals.totalETHDrawn); } // --- Helper functions --- @@ -1384,14 +1368,15 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana * 2) increases the baseRate based on the amount redeemed, as a proportion of total supply */ function _getUpdatedBaseRateFromRedemption(uint256 _redeemAmount, uint256 _totalBoldSupply) - internal view + 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 = _redeemAmount * DECIMAL_PRECISION / _totalBoldSupply; + uint256 redeemedBoldFraction = _redeemAmount * DECIMAL_PRECISION / _totalBoldSupply; uint256 newBaseRate = decayedBaseRate + redeemedBoldFraction / BETA; newBaseRate = LiquityMath._min(newBaseRate, DECIMAL_PRECISION); // cap baseRate at a maximum of 100% @@ -1404,7 +1389,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana internal returns (uint256) { - uint256 newBaseRate = _getUpdatedBaseRateFromRedemption(_ETHDrawn * _price / DECIMAL_PRECISION, _totalBoldSupply); + 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 @@ -1441,7 +1427,12 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return _calcRedemptionFee(getRedemptionRateWithDecay(), _ETHDrawn); } - function getEffectiveRedemptionFee(uint256 _redeemAmount, uint256 _price) external view override returns (uint256) { + 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); @@ -1527,10 +1518,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana require(TroveIdsArrayLength > 1 && sortedTroves.getSize() > 1, "TroveManager: Only one trove in the system"); } - function _requireTCRoverMCR(uint256 _price) internal view { - require(_getTCR(_price) >= MCR, "TroveManager: Cannot redeem when TCR < MCR"); - } - // --- Trove property getters --- function getTroveStatus(uint256 _troveId) external view override returns (uint256) { @@ -1565,11 +1552,15 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana return block.timestamp - Troves[_troveId].lastDebtUpdateTime > STALE_TROVE_DURATION; } - function getUnbackedPortion() external view returns (uint256) { + function getUnbackedPortionPriceAndRedeemability() external returns (uint256, uint256, bool) { uint256 totalDebt = getEntireSystemDebt(); uint256 spSize = stabilityPool.getTotalBoldDeposits(); + uint256 unbackedPortion = totalDebt - spSize; + + uint256 price = priceFeed.fetchPrice(); + bool redeemable = _getTCR(price) >= _100pct; - return totalDebt - spSize; + return (unbackedPortion, price, redeemable); } // --- Trove property setters, called by BorrowerOperations --- diff --git a/contracts/src/deployment.sol b/contracts/src/deployment.sol index 2bb48bea..df7a6515 100644 --- a/contracts/src/deployment.sol +++ b/contracts/src/deployment.sol @@ -78,7 +78,7 @@ function _deployAndConnectContracts(uint256 _numCollaterals) } collateralRegistry = new CollateralRegistry(boldToken, collaterals, troveManagers); - + boldToken.setCollateralRegistry(address(collateralRegistry)); // Set registry in TroveManagers for (uint256 i = 0; i < _numCollaterals; i++) { contractsArray[i].troveManager.setCollateralRegistry(address(collateralRegistry)); diff --git a/contracts/src/test/multicollateral.t.sol b/contracts/src/test/multicollateral.t.sol index efeeeeab..3349d136 100644 --- a/contracts/src/test/multicollateral.t.sol +++ b/contracts/src/test/multicollateral.t.sol @@ -130,8 +130,8 @@ contract MulticollateralTest is DevTestSetup { // 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"); + //assertApproxEqAbs(boldToken.balanceOf(A), 14400e18, 10, "Wrong Bold balance after redemption"); + assertApproxEqAbs(boldToken.balanceOf(A), 13600e18, 10, "Wrong Bold balance after redemption"); // Check collateral balances // final balances @@ -225,10 +225,14 @@ contract MulticollateralTest is DevTestSetup { 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); + 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"); @@ -240,9 +244,7 @@ contract MulticollateralTest is DevTestSetup { vm.stopPrank(); // Check bold balance - assertApproximatelyEqual( - boldToken.balanceOf(A), boldBalance - redeemAmount, 10, "Wrong Bold balance after redemption" - ); + assertApproxEqAbs(boldToken.balanceOf(A), boldBalance - redeemAmount, 10, "Wrong Bold balance after redemption"); // Check collateral balances // final balances diff --git a/contracts/test/OwnershipTest.js b/contracts/test/OwnershipTest.js index b28550ee..681700c9 100644 --- a/contracts/test/OwnershipTest.js +++ b/contracts/test/OwnershipTest.js @@ -46,17 +46,19 @@ contract("All Liquity functions with onlyOwner modifier", async (accounts) => { } }; - const testSetAddresses = async (contract, numberOfAddresses, twice=true, method='setAddresses') => { + const testSetAddresses = async (contract, numberOfAddresses, checkContract=true, twice=true, method='setAddresses') => { const dumbContract = await GasPool.new(); const params = Array(numberOfAddresses).fill(dumbContract.address); // Attempt call from alice await th.assertRevert(contract[method](...params, { from: alice })); - // Attempt to use zero address - await testZeroAddress(contract, params, method); - // Attempt to use non contract - await testNonContractAddress(contract, params, method); + if (checkContract) { + // Attempt to use zero address + await testZeroAddress(contract, params, method); + // Attempt to use non contract + await testNonContractAddress(contract, params, method); + } // Owner can successfully set any address const txOwner = await contract[method](...params, { from: owner }); @@ -67,12 +69,21 @@ contract("All Liquity functions with onlyOwner modifier", async (accounts) => { } }; + describe("BoldToken", async (accounts) => { + it("setBranchAddresses(): reverts when called by non-owner, with wrong addresses", async () => { + await testSetAddresses(boldToken, 4, false, false, 'setBranchAddresses'); + }); + it("setCollateralRegistry(): reverts when called by non-owner, with wrong address, or twice", async () => { + await testSetAddresses(boldToken, 1, false, true, 'setCollateralRegistry'); + }); + }); + describe("TroveManager", async (accounts) => { it("setAddresses(): reverts when called by non-owner, with wrong addresses", async () => { - await testSetAddresses(troveManager, 9, false); + await testSetAddresses(troveManager, 9, false, false); }); it("setCollateralRegistry(): reverts when called by non-owner, with wrong address, or twice", async () => { - await testSetAddresses(troveManager, 1, true, 'setCollateralRegistry'); + await testSetAddresses(troveManager, 1, false, true, 'setCollateralRegistry'); }); }); From ec8c58dea942f507a61548cf75a205de18f62644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Mon, 29 Apr 2024 18:23:13 +0100 Subject: [PATCH 3/9] chore: Deprecate assertApproximatelyEqual Convert it to a wrapper of assertApproxEqAbs --- contracts/src/test/TestContracts/BaseTest.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/src/test/TestContracts/BaseTest.sol b/contracts/src/test/TestContracts/BaseTest.sol index fb8d1b79..34f867f0 100644 --- a/contracts/src/test/TestContracts/BaseTest.sol +++ b/contracts/src/test/TestContracts/BaseTest.sol @@ -255,11 +255,10 @@ contract BaseTest is Test { } function assertApproximatelyEqual(uint256 _x, uint256 _y, uint256 _margin) public { - assertApproximatelyEqual(_x, _y, _margin, ""); + assertApproxEqAbs(_x, _y, _margin, ""); } function assertApproximatelyEqual(uint256 _x, uint256 _y, uint256 _margin, string memory _reason) public { - uint256 diff = abs(_x, _y); - assertLe(diff, _margin, _reason); + assertApproxEqAbs(_x, _y, _margin, _reason); } } From 5bfc0aebe56490712aec67f7c1e472a97eacfe45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Mon, 29 Apr 2024 20:58:27 +0100 Subject: [PATCH 4/9] fix: Make redemption rate global --- contracts/src/CollateralRegistry.sol | 187 ++++++++++++++++-- .../src/Interfaces/ICollateralRegistry.sol | 9 + contracts/src/Interfaces/ITroveManager.sol | 18 +- contracts/src/TroveManager.sol | 161 ++------------- .../test/TestContracts/TroveManagerTester.sol | 2 + contracts/src/test/multicollateral.t.sol | 33 ++-- contracts/src/test/troveManager.t.sol | 8 +- 7 files changed, 219 insertions(+), 199 deletions(-) diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index 6e20024b..a102be5e 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -42,6 +42,30 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { IBoldToken public immutable boldToken; + uint256 public constant SECONDS_IN_ONE_MINUTE = 60; + + /* + * Half-life of 12h. 12h = 720 min + * (1/2) = d^720 => d = (1/2)^(1/720) + */ + uint256 public constant MINUTE_DECAY_FACTOR = 999037758833783000; + // 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% + + /* + * BETA: 18 digit decimal. Parameter by which to divide the redeemed fraction, in order to calc the new base rate from a redemption. + * Corresponds to (1 / ALPHA) in the white paper. + */ + uint256 public constant BETA = 2; + + uint256 public baseRate; + + // The timestamp of the latest fee operation (redemption or new Bold issuance) + uint256 public lastFeeOperationTime; + + event BaseRateUpdated(uint256 _baseRate); + event LastFeeOpTimeUpdated(uint256 _lastFeeOpTime); + constructor(IBoldToken _boldToken, IERC20[] memory _tokens, ITroveManager[] memory _troveManagers) { //checkContract(address(_boldToken)); @@ -82,12 +106,17 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { _token9 = numTokens > 9 ? _tokens[9] : IERC20(address(0)); _troveManager9 = numTokens > 9 ? _troveManagers[9] : ITroveManager(address(0)); + + // Update the baseRate state variable + // To prevent redemptions unless Bold depegs below 0.95 and allow the system to take off + baseRate = INITIAL_REDEMPTION_RATE; + emit BaseRateUpdated(INITIAL_REDEMPTION_RATE); } struct RedemptionTotals { - uint256 totalUnbacked; - uint256 totalFeePercentage; - uint256 totalRedeemedAmount; + uint256 numCollaterals; + uint256 unbacked; + uint256 redeemedAmount; } function redeemCollateral(uint256 _boldAmount, uint256 _maxIterations, uint256 _maxFeePercentage) external { @@ -95,51 +124,167 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { _requireAmountGreaterThanZero(_boldAmount); _requireBoldBalanceCoversRedemption(boldToken, msg.sender, _boldAmount); - uint256 numCollaterals = totalCollaterals; - uint256[] memory unbackedPortions = new uint256[](numCollaterals); - uint256[] memory prices = new uint256[](numCollaterals); - RedemptionTotals memory totals; + totals.numCollaterals = totalCollaterals; + uint256[] memory unbackedPortions = new uint256[](totals.numCollaterals); + uint256[] memory prices = new uint256[](totals.numCollaterals); + + // 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. + // TODO: what if the final redeemed amount is less than the requested amount? + uint256 redemptionRate = _updateBaseRateAndGetRedemptionRate(boldToken, _boldAmount); + require(redemptionRate <= _maxFeePercentage, "CR: Fee exceeded provided maximum"); + // Implicit by the above and the _requireValidMaxFeePercentage checks + //require(newBaseRate < DECIMAL_PRECISION, "CR: Fee would eat up all collateral"); + // Gather and accumulate unbacked portions - for (uint256 index = 0; index < numCollaterals; index++) { + for (uint256 index = 0; index < totals.numCollaterals; index++) { ITroveManager troveManager = getTroveManager(index); (uint256 unbackedPortion, uint256 price, bool redeemable) = troveManager.getUnbackedPortionPriceAndRedeemability(); if (redeemable) { - totals.totalUnbacked += unbackedPortion; + totals.unbacked += unbackedPortion; unbackedPortions[index] = unbackedPortion; prices[index] = price; } } // The amount redeemed has to be outside SPs, and therefore unbacked - assert(totals.totalUnbacked > _boldAmount); + assert(totals.unbacked > _boldAmount); // Compute redemption amount for each collateral and redeem against the corresponding TroveManager - for (uint256 index = 0; index < numCollaterals; index++) { + for (uint256 index = 0; index < totals.numCollaterals; index++) { //uint256 unbackedPortion = unbackedPortions[index]; if (unbackedPortions[index] > 0) { - uint256 redeemAmount = _boldAmount * unbackedPortions[index] / totals.totalUnbacked; + uint256 redeemAmount = _boldAmount * unbackedPortions[index] / totals.unbacked; if (redeemAmount > 0) { ITroveManager troveManager = getTroveManager(index); - (uint256 redeemedAmount, uint256 feePercentage) = - troveManager.redeemCollateral(msg.sender, redeemAmount, prices[index], _maxIterations); - totals.totalFeePercentage += feePercentage * redeemedAmount; - totals.totalRedeemedAmount += redeemedAmount; + uint256 redeemedAmount = troveManager.redeemCollateral( + msg.sender, redeemAmount, prices[index], redemptionRate, _maxIterations + ); + totals.redeemedAmount += redeemedAmount; } } } // Burn the total Bold that is cancelled with debt - if (totals.totalRedeemedAmount > 0) { - boldToken.burn(msg.sender, totals.totalRedeemedAmount); + if (totals.redeemedAmount > 0) { + boldToken.burn(msg.sender, totals.redeemedAmount); + } + assert(totals.redeemedAmount * DECIMAL_PRECISION / _boldAmount > 1e18 - 1e14); // 0.01% error + } + + // --- Internal fee functions --- + + // Update the last fee operation time only if time passed >= decay interval. This prevents base rate griefing. + function _updateLastFeeOpTime() internal { + uint256 timePassed = block.timestamp - lastFeeOperationTime; + + if (timePassed >= SECONDS_IN_ONE_MINUTE) { + lastFeeOperationTime = block.timestamp; + emit LastFeeOpTimeUpdated(block.timestamp); } - assert(totals.totalRedeemedAmount * DECIMAL_PRECISION / _boldAmount > 1e18 - 1e14); // 0.01% error - totals.totalFeePercentage = totals.totalFeePercentage / totals.totalRedeemedAmount; - require(totals.totalFeePercentage <= _maxFeePercentage, "Fee exceeded provided maximum"); } + function _minutesPassedSinceLastFeeOp() internal view returns (uint256) { + return (block.timestamp - lastFeeOperationTime) / SECONDS_IN_ONE_MINUTE; + } + + // Updates the `baseRate` state with math from `_getUpdatedBaseRateFromRedemption` + function _updateBaseRateAndGetRedemptionRate(IBoldToken _boldToken, uint256 _boldAmount) + internal + returns (uint256) + { + uint256 totalBoldSupplyAtStart = _boldToken.totalSupply(); + + uint256 newBaseRate = _getUpdatedBaseRateFromRedemption(_boldAmount, totalBoldSupplyAtStart); + + //assert(newBaseRate <= DECIMAL_PRECISION); // This is already enforced in the line above + assert(newBaseRate > 0); // Base rate is always non-zero after redemption + + // Update the baseRate state variable + baseRate = newBaseRate; + emit BaseRateUpdated(newBaseRate); + + _updateLastFeeOpTime(); + + return _calcRedemptionRate(newBaseRate); + } + + /* + * 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 = _redeemAmount * DECIMAL_PRECISION / _totalBoldSupply; + + uint256 newBaseRate = decayedBaseRate + redeemedBoldFraction / BETA; + newBaseRate = LiquityMath._min(newBaseRate, DECIMAL_PRECISION); // cap baseRate at a maximum of 100% + + return newBaseRate; + } + + function _calcDecayedBaseRate() internal view returns (uint256) { + uint256 minutesPassed = _minutesPassedSinceLastFeeOp(); + uint256 decayFactor = LiquityMath._decPow(MINUTE_DECAY_FACTOR, minutesPassed); + + return baseRate * decayFactor / DECIMAL_PRECISION; + } + + function _calcRedemptionRate(uint256 _baseRate) internal pure returns (uint256) { + return LiquityMath._min( + REDEMPTION_FEE_FLOOR + _baseRate, + DECIMAL_PRECISION // cap at a maximum of 100% + ); + } + + function _calcRedemptionFee(uint256 _redemptionRate, uint256 _amount) internal pure returns (uint256) { + uint256 redemptionFee = _redemptionRate * _amount / DECIMAL_PRECISION; + return redemptionFee; + } + + // external redemption rate/fee getters + + function getRedemptionRate() external view override returns (uint256) { + return _calcRedemptionRate(baseRate); + } + + function getRedemptionRateWithDecay() public view override returns (uint256) { + return _calcRedemptionRate(_calcDecayedBaseRate()); + } + + function getRedemptionFeeWithDecay(uint256 _ETHDrawn) external view override returns (uint256) { + return _calcRedemptionFee(getRedemptionRateWithDecay(), _ETHDrawn); + } + + function getEffectiveRedemptionFeeInBold(uint256 _redeemAmount) public view override returns (uint256) { + uint256 totalBoldSupply = boldToken.totalSupply(); + uint256 newBaseRate = _getUpdatedBaseRateFromRedemption(_redeemAmount, totalBoldSupply); + return _calcRedemptionFee(_calcRedemptionRate(newBaseRate), _redeemAmount); + } + + function getEffectiveRedemptionFee(uint256 _redeemAmount, uint256 _price) + external + view + override + returns (uint256) + { + return getEffectiveRedemptionFeeInBold(_redeemAmount) * DECIMAL_PRECISION / _price; + } + + // getters + function getTroveManager(uint256 _index) public view returns (ITroveManager) { if (_index == 0) return _troveManager0; else if (_index == 1) return _troveManager1; diff --git a/contracts/src/Interfaces/ICollateralRegistry.sol b/contracts/src/Interfaces/ICollateralRegistry.sol index 239fac33..d26355d9 100644 --- a/contracts/src/Interfaces/ICollateralRegistry.sol +++ b/contracts/src/Interfaces/ICollateralRegistry.sol @@ -4,9 +4,18 @@ import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "./ITroveManager.sol"; interface ICollateralRegistry { + function baseRate() external view returns (uint256); + 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); + + function getRedemptionRate() external view returns (uint256); + 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 getEffectiveRedemptionFeeInBold(uint256 _redeemAmount) external view returns (uint256); } diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 3ef9139b..5ba23cda 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -31,8 +31,6 @@ interface ITroveManager is IERC721, ILiquityBase { // function BOLD_GAS_COMPENSATION() external view returns (uint256); - function baseRate() external view returns (uint256); - function getTroveIdsCount() external view returns (uint256); function getTroveFromTroveIdsArray(uint256 _index) external view returns (uint256); @@ -43,9 +41,13 @@ interface ITroveManager is IERC721, ILiquityBase { function batchLiquidateTroves(uint256[] calldata _troveArray) external; - function redeemCollateral(address _sender, uint256 _boldAmount, uint256 _price, uint256 _maxIterations) - external - returns (uint256 _redemeedAmount, uint256 _feePercentage); + function redeemCollateral( + address _sender, + uint256 _boldAmount, + uint256 _price, + uint256 _redemptionRate, + uint256 _maxIterations + ) external returns (uint256 _redemeedAmount); function updateStakeAndTotalStakes(uint256 _troveId) external returns (uint256); @@ -78,12 +80,6 @@ interface ITroveManager is IERC721, ILiquityBase { function removeStake(uint256 _troveId) external; - function getRedemptionRate() external view returns (uint256); - 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); function getTroveStake(uint256 _troveId) external view returns (uint256); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 0fbe1e3e..1f114811 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -31,29 +31,9 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { // --- Data structures --- - uint256 public constant SECONDS_IN_ONE_MINUTE = 60; uint256 public constant SECONDS_IN_ONE_YEAR = 31536000; // 60 * 60 * 24 * 365, uint256 public constant STALE_TROVE_DURATION = 7776000; // 90 days: 60*60*24*90 = 7776000 - /* - * Half-life of 12h. 12h = 720 min - * (1/2) = d^720 => d = (1/2)^(1/720) - */ - uint256 public constant MINUTE_DECAY_FACTOR = 999037758833783000; - // 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% - - /* - * BETA: 18 digit decimal. Parameter by which to divide the redeemed fraction, in order to calc the new base rate from a redemption. - * Corresponds to (1 / ALPHA) in the white paper. - */ - uint256 public constant BETA = 2; - - uint256 public baseRate; - - // The timestamp of the latest fee operation (redemption or new Bold issuance) - uint256 public lastFeeOperationTime; - enum Status { nonExistent, active, @@ -210,7 +190,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { uint256 ETHToSendToRedeemer; uint256 decayedBaseRate; uint256 price; - uint256 totalBoldSupplyAtStart; uint256 totalRedistDebtGains; uint256 totalNewRecordedTroveDebts; uint256 totalOldRecordedTroveDebts; @@ -249,8 +228,6 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { uint256 indexed _troveId, uint256 _debt, uint256 _coll, uint256 _stake, TroveManagerOperation _operation ); event TroveLiquidated(uint256 indexed _troveId, uint256 _debt, uint256 _coll, TroveManagerOperation _operation); - event BaseRateUpdated(uint256 _baseRate); - event LastFeeOpTimeUpdated(uint256 _lastFeeOpTime); event TotalStakesUpdated(uint256 _newTotalStakes); event SystemSnapshotsUpdated(uint256 _totalStakesSnapshot, uint256 _totalCollateralSnapshot); event LTermsUpdated(uint256 _L_ETH, uint256 _L_boldDebt); @@ -264,12 +241,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { redeemCollateral } - constructor() ERC721(NAME, SYMBOL) { - // Update the baseRate state variable - // To prevent redemptions unless Bold depegs below 0.95 and allow the system to take off - baseRate = INITIAL_REDEMPTION_RATE; - emit BaseRateUpdated(INITIAL_REDEMPTION_RATE); - } + constructor() ERC721(NAME, SYMBOL) {} // --- Dependency setter --- @@ -936,19 +908,19 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { * 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(address _sender, uint256 _boldamount, uint256 _price, uint256 _maxIterations) - external - override - returns (uint256 _redemeedAmount, uint256 _feePercentage) - { + function redeemCollateral( + address _sender, + uint256 _boldamount, + uint256 _price, + uint256 _redemptionRate, + uint256 _maxIterations + ) external override returns (uint256 _redemeedAmount) { _requireIsCollateralRegistry(); ContractsCache memory contractsCache = ContractsCache(activePool, defaultPool, boldToken, sortedTroves, collSurplusPool, gasPoolAddress); RedemptionTotals memory totals; - totals.totalBoldSupplyAtStart = getEntireSystemDebt(); - totals.remainingBold = _boldamount; uint256 currentTroveId; @@ -991,12 +963,8 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { // 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. - _updateBaseRateFromRedemption(totals.totalETHDrawn, _price, totals.totalBoldSupplyAtStart); - // Calculate the ETH fee - totals.ETHFee = _getRedemptionFee(totals.totalETHDrawn); + totals.ETHFee = _getRedemptionFee(totals.totalETHDrawn, _redemptionRate); // Do nothing with the fee - the funds remain in ActivePool. TODO: replace with new redemption fee scheme totals.ETHToSendToRedeemer = totals.totalETHDrawn - totals.ETHFee; @@ -1016,7 +984,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { contractsCache.activePool.sendETH(_sender, totals.ETHToSendToRedeemer); // We’ll burn all the Bold together out in the CollateralRegistry, to save gas - return (totals.totalBoldToRedeem, totals.ETHFee * DECIMAL_PRECISION / totals.totalETHDrawn); + return totals.totalBoldToRedeem; } // --- Helper functions --- @@ -1359,118 +1327,15 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { return TCR < CCR; } - // --- 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 _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 = _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 - - // Update the baseRate state variable - baseRate = newBaseRate; - emit BaseRateUpdated(newBaseRate); - - _updateLastFeeOpTime(); - - return newBaseRate; - } - - function getRedemptionRate() public view override returns (uint256) { - return _calcRedemptionRate(baseRate); - } - - function getRedemptionRateWithDecay() public view override returns (uint256) { - return _calcRedemptionRate(_calcDecayedBaseRate()); - } - - function _calcRedemptionRate(uint256 _baseRate) internal pure returns (uint256) { - return LiquityMath._min( - REDEMPTION_FEE_FLOOR + _baseRate, - DECIMAL_PRECISION // cap at a maximum of 100% - ); - } - - function _getRedemptionFee(uint256 _ETHDrawn) internal view returns (uint256) { - return _calcRedemptionFee(getRedemptionRate(), _ETHDrawn); - } - - function getRedemptionFeeWithDecay(uint256 _ETHDrawn) external view override returns (uint256) { - 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 checkTroveIsActive(uint256 _troveId) public view returns (bool) { + return Troves[_troveId].status == Status.active; } - function _calcRedemptionFee(uint256 _redemptionRate, uint256 _ETHDrawn) internal pure returns (uint256) { + function _getRedemptionFee(uint256 _ETHDrawn, uint256 _redemptionRate) internal view returns (uint256) { uint256 redemptionFee = _redemptionRate * _ETHDrawn / DECIMAL_PRECISION; - require(redemptionFee < _ETHDrawn, "TroveManager: Fee would eat up all returned collateral"); return redemptionFee; } - // --- Internal fee functions --- - - // Update the last fee operation time only if time passed >= decay interval. This prevents base rate griefing. - function _updateLastFeeOpTime() internal { - uint256 timePassed = block.timestamp - lastFeeOperationTime; - - if (timePassed >= SECONDS_IN_ONE_MINUTE) { - lastFeeOperationTime = block.timestamp; - emit LastFeeOpTimeUpdated(block.timestamp); - } - } - - function _calcDecayedBaseRate() internal view returns (uint256) { - uint256 minutesPassed = _minutesPassedSinceLastFeeOp(); - uint256 decayFactor = LiquityMath._decPow(MINUTE_DECAY_FACTOR, minutesPassed); - - return baseRate * decayFactor / DECIMAL_PRECISION; - } - - function _minutesPassedSinceLastFeeOp() internal view returns (uint256) { - return (block.timestamp - lastFeeOperationTime) / SECONDS_IN_ONE_MINUTE; - } - - function checkTroveIsActive(uint256 _troveId) public view returns (bool) { - return Troves[_troveId].status == Status.active; - } - // --- Interest rate calculations --- // TODO: analyze precision loss in interest functions and decide upon the minimum granularity diff --git a/contracts/src/test/TestContracts/TroveManagerTester.sol b/contracts/src/test/TestContracts/TroveManagerTester.sol index f3f2dfb6..ec4388d0 100644 --- a/contracts/src/test/TestContracts/TroveManagerTester.sol +++ b/contracts/src/test/TestContracts/TroveManagerTester.sol @@ -24,6 +24,7 @@ contract TroveManagerTester is TroveManager { return _getCompositeDebt(_debt); } + /* function unprotectedDecayBaseRateFromBorrowing() external returns (uint256) { baseRate = _calcDecayedBaseRate(); assert(baseRate >= 0 && baseRate <= DECIMAL_PRECISION); @@ -47,6 +48,7 @@ contract TroveManagerTester is TroveManager { function callGetRedemptionFee(uint256 _ETHDrawn) external view returns (uint256) { return _getRedemptionFee(_ETHDrawn); } + */ function getActualDebtFromComposite(uint256 _debtVal) external pure returns (uint256) { return _getNetDebt(_debtVal); diff --git a/contracts/src/test/multicollateral.t.sol b/contracts/src/test/multicollateral.t.sol index 3349d136..c6d7fc6e 100644 --- a/contracts/src/test/multicollateral.t.sol +++ b/contracts/src/test/multicollateral.t.sol @@ -119,9 +119,10 @@ contract MulticollateralTest is DevTestSetup { 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); + uint256 fee = collateralRegistry.getEffectiveRedemptionFeeInBold(1600e18) * DECIMAL_PRECISION / price; + uint256 fee1 = fee * 10 / 16; + uint256 fee2 = fee * 5 / 16; + uint256 fee3 = fee / 16; // A redeems 1.6k vm.startPrank(A); @@ -225,14 +226,12 @@ contract MulticollateralTest is DevTestSetup { 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); + uint256 fee = collateralRegistry.getEffectiveRedemptionFeeInBold(redeemAmount); + testValues1.fee = fee * testValues1.redeemAmount / redeemAmount * DECIMAL_PRECISION / testValues1.price; + testValues2.fee = fee * testValues2.redeemAmount / redeemAmount * DECIMAL_PRECISION / testValues2.price; + testValues3.fee = fee * testValues3.redeemAmount / redeemAmount * DECIMAL_PRECISION / testValues3.price; + testValues4.fee = fee * testValues4.redeemAmount / redeemAmount * DECIMAL_PRECISION / testValues4.price; + console.log(testValues1.fee, "fee1"); console.log(testValues2.fee, "fee2"); console.log(testValues3.fee, "fee3"); @@ -257,24 +256,28 @@ contract MulticollateralTest is DevTestSetup { console.log(testValues1.unbackedPortion, "testValues1.unbackedPortion"); console.log(totalUnbacked, "totalUnbacked"); console.log(testValues1.redeemAmount, "partial redeem amount 1"); - assertEq( + assertApproxEqAbs( testValues1.collFinalBalance - testValues1.collInitialBalance, testValues1.redeemAmount * DECIMAL_PRECISION / testValues1.price - testValues1.fee, + 10, "Wrong Collateral 1 balance" ); - assertEq( + assertApproxEqAbs( testValues2.collFinalBalance - testValues2.collInitialBalance, testValues2.redeemAmount * DECIMAL_PRECISION / testValues2.price - testValues2.fee, + 10, "Wrong Collateral 2 balance" ); - assertEq( + assertApproxEqAbs( testValues3.collFinalBalance - testValues3.collInitialBalance, testValues3.redeemAmount * DECIMAL_PRECISION / testValues3.price - testValues3.fee, + 10, "Wrong Collateral 3 balance" ); - assertEq( + assertApproxEqAbs( testValues4.collFinalBalance - testValues4.collInitialBalance, testValues4.redeemAmount * DECIMAL_PRECISION / testValues4.price - testValues4.fee, + 10, "Wrong Collateral 4 balance" ); } diff --git a/contracts/src/test/troveManager.t.sol b/contracts/src/test/troveManager.t.sol index cdb57331..c7f85df9 100644 --- a/contracts/src/test/troveManager.t.sol +++ b/contracts/src/test/troveManager.t.sol @@ -45,11 +45,11 @@ contract TroveManagerTest is DevTestSetup { } function testInitialRedemptionBaseRate() public { - assertEq(troveManager.baseRate(), 5e16); + assertEq(collateralRegistry.baseRate(), 5e16); } function testRedemptionBaseRateAfter2Weeks() public { - assertEq(troveManager.baseRate(), 5e16); + assertEq(collateralRegistry.baseRate(), 5e16); // Two weeks go by vm.warp(block.timestamp + 14 days); @@ -61,7 +61,7 @@ contract TroveManagerTest is DevTestSetup { collateralRegistry.redeemCollateral(1e16, 10, 1e18); vm.stopPrank(); - console.log(troveManager.baseRate(), "baseRate"); - assertLt(troveManager.baseRate(), 3e10); // Goes down below 3e-8, i.e., below 0.000003% + console.log(collateralRegistry.baseRate(), "baseRate"); + assertLt(collateralRegistry.baseRate(), 3e10); // Goes down below 3e-8, i.e., below 0.000003% } } From 2892b78c47e646aca6529d374cf5cd28d10b1467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Tue, 30 Apr 2024 11:17:34 +0100 Subject: [PATCH 5/9] chore: Rename CollateralRegistry vars --- contracts/src/CollateralRegistry.sol | 166 +++++++++++++-------------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index a102be5e..b3207ae1 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -18,27 +18,27 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { // 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; + 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; @@ -77,35 +77,35 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { boldToken = _boldToken; - _token0 = _tokens[0]; - _troveManager0 = _troveManagers[0]; + token0 = _tokens[0]; + troveManager0 = _troveManagers[0]; - _token1 = numTokens > 1 ? _tokens[1] : IERC20(address(0)); - _troveManager1 = numTokens > 1 ? _troveManagers[1] : ITroveManager(address(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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + token9 = numTokens > 9 ? _tokens[9] : IERC20(address(0)); + troveManager9 = numTokens > 9 ? _troveManagers[9] : ITroveManager(address(0)); // Update the baseRate state variable // To prevent redemptions unless Bold depegs below 0.95 and allow the system to take off @@ -286,30 +286,30 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { // getters 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; + 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; + 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"); } @@ -340,33 +340,33 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { /* 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; } + 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; } + 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"); } From d5682f57b1d20cd8f5f038c2a13a8d27843f56c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Tue, 30 Apr 2024 11:17:58 +0100 Subject: [PATCH 6/9] test: Rename OwnershipTest function --- contracts/test/OwnershipTest.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/test/OwnershipTest.js b/contracts/test/OwnershipTest.js index 681700c9..0f5bc12d 100644 --- a/contracts/test/OwnershipTest.js +++ b/contracts/test/OwnershipTest.js @@ -46,7 +46,7 @@ contract("All Liquity functions with onlyOwner modifier", async (accounts) => { } }; - const testSetAddresses = async (contract, numberOfAddresses, checkContract=true, twice=true, method='setAddresses') => { + const testDeploymentSetter = async (contract, numberOfAddresses, checkContract=true, twice=true, method='setAddresses') => { const dumbContract = await GasPool.new(); const params = Array(numberOfAddresses).fill(dumbContract.address); @@ -71,43 +71,43 @@ contract("All Liquity functions with onlyOwner modifier", async (accounts) => { describe("BoldToken", async (accounts) => { it("setBranchAddresses(): reverts when called by non-owner, with wrong addresses", async () => { - await testSetAddresses(boldToken, 4, false, false, 'setBranchAddresses'); + await testDeploymentSetter(boldToken, 4, false, false, 'setBranchAddresses'); }); it("setCollateralRegistry(): reverts when called by non-owner, with wrong address, or twice", async () => { - await testSetAddresses(boldToken, 1, false, true, 'setCollateralRegistry'); + await testDeploymentSetter(boldToken, 1, false, true, 'setCollateralRegistry'); }); }); describe("TroveManager", async (accounts) => { it("setAddresses(): reverts when called by non-owner, with wrong addresses", async () => { - await testSetAddresses(troveManager, 9, false, false); + await testDeploymentSetter(troveManager, 9, false, false); }); it("setCollateralRegistry(): reverts when called by non-owner, with wrong address, or twice", async () => { - await testSetAddresses(troveManager, 1, false, true, 'setCollateralRegistry'); + await testDeploymentSetter(troveManager, 1, false, true, 'setCollateralRegistry'); }); }); describe("BorrowerOperations", async (accounts) => { it("setAddresses(): reverts when called by non-owner, with wrong addresses, or twice", async () => { - await testSetAddresses(borrowerOperations, 9); + await testDeploymentSetter(borrowerOperations, 9); }); }); describe("DefaultPool", async (accounts) => { it("setAddresses(): reverts when called by non-owner, with wrong addresses, or twice", async () => { - await testSetAddresses(defaultPool, 2); + await testDeploymentSetter(defaultPool, 2); }); }); describe("StabilityPool", async (accounts) => { it("setAddresses(): reverts when called by non-owner, with wrong addresses, or twice", async () => { - await testSetAddresses(stabilityPool, 6); + await testDeploymentSetter(stabilityPool, 6); }); }); describe("ActivePool", async (accounts) => { it("setAddresses(): reverts when called by non-owner, with wrong addresses, or twice", async () => { - await testSetAddresses(activePool, 6); + await testDeploymentSetter(activePool, 6); }); }); From 4705004b15f40241bf39850742822d1901bc3324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Tue, 30 Apr 2024 11:18:55 +0100 Subject: [PATCH 7/9] fix: Update base rate after redemptions Address PR #127 comments. --- contracts/src/CollateralRegistry.sol | 36 +++++++++++++--------------- contracts/src/TroveManager.sol | 2 +- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index b3207ae1..73590e0c 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -67,8 +67,6 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { event LastFeeOpTimeUpdated(uint256 _lastFeeOpTime); 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"); @@ -115,11 +113,12 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { struct RedemptionTotals { uint256 numCollaterals; + uint256 boldSupplyAtStart; uint256 unbacked; uint256 redeemedAmount; } - function redeemCollateral(uint256 _boldAmount, uint256 _maxIterations, uint256 _maxFeePercentage) external { + function redeemCollateral(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _maxFeePercentage) external { _requireValidMaxFeePercentage(_maxFeePercentage); _requireAmountGreaterThanZero(_boldAmount); _requireBoldBalanceCoversRedemption(boldToken, msg.sender, _boldAmount); @@ -130,10 +129,13 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { uint256[] memory unbackedPortions = new uint256[](totals.numCollaterals); uint256[] memory prices = new uint256[](totals.numCollaterals); + totals.boldSupplyAtStart = boldToken.totalSupply(); // 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. - // TODO: what if the final redeemed amount is less than the requested amount? - uint256 redemptionRate = _updateBaseRateAndGetRedemptionRate(boldToken, _boldAmount); + // We only compute it here, and update it at the end, + // because the final redeemed amount may be less than the requested amount + // Redeemers should take this into account in order to request the optimal amount to not overpay + uint256 redemptionRate = _calcRedemptionRate(_getUpdatedBaseRateFromRedemption(_boldAmount, totals.boldSupplyAtStart)); require(redemptionRate <= _maxFeePercentage, "CR: Fee exceeded provided maximum"); // Implicit by the above and the _requireValidMaxFeePercentage checks //require(newBaseRate < DECIMAL_PRECISION, "CR: Fee would eat up all collateral"); @@ -151,7 +153,7 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { } // The amount redeemed has to be outside SPs, and therefore unbacked - assert(totals.unbacked > _boldAmount); + assert(totals.unbacked >= _boldAmount); // Compute redemption amount for each collateral and redeem against the corresponding TroveManager for (uint256 index = 0; index < totals.numCollaterals; index++) { @@ -161,18 +163,19 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { if (redeemAmount > 0) { ITroveManager troveManager = getTroveManager(index); uint256 redeemedAmount = troveManager.redeemCollateral( - msg.sender, redeemAmount, prices[index], redemptionRate, _maxIterations + msg.sender, redeemAmount, prices[index], redemptionRate, _maxIterationsPerCollateral ); totals.redeemedAmount += redeemedAmount; } } } + _updateBaseRateAndGetRedemptionRate(boldToken, totals.redeemedAmount, totals.boldSupplyAtStart); + // Burn the total Bold that is cancelled with debt if (totals.redeemedAmount > 0) { boldToken.burn(msg.sender, totals.redeemedAmount); } - assert(totals.redeemedAmount * DECIMAL_PRECISION / _boldAmount > 1e18 - 1e14); // 0.01% error } // --- Internal fee functions --- @@ -192,15 +195,12 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { } // Updates the `baseRate` state with math from `_getUpdatedBaseRateFromRedemption` - function _updateBaseRateAndGetRedemptionRate(IBoldToken _boldToken, uint256 _boldAmount) + function _updateBaseRateAndGetRedemptionRate(IBoldToken _boldToken, uint256 _boldAmount, uint256 _totalBoldSupplyAtStart) internal - returns (uint256) { - uint256 totalBoldSupplyAtStart = _boldToken.totalSupply(); + uint256 newBaseRate = _getUpdatedBaseRateFromRedemption(_boldAmount, _totalBoldSupplyAtStart); - uint256 newBaseRate = _getUpdatedBaseRateFromRedemption(_boldAmount, totalBoldSupplyAtStart); - - //assert(newBaseRate <= DECIMAL_PRECISION); // This is already enforced in the line above + //assert(newBaseRate <= DECIMAL_PRECISION); // This is already enforced in `_getUpdatedBaseRateFromRedemption` assert(newBaseRate > 0); // Base rate is always non-zero after redemption // Update the baseRate state variable @@ -208,12 +208,10 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { emit BaseRateUpdated(newBaseRate); _updateLastFeeOpTime(); - - return _calcRedemptionRate(newBaseRate); } /* - * This function has two impacts on the baseRate state variable: + * This function calculates the new baseRate in the following way: * 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 @@ -223,10 +221,10 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { view returns (uint256) { + // decay the base rate 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. */ + // get the fraction of total supply that was redeemed uint256 redeemedBoldFraction = _redeemAmount * DECIMAL_PRECISION / _totalBoldSupply; uint256 newBaseRate = decayedBaseRate + redeemedBoldFraction / BETA; diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 1f114811..eeaa5cb2 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -1331,7 +1331,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, ITroveManager { return Troves[_troveId].status == Status.active; } - function _getRedemptionFee(uint256 _ETHDrawn, uint256 _redemptionRate) internal view returns (uint256) { + function _getRedemptionFee(uint256 _ETHDrawn, uint256 _redemptionRate) internal pure returns (uint256) { uint256 redemptionFee = _redemptionRate * _ETHDrawn / DECIMAL_PRECISION; return redemptionFee; } From ef30a4a31cda6bfd77d49271ba1d7b87d56bea91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Tue, 30 Apr 2024 11:53:35 +0100 Subject: [PATCH 8/9] test: Add CollateralRegistry to javascript tests --- .../CollateralRegistryTester.sol | 32 +++++++++++++++++++ contracts/test/BorrowerOperationsTest.js | 12 ++++--- contracts/test/FeeArithmeticTest.js | 24 +++++++------- contracts/utils/deploymentHelpers.js | 6 ++++ 4 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 contracts/src/test/TestContracts/CollateralRegistryTester.sol diff --git a/contracts/src/test/TestContracts/CollateralRegistryTester.sol b/contracts/src/test/TestContracts/CollateralRegistryTester.sol new file mode 100644 index 00000000..a69242e5 --- /dev/null +++ b/contracts/src/test/TestContracts/CollateralRegistryTester.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import "../../CollateralRegistry.sol"; + +/* Tester contract inherits from CollateralRegistry, and provides external functions +for testing the parent's internal functions. */ + +contract CollateralRegistryTester is CollateralRegistry { + constructor(IBoldToken _boldToken, IERC20[] memory _tokens, ITroveManager[] memory _troveManagers) CollateralRegistry(_boldToken, _tokens, _troveManagers) {} + + function unprotectedDecayBaseRateFromBorrowing() external returns (uint256) { + baseRate = _calcDecayedBaseRate(); + assert(baseRate >= 0 && baseRate <= DECIMAL_PRECISION); + + _updateLastFeeOpTime(); + return baseRate; + } + + function minutesPassedSinceLastFeeOp() external view returns (uint256) { + return _minutesPassedSinceLastFeeOp(); + } + + function setLastFeeOpTimeToNow() external { + lastFeeOperationTime = block.timestamp; + } + + function setBaseRate(uint256 _baseRate) external { + baseRate = _baseRate; + } +} diff --git a/contracts/test/BorrowerOperationsTest.js b/contracts/test/BorrowerOperationsTest.js index 3c800520..45d237df 100644 --- a/contracts/test/BorrowerOperationsTest.js +++ b/contracts/test/BorrowerOperationsTest.js @@ -5,6 +5,7 @@ const BorrowerOperationsTester = artifacts.require( "./BorrowerOperationsTester.sol", ); const TroveManagerTester = artifacts.require("TroveManagerTester"); +const CollateralRegistryTester = artifacts.require("CollateralRegistryTester"); const { dec, toBN, assertRevert } = th; @@ -42,6 +43,7 @@ contract("BorrowerOperations", async (accounts) => { let activePool; let defaultPool; let borrowerOperations; + let collateralRegistry; let BOLD_GAS_COMPENSATION; let MIN_NET_DEBT; @@ -60,6 +62,7 @@ contract("BorrowerOperations", async (accounts) => { mocks: { BorrowerOperations: BorrowerOperationsTester, TroveManager: TroveManagerTester, + CollateralRegistry: CollateralRegistryTester, }, callback: async (contracts) => { const { borrowerOperations } = contracts; @@ -92,6 +95,7 @@ contract("BorrowerOperations", async (accounts) => { activePool = contracts.activePool; defaultPool = contracts.defaultPool; borrowerOperations = contracts.borrowerOperations; + collateralRegistry = contracts.collateralRegistry; BOLD_GAS_COMPENSATION = result.BOLD_GAS_COMPENSATION; MIN_NET_DEBT = result.MIN_NET_DEBT; @@ -3517,13 +3521,13 @@ contract("BorrowerOperations", async (accounts) => { assert.isTrue(B_Coll.eq(B_emittedColl)); assert.isTrue(C_Coll.eq(C_emittedColl)); - const baseRateBefore = await troveManager.baseRate(); + const baseRateBefore = await collateralRegistry.baseRate(); // Artificially make baseRate 6% (higher than the intital 5%) - await troveManager.setBaseRate(dec(6, 16)); - await troveManager.setLastFeeOpTimeToNow(); + await collateralRegistry.setBaseRate(dec(6, 16)); + await collateralRegistry.setLastFeeOpTimeToNow(); - assert.isTrue((await troveManager.baseRate()).gt(baseRateBefore)); + assert.isTrue((await collateralRegistry.baseRate()).gt(baseRateBefore)); const { troveId: DTroveId, tx: txD } = await openTrove({ extraBoldAmount: toBN(dec(5000, 18)), diff --git a/contracts/test/FeeArithmeticTest.js b/contracts/test/FeeArithmeticTest.js index 2da8c064..0e1b6a12 100644 --- a/contracts/test/FeeArithmeticTest.js +++ b/contracts/test/FeeArithmeticTest.js @@ -3,7 +3,7 @@ const Decimal = require("decimal.js"); const { BNConverter } = require("../utils/BNConverter.js"); const testHelpers = require("../utils/testHelpers.js"); const { createDeployAndFundFixture } = require("../utils/testFixtures.js"); -const TroveManagerTester = artifacts.require("./TroveManagerTester.sol"); +const CollateralRegistryTester = artifacts.require("./CollateralRegistryTester.sol"); const LiquityMathTester = artifacts.require("./LiquityMathTester.sol"); const th = testHelpers.TestHelper; @@ -14,7 +14,7 @@ const getDifference = th.getDifference; contract("Fee arithmetic tests", async (accounts) => { let contracts; - let troveManagerTester; + let collateralRegistryTester; let mathTester; const [bountyAddress, lpRewardsAddress, multisig] = accounts.slice(997, 1000); @@ -331,40 +331,42 @@ contract("Fee arithmetic tests", async (accounts) => { ]; const deployFixture = createDeployAndFundFixture({ - callback: async () => { - const troveManagerTester = await TroveManagerTester.new(); - TroveManagerTester.setAsDeployed(troveManagerTester); + mocks: { + CollateralRegistry: CollateralRegistryTester, + }, + callback: async (contracts) => { + const { collateralRegistry: collateralRegistryTester } = contracts; const mathTester = await LiquityMathTester.new(); LiquityMathTester.setAsDeployed(mathTester); - return { mathTester, troveManagerTester }; + return { mathTester, collateralRegistryTester }; }, }); beforeEach(async () => { const result = await deployFixture(); contracts = result.contracts; - troveManagerTester = result.troveManagerTester; + collateralRegistryTester = result.collateralRegistryTester; mathTester = result.mathTester; }); it("minutesPassedSinceLastFeeOp(): returns minutes passed for no time increase", async () => { - await troveManagerTester.setLastFeeOpTimeToNow(); - const minutesPassed = await troveManagerTester.minutesPassedSinceLastFeeOp(); + await collateralRegistryTester.setLastFeeOpTimeToNow(); + const minutesPassed = await collateralRegistryTester.minutesPassedSinceLastFeeOp(); assert.equal(minutesPassed, "0"); }); it("minutesPassedSinceLastFeeOp(): returns minutes passed between time of last fee operation and current block.timestamp, rounded down to nearest minutes", async () => { for (const [seconds, expectedMinutesPassed] of secondsToMinutesRoundedDown) { - await troveManagerTester.setLastFeeOpTimeToNow(); + await collateralRegistryTester.setLastFeeOpTimeToNow(); if (seconds > 0) { await time.increase(seconds); } - const minutesPassed = await troveManagerTester.minutesPassedSinceLastFeeOp(); + const minutesPassed = await collateralRegistryTester.minutesPassedSinceLastFeeOp(); assert.equal(expectedMinutesPassed.toString(), minutesPassed.toString()); } diff --git a/contracts/utils/deploymentHelpers.js b/contracts/utils/deploymentHelpers.js index e43e53d5..42959066 100644 --- a/contracts/utils/deploymentHelpers.js +++ b/contracts/utils/deploymentHelpers.js @@ -13,6 +13,7 @@ const StabilityPool = artifacts.require("./StabilityPool.sol"); const PriceFeedMock = artifacts.require("./PriceFeedMock.sol"); const MockInterestRouter = artifacts.require("./MockInterestRouter.sol"); const ERC20 = artifacts.require("./ERC20MinterMock.sol"); +const CollateralRegistry = artifacts.require("./CollateralRegistry.sol"); // "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol" // "../node_modules/@openzeppelin/contracts/build/contracts/ERC20PresetMinterPauser.json" // ); @@ -48,6 +49,7 @@ class DeploymentHelper { TroveManager, BoldToken, HintHelpers, + CollateralRegistry, }) .map(([name, contract]) => [ name, @@ -74,6 +76,8 @@ class DeploymentHelper { activePool, }, Contracts.BoldToken); + const collateralRegistry = await Contracts.CollateralRegistry.new(boldToken.address, [WETH.address], [troveManager.address]); + const mockInterestRouter = await MockInterestRouter.new(); const hintHelpers = await Contracts.HintHelpers.new(); @@ -100,6 +104,7 @@ class DeploymentHelper { Contracts.BorrowerOperations.setAsDeployed(borrowerOperations); Contracts.HintHelpers.setAsDeployed(hintHelpers); MockInterestRouter.setAsDeployed(mockInterestRouter); + Contracts.CollateralRegistry.setAsDeployed(troveManager); const coreContracts = { WETH, @@ -115,6 +120,7 @@ class DeploymentHelper { borrowerOperations, hintHelpers, mockInterestRouter, + collateralRegistry, }; return coreContracts; } From 6036ad70c203bdb3ae58149e401c557903153a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Wed, 1 May 2024 10:16:34 +0100 Subject: [PATCH 9/9] fix: Remove unused parameter --- contracts/src/CollateralRegistry.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index 73590e0c..40b1a4e0 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -170,7 +170,7 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { } } - _updateBaseRateAndGetRedemptionRate(boldToken, totals.redeemedAmount, totals.boldSupplyAtStart); + _updateBaseRateAndGetRedemptionRate(totals.redeemedAmount, totals.boldSupplyAtStart); // Burn the total Bold that is cancelled with debt if (totals.redeemedAmount > 0) { @@ -195,7 +195,7 @@ contract CollateralRegistry is LiquityBase, ICollateralRegistry { } // Updates the `baseRate` state with math from `_getUpdatedBaseRateFromRedemption` - function _updateBaseRateAndGetRedemptionRate(IBoldToken _boldToken, uint256 _boldAmount, uint256 _totalBoldSupplyAtStart) + function _updateBaseRateAndGetRedemptionRate(uint256 _boldAmount, uint256 _totalBoldSupplyAtStart) internal { uint256 newBaseRate = _getUpdatedBaseRateFromRedemption(_boldAmount, _totalBoldSupplyAtStart);