From 147e8ffca22103a2524f5560547183c0ba7a6eed Mon Sep 17 00:00:00 2001 From: RedVeil Date: Mon, 15 Sep 2025 13:10:53 +0200 Subject: [PATCH 1/5] updated whitelist + converter --- contracts/src/BorrowerOperations.sol | 18 +- contracts/src/CollateralRegistry.sol | 4 +- .../src/Dependencies/AddRemoveManagers.sol | 46 +- contracts/src/Dependencies/BoldConverter.sol | 30 +- contracts/src/Dependencies/HasWhitelist.sol | 18 +- contracts/src/Dependencies/Whitelist.sol | 80 +- contracts/src/Interfaces/ITroveManager.sol | 2 +- contracts/src/Interfaces/IWhitelist.sol | 9 +- contracts/src/StabilityPool.sol | 2 +- contracts/src/TroveManager.sol | 975 +++++++++++++----- contracts/src/TroveNFT.sol | 2 +- contracts/test/BoldConverter.t.sol | 339 +++--- .../test/TestContracts/WhitelistTestSetup.sol | 8 +- contracts/test/basicOpsWhitelist.t.sol | 9 +- contracts/test/collateralRegistry.t.sol | 296 +++++- contracts/test/multiCollateralWhitelist.t.sol | 206 +++- contracts/test/troveNFT.t.sol | 11 +- contracts/test/whitelist.t.sol | 71 +- contracts/test/whitelistRedemptions.t.sol | 41 +- 19 files changed, 1611 insertions(+), 556 deletions(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 06b822bb6..bb479c595 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -238,7 +238,7 @@ contract BorrowerOperations is ) external override returns (uint256) { _requireValidAnnualInterestRate(_annualInterestRate); - _checkWhitelisted(_owner, msg.sender, _receiver); + _checkWhitelisted(this.openTrove.selector, _owner, msg.sender, _receiver); OpenTroveVars memory vars; @@ -281,7 +281,12 @@ contract BorrowerOperations is ) external override returns (uint256) { _requireValidInterestBatchManager(_params.interestBatchManager); - _checkWhitelisted(_params.owner, msg.sender, _params.receiver); + _checkWhitelisted( + this.openTroveAndJoinInterestBatchManager.selector, + _params.owner, + msg.sender, + _params.receiver + ); OpenTroveVars memory vars; vars.troveManager = troveManager; @@ -733,7 +738,7 @@ contract BorrowerOperations is } // _requireNonZeroAdjustment(_troveChange); - if ( + if ( _troveChange.collIncrease == 0 && _troveChange.collDecrease == 0 && _troveChange.debtIncrease == 0 && @@ -2093,16 +2098,17 @@ contract BorrowerOperations is } function _checkWhitelisted( + bytes4 _funcSig, address _owner, address _sender, address _receiver ) internal view { IWhitelist _whitelist = whitelist; if (address(_whitelist) != address(0)) { - _requireWhitelisted(_whitelist, _owner); - _requireWhitelisted(_whitelist, _sender); + _requireWhitelisted(_whitelist, _funcSig, _owner); + _requireWhitelisted(_whitelist, _funcSig, _sender); if (_receiver != address(0)) { - _requireWhitelisted(whitelist, _receiver); + _requireWhitelisted(whitelist, _funcSig, _receiver); } } } diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index cfc72b276..573385abf 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -175,7 +175,7 @@ contract CollateralRegistry is Owned, ICollateralRegistry { bool redeemable ) = troveManager.getUnbackedPortionPriceAndRedeemability(); prices[index] = price; - if (redeemable && troveManager.isWhitelisted(msg.sender)) { + if (redeemable && troveManager.isWhitelisted(msg.sender, ITroveManager.redeemCollateral.selector)) { totals.unbacked += unbackedPortion; unbackedPortions[index] = unbackedPortion; } @@ -189,7 +189,7 @@ contract CollateralRegistry is Owned, ICollateralRegistry { ITroveManager troveManager = getTroveManager(index); (, , bool redeemable) = troveManager .getUnbackedPortionPriceAndRedeemability(); - if (redeemable && troveManager.isWhitelisted(msg.sender)) { + if (redeemable && troveManager.isWhitelisted(msg.sender, ITroveManager.redeemCollateral.selector)) { uint256 unbackedPortion = troveManager .getEntireBranchDebt(); totals.unbacked += unbackedPortion; diff --git a/contracts/src/Dependencies/AddRemoveManagers.sol b/contracts/src/Dependencies/AddRemoveManagers.sol index 418b485cb..66227f3b7 100644 --- a/contracts/src/Dependencies/AddRemoveManagers.sol +++ b/contracts/src/Dependencies/AddRemoveManagers.sol @@ -42,7 +42,11 @@ contract AddRemoveManagers is HasWhitelist, IAddRemoveManagers { event TroveNFTAddressChanged(address _newTroveNFTAddress); event AddManagerUpdated(uint256 indexed _troveId, address _newAddManager); - event RemoveManagerAndReceiverUpdated(uint256 indexed _troveId, address _newRemoveManager, address _newReceiver); + event RemoveManagerAndReceiverUpdated( + uint256 indexed _troveId, + address _newRemoveManager, + address _newReceiver + ); constructor(IAddressesRegistry _addressesRegistry) { troveNFT = _addressesRegistry.troveNFT(); @@ -59,18 +63,30 @@ contract AddRemoveManagers is HasWhitelist, IAddRemoveManagers { emit AddManagerUpdated(_troveId, _manager); } - function setRemoveManagerWithReceiver(uint256 _troveId, address _manager, address _receiver) public { + function setRemoveManagerWithReceiver( + uint256 _troveId, + address _manager, + address _receiver + ) public { _requireCallerIsBorrower(_troveId); - + IWhitelist _whitelist = whitelist; if (address(_whitelist) != address(0)) { - _requireWhitelisted(_whitelist, _receiver); + _requireWhitelisted( + _whitelist, + this.setRemoveManagerWithReceiver.selector, + _receiver + ); } _setRemoveManagerAndReceiver(_troveId, _manager, _receiver); } - function _setRemoveManagerAndReceiver(uint256 _troveId, address _manager, address _receiver) internal { + function _setRemoveManagerAndReceiver( + uint256 _troveId, + address _manager, + address _receiver + ) internal { // _requireNonZeroManagerUnlessWiping(_manager, _receiver); if (_manager == address(0) && _receiver != address(0)) { revert EmptyManager(); @@ -99,18 +115,24 @@ contract AddRemoveManagers is HasWhitelist, IAddRemoveManagers { } } - function _requireSenderIsOwnerOrAddManager(uint256 _troveId, address _owner) internal view { + function _requireSenderIsOwnerOrAddManager( + uint256 _troveId, + address _owner + ) internal view { address addManager = addManagerOf[_troveId]; - if (msg.sender != _owner && addManager != address(0) && msg.sender != addManager) { + if ( + msg.sender != _owner && + addManager != address(0) && + msg.sender != addManager + ) { revert NotOwnerNorAddManager(); } } - function _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(uint256 _troveId, address _owner) - internal - view - returns (address) - { + function _requireSenderIsOwnerOrRemoveManagerAndGetReceiver( + uint256 _troveId, + address _owner + ) internal view returns (address) { address manager = removeManagerReceiverOf[_troveId].manager; address receiver = removeManagerReceiverOf[_troveId].receiver; if (msg.sender != _owner && msg.sender != manager) { diff --git a/contracts/src/Dependencies/BoldConverter.sol b/contracts/src/Dependencies/BoldConverter.sol index 20c4c16bd..149dd2c27 100644 --- a/contracts/src/Dependencies/BoldConverter.sol +++ b/contracts/src/Dependencies/BoldConverter.sol @@ -7,8 +7,9 @@ import {ReentrancyGuard} from "openzeppelin-contracts/contracts/security/Reentra import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {IBoldToken} from "../Interfaces/IBoldToken.sol"; import "./Owned.sol"; +import "./HasWhitelist.sol"; -contract BoldConverter is Owned, ReentrancyGuard { +contract BoldConverter is Owned, HasWhitelist, ReentrancyGuard { uint256 public constant MAX_FEE = 10000; IBoldToken public bvUSD; @@ -33,6 +34,8 @@ contract BoldConverter is Owned, ReentrancyGuard { bvUSD = IBoldToken(bvUSD_); } + // --- View functions --- + function isValidPath( IERC20Metadata underlying ) external view returns (bool) { @@ -45,12 +48,19 @@ contract BoldConverter is Owned, ReentrancyGuard { path = _underlyingPaths[underlying]; } + // --- Deposit functions --- + // amount in underlying token decimals function deposit( IERC20Metadata underlying, uint256 amount, address to - ) public nonReentrant returns (uint256 boldAmount) { + ) + public + nonReentrant + checkWhitelisted(bytes4(keccak256("deposit(address,uint256,address)"))) + returns (uint256 boldAmount) + { Path memory path = _underlyingPaths[underlying]; require(path.underlyingReceiver != address(0), "Invalid path"); @@ -77,12 +87,18 @@ contract BoldConverter is Owned, ReentrancyGuard { return deposit(underlying, amount, msg.sender); } + // --- Withdraw functions --- function withdraw( IERC20Metadata underlying, uint256 amount, address to - ) public nonReentrant returns (uint256 underlyingOut) { + ) + public + nonReentrant + checkWhitelisted(bytes4(keccak256("withdraw(address,uint256,address)"))) + returns (uint256 underlyingOut) + { Path memory path = _underlyingPaths[underlying]; require(path.underlyingReceiver != address(0), "Invalid path"); @@ -115,6 +131,8 @@ contract BoldConverter is Owned, ReentrancyGuard { return withdraw(underlying, amount, msg.sender); } + // --- Path management functions --- + function deletePaths( IERC20Metadata[] memory underlyings ) external onlyOwner { @@ -158,4 +176,10 @@ contract BoldConverter is Owned, ReentrancyGuard { emit NewPath(underlying); } } + + // --- Whitelist management functions --- + + function setWhitelist(IWhitelist _whitelist) external onlyOwner { + _setWhitelist(_whitelist); + } } diff --git a/contracts/src/Dependencies/HasWhitelist.sol b/contracts/src/Dependencies/HasWhitelist.sol index 1292486f2..a6e85b0ab 100644 --- a/contracts/src/Dependencies/HasWhitelist.sol +++ b/contracts/src/Dependencies/HasWhitelist.sol @@ -12,9 +12,21 @@ abstract contract HasWhitelist { whitelist = _whitelist; } - function _requireWhitelisted(IWhitelist _whitelist, address _user) internal view { - if (!_whitelist.isWhitelisted(address(this), _user)) { + function _requireWhitelisted( + IWhitelist _whitelist, + bytes4 _funcSig, + address _user + ) internal view { + if (!_whitelist.isWhitelisted(address(this), _funcSig, _user)) { revert NotWhitelisted(_user); } } -} \ No newline at end of file + + modifier checkWhitelisted(bytes4 _funcSig) { + IWhitelist _whitelist = whitelist; + if (address(_whitelist) != address(0)) { + _requireWhitelisted(_whitelist, _funcSig, msg.sender); + } + _; + } +} diff --git a/contracts/src/Dependencies/Whitelist.sol b/contracts/src/Dependencies/Whitelist.sol index 7052a941e..91e33b82b 100644 --- a/contracts/src/Dependencies/Whitelist.sol +++ b/contracts/src/Dependencies/Whitelist.sol @@ -7,27 +7,83 @@ import {Owned} from "./Owned.sol"; import {IWhitelist} from "../Interfaces/IWhitelist.sol"; contract Whitelist is IWhitelist, Owned { - // calling contract -> user -> whitelisted - mapping(address => mapping(address => bool)) whitelist; + // calling contract -> funcSig -> whitelisted + mapping(address => mapping(bytes4 => bool)) public whitelistedFunc; + // calling contract -> funcSig -> user -> whitelisted + mapping(address => mapping(bytes4 => mapping(address => bool))) whitelist; - event Whitelisted(address callingContract, address user); - event WhitelistRemoved(address callingContract, address user); + event WhitelistedFuncAdded(address callingContract, bytes4 funcSig); + event WhitelistFuncRemoved( + address callingContract, + bytes4 funcSig + ); + event Whitelisted(address callingContract, bytes4 funcSig, address user); + event WhitelistRemoved( + address callingContract, + bytes4 funcSig, + address user + ); + + error FuncNotWhitelisted(); constructor(address owner) Owned(owner) {} - function addToWhitelist(address callingContract, address user) external override onlyOwner { - whitelist[callingContract][user] = true; + function addWhitelistedFunc( + address callingContract, + bytes4 funcSig + ) external override onlyOwner { + whitelistedFunc[callingContract][funcSig] = true; - emit Whitelisted(callingContract, user); + emit WhitelistedFuncAdded(callingContract, funcSig); } - function removeFromWhitelist(address callingContract, address user) external override onlyOwner { - whitelist[callingContract][user] = false; + function removeWhitelistedFunc( + address callingContract, + bytes4 funcSig + ) external override onlyOwner { + whitelistedFunc[callingContract][funcSig] = false; + + emit WhitelistFuncRemoved(callingContract, funcSig); + } + + function addToWhitelist( + address callingContract, + bytes4 funcSig, + address user + ) external override onlyOwner { + if (!whitelistedFunc[callingContract][funcSig]) { + revert FuncNotWhitelisted(); + } + whitelist[callingContract][funcSig][user] = true; + + emit Whitelisted(callingContract, funcSig, user); + } + + function removeFromWhitelist( + address callingContract, + bytes4 funcSig, + address user + ) external override onlyOwner { + whitelist[callingContract][funcSig][user] = false; + + emit WhitelistRemoved(callingContract, funcSig, user); + } - emit WhitelistRemoved(callingContract, user); + function isWhitelisted( + address callingContract, + bytes4 funcSig, + address user + ) external view override returns (bool) { + if (!whitelistedFunc[callingContract][funcSig]) { + return true; + } + return whitelist[callingContract][funcSig][user]; } - function isWhitelisted(address callingContract, address user) external view override returns (bool) { - return whitelist[callingContract][user]; + function isWhitelistedFunc( + address callingContract, + bytes4 funcSig + ) external view returns (bool) { + return whitelistedFunc[callingContract][funcSig]; } } diff --git a/contracts/src/Interfaces/ITroveManager.sol b/contracts/src/Interfaces/ITroveManager.sol index 7367f8acf..90a8671a3 100644 --- a/contracts/src/Interfaces/ITroveManager.sol +++ b/contracts/src/Interfaces/ITroveManager.sol @@ -33,7 +33,7 @@ interface ITroveManager is ILiquityBase { function updateCRs(uint256 newCCR, uint256 newSCR, uint256 newMCR) external; function updateLiquidationValues(uint256 newLiquidationPenaltySP, uint256 newliquidationPenaltyRedistribution) external; - function isWhitelisted(address user) external view returns (bool); + function isWhitelisted(address user, bytes4 funcSig) external view returns (bool); function Troves(uint256 _id) external diff --git a/contracts/src/Interfaces/IWhitelist.sol b/contracts/src/Interfaces/IWhitelist.sol index 4ebcd3836..2020e6145 100644 --- a/contracts/src/Interfaces/IWhitelist.sol +++ b/contracts/src/Interfaces/IWhitelist.sol @@ -3,7 +3,10 @@ pragma solidity ^0.8.0; interface IWhitelist { - function addToWhitelist(address callingContract, address user) external; - function removeFromWhitelist(address callingContract, address user) external; - function isWhitelisted(address callingContract, address user) external view returns (bool); + function addWhitelistedFunc(address callingContract, bytes4 funcSig) external; + function removeWhitelistedFunc(address callingContract, bytes4 funcSig) external; + function addToWhitelist(address callingContract, bytes4 funcSig, address user) external; + function removeFromWhitelist(address callingContract, bytes4 funcSig, address user) external; + function isWhitelisted(address callingContract, bytes4 funcSig, address user) external view returns (bool); + function isWhitelistedFunc(address callingContract, bytes4 funcSig) external view returns (bool); } diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 3958b4d14..eb22b80bc 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -227,7 +227,7 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { function provideToSP(uint256 _topUp, bool _doClaim) external override { IWhitelist _whitelist = whitelist; if (address(_whitelist) != address(0)) { - _requireWhitelisted(_whitelist, msg.sender); + _requireWhitelisted(_whitelist, this.provideToSP.selector, msg.sender); } _requireNonZeroAmount(_topUp); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 6675fa365..0f17599f9 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -88,13 +88,13 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 internal totalCollateralSnapshot; /* - * L_coll and L_boldDebt track the sums of accumulated liquidation rewards per unit staked. During its lifetime, each stake earns: - * - * An Coll gain of ( stake * [L_coll - L_coll(0)] ) - * A boldDebt increase of ( stake * [L_boldDebt - L_boldDebt(0)] ) - * - * Where L_coll(0) and L_boldDebt(0) are snapshots of L_coll and L_boldDebt for the active Trove taken at the instant the stake was made - */ + * L_coll and L_boldDebt track the sums of accumulated liquidation rewards per unit staked. During its lifetime, each stake earns: + * + * An Coll gain of ( stake * [L_coll - L_coll(0)] ) + * A boldDebt increase of ( stake * [L_boldDebt - L_boldDebt(0)] ) + * + * Where L_coll(0) and L_boldDebt(0) are snapshots of L_coll and L_boldDebt for the active Trove taken at the instant the stake was made + */ uint256 internal L_coll; uint256 internal L_boldDebt; @@ -122,11 +122,11 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 public shutdownTime; /* - * --- Variable container structs for liquidations --- - * - * These structs are used to hold, return and assign variables inside the liquidation functions, - * in order to avoid the error: "CompilerError: Stack too deep". - **/ + * --- Variable container structs for liquidations --- + * + * These structs are used to hold, return and assign variables inside the liquidation functions, + * in order to avoid the error: "CompilerError: Stack too deep". + **/ struct LiquidationValues { uint256 collGasCompensation; @@ -173,7 +173,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // --- Events --- event TroveNFTAddressChanged(address _newTroveNFTAddress); - event BorrowerOperationsAddressChanged(address _newBorrowerOperationsAddress); + event BorrowerOperationsAddressChanged( + address _newBorrowerOperationsAddress + ); event BoldTokenAddressChanged(address _newBoldTokenAddress); event StabilityPoolAddressChanged(address _stabilityPoolAddress); event GasPoolAddressChanged(address _gasPoolAddress); @@ -181,14 +183,20 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { event SortedTrovesAddressChanged(address _sortedTrovesAddress); event CollateralRegistryAddressChanged(address _collateralRegistryAddress); event CRsChanged(uint256 newCCR, uint256 newSCR, uint256 newMCR); - event LiquidationValuesChanged(uint256 newLiquidationPenaltySP, uint256 newliquidationPenaltyRedistribution); - - constructor(IAddressesRegistry _addressesRegistry) LiquityBase(_addressesRegistry) { + event LiquidationValuesChanged( + uint256 newLiquidationPenaltySP, + uint256 newliquidationPenaltyRedistribution + ); + + constructor( + IAddressesRegistry _addressesRegistry + ) LiquityBase(_addressesRegistry) { CCR = _addressesRegistry.CCR(); MCR = _addressesRegistry.MCR(); SCR = _addressesRegistry.SCR(); LIQUIDATION_PENALTY_SP = _addressesRegistry.LIQUIDATION_PENALTY_SP(); - LIQUIDATION_PENALTY_REDISTRIBUTION = _addressesRegistry.LIQUIDATION_PENALTY_REDISTRIBUTION(); + LIQUIDATION_PENALTY_REDISTRIBUTION = _addressesRegistry + .LIQUIDATION_PENALTY_REDISTRIBUTION(); troveNFT = _addressesRegistry.troveNFT(); borrowerOperations = _addressesRegistry.borrowerOperations(); @@ -216,12 +224,18 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { return TroveIds.length; } - function getTroveFromTroveIdsArray(uint256 _index) external view override returns (uint256) { + function getTroveFromTroveIdsArray( + uint256 _index + ) external view override returns (uint256) { return TroveIds[_index]; } // --- Contracts update logic --- - function updateCRs(uint256 newCCR, uint256 newSCR, uint256 newMCR) external override { + function updateCRs( + uint256 newCCR, + uint256 newSCR, + uint256 newMCR + ) external override { _requireCallerIsAddressesRegistry(); CCR = newCCR; @@ -231,16 +245,19 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { emit CRsChanged(newCCR, newSCR, newMCR); } - function updateLiquidationValues(uint256 newLiquidationPenaltySP, uint256 newliquidationPenaltyRedistribution) - external - override - { + function updateLiquidationValues( + uint256 newLiquidationPenaltySP, + uint256 newliquidationPenaltyRedistribution + ) external override { _requireCallerIsAddressesRegistry(); LIQUIDATION_PENALTY_SP = newLiquidationPenaltySP; LIQUIDATION_PENALTY_REDISTRIBUTION = newliquidationPenaltyRedistribution; - emit LiquidationValuesChanged(newLiquidationPenaltySP, newliquidationPenaltyRedistribution); + emit LiquidationValuesChanged( + newLiquidationPenaltySP, + newliquidationPenaltyRedistribution + ); } // --- Trove Liquidation functions --- @@ -264,10 +281,17 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { LatestBatchData memory batch; if (isTroveInBatch) _getLatestBatchData(batchAddress, batch); - _movePendingTroveRewardsToActivePool(_defaultPool, trove.redistBoldDebtGain, trove.redistCollGain); + _movePendingTroveRewardsToActivePool( + _defaultPool, + trove.redistBoldDebtGain, + trove.redistCollGain + ); - singleLiquidation.collGasCompensation = _getCollGasCompensation(trove.entireColl); - uint256 collToLiquidate = trove.entireColl - singleLiquidation.collGasCompensation; + singleLiquidation.collGasCompensation = _getCollGasCompensation( + trove.entireColl + ); + uint256 collToLiquidate = trove.entireColl - + singleLiquidation.collGasCompensation; ( singleLiquidation.debtToOffset, @@ -275,7 +299,12 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { singleLiquidation.debtToRedistribute, singleLiquidation.collToRedistribute, singleLiquidation.collSurplus - ) = _getOffsetAndRedistributionVals(trove.entireDebt, collToLiquidate, _boldInSPForOffsets, _price); + ) = _getOffsetAndRedistributionVals( + trove.entireDebt, + collToLiquidate, + _boldInSPForOffsets, + _price + ); TroveChange memory troveChange; troveChange.collDecrease = trove.entireColl; @@ -293,22 +322,36 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { if (isTroveInBatch) { singleLiquidation.oldWeightedRecordedDebt = - batch.weightedRecordedDebt + (trove.entireDebt - trove.redistBoldDebtGain) * batch.annualInterestRate; - singleLiquidation.newWeightedRecordedDebt = batch.entireDebtWithoutRedistribution * batch.annualInterestRate; + batch.weightedRecordedDebt + + (trove.entireDebt - trove.redistBoldDebtGain) * + batch.annualInterestRate; + singleLiquidation.newWeightedRecordedDebt = + batch.entireDebtWithoutRedistribution * + batch.annualInterestRate; // Mint batch management fee troveChange.batchAccruedManagementFee = batch.accruedManagementFee; - troveChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee - + (trove.entireDebt - trove.redistBoldDebtGain) * batch.annualManagementFee; + troveChange.oldWeightedRecordedBatchManagementFee = + batch.weightedRecordedBatchManagementFee + + (trove.entireDebt - trove.redistBoldDebtGain) * + batch.annualManagementFee; troveChange.newWeightedRecordedBatchManagementFee = - batch.entireDebtWithoutRedistribution * batch.annualManagementFee; - activePool.mintBatchManagementFeeAndAccountForChange(troveChange, batchAddress); + batch.entireDebtWithoutRedistribution * + batch.annualManagementFee; + activePool.mintBatchManagementFeeAndAccountForChange( + troveChange, + batchAddress + ); } else { - singleLiquidation.oldWeightedRecordedDebt = trove.weightedRecordedDebt; + singleLiquidation.oldWeightedRecordedDebt = trove + .weightedRecordedDebt; } // Differencen between liquidation penalty and liquidation threshold if (singleLiquidation.collSurplus > 0) { - collSurplusPool.accountSurplus(owner, singleLiquidation.collSurplus); + collSurplusPool.accountSurplus( + owner, + singleLiquidation.collSurplus + ); } // Wipe out state in BO @@ -350,13 +393,19 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } // Return the amount of Coll to be drawn from a trove's collateral and sent as gas compensation. - function _getCollGasCompensation(uint256 _entireColl) internal pure returns (uint256) { - return LiquityMath._min(_entireColl / COLL_GAS_COMPENSATION_DIVISOR, COLL_GAS_COMPENSATION_CAP); + function _getCollGasCompensation( + uint256 _entireColl + ) internal pure returns (uint256) { + return + LiquityMath._min( + _entireColl / COLL_GAS_COMPENSATION_DIVISOR, + COLL_GAS_COMPENSATION_CAP + ); } /* In a full liquidation, returns the values for a trove's coll and debt to be offset, and coll and debt to be - * redistributed to active troves. - */ + * redistributed to active troves. + */ function _getOffsetAndRedistributionVals( uint256 _entireTroveDebt, uint256 _collToLiquidate, // gas compensation is already subtracted @@ -385,16 +434,26 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { * */ if (_boldInSPForOffsets > 0) { - debtToOffset = LiquityMath._min(_entireTroveDebt, _boldInSPForOffsets); - collSPPortion = _collToLiquidate * debtToOffset / _entireTroveDebt; - (collToSendToSP, collSurplus) = - _getCollPenaltyAndSurplus(collSPPortion, debtToOffset, LIQUIDATION_PENALTY_SP, _price); + debtToOffset = LiquityMath._min( + _entireTroveDebt, + _boldInSPForOffsets + ); + collSPPortion = + (_collToLiquidate * debtToOffset) / + _entireTroveDebt; + (collToSendToSP, collSurplus) = _getCollPenaltyAndSurplus( + collSPPortion, + debtToOffset, + LIQUIDATION_PENALTY_SP, + _price + ); } // Redistribution debtToRedistribute = _entireTroveDebt - debtToOffset; if (debtToRedistribute > 0) { - uint256 collRedistributionPortion = _collToLiquidate - collSPPortion; + uint256 collRedistributionPortion = _collToLiquidate - + collSPPortion; if (collRedistributionPortion > 0) { (collToRedistribute, collSurplus) = _getCollPenaltyAndSurplus( collRedistributionPortion + collSurplus, // Coll surplus from offset can be eaten up by red. penalty @@ -413,7 +472,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 _penaltyRatio, uint256 _price ) internal pure returns (uint256 seizedColl, uint256 collSurplus) { - uint256 maxSeizedColl = _debtToLiquidate * (DECIMAL_PRECISION + _penaltyRatio) / _price; + uint256 maxSeizedColl = (_debtToLiquidate * + (DECIMAL_PRECISION + _penaltyRatio)) / _price; if (_collToLiquidate > maxSeizedColl) { seizedColl = maxSeizedColl; collSurplus = _collToLiquidate - maxSeizedColl; @@ -426,7 +486,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { /* * Attempt to liquidate a custom list of troves provided by the caller. */ - function batchLiquidateTroves(uint256[] memory _troveArray) public override { + function batchLiquidateTroves( + uint256[] memory _troveArray + ) public override { if (_troveArray.length == 0) { revert EmptyData(); } @@ -438,37 +500,62 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { TroveChange memory troveChange; LiquidationValues memory totals; - (uint256 price,) = priceFeed.fetchPrice(); + (uint256 price, ) = priceFeed.fetchPrice(); // - If the SP has total deposits >= 1e18, we leave 1e18 in it untouched. // - If it has 0 < x < 1e18 total deposits, we leave x in it. uint256 totalBoldDeposits = stabilityPoolCached.getTotalBoldDeposits(); - uint256 boldToLeaveInSP = LiquityMath._min(MIN_BOLD_IN_SP, totalBoldDeposits); + uint256 boldToLeaveInSP = LiquityMath._min( + MIN_BOLD_IN_SP, + totalBoldDeposits + ); uint256 boldInSPForOffsets = totalBoldDeposits - boldToLeaveInSP; // Perform the appropriate liquidation sequence - tally values and obtain their totals. - _batchLiquidateTroves(defaultPoolCached, price, boldInSPForOffsets, _troveArray, totals, troveChange); + _batchLiquidateTroves( + defaultPoolCached, + price, + boldInSPForOffsets, + _troveArray, + totals, + troveChange + ); if (troveChange.debtDecrease == 0) { revert NothingToLiquidate(); } - activePoolCached.mintAggInterestAndAccountForTroveChange(troveChange, address(0)); + activePoolCached.mintAggInterestAndAccountForTroveChange( + troveChange, + address(0) + ); // Move liquidated Coll and Bold to the appropriate pools if (totals.debtToOffset > 0 || totals.collToSendToSP > 0) { - stabilityPoolCached.offset(totals.debtToOffset, totals.collToSendToSP); + stabilityPoolCached.offset( + totals.debtToOffset, + totals.collToSendToSP + ); } // we check amount is not zero inside _redistributeDebtAndColl( - activePoolCached, defaultPoolCached, totals.debtToRedistribute, totals.collToRedistribute + activePoolCached, + defaultPoolCached, + totals.debtToRedistribute, + totals.collToRedistribute ); if (totals.collSurplus > 0) { - activePoolCached.sendColl(address(collSurplusPool), totals.collSurplus); + activePoolCached.sendColl( + address(collSurplusPool), + totals.collSurplus + ); } // Update system snapshots - _updateSystemSnapshots_excludeCollRemainder(activePoolCached, totals.collGasCompensation); + _updateSystemSnapshots_excludeCollRemainder( + activePoolCached, + totals.collGasCompensation + ); emit Liquidation( totals.debtToOffset, @@ -484,7 +571,12 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { ); // Send gas compensation to caller - _sendGasCompensation(activePoolCached, msg.sender, totals.ETHGasCompensation, totals.collGasCompensation); + _sendGasCompensation( + activePoolCached, + msg.sender, + totals.ETHGasCompensation, + totals.collGasCompensation + ); } function _isActiveOrZombie(Status _status) internal pure returns (bool) { @@ -513,11 +605,23 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { LiquidationValues memory singleLiquidation; LatestTroveData memory trove; - _liquidate(_defaultPool, troveId, remainingBoldInSPForOffsets, _price, trove, singleLiquidation); + _liquidate( + _defaultPool, + troveId, + remainingBoldInSPForOffsets, + _price, + trove, + singleLiquidation + ); remainingBoldInSPForOffsets -= singleLiquidation.debtToOffset; // Add liquidation values to their respective running totals - _addLiquidationValuesToTotals(trove, singleLiquidation, totals, troveChange); + _addLiquidationValuesToTotals( + trove, + singleLiquidation, + totals, + troveChange + ); } } } @@ -537,8 +641,10 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { troveChange.debtDecrease += _trove.entireDebt; troveChange.collDecrease += _trove.entireColl; troveChange.appliedRedistBoldDebtGain += _trove.redistBoldDebtGain; - troveChange.oldWeightedRecordedDebt += _singleLiquidation.oldWeightedRecordedDebt; - troveChange.newWeightedRecordedDebt += _singleLiquidation.newWeightedRecordedDebt; + troveChange.oldWeightedRecordedDebt += _singleLiquidation + .oldWeightedRecordedDebt; + troveChange.newWeightedRecordedDebt += _singleLiquidation + .newWeightedRecordedDebt; totals.debtToOffset += _singleLiquidation.debtToOffset; totals.collToSendToSP += _singleLiquidation.collToSendToSP; totals.debtToRedistribute += _singleLiquidation.debtToRedistribute; @@ -546,7 +652,12 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { totals.collSurplus += _singleLiquidation.collSurplus; } - function _sendGasCompensation(IActivePool _activePool, address _liquidator, uint256 _eth, uint256 _coll) internal { + function _sendGasCompensation( + IActivePool _activePool, + address _liquidator, + uint256 _eth, + uint256 _coll + ) internal { if (_eth > 0) { WETH.transferFrom(gasPoolAddress, _liquidator, _eth); } @@ -557,7 +668,11 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } // Move a Trove's pending debt and collateral rewards from distributions, from the Default Pool to the Active Pool - function _movePendingTroveRewardsToActivePool(IDefaultPool _defaultPool, uint256 _bold, uint256 _coll) internal { + function _movePendingTroveRewardsToActivePool( + IDefaultPool _defaultPool, + uint256 _bold, + uint256 _coll + ) internal { if (_bold > 0) { _defaultPool.decreaseBoldDebt(_bold); } @@ -575,32 +690,55 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { bool _isTroveInBatch ) internal returns (uint256) { // Decrease the debt and collateral of the current Trove according to the Bold lot and corresponding ETH to send - uint256 newDebt = _singleRedemption.trove.entireDebt - _singleRedemption.boldLot; - uint256 newColl = _singleRedemption.trove.entireColl - _singleRedemption.collLot; + uint256 newDebt = _singleRedemption.trove.entireDebt - + _singleRedemption.boldLot; + uint256 newColl = _singleRedemption.trove.entireColl - + _singleRedemption.collLot; - _singleRedemption.appliedRedistBoldDebtGain = _singleRedemption.trove.redistBoldDebtGain; + _singleRedemption.appliedRedistBoldDebtGain = _singleRedemption + .trove + .redistBoldDebtGain; if (_isTroveInBatch) { - _getLatestBatchData(_singleRedemption.batchAddress, _singleRedemption.batch); + _getLatestBatchData( + _singleRedemption.batchAddress, + _singleRedemption.batch + ); // We know boldLot <= trove entire debt, so this subtraction is safe - uint256 newAmountForWeightedDebt = _singleRedemption.batch.entireDebtWithoutRedistribution - + _singleRedemption.trove.redistBoldDebtGain - _singleRedemption.boldLot; - _singleRedemption.oldWeightedRecordedDebt = _singleRedemption.batch.weightedRecordedDebt; + uint256 newAmountForWeightedDebt = _singleRedemption + .batch + .entireDebtWithoutRedistribution + + _singleRedemption.trove.redistBoldDebtGain - + _singleRedemption.boldLot; + _singleRedemption.oldWeightedRecordedDebt = _singleRedemption + .batch + .weightedRecordedDebt; _singleRedemption.newWeightedRecordedDebt = - newAmountForWeightedDebt * _singleRedemption.batch.annualInterestRate; + newAmountForWeightedDebt * + _singleRedemption.batch.annualInterestRate; TroveChange memory troveChange; troveChange.debtDecrease = _singleRedemption.boldLot; troveChange.collDecrease = _singleRedemption.collLot; - troveChange.appliedRedistBoldDebtGain = _singleRedemption.trove.redistBoldDebtGain; - troveChange.appliedRedistCollGain = _singleRedemption.trove.redistCollGain; + troveChange.appliedRedistBoldDebtGain = _singleRedemption + .trove + .redistBoldDebtGain; + troveChange.appliedRedistCollGain = _singleRedemption + .trove + .redistCollGain; // batchAccruedManagementFee is handled in the outer function - troveChange.oldWeightedRecordedBatchManagementFee = - _singleRedemption.batch.weightedRecordedBatchManagementFee; + troveChange + .oldWeightedRecordedBatchManagementFee = _singleRedemption + .batch + .weightedRecordedBatchManagementFee; troveChange.newWeightedRecordedBatchManagementFee = - newAmountForWeightedDebt * _singleRedemption.batch.annualManagementFee; + newAmountForWeightedDebt * + _singleRedemption.batch.annualManagementFee; - activePool.mintBatchManagementFeeAndAccountForChange(troveChange, _singleRedemption.batchAddress); + activePool.mintBatchManagementFeeAndAccountForChange( + troveChange, + _singleRedemption.batchAddress + ); Troves[_singleRedemption.troveId].coll = newColl; // interest and fee were updated in the outer function @@ -616,16 +754,27 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { false // _checkBatchSharesRatio ); } else { - _singleRedemption.oldWeightedRecordedDebt = _singleRedemption.trove.weightedRecordedDebt; - _singleRedemption.newWeightedRecordedDebt = newDebt * _singleRedemption.trove.annualInterestRate; + _singleRedemption.oldWeightedRecordedDebt = _singleRedemption + .trove + .weightedRecordedDebt; + _singleRedemption.newWeightedRecordedDebt = + newDebt * + _singleRedemption.trove.annualInterestRate; Troves[_singleRedemption.troveId].debt = newDebt; Troves[_singleRedemption.troveId].coll = newColl; - Troves[_singleRedemption.troveId].lastDebtUpdateTime = uint64(block.timestamp); + Troves[_singleRedemption.troveId].lastDebtUpdateTime = uint64( + block.timestamp + ); } - _singleRedemption.newStake = _updateStakeAndTotalStakes(_singleRedemption.troveId, newColl); + _singleRedemption.newStake = _updateStakeAndTotalStakes( + _singleRedemption.troveId, + newColl + ); _movePendingTroveRewardsToActivePool( - _defaultPool, _singleRedemption.trove.redistBoldDebtGain, _singleRedemption.trove.redistCollGain + _defaultPool, + _singleRedemption.trove.redistBoldDebtGain, + _singleRedemption.trove.redistCollGain ); _updateTroveRewardSnapshots(_singleRedemption.troveId); @@ -633,7 +782,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { emit BatchedTroveUpdated({ _troveId: _singleRedemption.troveId, _interestBatchManager: _singleRedemption.batchAddress, - _batchDebtShares: Troves[_singleRedemption.troveId].batchDebtShares, + _batchDebtShares: Troves[_singleRedemption.troveId] + .batchDebtShares, _coll: newColl, _stake: _singleRedemption.newStake, _snapshotOfTotalCollRedist: L_coll, @@ -669,13 +819,19 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { _debt: batches[_singleRedemption.batchAddress].debt, _coll: batches[_singleRedemption.batchAddress].coll, _annualInterestRate: _singleRedemption.batch.annualInterestRate, - _annualManagementFee: _singleRedemption.batch.annualManagementFee, - _totalDebtShares: batches[_singleRedemption.batchAddress].totalDebtShares, + _annualManagementFee: _singleRedemption + .batch + .annualManagementFee, + _totalDebtShares: batches[_singleRedemption.batchAddress] + .totalDebtShares, _debtIncreaseFromUpfrontFee: 0 }); } - emit RedemptionFeePaidToTrove(_singleRedemption.troveId, _singleRedemption.collFee); + emit RedemptionFeePaidToTrove( + _singleRedemption.troveId, + _singleRedemption.collFee + ); return newDebt; } @@ -691,17 +847,29 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { _getLatestTroveData(_singleRedemption.troveId, _singleRedemption.trove); // Determine the remaining amount (lot) to be redeemed, capped by the entire debt of the Trove - _singleRedemption.boldLot = LiquityMath._min(_maxBoldamount, _singleRedemption.trove.entireDebt); + _singleRedemption.boldLot = LiquityMath._min( + _maxBoldamount, + _singleRedemption.trove.entireDebt + ); // Get the amount of Coll equal in USD value to the boldLot redeemed - uint256 correspondingColl = _singleRedemption.boldLot * DECIMAL_PRECISION / _price; + uint256 correspondingColl = (_singleRedemption.boldLot * + DECIMAL_PRECISION) / _price; // Calculate the collFee separately (for events) - _singleRedemption.collFee = correspondingColl * _redemptionRate / DECIMAL_PRECISION; + _singleRedemption.collFee = + (correspondingColl * _redemptionRate) / + DECIMAL_PRECISION; // Get the final collLot to send to redeemer, leaving the fee in the Trove - _singleRedemption.collLot = correspondingColl - _singleRedemption.collFee; + _singleRedemption.collLot = + correspondingColl - + _singleRedemption.collFee; bool isTroveInBatch = _singleRedemption.batchAddress != address(0); - uint256 newDebt = _applySingleRedemption(_defaultPool, _singleRedemption, isTroveInBatch); + uint256 newDebt = _applySingleRedemption( + _defaultPool, + _singleRedemption, + isTroveInBatch + ); // Make Trove zombie if it's tiny (and it wasn’t already), in order to prevent griefing future (normal, sequential) redemptions if (newDebt < MIN_DEBT) { @@ -726,7 +894,10 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // however we don't do that here, as it would require hints for re-insertion into `SortedTroves`. } - function _updateBatchInterestPriorToRedemption(IActivePool _activePool, address _batchAddress) internal { + function _updateBatchInterestPriorToRedemption( + IActivePool _activePool, + address _batchAddress + ) internal { LatestBatchData memory batch; _getLatestBatchData(_batchAddress, batch); batches[_batchAddress].debt = batch.entireDebtWithoutRedistribution; @@ -734,28 +905,35 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // As we are updating the batch, we update the ActivePool weighted sum too TroveChange memory batchTroveChange; batchTroveChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - batchTroveChange.newWeightedRecordedDebt = batch.entireDebtWithoutRedistribution * batch.annualInterestRate; + batchTroveChange.newWeightedRecordedDebt = + batch.entireDebtWithoutRedistribution * + batch.annualInterestRate; batchTroveChange.batchAccruedManagementFee = batch.accruedManagementFee; - batchTroveChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; + batchTroveChange.oldWeightedRecordedBatchManagementFee = batch + .weightedRecordedBatchManagementFee; batchTroveChange.newWeightedRecordedBatchManagementFee = - batch.entireDebtWithoutRedistribution * batch.annualManagementFee; + batch.entireDebtWithoutRedistribution * + batch.annualManagementFee; - _activePool.mintAggInterestAndAccountForTroveChange(batchTroveChange, _batchAddress); + _activePool.mintAggInterestAndAccountForTroveChange( + batchTroveChange, + _batchAddress + ); } /* Send _boldamount Bold to the system and redeem the corresponding amount of collateral from as many Troves as are needed to fill the redemption - * request. Applies redistribution gains to a Trove before reducing its debt and coll. - * - * Note that if _amount is very large, this function can run out of gas, specially if traversed troves are small. This can be easily avoided by - * splitting the total _amount in appropriate chunks and calling the function multiple times. - * - * Param `_maxIterations` can also be provided, so the loop through Troves is capped (if it’s zero, it will be ignored).This makes it easier to - * avoid OOG for the frontend, as only knowing approximately the average cost of an iteration is enough, without needing to know the “topology” - * of the trove list. It also avoids the need to set the cap in stone in the contract, nor doing gas calculations, as both gas price and opcode - * costs can vary. - * - * All Troves that are redeemed from -- with the likely exception of the last one -- will end up with no debt left, and therefore in “zombie” state - */ + * request. Applies redistribution gains to a Trove before reducing its debt and coll. + * + * Note that if _amount is very large, this function can run out of gas, specially if traversed troves are small. This can be easily avoided by + * splitting the total _amount in appropriate chunks and calling the function multiple times. + * + * Param `_maxIterations` can also be provided, so the loop through Troves is capped (if it’s zero, it will be ignored).This makes it easier to + * avoid OOG for the frontend, as only knowing approximately the average cost of an iteration is enough, without needing to know the “topology” + * of the trove list. It also avoids the need to set the cap in stone in the contract, nor doing gas calculations, as both gas price and opcode + * costs can vary. + * + * All Troves that are redeemed from -- with the likely exception of the last one -- will end up with no debt left, and therefore in “zombie” state + */ function redeemCollateral( address _redeemer, uint256 _boldamount, @@ -785,14 +963,20 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // Loop through the Troves starting from the one with lowest interest rate until _amount of Bold is exchanged for collateral if (_maxIterations == 0) _maxIterations = type(uint256).max; - while (singleRedemption.troveId != 0 && remainingBold > 0 && _maxIterations > 0) { + while ( + singleRedemption.troveId != 0 && + remainingBold > 0 && + _maxIterations > 0 + ) { _maxIterations--; // Save the uint256 of the Trove preceding the current one uint256 nextUserToCheck; if (singleRedemption.isZombieTrove) { nextUserToCheck = sortedTrovesCached.getLast(); } else { - nextUserToCheck = sortedTrovesCached.getPrev(singleRedemption.troveId); + nextUserToCheck = sortedTrovesCached.getPrev( + singleRedemption.troveId + ); } // Skip if ICR < 100%, to make sure that redemptions don’t decrease the CR of hit Troves @@ -804,24 +988,39 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // If it’s in a batch, we need to update interest first // We do it here outside, to avoid repeating for each trove in the same batch - singleRedemption.batchAddress = _getBatchManager(singleRedemption.troveId); + singleRedemption.batchAddress = _getBatchManager( + singleRedemption.troveId + ); if ( - singleRedemption.batchAddress != address(0) && singleRedemption.batchAddress != lastBatchUpdatedInterest + singleRedemption.batchAddress != address(0) && + singleRedemption.batchAddress != lastBatchUpdatedInterest ) { - _updateBatchInterestPriorToRedemption(activePoolCached, singleRedemption.batchAddress); + _updateBatchInterestPriorToRedemption( + activePoolCached, + singleRedemption.batchAddress + ); lastBatchUpdatedInterest = singleRedemption.batchAddress; } - _redeemCollateralFromTrove(defaultPool, singleRedemption, remainingBold, _price, _redemptionRate); + _redeemCollateralFromTrove( + defaultPool, + singleRedemption, + remainingBold, + _price, + _redemptionRate + ); totalsTroveChange.collDecrease += singleRedemption.collLot; totalsTroveChange.debtDecrease += singleRedemption.boldLot; - totalsTroveChange.appliedRedistBoldDebtGain += singleRedemption.appliedRedistBoldDebtGain; + totalsTroveChange.appliedRedistBoldDebtGain += singleRedemption + .appliedRedistBoldDebtGain; // For recorded and weighted recorded debt totals, we need to capture the increases and decreases, // since the net debt change for a given Trove could be positive or negative: redemptions decrease a Trove's recorded // (and weighted recorded) debt, but the accrued interest increases it. - totalsTroveChange.newWeightedRecordedDebt += singleRedemption.newWeightedRecordedDebt; - totalsTroveChange.oldWeightedRecordedDebt += singleRedemption.oldWeightedRecordedDebt; + totalsTroveChange.newWeightedRecordedDebt += singleRedemption + .newWeightedRecordedDebt; + totalsTroveChange.oldWeightedRecordedDebt += singleRedemption + .oldWeightedRecordedDebt; totalCollFee += singleRedemption.collFee; remainingBold -= singleRedemption.boldLot; @@ -833,10 +1032,17 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { //require(totals.totalCollDrawn > 0, "TroveManager: Unable to redeem any amount"); emit Redemption( - _boldamount, totalsTroveChange.debtDecrease, totalsTroveChange.collDecrease, totalCollFee, _price + _boldamount, + totalsTroveChange.debtDecrease, + totalsTroveChange.collDecrease, + totalCollFee, + _price ); - activePoolCached.mintAggInterestAndAccountForTroveChange(totalsTroveChange, address(0)); + activePoolCached.mintAggInterestAndAccountForTroveChange( + totalsTroveChange, + address(0) + ); // Send the redeemed Coll to sender activePoolCached.sendColl(_redeemer, totalsTroveChange.collDecrease); @@ -853,15 +1059,22 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { SingleRedemptionValues memory _singleRedemption ) internal { // Determine the remaining amount (lot) to be redeemed, capped by the entire debt of the Trove minus the liquidation reserve - _singleRedemption.boldLot = LiquityMath._min(_maxBoldamount, _singleRedemption.trove.entireDebt); + _singleRedemption.boldLot = LiquityMath._min( + _maxBoldamount, + _singleRedemption.trove.entireDebt + ); // Get the amount of ETH equal in USD value to the BOLD lot redeemed - _singleRedemption.collLot = _singleRedemption.boldLot * (DECIMAL_PRECISION + URGENT_REDEMPTION_BONUS) / _price; + _singleRedemption.collLot = + (_singleRedemption.boldLot * + (DECIMAL_PRECISION + URGENT_REDEMPTION_BONUS)) / + _price; // As here we can redeem when CR < 101% (accounting for 1% bonus), we need to cap by collateral too if (_singleRedemption.collLot > _singleRedemption.trove.entireColl) { _singleRedemption.collLot = _singleRedemption.trove.entireColl; _singleRedemption.boldLot = - _singleRedemption.trove.entireColl * _price / (DECIMAL_PRECISION + URGENT_REDEMPTION_BONUS); + (_singleRedemption.trove.entireColl * _price) / + (DECIMAL_PRECISION + URGENT_REDEMPTION_BONUS); } bool isTroveInBatch = _singleRedemption.batchAddress != address(0); @@ -872,21 +1085,29 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // - Urgent redemptions aren't sequential, so they can't be griefed by tiny Troves. } - function urgentRedemption(uint256 _boldAmount, uint256[] calldata _troveIds, uint256 _minCollateral) external { + function urgentRedemption( + uint256 _boldAmount, + uint256[] calldata _troveIds, + uint256 _minCollateral + ) external { _requireIsShutDown(); _requireAmountGreaterThanZero(_boldAmount); _requireBoldBalanceCoversRedemption(boldToken, msg.sender, _boldAmount); IWhitelist whitelist = whitelist; if (address(whitelist) != address(0)) { - _requireWhitelisted(whitelist, msg.sender); + _requireWhitelisted( + whitelist, + this.urgentRedemption.selector, + msg.sender + ); } IActivePool activePoolCached = activePool; TroveChange memory totalsTroveChange; // Use the standard fetchPrice here, since if branch has shut down we don't worry about small redemption arbs - (uint256 price,) = priceFeed.fetchPrice(); + (uint256 price, ) = priceFeed.fetchPrice(); uint256 remainingBold = _boldAmount; for (uint256 i = 0; i < _troveIds.length; i++) { @@ -894,29 +1115,48 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { SingleRedemptionValues memory singleRedemption; singleRedemption.troveId = _troveIds[i]; - _getLatestTroveData(singleRedemption.troveId, singleRedemption.trove); + _getLatestTroveData( + singleRedemption.troveId, + singleRedemption.trove + ); - if (!_isActiveOrZombie(Troves[singleRedemption.troveId].status) || singleRedemption.trove.entireDebt == 0) { + if ( + !_isActiveOrZombie(Troves[singleRedemption.troveId].status) || + singleRedemption.trove.entireDebt == 0 + ) { continue; } // If it’s in a batch, we need to update interest first // As we don’t have them ordered now, we cannot avoid repeating for each trove in the same batch - singleRedemption.batchAddress = _getBatchManager(singleRedemption.troveId); + singleRedemption.batchAddress = _getBatchManager( + singleRedemption.troveId + ); if (singleRedemption.batchAddress != address(0)) { - _updateBatchInterestPriorToRedemption(activePoolCached, singleRedemption.batchAddress); + _updateBatchInterestPriorToRedemption( + activePoolCached, + singleRedemption.batchAddress + ); } - _urgentRedeemCollateralFromTrove(defaultPool, remainingBold, price, singleRedemption); + _urgentRedeemCollateralFromTrove( + defaultPool, + remainingBold, + price, + singleRedemption + ); totalsTroveChange.collDecrease += singleRedemption.collLot; totalsTroveChange.debtDecrease += singleRedemption.boldLot; - totalsTroveChange.appliedRedistBoldDebtGain += singleRedemption.appliedRedistBoldDebtGain; + totalsTroveChange.appliedRedistBoldDebtGain += singleRedemption + .appliedRedistBoldDebtGain; // For recorded and weighted recorded debt totals, we need to capture the increases and decreases, // since the net debt change for a given Trove could be positive or negative: redemptions decrease a Trove's recorded // (and weighted recorded) debt, but the accrued interest increases it. - totalsTroveChange.newWeightedRecordedDebt += singleRedemption.newWeightedRecordedDebt; - totalsTroveChange.oldWeightedRecordedDebt += singleRedemption.oldWeightedRecordedDebt; + totalsTroveChange.newWeightedRecordedDebt += singleRedemption + .newWeightedRecordedDebt; + totalsTroveChange.oldWeightedRecordedDebt += singleRedemption + .oldWeightedRecordedDebt; remainingBold -= singleRedemption.boldLot; } @@ -925,11 +1165,20 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { revert MinCollNotReached(totalsTroveChange.collDecrease); } - emit Redemption(_boldAmount, totalsTroveChange.debtDecrease, totalsTroveChange.collDecrease, 0, price); + emit Redemption( + _boldAmount, + totalsTroveChange.debtDecrease, + totalsTroveChange.collDecrease, + 0, + price + ); // Since this branch is shut down, this will mint 0 interest. // We call this only to update the aggregate debt and weighted debt trackers. - activePoolCached.mintAggInterestAndAccountForTroveChange(totalsTroveChange, address(0)); + activePoolCached.mintAggInterestAndAccountForTroveChange( + totalsTroveChange, + address(0) + ); // Send the redeemed coll to caller activePoolCached.sendColl(msg.sender, totalsTroveChange.collDecrease); @@ -946,10 +1195,14 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // --- Helper functions --- // Return the current collateral ratio (ICR) of a given Trove. Takes a trove's pending coll and debt rewards from redistributions into account. - function getCurrentICR(uint256 _troveId, uint256 _price) public view override returns (uint256) { + function getCurrentICR( + uint256 _troveId, + uint256 _price + ) public view override returns (uint256) { LatestTroveData memory trove; _getLatestTroveData(_troveId, trove); - return LiquityMath._computeCR(trove.entireColl, trove.entireDebt, _price); + return + LiquityMath._computeCR(trove.entireColl, trove.entireDebt, _price); } function _updateTroveRewardSnapshots(uint256 _troveId) internal { @@ -958,7 +1211,10 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } // Return the Troves entire debt and coll, including redistribution gains from redistributions. - function _getLatestTroveData(uint256 _troveId, LatestTroveData memory trove) internal view { + function _getLatestTroveData( + uint256 _troveId, + LatestTroveData memory trove + ) internal view { // If trove belongs to a batch, we fetch the batch and apply its share to obtained values address batchAddress = _getBatchManager(_troveId); if (batchAddress != address(0)) { @@ -969,19 +1225,34 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } uint256 stake = Troves[_troveId].stake; - trove.redistBoldDebtGain = stake * (L_boldDebt - rewardSnapshots[_troveId].boldDebt) / DECIMAL_PRECISION; - trove.redistCollGain = stake * (L_coll - rewardSnapshots[_troveId].coll) / DECIMAL_PRECISION; + trove.redistBoldDebtGain = + (stake * (L_boldDebt - rewardSnapshots[_troveId].boldDebt)) / + DECIMAL_PRECISION; + trove.redistCollGain = + (stake * (L_coll - rewardSnapshots[_troveId].coll)) / + DECIMAL_PRECISION; trove.recordedDebt = Troves[_troveId].debt; trove.annualInterestRate = Troves[_troveId].annualInterestRate; - trove.weightedRecordedDebt = trove.recordedDebt * trove.annualInterestRate; + trove.weightedRecordedDebt = + trove.recordedDebt * + trove.annualInterestRate; - uint256 period = _getInterestPeriod(Troves[_troveId].lastDebtUpdateTime); - trove.accruedInterest = _calcInterest(trove.weightedRecordedDebt, period); + uint256 period = _getInterestPeriod( + Troves[_troveId].lastDebtUpdateTime + ); + trove.accruedInterest = _calcInterest( + trove.weightedRecordedDebt, + period + ); - trove.entireDebt = trove.recordedDebt + trove.redistBoldDebtGain + trove.accruedInterest; + trove.entireDebt = + trove.recordedDebt + + trove.redistBoldDebtGain + + trove.accruedInterest; trove.entireColl = Troves[_troveId].coll + trove.redistCollGain; - trove.lastInterestRateAdjTime = Troves[_troveId].lastInterestRateAdjTime; + trove.lastInterestRateAdjTime = Troves[_troveId] + .lastInterestRateAdjTime; } function _getLatestTroveDataFromBatch( @@ -996,31 +1267,53 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 stake = trove.stake; _latestTroveData.redistBoldDebtGain = - stake * (L_boldDebt - rewardSnapshots[_troveId].boldDebt) / DECIMAL_PRECISION; - _latestTroveData.redistCollGain = stake * (L_coll - rewardSnapshots[_troveId].coll) / DECIMAL_PRECISION; + (stake * (L_boldDebt - rewardSnapshots[_troveId].boldDebt)) / + DECIMAL_PRECISION; + _latestTroveData.redistCollGain = + (stake * (L_coll - rewardSnapshots[_troveId].coll)) / + DECIMAL_PRECISION; if (totalDebtShares > 0) { - _latestTroveData.recordedDebt = _latestBatchData.recordedDebt * batchDebtShares / totalDebtShares; - _latestTroveData.weightedRecordedDebt = _latestTroveData.recordedDebt * _latestBatchData.annualInterestRate; - _latestTroveData.accruedInterest = _latestBatchData.accruedInterest * batchDebtShares / totalDebtShares; + _latestTroveData.recordedDebt = + (_latestBatchData.recordedDebt * batchDebtShares) / + totalDebtShares; + _latestTroveData.weightedRecordedDebt = + _latestTroveData.recordedDebt * + _latestBatchData.annualInterestRate; + _latestTroveData.accruedInterest = + (_latestBatchData.accruedInterest * batchDebtShares) / + totalDebtShares; _latestTroveData.accruedBatchManagementFee = - _latestBatchData.accruedManagementFee * batchDebtShares / totalDebtShares; + (_latestBatchData.accruedManagementFee * batchDebtShares) / + totalDebtShares; } - _latestTroveData.annualInterestRate = _latestBatchData.annualInterestRate; + _latestTroveData.annualInterestRate = _latestBatchData + .annualInterestRate; // We can’t do pro-rata batch entireDebt, because redist gains are proportional to coll, not to debt - _latestTroveData.entireDebt = _latestTroveData.recordedDebt + _latestTroveData.redistBoldDebtGain - + _latestTroveData.accruedInterest + _latestTroveData.accruedBatchManagementFee; - _latestTroveData.entireColl = trove.coll + _latestTroveData.redistCollGain; - _latestTroveData.lastInterestRateAdjTime = - LiquityMath._max(_latestBatchData.lastInterestRateAdjTime, trove.lastInterestRateAdjTime); + _latestTroveData.entireDebt = + _latestTroveData.recordedDebt + + _latestTroveData.redistBoldDebtGain + + _latestTroveData.accruedInterest + + _latestTroveData.accruedBatchManagementFee; + _latestTroveData.entireColl = + trove.coll + + _latestTroveData.redistCollGain; + _latestTroveData.lastInterestRateAdjTime = LiquityMath._max( + _latestBatchData.lastInterestRateAdjTime, + trove.lastInterestRateAdjTime + ); } - function getLatestTroveData(uint256 _troveId) external view returns (LatestTroveData memory trove) { + function getLatestTroveData( + uint256 _troveId + ) external view returns (LatestTroveData memory trove) { _getLatestTroveData(_troveId, trove); } - function getTroveAnnualInterestRate(uint256 _troveId) external view returns (uint256) { + function getTroveAnnualInterestRate( + uint256 _troveId + ) external view returns (uint256) { Trove memory trove = Troves[_troveId]; address batchAddress = _getBatchManager(trove); if (batchAddress != address(0)) { @@ -1029,41 +1322,64 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { return trove.annualInterestRate; } - function _getBatchManager(uint256 _troveId) internal view returns (address) { + function _getBatchManager( + uint256 _troveId + ) internal view returns (address) { return Troves[_troveId].interestBatchManager; } - function _getBatchManager(Trove memory trove) internal pure returns (address) { + function _getBatchManager( + Trove memory trove + ) internal pure returns (address) { return trove.interestBatchManager; } // Return the Batch entire debt and coll, including redistribution gains from redistributions. - function _getLatestBatchData(address _batchAddress, LatestBatchData memory latestBatchData) internal view { + function _getLatestBatchData( + address _batchAddress, + LatestBatchData memory latestBatchData + ) internal view { Batch memory batch = batches[_batchAddress]; latestBatchData.recordedDebt = batch.debt; latestBatchData.annualInterestRate = batch.annualInterestRate; - latestBatchData.weightedRecordedDebt = latestBatchData.recordedDebt * latestBatchData.annualInterestRate; + latestBatchData.weightedRecordedDebt = + latestBatchData.recordedDebt * + latestBatchData.annualInterestRate; uint256 period = _getInterestPeriod(batch.lastDebtUpdateTime); - latestBatchData.accruedInterest = _calcInterest(latestBatchData.weightedRecordedDebt, period); + latestBatchData.accruedInterest = _calcInterest( + latestBatchData.weightedRecordedDebt, + period + ); latestBatchData.annualManagementFee = batch.annualManagementFee; latestBatchData.weightedRecordedBatchManagementFee = - latestBatchData.recordedDebt * latestBatchData.annualManagementFee; - latestBatchData.accruedManagementFee = _calcInterest(latestBatchData.weightedRecordedBatchManagementFee, period); + latestBatchData.recordedDebt * + latestBatchData.annualManagementFee; + latestBatchData.accruedManagementFee = _calcInterest( + latestBatchData.weightedRecordedBatchManagementFee, + period + ); latestBatchData.entireDebtWithoutRedistribution = - latestBatchData.recordedDebt + latestBatchData.accruedInterest + latestBatchData.accruedManagementFee; + latestBatchData.recordedDebt + + latestBatchData.accruedInterest + + latestBatchData.accruedManagementFee; latestBatchData.entireCollWithoutRedistribution = batch.coll; latestBatchData.lastDebtUpdateTime = batch.lastDebtUpdateTime; latestBatchData.lastInterestRateAdjTime = batch.lastInterestRateAdjTime; } - function getLatestBatchData(address _batchAddress) external view returns (LatestBatchData memory batch) { + function getLatestBatchData( + address _batchAddress + ) external view returns (LatestBatchData memory batch) { _getLatestBatchData(_batchAddress, batch); } // Update borrower's stake based on their latest collateral value - function _updateStakeAndTotalStakes(uint256 _troveId, uint256 _coll) internal returns (uint256 newStake) { + function _updateStakeAndTotalStakes( + uint256 _troveId, + uint256 _coll + ) internal returns (uint256 newStake) { newStake = _computeNewStake(_coll); uint256 oldStake = Troves[_troveId].stake; Troves[_troveId].stake = newStake; @@ -1078,13 +1394,13 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { stake = _coll; } else { /* - * The following assert() holds true because: - * - The system always contains >= 1 trove - * - When we close or liquidate a trove, we redistribute the redistribution gains, so if all troves were closed/liquidated, - * rewards would’ve been emptied and totalCollateralSnapshot would be zero too. - */ + * The following assert() holds true because: + * - The system always contains >= 1 trove + * - When we close or liquidate a trove, we redistribute the redistribution gains, so if all troves were closed/liquidated, + * rewards would’ve been emptied and totalCollateralSnapshot would be zero too. + */ // assert(totalStakesSnapshot > 0); - stake = _coll * totalStakesSnapshot / totalCollateralSnapshot; + stake = (_coll * totalStakesSnapshot) / totalCollateralSnapshot; } return stake; } @@ -1098,25 +1414,35 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { if (_debtToRedistribute == 0) return; // Otherwise _collToRedistribute > 0 too /* - * Add distributed coll and debt rewards-per-unit-staked to the running totals. Division uses a "feedback" - * error correction, to keep the cumulative error low in the running totals L_coll and L_boldDebt: - * - * 1) Form numerators which compensate for the floor division errors that occurred the last time this - * function was called. - * 2) Calculate "per-unit-staked" ratios. - * 3) Multiply each ratio back by its denominator, to reveal the current floor division error. - * 4) Store these errors for use in the next correction when this function is called. - * 5) Note: static analysis tools complain about this "division before multiplication", however, it is intended. - */ - uint256 collNumerator = _collToRedistribute * DECIMAL_PRECISION + lastCollError_Redistribution; - uint256 boldDebtNumerator = _debtToRedistribute * DECIMAL_PRECISION + lastBoldDebtError_Redistribution; + * Add distributed coll and debt rewards-per-unit-staked to the running totals. Division uses a "feedback" + * error correction, to keep the cumulative error low in the running totals L_coll and L_boldDebt: + * + * 1) Form numerators which compensate for the floor division errors that occurred the last time this + * function was called. + * 2) Calculate "per-unit-staked" ratios. + * 3) Multiply each ratio back by its denominator, to reveal the current floor division error. + * 4) Store these errors for use in the next correction when this function is called. + * 5) Note: static analysis tools complain about this "division before multiplication", however, it is intended. + */ + uint256 collNumerator = _collToRedistribute * + DECIMAL_PRECISION + + lastCollError_Redistribution; + uint256 boldDebtNumerator = _debtToRedistribute * + DECIMAL_PRECISION + + lastBoldDebtError_Redistribution; // Get the per-unit-staked terms uint256 collRewardPerUnitStaked = collNumerator / totalStakes; uint256 boldDebtRewardPerUnitStaked = boldDebtNumerator / totalStakes; - lastCollError_Redistribution = collNumerator - collRewardPerUnitStaked * totalStakes; - lastBoldDebtError_Redistribution = boldDebtNumerator - boldDebtRewardPerUnitStaked * totalStakes; + lastCollError_Redistribution = + collNumerator - + collRewardPerUnitStaked * + totalStakes; + lastBoldDebtError_Redistribution = + boldDebtNumerator - + boldDebtRewardPerUnitStaked * + totalStakes; // Add per-unit-staked terms to the running totals L_coll = L_coll + collRewardPerUnitStaked; @@ -1127,10 +1453,13 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } /* - * Updates snapshots of system total stakes and total collateral, excluding a given collateral remainder from the calculation. - * Used in a liquidation sequence. - */ - function _updateSystemSnapshots_excludeCollRemainder(IActivePool _activePool, uint256 _collRemainder) internal { + * Updates snapshots of system total stakes and total collateral, excluding a given collateral remainder from the calculation. + * Used in a liquidation sequence. + */ + function _updateSystemSnapshots_excludeCollRemainder( + IActivePool _activePool, + uint256 _collRemainder + ) internal { totalStakesSnapshot = totalStakes; uint256 activeColl = _activePool.getCollBalance(); @@ -1139,10 +1468,13 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } /* - * Remove a Trove owner from the TroveIds array, not preserving array order. Removing owner 'B' does the following: - * [A B C D E] => [A E C D], and updates E's Trove struct to point to its new array index. - */ - function _removeTroveId(uint256 _troveId, uint256 TroveIdsArrayLength) internal { + * Remove a Trove owner from the TroveIds array, not preserving array order. Removing owner 'B' does the following: + * [A B C D E] => [A E C D], and updates E's Trove struct to point to its new array index. + */ + function _removeTroveId( + uint256 _troveId, + uint256 TroveIdsArrayLength + ) internal { uint64 index = Troves[_troveId].arrayIndex; uint256 idxLast = TroveIdsArrayLength - 1; @@ -1156,21 +1488,25 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { TroveIds.pop(); } - function getTroveStatus(uint256 _troveId) external view override returns (Status) { + function getTroveStatus( + uint256 _troveId + ) external view override returns (Status) { return Troves[_troveId].status; } - function isWhitelisted(address user) external view override returns (bool) { + function isWhitelisted(address user, bytes4 funcSig) external view override returns (bool) { IWhitelist _whitelist = whitelist; if (address(_whitelist) != address(0)) { - return _whitelist.isWhitelisted(address(this), user); + return _whitelist.isWhitelisted(address(this), funcSig, user); } return true; } // --- Interest rate calculations --- - function _getInterestPeriod(uint256 _lastDebtUpdateTime) internal view returns (uint256) { + function _getInterestPeriod( + uint256 _lastDebtUpdateTime + ) internal view returns (uint256) { if (shutdownTime == 0) { // If branch is not shut down, interest is earned up to now. return block.timestamp - _lastDebtUpdateTime; @@ -1198,7 +1534,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } } - function _requireMoreThanOneTroveInSystem(uint256 TroveIdsArrayLength) internal pure { + function _requireMoreThanOneTroveInSystem( + uint256 TroveIdsArrayLength + ) internal pure { if (TroveIdsArrayLength == 1) { revert OnlyOneTroveLeft(); } @@ -1216,10 +1554,11 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } } - function _requireBoldBalanceCoversRedemption(IBoldToken _boldToken, address _redeemer, uint256 _amount) - internal - view - { + function _requireBoldBalanceCoversRedemption( + IBoldToken _boldToken, + address _redeemer, + uint256 _amount + ) internal view { uint256 boldBalance = _boldToken.balanceOf(_redeemer); if (boldBalance < _amount) { revert NotEnoughBoldBalance(); @@ -1228,12 +1567,15 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // --- Trove property getters --- - function getUnbackedPortionPriceAndRedeemability() external returns (uint256, uint256, bool) { + function getUnbackedPortionPriceAndRedeemability() + external + returns (uint256, uint256, bool) + { uint256 totalDebt = getEntireBranchDebt(); uint256 spSize = stabilityPool.getTotalBoldDeposits(); uint256 unbackedPortion = totalDebt > spSize ? totalDebt - spSize : 0; - (uint256 price,) = priceFeed.fetchRedemptionPrice(); + (uint256 price, ) = priceFeed.fetchRedemptionPrice(); // It's redeemable if the TCR is above the shutdown threshold, and branch has not been shut down bool redeemable = _getTCR(price) >= SCR && shutdownTime == 0; @@ -1242,15 +1584,20 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // --- Trove property setters, called by BorrowerOperations --- - function onOpenTrove(address _owner, uint256 _troveId, TroveChange memory _troveChange, uint256 _annualInterestRate) - external - { + function onOpenTrove( + address _owner, + uint256 _troveId, + TroveChange memory _troveChange, + uint256 _annualInterestRate + ) external { _requireCallerIsBorrowerOperations(); uint256 newStake = _computeNewStake(_troveChange.collIncrease); // Trove memory newTrove; - Troves[_troveId].debt = _troveChange.debtIncrease + _troveChange.upfrontFee; + Troves[_troveId].debt = + _troveChange.debtIncrease + + _troveChange.upfrontFee; Troves[_troveId].coll = _troveChange.collIncrease; Troves[_troveId].stake = newStake; Troves[_troveId].status = Status.active; @@ -1320,7 +1667,13 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { assert(_troveChange.debtIncrease > 0); // TODO: remove before deployment _updateBatchShares( - _troveId, _batchAddress, _troveChange, _troveChange.debtIncrease, _batchColl, _batchDebt, true + _troveId, + _batchAddress, + _troveChange, + _troveChange.debtIncrease, + _batchColl, + _batchDebt, + true ); uint256 newTotalStakes = totalStakes + newStake; @@ -1388,7 +1741,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { Troves[_troveId].lastInterestRateAdjTime = uint64(block.timestamp); _movePendingTroveRewardsToActivePool( - defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain + defaultPool, + _troveChange.appliedRedistBoldDebtGain, + _troveChange.appliedRedistCollGain ); _updateTroveRewardSnapshots(_troveId); @@ -1415,9 +1770,12 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { }); } - function onAdjustTrove(uint256 _troveId, uint256 _newColl, uint256 _newDebt, TroveChange calldata _troveChange) - external - { + function onAdjustTrove( + uint256 _troveId, + uint256 _newColl, + uint256 _newDebt, + TroveChange calldata _troveChange + ) external { _requireCallerIsBorrowerOperations(); Troves[_troveId].coll = _newColl; @@ -1425,7 +1783,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { Troves[_troveId].lastDebtUpdateTime = uint64(block.timestamp); _movePendingTroveRewardsToActivePool( - defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain + defaultPool, + _troveChange.appliedRedistBoldDebtGain, + _troveChange.appliedRedistCollGain ); uint256 newStake = _updateStakeAndTotalStakes(_troveId, _newColl); @@ -1447,9 +1807,11 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { _annualInterestRate: Troves[_troveId].annualInterestRate, _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain, _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee, - _debtChangeFromOperation: int256(_troveChange.debtIncrease) - int256(_troveChange.debtDecrease), + _debtChangeFromOperation: int256(_troveChange.debtIncrease) - + int256(_troveChange.debtDecrease), _collIncreaseFromRedist: _troveChange.appliedRedistCollGain, - _collChangeFromOperation: int256(_troveChange.collIncrease) - int256(_troveChange.collDecrease) + _collChangeFromOperation: int256(_troveChange.collIncrease) - + int256(_troveChange.collDecrease) }); } @@ -1461,9 +1823,18 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 _newBatchDebt // entire, with interest and batch fee ) external override { _requireCallerIsBorrowerOperations(); - _closeTrove(_troveId, _troveChange, _batchAddress, _newBatchColl, _newBatchDebt, Status.closedByOwner); + _closeTrove( + _troveId, + _troveChange, + _batchAddress, + _newBatchColl, + _newBatchDebt, + Status.closedByOwner + ); _movePendingTroveRewardsToActivePool( - defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain + defaultPool, + _troveChange.appliedRedistBoldDebtGain, + _troveChange.appliedRedistCollGain ); emit TroveUpdated({ @@ -1482,9 +1853,11 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { _annualInterestRate: 0, _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain, _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee, - _debtChangeFromOperation: int256(_troveChange.debtIncrease) - int256(_troveChange.debtDecrease), + _debtChangeFromOperation: int256(_troveChange.debtIncrease) - + int256(_troveChange.debtDecrease), _collIncreaseFromRedist: _troveChange.appliedRedistCollGain, - _collChangeFromOperation: int256(_troveChange.collIncrease) - int256(_troveChange.collDecrease) + _collChangeFromOperation: int256(_troveChange.collIncrease) - + int256(_troveChange.collDecrease) }); if (_batchAddress != address(0)) { @@ -1494,7 +1867,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { _debt: batches[_batchAddress].debt, _coll: batches[_batchAddress].coll, _annualInterestRate: batches[_batchAddress].annualInterestRate, - _annualManagementFee: batches[_batchAddress].annualManagementFee, + _annualManagementFee: batches[_batchAddress] + .annualManagementFee, _totalDebtShares: batches[_batchAddress].totalDebtShares, _debtIncreaseFromUpfrontFee: 0 }); @@ -1522,7 +1896,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { if (_batchAddress != address(0)) { if (trove.status == Status.active) { sortedTroves.removeFromBatch(_troveId); - } else if (trove.status == Status.zombie && lastZombieTroveId == _troveId) { + } else if ( + trove.status == Status.zombie && lastZombieTroveId == _troveId + ) { lastZombieTroveId = 0; } @@ -1538,7 +1914,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } else { if (trove.status == Status.active) { sortedTroves.remove(_troveId); - } else if (trove.status == Status.zombie && lastZombieTroveId == _troveId) { + } else if ( + trove.status == Status.zombie && lastZombieTroveId == _troveId + ) { lastZombieTroveId = 0; } } @@ -1575,10 +1953,20 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // Batch assert(_newTroveDebt > 0); // TODO: remove before deployment - _updateBatchShares(_troveId, _batchAddress, _troveChange, _newTroveDebt, _newBatchColl, _newBatchDebt, true); + _updateBatchShares( + _troveId, + _batchAddress, + _troveChange, + _newTroveDebt, + _newBatchColl, + _newBatchDebt, + true + ); _movePendingTroveRewardsToActivePool( - defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain + defaultPool, + _troveChange.appliedRedistBoldDebtGain, + _troveChange.appliedRedistCollGain ); emit BatchedTroveUpdated({ @@ -1597,9 +1985,11 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { _annualInterestRate: batches[_batchAddress].annualInterestRate, _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain, _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee, - _debtChangeFromOperation: int256(_troveChange.debtIncrease) - int256(_troveChange.debtDecrease), + _debtChangeFromOperation: int256(_troveChange.debtIncrease) - + int256(_troveChange.debtDecrease), _collIncreaseFromRedist: _troveChange.appliedRedistCollGain, - _collChangeFromOperation: int256(_troveChange.collIncrease) - int256(_troveChange.collDecrease) + _collChangeFromOperation: int256(_troveChange.collIncrease) - + int256(_troveChange.collDecrease) }); emit BatchUpdated({ @@ -1631,7 +2021,15 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { if (_batchAddress != address(0)) { assert(_newTroveDebt > 0); // TODO: remove before deployment - _updateBatchShares(_troveId, _batchAddress, _troveChange, _newTroveDebt, _newBatchColl, _newBatchDebt, true); + _updateBatchShares( + _troveId, + _batchAddress, + _troveChange, + _newTroveDebt, + _newBatchColl, + _newBatchDebt, + true + ); emit BatchUpdated({ _interestBatchManager: _batchAddress, @@ -1639,7 +2037,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { _debt: _newBatchDebt, _coll: _newBatchColl, _annualInterestRate: batches[_batchAddress].annualInterestRate, - _annualManagementFee: batches[_batchAddress].annualManagementFee, + _annualManagementFee: batches[_batchAddress] + .annualManagementFee, _totalDebtShares: batches[_batchAddress].totalDebtShares, _debtIncreaseFromUpfrontFee: 0 }); @@ -1649,7 +2048,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } _movePendingTroveRewardsToActivePool( - defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain + defaultPool, + _troveChange.appliedRedistBoldDebtGain, + _troveChange.appliedRedistCollGain ); _updateTroveRewardSnapshots(_troveId); @@ -1670,15 +2071,19 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { _annualInterestRate: Troves[_troveId].annualInterestRate, _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain, _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee, - _debtChangeFromOperation: int256(_troveChange.debtIncrease) - int256(_troveChange.debtDecrease), + _debtChangeFromOperation: int256(_troveChange.debtIncrease) - + int256(_troveChange.debtDecrease), _collIncreaseFromRedist: _troveChange.appliedRedistCollGain, - _collChangeFromOperation: int256(_troveChange.collIncrease) - int256(_troveChange.collDecrease) + _collChangeFromOperation: int256(_troveChange.collIncrease) - + int256(_troveChange.collDecrease) }); } - function onRegisterBatchManager(address _account, uint256 _annualInterestRate, uint256 _annualManagementFee) - external - { + function onRegisterBatchManager( + address _account, + uint256 _annualInterestRate, + uint256 _annualManagementFee + ) external { _requireCallerIsBorrowerOperations(); batches[_account].arrayIndex = uint64(batchIds.length); @@ -1738,7 +2143,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { batches[_batchAddress].debt = _newDebt; batches[_batchAddress].annualInterestRate = _newAnnualInterestRate; batches[_batchAddress].lastDebtUpdateTime = uint64(block.timestamp); - batches[_batchAddress].lastInterestRateAdjTime = uint64(block.timestamp); + batches[_batchAddress].lastInterestRateAdjTime = uint64( + block.timestamp + ); emit BatchUpdated({ _interestBatchManager: _batchAddress, @@ -1752,7 +2159,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { }); } - function onSetInterestBatchManager(OnSetInterestBatchManagerParams calldata _params) external { + function onSetInterestBatchManager( + OnSetInterestBatchManagerParams calldata _params + ) external { _requireCallerIsBorrowerOperations(); TroveChange memory _troveChange = _params.troveChange; @@ -1767,10 +2176,17 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { Troves[_params.troveId].coll = _params.troveColl; Troves[_params.troveId].interestBatchManager = _params.newBatchAddress; - Troves[_params.troveId].lastInterestRateAdjTime = uint64(block.timestamp); + Troves[_params.troveId].lastInterestRateAdjTime = uint64( + block.timestamp + ); - _troveChange.collIncrease = _params.troveColl - _troveChange.appliedRedistCollGain; - _troveChange.debtIncrease = _params.troveDebt - _troveChange.appliedRedistBoldDebtGain - _troveChange.upfrontFee; + _troveChange.collIncrease = + _params.troveColl - + _troveChange.appliedRedistCollGain; + _troveChange.debtIncrease = + _params.troveDebt - + _troveChange.appliedRedistBoldDebtGain - + _troveChange.upfrontFee; assert(_params.troveDebt > 0); // TODO: remove before deployment _updateBatchShares( _params.troveId, @@ -1783,7 +2199,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { ); _movePendingTroveRewardsToActivePool( - defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain + defaultPool, + _troveChange.appliedRedistBoldDebtGain, + _troveChange.appliedRedistCollGain ); emit BatchedTroveUpdated({ @@ -1799,7 +2217,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { emit TroveOperation({ _troveId: _params.troveId, _operation: Operation.setInterestBatchManager, - _annualInterestRate: batches[_params.newBatchAddress].annualInterestRate, + _annualInterestRate: batches[_params.newBatchAddress] + .annualInterestRate, _debtIncreaseFromRedist: _troveChange.appliedRedistBoldDebtGain, _debtIncreaseFromUpfrontFee: _troveChange.upfrontFee, _debtChangeFromOperation: 0, @@ -1812,8 +2231,10 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { _operation: BatchOperation.joinBatch, _debt: batches[_params.newBatchAddress].debt, _coll: batches[_params.newBatchAddress].coll, - _annualInterestRate: batches[_params.newBatchAddress].annualInterestRate, - _annualManagementFee: batches[_params.newBatchAddress].annualManagementFee, + _annualInterestRate: batches[_params.newBatchAddress] + .annualInterestRate, + _annualManagementFee: batches[_params.newBatchAddress] + .annualManagementFee, _totalDebtShares: batches[_params.newBatchAddress].totalDebtShares, // Although the Trove joining the batch may pay an upfront fee, // it is an individual fee, so we don't include it here @@ -1834,8 +2255,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // Debt uint256 currentBatchDebtShares = batches[_batchAddress].totalDebtShares; uint256 batchDebtSharesDelta; - uint256 debtIncrease = - _troveChange.debtIncrease + _troveChange.upfrontFee + _troveChange.appliedRedistBoldDebtGain; + uint256 debtIncrease = _troveChange.debtIncrease + + _troveChange.upfrontFee + + _troveChange.appliedRedistBoldDebtGain; uint256 debtDecrease; if (debtIncrease > _troveChange.debtDecrease) { debtIncrease -= _troveChange.debtDecrease; @@ -1853,14 +2275,22 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { batchDebtSharesDelta = debtIncrease; } else { // To avoid rebasing issues, let’s make sure the ratio debt / shares is not too high - _requireBelowMaxSharesRatio(currentBatchDebtShares, _batchDebt, _checkBatchSharesRatio); - - batchDebtSharesDelta = currentBatchDebtShares * debtIncrease / _batchDebt; + _requireBelowMaxSharesRatio( + currentBatchDebtShares, + _batchDebt, + _checkBatchSharesRatio + ); + + batchDebtSharesDelta = + (currentBatchDebtShares * debtIncrease) / + _batchDebt; } Troves[_troveId].batchDebtShares += batchDebtSharesDelta; batches[_batchAddress].debt = _batchDebt + debtIncrease; - batches[_batchAddress].totalDebtShares = currentBatchDebtShares + batchDebtSharesDelta; + batches[_batchAddress].totalDebtShares = + currentBatchDebtShares + + batchDebtSharesDelta; } else if (debtDecrease > 0) { // Subtract debt // We make sure that if final trove debt is zero, shares are too (avoiding rounding issues) @@ -1868,14 +2298,20 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // In redemptions we don’t do that because we don’t want to kick the trove out of the batch (it’d be bad UX) if (_newTroveDebt == 0) { batches[_batchAddress].debt = _batchDebt - debtDecrease; - batches[_batchAddress].totalDebtShares = currentBatchDebtShares - Troves[_troveId].batchDebtShares; + batches[_batchAddress].totalDebtShares = + currentBatchDebtShares - + Troves[_troveId].batchDebtShares; Troves[_troveId].batchDebtShares = 0; } else { - batchDebtSharesDelta = currentBatchDebtShares * debtDecrease / _batchDebt; + batchDebtSharesDelta = + (currentBatchDebtShares * debtDecrease) / + _batchDebt; Troves[_troveId].batchDebtShares -= batchDebtSharesDelta; batches[_batchAddress].debt = _batchDebt - debtDecrease; - batches[_batchAddress].totalDebtShares = currentBatchDebtShares - batchDebtSharesDelta; + batches[_batchAddress].totalDebtShares = + currentBatchDebtShares - + batchDebtSharesDelta; } } } @@ -1883,7 +2319,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { batches[_batchAddress].lastDebtUpdateTime = uint64(block.timestamp); // Collateral - uint256 collIncrease = _troveChange.collIncrease + _troveChange.appliedRedistCollGain; + uint256 collIncrease = _troveChange.collIncrease + + _troveChange.appliedRedistCollGain; uint256 collDecrease; if (collIncrease > _troveChange.collDecrease) { collIncrease -= _troveChange.collDecrease; @@ -1915,7 +2352,10 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { bool _checkBatchSharesRatio ) internal pure { // debt / shares should be below MAX_BATCH_SHARES_RATIO - if (_currentBatchDebtShares * MAX_BATCH_SHARES_RATIO < _batchDebt && _checkBatchSharesRatio) { + if ( + _currentBatchDebtShares * MAX_BATCH_SHARES_RATIO < _batchDebt && + _checkBatchSharesRatio + ) { revert BatchSharesRatioTooHigh(); } } @@ -1935,7 +2375,13 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // Subtract from batch _removeTroveSharesFromBatch( - _troveId, _newTroveColl, _newTroveDebt, _troveChange, _batchAddress, _newBatchColl, _newBatchDebt + _troveId, + _newTroveColl, + _newTroveDebt, + _troveChange, + _batchAddress, + _newBatchColl, + _newBatchDebt ); // Restore Trove state @@ -1947,7 +2393,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { _updateTroveRewardSnapshots(_troveId); _movePendingTroveRewardsToActivePool( - defaultPool, _troveChange.appliedRedistBoldDebtGain, _troveChange.appliedRedistCollGain + defaultPool, + _troveChange.appliedRedistBoldDebtGain, + _troveChange.appliedRedistCollGain ); emit TroveUpdated({ @@ -2001,8 +2449,11 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // We don’t need to increase the shares corresponding to redistribution first, because they would be subtracted immediately after // We don’t need to account for interest nor batch fee because it’s proportional to debt shares - uint256 batchDebtDecrease = _newTroveDebt - _troveChange.upfrontFee - _troveChange.appliedRedistBoldDebtGain; - uint256 batchCollDecrease = _newTroveColl - _troveChange.appliedRedistCollGain; + uint256 batchDebtDecrease = _newTroveDebt - + _troveChange.upfrontFee - + _troveChange.appliedRedistBoldDebtGain; + uint256 batchCollDecrease = _newTroveColl - + _troveChange.appliedRedistCollGain; batches[_batchAddress].totalDebtShares -= trove.batchDebtShares; batches[_batchAddress].debt = _newBatchDebt - batchDebtDecrease; diff --git a/contracts/src/TroveNFT.sol b/contracts/src/TroveNFT.sol index f3b06bb3e..6ecd7a70e 100644 --- a/contracts/src/TroveNFT.sol +++ b/contracts/src/TroveNFT.sol @@ -61,7 +61,7 @@ contract TroveNFT is HasWhitelist, ERC721, ITroveNFT { // adds receiver whitelist check function transferFrom(address from, address to, uint256 tokenId) public override(ERC721, IERC721) { - _requireWhitelisted(whitelist, to); + _requireWhitelisted(whitelist, this.transferFrom.selector, to); super.transferFrom(from, to, tokenId); } diff --git a/contracts/test/BoldConverter.t.sol b/contracts/test/BoldConverter.t.sol index b1666a427..911e8db3b 100644 --- a/contracts/test/BoldConverter.t.sol +++ b/contracts/test/BoldConverter.t.sol @@ -3,8 +3,9 @@ pragma solidity 0.8.24; import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {BoldConverter} from "src/Dependencies/BoldConverter.sol"; +import {BoldConverter, HasWhitelist} from "src/Dependencies/BoldConverter.sol"; import {BoldToken, IBoldToken} from "src/BoldToken.sol"; +import {Whitelist, IWhitelist} from "src/Dependencies/Whitelist.sol"; import "forge-std/Test.sol"; @@ -30,12 +31,18 @@ contract StableCoinConverterTest is Test { IERC20Metadata public firstAsset; IBoldToken public bvUSD; + IWhitelist public whitelist; address public userA; address public firstReceiver; address public converterAddr; + bytes4 public depositSelector = + bytes4(keccak256("deposit(address,uint256,address)")); + bytes4 public withdrawSelector = + bytes4(keccak256("withdraw(address,uint256,address)")); + function setUp() public { firstAsset = IERC20Metadata(new TestToken(6, "USDC", "USDC")); bvUSD = IBoldToken(0x876aac7648D79f87245E73316eB2D100e75F3Df1); // katana fork @@ -51,137 +58,30 @@ contract StableCoinConverterTest is Test { converter = new BoldConverter(assets, paths, address(bvUSD)); converterAddr = address(converter); + whitelist = IWhitelist(address(new Whitelist(bvUSD.owner()))); + converter.setWhitelist(whitelist); + vm.startPrank(bvUSD.owner()); bvUSD.setMinter(converterAddr, true); bvUSD.setBurner(converterAddr, true); vm.stopPrank(); } - function test_deposit_six_decimals(uint256 depositAmount) public { - vm.assume(depositAmount < 1000000000000000000000); - _testDeposit(userA, firstReceiver, firstAsset, depositAmount); - } - - function test_withdraw( - uint256 depositAmount, - uint256 withdrawAmount - ) public { - vm.assume(depositAmount >= withdrawAmount); - vm.assume(depositAmount < 1000000000000000000000); - - _testDeposit(userA, firstReceiver, firstAsset, depositAmount); - _testWithdraw(userA, firstReceiver, firstAsset, withdrawAmount, 100); - } - - function test_withdraw_noReceiverApproval() public { - deal(address(firstAsset), userA, 10e18); - - // deposit - _deposit(userA, firstAsset, 10e18); - - // underlying asset holder does not approve assets to be pulled - vm.expectRevert("ERC20: insufficient allowance"); - vm.prank(userA); - converter.withdraw(firstAsset, 1e18, userA); - } - - function test_multiplePaths_18_decimals( - uint256 depositAmount, - uint256 withdrawAmount - ) public { - vm.assume(depositAmount >= withdrawAmount); - vm.assume(depositAmount < 1000000000000000000000); - - IERC20Metadata newAsset = IERC20Metadata( - new TestToken(18, "USDC2", "USDC2") - ); - - address receiver2 = makeAddr("receiverB"); - - assertFalse(converter.isValidPath(newAsset)); - - BoldConverter.Path[] memory paths = new BoldConverter.Path[](1); - paths[0] = BoldConverter.Path(receiver2, 0, 100); - - IERC20Metadata[] memory assets = new IERC20Metadata[](1); - assets[0] = newAsset; - - vm.prank(address(this)); - converter.setPaths(assets, paths); - - assertTrue(converter.isValidPath(newAsset)); - - _testDeposit(userA, receiver2, newAsset, depositAmount); - - _testWithdraw(userA, receiver2, newAsset, withdrawAmount, 100); - } - - function test_addPath_onlyOwner() public { - IERC20Metadata newAsset = IERC20Metadata( - new TestToken(6, "USDC2", "USDC2") - ); - - assertFalse(converter.isValidPath(newAsset)); - - BoldConverter.Path[] memory paths = new BoldConverter.Path[](1); - paths[0] = BoldConverter.Path(firstReceiver, 0, 0); - - IERC20Metadata[] memory assets = new IERC20Metadata[](1); - assets[0] = newAsset; - - vm.prank(address(this)); - converter.setPaths(assets, paths); - - assertTrue(converter.isValidPath(newAsset)); - vm.prank(userA); - vm.expectRevert("Owned/not-owner"); - converter.setPaths(assets, paths); - } - - function test_removePath_onlyOwner() public { - IERC20Metadata newAsset = IERC20Metadata( - new TestToken(6, "USDC2", "USDC2") - ); - - assertFalse(converter.isValidPath(newAsset)); + // --- Helper functions --- - BoldConverter.Path[] memory paths = new BoldConverter.Path[](1); - paths[0] = BoldConverter.Path(firstReceiver, 0, 0); - - IERC20Metadata[] memory assets = new IERC20Metadata[](1); - assets[0] = newAsset; - - vm.prank(address(this)); - converter.setPaths(assets, paths); - - assertTrue(converter.isValidPath(newAsset)); - vm.prank(address(this)); - converter.deletePaths(assets); - - assertFalse(converter.isValidPath(newAsset)); - - vm.prank(userA); - vm.expectRevert("Owned/not-owner"); - converter.deletePaths(assets); - } + function _deposit( + address who, + IERC20Metadata asset, + uint256 amount + ) internal returns (uint256 bvUSDOut) { + vm.startPrank(who); + asset.approve(converterAddr, amount); - function test_deposit_invalidPath() public { - IERC20Metadata invalidAsset = IERC20Metadata( - new TestToken(6, "USDC", "USDC") - ); - vm.expectRevert("Invalid path"); - vm.prank(userA); - converter.deposit(invalidAsset, 1e18); + bvUSDOut = converter.deposit(asset, amount); + vm.stopPrank(); } - function test_withdraw_invalidPath() public { - IERC20Metadata invalidAsset = IERC20Metadata( - new TestToken(6, "USDC", "USDC") - ); - vm.expectRevert("Invalid path"); - vm.prank(userA); - converter.withdraw(invalidAsset, 1e18, userA); - } + // --- Deposit tests --- function _testDeposit( address user, @@ -251,6 +151,22 @@ contract StableCoinConverterTest is Test { } } + function test_deposit_six_decimals(uint256 depositAmount) public { + vm.assume(depositAmount < 1000000000000000000000); + _testDeposit(userA, firstReceiver, firstAsset, depositAmount); + } + + function test_deposit_invalidPath() public { + IERC20Metadata invalidAsset = IERC20Metadata( + new TestToken(6, "USDC", "USDC") + ); + vm.expectRevert("Invalid path"); + vm.prank(userA); + converter.deposit(invalidAsset, 1e18); + } + + // --- Withdraw tests --- + function _testWithdraw( address user, address receiver, @@ -322,18 +238,121 @@ contract StableCoinConverterTest is Test { } } - function _deposit( - address who, - IERC20Metadata asset, - uint256 amount - ) internal returns (uint256 bvUSDOut) { - vm.startPrank(who); - asset.approve(converterAddr, amount); + function test_withdraw( + uint256 depositAmount, + uint256 withdrawAmount + ) public { + vm.assume(depositAmount >= withdrawAmount); + vm.assume(depositAmount < 1000000000000000000000); - bvUSDOut = converter.deposit(asset, amount); - vm.stopPrank(); + _testDeposit(userA, firstReceiver, firstAsset, depositAmount); + _testWithdraw(userA, firstReceiver, firstAsset, withdrawAmount, 100); + } + + function test_withdraw_noReceiverApproval() public { + deal(address(firstAsset), userA, 10e18); + + // deposit + _deposit(userA, firstAsset, 10e18); + + // underlying asset holder does not approve assets to be pulled + vm.expectRevert("ERC20: insufficient allowance"); + vm.prank(userA); + converter.withdraw(firstAsset, 1e18, userA); + } + + function test_withdraw_invalidPath() public { + IERC20Metadata invalidAsset = IERC20Metadata( + new TestToken(6, "USDC", "USDC") + ); + vm.expectRevert("Invalid path"); + vm.prank(userA); + converter.withdraw(invalidAsset, 1e18, userA); + } + + // --- Roundtrip tests --- + + function test_multiplePaths_18_decimals( + uint256 depositAmount, + uint256 withdrawAmount + ) public { + vm.assume(depositAmount >= withdrawAmount); + vm.assume(depositAmount < 1000000000000000000000); + + IERC20Metadata newAsset = IERC20Metadata( + new TestToken(18, "USDC2", "USDC2") + ); + + address receiver2 = makeAddr("receiverB"); + + assertFalse(converter.isValidPath(newAsset)); + + BoldConverter.Path[] memory paths = new BoldConverter.Path[](1); + paths[0] = BoldConverter.Path(receiver2, 0, 100); + + IERC20Metadata[] memory assets = new IERC20Metadata[](1); + assets[0] = newAsset; + + vm.prank(address(this)); + converter.setPaths(assets, paths); + + assertTrue(converter.isValidPath(newAsset)); + + _testDeposit(userA, receiver2, newAsset, depositAmount); + + _testWithdraw(userA, receiver2, newAsset, withdrawAmount, 100); + } + + // --- Path management tests --- + + function test_addPath_onlyOwner() public { + IERC20Metadata newAsset = IERC20Metadata( + new TestToken(6, "USDC2", "USDC2") + ); + + assertFalse(converter.isValidPath(newAsset)); + + BoldConverter.Path[] memory paths = new BoldConverter.Path[](1); + paths[0] = BoldConverter.Path(firstReceiver, 0, 0); + + IERC20Metadata[] memory assets = new IERC20Metadata[](1); + assets[0] = newAsset; + + vm.prank(address(this)); + converter.setPaths(assets, paths); + + assertTrue(converter.isValidPath(newAsset)); + vm.prank(userA); + vm.expectRevert("Owned/not-owner"); + converter.setPaths(assets, paths); } + function test_removePath_onlyOwner() public { + IERC20Metadata newAsset = IERC20Metadata( + new TestToken(6, "USDC2", "USDC2") + ); + + assertFalse(converter.isValidPath(newAsset)); + + BoldConverter.Path[] memory paths = new BoldConverter.Path[](1); + paths[0] = BoldConverter.Path(firstReceiver, 0, 0); + + IERC20Metadata[] memory assets = new IERC20Metadata[](1); + assets[0] = newAsset; + + vm.prank(address(this)); + converter.setPaths(assets, paths); + + assertTrue(converter.isValidPath(newAsset)); + vm.prank(address(this)); + converter.deletePaths(assets); + + assertFalse(converter.isValidPath(newAsset)); + + vm.prank(userA); + vm.expectRevert("Owned/not-owner"); + converter.deletePaths(assets); + } function test_failing() public { uint256 depositAmount = 489877346851431045; @@ -362,4 +381,62 @@ contract StableCoinConverterTest is Test { _testWithdraw(userA, receiver2, newAsset, withdrawAmount, 100); } + + // --- Whitelist management tests --- + + function test_setWhitelist() public { + converter.setWhitelist(IWhitelist(address(0))); + assertEq(address(converter.whitelist()), address(0)); + } + + function test_setWhitelist_onlyOwner() public { + vm.startPrank(userA); + vm.expectRevert("Owned/not-owner"); + converter.setWhitelist(whitelist); + } + + // --- Whitelist interaction tests --- + + function test_deposit_whitelisted() public { + vm.startPrank(bvUSD.owner()); + whitelist.addWhitelistedFunc(address(converter), depositSelector); + whitelist.addToWhitelist(address(converter), depositSelector, userA); + vm.stopPrank(); + + _testDeposit(userA, firstReceiver, firstAsset, 1e18); + } + + function test_withdraw_whitelisted() public { + vm.startPrank(bvUSD.owner()); + whitelist.addWhitelistedFunc(address(converter), withdrawSelector); + whitelist.addToWhitelist(address(converter), withdrawSelector, userA); + vm.stopPrank(); + + _testDeposit(userA, firstReceiver, firstAsset, 1e18); + _testWithdraw(userA, firstReceiver, firstAsset, 1e18, 100); + } + + function test_deposit_notWhitelisted() public { + vm.startPrank(bvUSD.owner()); + whitelist.addWhitelistedFunc(address(converter), depositSelector); + vm.stopPrank(); + + vm.startPrank(userA); + vm.expectRevert( + abi.encodeWithSelector(HasWhitelist.NotWhitelisted.selector, userA) + ); + converter.deposit(firstAsset, 1e18); + } + + function test_withdraw_notWhitelisted() public { + vm.startPrank(bvUSD.owner()); + whitelist.addWhitelistedFunc(address(converter), withdrawSelector); + vm.stopPrank(); + + vm.startPrank(userA); + vm.expectRevert( + abi.encodeWithSelector(HasWhitelist.NotWhitelisted.selector, userA) + ); + converter.withdraw(firstAsset, 1e18, userA); + } } diff --git a/contracts/test/TestContracts/WhitelistTestSetup.sol b/contracts/test/TestContracts/WhitelistTestSetup.sol index 8a1251573..e5367dc42 100644 --- a/contracts/test/TestContracts/WhitelistTestSetup.sol +++ b/contracts/test/TestContracts/WhitelistTestSetup.sol @@ -22,8 +22,10 @@ contract WhitelistTestSetup is DevTestSetup { addressesRegistry.setWhitelist(whitelist); } - function _addToWhitelist(address callingContract, address who) internal { - vm.prank(owner); - whitelist.addToWhitelist(callingContract, who); + function _addToWhitelist(address callingContract, bytes4 funcSig, address who) internal { + vm.startPrank(owner); + whitelist.addWhitelistedFunc(callingContract, funcSig); + whitelist.addToWhitelist(callingContract, funcSig, who); + vm.stopPrank(); } } diff --git a/contracts/test/basicOpsWhitelist.t.sol b/contracts/test/basicOpsWhitelist.t.sol index 1e87b024d..479f824e6 100644 --- a/contracts/test/basicOpsWhitelist.t.sol +++ b/contracts/test/basicOpsWhitelist.t.sol @@ -23,9 +23,12 @@ contract WhitelistedBasicOps is BasicOps, WhitelistTestSetup { // whitelist users whitelistedUsers = [A, B, G]; for (uint8 i = 0; i < 3; i++) { - _addToWhitelist(address(borrowerOperations), whitelistedUsers[i]); - _addToWhitelist(address(stabilityPool), whitelistedUsers[i]); - _addToWhitelist(address(troveManager), whitelistedUsers[i]); + _addToWhitelist(address(borrowerOperations), BorrowerOperations.openTrove.selector, whitelistedUsers[i]); + _addToWhitelist(address(borrowerOperations), BorrowerOperations.openTroveAndJoinInterestBatchManager.selector, whitelistedUsers[i]); + _addToWhitelist(address(borrowerOperations), AddRemoveManagers.setRemoveManagerWithReceiver.selector, whitelistedUsers[i]); + _addToWhitelist(address(stabilityPool), StabilityPool.provideToSP.selector, whitelistedUsers[i]); + _addToWhitelist(address(troveManager), TroveManager.urgentRedemption.selector, whitelistedUsers[i]); + _addToWhitelist(address(troveManager), TroveManager.redeemCollateral.selector, whitelistedUsers[i]); } // set a not whitelisted address diff --git a/contracts/test/collateralRegistry.t.sol b/contracts/test/collateralRegistry.t.sol index 7329b3861..9a3bae786 100644 --- a/contracts/test/collateralRegistry.t.sol +++ b/contracts/test/collateralRegistry.t.sol @@ -23,9 +23,38 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { // whitelist all users involved in base tests whitelistedUsers = [A, B, C, D, E]; for (uint8 i = 0; i < 5; i++) { - _addToWhitelist(address(contractsArray[0].borrowerOperations), whitelistedUsers[i]); - _addToWhitelist(address(contractsArray[0].stabilityPool), whitelistedUsers[i]); - _addToWhitelist(address(contractsArray[0].troveManager), whitelistedUsers[i]); + _addToWhitelist( + address(contractsArray[0].borrowerOperations), + BorrowerOperations.openTrove.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(contractsArray[0].borrowerOperations), + BorrowerOperations + .openTroveAndJoinInterestBatchManager + .selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(contractsArray[0].borrowerOperations), + AddRemoveManagers.setRemoveManagerWithReceiver.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(contractsArray[0].stabilityPool), + IStabilityPool.provideToSP.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(contractsArray[0].troveManager), + TroveManager.urgentRedemption.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(contractsArray[0].troveManager), + TroveManager.redeemCollateral.selector, + whitelistedUsers[i] + ); } // set a non whitelisted address @@ -34,10 +63,23 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { function deployNewCollateralBranch() public - returns (ITroveManager newTroveManager, IERC20Metadata newCollateralToken) + returns ( + ITroveManager newTroveManager, + IERC20Metadata newCollateralToken + ) { - TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = new TestDeployer.TroveManagerParams[](1); - troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16); + TestDeployer.TroveManagerParams[] + memory troveManagerParamsArray = new TestDeployer.TroveManagerParams[]( + 1 + ); + troveManagerParamsArray[0] = TestDeployer.TroveManagerParams( + 150e16, + 110e16, + 10e16, + 110e16, + 5e16, + 10e16 + ); TestDeployer.LiquityContractsDev memory _contractsArray; @@ -48,8 +90,13 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { 1 days // _tapPeriod ); - (_contractsArray,) = - deployer.deployBranch(troveManagerParamsArray[0], collToken, WETH, boldToken, collateralRegistry); + (_contractsArray, ) = deployer.deployBranch( + troveManagerParamsArray[0], + collToken, + WETH, + boldToken, + collateralRegistry + ); contractsArray.push(_contractsArray); @@ -62,11 +109,17 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { for (uint256 i = 0; i < 6; i++) { // A to F giveAndApproveCollateral( - collToken, accountsList[i], initialCollateralAmount, address(contractsArray[4].borrowerOperations) + collToken, + accountsList[i], + initialCollateralAmount, + address(contractsArray[4].borrowerOperations) ); // Approve WETH for gas compensation in all branches vm.startPrank(accountsList[i]); - WETH.approve(address(contractsArray[4].borrowerOperations), type(uint256).max); + WETH.approve( + address(contractsArray[4].borrowerOperations), + type(uint256).max + ); vm.stopPrank(); } @@ -95,7 +148,10 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { // add a branch after initialisation and then perform a multicollateral redemption function test_addNewBranch_multiCollateralRedemption() public { // deploy the branch - (ITroveManager troveManager, IERC20Metadata collateralToken) = deployNewCollateralBranch(); + ( + ITroveManager troveManager, + IERC20Metadata collateralToken + ) = deployNewCollateralBranch(); IERC20Metadata[] memory _tokens = new IERC20Metadata[](1); _tokens[0] = collateralToken; @@ -118,24 +174,62 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { uint256 redeemAmount = 1600e18; // First collateral unbacked Bold: 10k (SP empty) - but whitelisted - testValues1.troveId = openMulticollateralTroveNoHints100pctWithIndex(0, A, 0, 10e18, 10000e18, 5e16); + testValues1.troveId = openMulticollateralTroveNoHints100pctWithIndex( + 0, + A, + 0, + 10e18, + 10000e18, + 5e16 + ); // Second collateral unbacked Bold: 5k - testValues2.troveId = openMulticollateralTroveNoHints100pctWithIndex(1, A, 0, 100e18, 10000e18, 5e16); + testValues2.troveId = openMulticollateralTroveNoHints100pctWithIndex( + 1, + A, + 0, + 100e18, + 10000e18, + 5e16 + ); makeMulticollateralSPDepositAndClaim(1, A, 5000e18); // Third collateral unbacked Bold: 1k - testValues3.troveId = openMulticollateralTroveNoHints100pctWithIndex(2, A, 0, 10e18, 10000e18, 5e16); + testValues3.troveId = openMulticollateralTroveNoHints100pctWithIndex( + 2, + A, + 0, + 10e18, + 10000e18, + 5e16 + ); makeMulticollateralSPDepositAndClaim(2, A, 9000e18); // Fourth collateral unbacked Bold: 0 - testValues4.troveId = openMulticollateralTroveNoHints100pctWithIndex(3, A, 0, 10e18, 10000e18, 5e16); + testValues4.troveId = openMulticollateralTroveNoHints100pctWithIndex( + 3, + A, + 0, + 10e18, + 10000e18, + 5e16 + ); makeMulticollateralSPDepositAndClaim(3, A, 10000e18); // Fifth collateral unbacked Bold: 1k - testValues5.troveId = openMulticollateralTroveNoHints100pctWithIndex(4, A, 0, 10e18, 10000e18, 5e16); + testValues5.troveId = openMulticollateralTroveNoHints100pctWithIndex( + 4, + A, + 0, + 10e18, + 10000e18, + 5e16 + ); vm.prank(A); - boldToken.approve(address(contractsArray[4].stabilityPool), type(uint256).max); + boldToken.approve( + address(contractsArray[4].stabilityPool), + type(uint256).max + ); makeMulticollateralSPDepositAndClaim(4, A, 9000e18); @@ -143,11 +237,21 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { vm.warp(block.timestamp + 1 days); // initial balances - testValues1.collInitialBalance = contractsArray[0].collToken.balanceOf(nonWhitelistedUser); - testValues2.collInitialBalance = contractsArray[1].collToken.balanceOf(nonWhitelistedUser); - testValues3.collInitialBalance = contractsArray[2].collToken.balanceOf(nonWhitelistedUser); - testValues4.collInitialBalance = contractsArray[3].collToken.balanceOf(nonWhitelistedUser); - testValues5.collInitialBalance = contractsArray[4].collToken.balanceOf(nonWhitelistedUser); + testValues1.collInitialBalance = contractsArray[0].collToken.balanceOf( + nonWhitelistedUser + ); + testValues2.collInitialBalance = contractsArray[1].collToken.balanceOf( + nonWhitelistedUser + ); + testValues3.collInitialBalance = contractsArray[2].collToken.balanceOf( + nonWhitelistedUser + ); + testValues4.collInitialBalance = contractsArray[3].collToken.balanceOf( + nonWhitelistedUser + ); + testValues5.collInitialBalance = contractsArray[4].collToken.balanceOf( + nonWhitelistedUser + ); testValues1.price = contractsArray[0].priceFeed.getPrice(); testValues2.price = contractsArray[1].priceFeed.getPrice(); @@ -155,34 +259,77 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { testValues4.price = contractsArray[3].priceFeed.getPrice(); testValues5.price = contractsArray[4].priceFeed.getPrice(); - testValues1.unbackedPortion = contractsArray[0].troveManager.getTroveEntireDebt(testValues1.troveId); - testValues2.unbackedPortion = contractsArray[1].troveManager.getTroveEntireDebt(testValues2.troveId) - 5000e18; - testValues3.unbackedPortion = contractsArray[2].troveManager.getTroveEntireDebt(testValues3.troveId) - 9000e18; - testValues4.unbackedPortion = contractsArray[3].troveManager.getTroveEntireDebt(testValues4.troveId) - 10000e18; - testValues5.unbackedPortion = contractsArray[4].troveManager.getTroveEntireDebt(testValues4.troveId) - 9000e18; + testValues1.unbackedPortion = contractsArray[0] + .troveManager + .getTroveEntireDebt(testValues1.troveId); + testValues2.unbackedPortion = + contractsArray[1].troveManager.getTroveEntireDebt( + testValues2.troveId + ) - + 5000e18; + testValues3.unbackedPortion = + contractsArray[2].troveManager.getTroveEntireDebt( + testValues3.troveId + ) - + 9000e18; + testValues4.unbackedPortion = + contractsArray[3].troveManager.getTroveEntireDebt( + testValues4.troveId + ) - + 10000e18; + testValues5.unbackedPortion = + contractsArray[4].troveManager.getTroveEntireDebt( + testValues4.troveId + ) - + 9000e18; // branch 1 is not counted as it's skipped - uint256 totalUnbacked = testValues2.unbackedPortion + testValues3.unbackedPortion + testValues4.unbackedPortion - + testValues5.unbackedPortion; + uint256 totalUnbacked = testValues2.unbackedPortion + + testValues3.unbackedPortion + + testValues4.unbackedPortion + + testValues5.unbackedPortion; // testValues1.redeemAmount = redeemAmount * testValues1.unbackedPortion / totalUnbacked; // whitelisted branch - testValues2.redeemAmount = redeemAmount * testValues2.unbackedPortion / totalUnbacked; - testValues3.redeemAmount = redeemAmount * testValues3.unbackedPortion / totalUnbacked; - testValues4.redeemAmount = redeemAmount * testValues4.unbackedPortion / totalUnbacked; - testValues5.redeemAmount = redeemAmount * testValues5.unbackedPortion / totalUnbacked; + testValues2.redeemAmount = + (redeemAmount * testValues2.unbackedPortion) / + totalUnbacked; + testValues3.redeemAmount = + (redeemAmount * testValues3.unbackedPortion) / + totalUnbacked; + testValues4.redeemAmount = + (redeemAmount * testValues4.unbackedPortion) / + totalUnbacked; + testValues5.redeemAmount = + (redeemAmount * testValues5.unbackedPortion) / + totalUnbacked; // fees - uint256 fee = collateralRegistry.getEffectiveRedemptionFeeInBold(redeemAmount); + 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; - testValues5.fee = fee * testValues5.redeemAmount / redeemAmount * DECIMAL_PRECISION / testValues5.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; + testValues5.fee = + (((fee * testValues5.redeemAmount) / redeemAmount) * + DECIMAL_PRECISION) / + testValues5.price; // Check redemption rate assertApproxEqAbs( collateralRegistry.getRedemptionFeeWithDecay(redeemAmount), - redeemAmount * (INITIAL_BASE_RATE / 16 + REDEMPTION_FEE_FLOOR) / DECIMAL_PRECISION, + (redeemAmount * (INITIAL_BASE_RATE / 16 + REDEMPTION_FEE_FLOOR)) / + DECIMAL_PRECISION, 1e7, "Wrong redemption fee with decay" ); @@ -190,7 +337,11 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { // Transfer bold from A to nonWhitelistedUser for redemption vm.prank(A); boldToken.transfer(nonWhitelistedUser, 16000e18); - assertEq(boldToken.balanceOf(nonWhitelistedUser), 16000e18, "Wrong Bold balance before redemption"); + assertEq( + boldToken.balanceOf(nonWhitelistedUser), + 16000e18, + "Wrong Bold balance before redemption" + ); uint256 initialBoldSupply = boldToken.totalSupply(); @@ -200,45 +351,77 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { // Check redemption rate assertApproxEqAbs( collateralRegistry.getRedemptionRate(), - INITIAL_BASE_RATE / 16 + REDEMPTION_FEE_FLOOR + redeemAmount * DECIMAL_PRECISION / initialBoldSupply, + INITIAL_BASE_RATE / + 16 + + REDEMPTION_FEE_FLOOR + + (redeemAmount * DECIMAL_PRECISION) / + initialBoldSupply, 1e5, "Wrong redemption rate" ); // Check bold balance - assertApproxEqAbs(boldToken.balanceOf(nonWhitelistedUser), 14400e18, 10, "Wrong Bold balance after redemption"); + assertApproxEqAbs( + boldToken.balanceOf(nonWhitelistedUser), + 14400e18, + 10, + "Wrong Bold balance after redemption" + ); // Check collateral balances // final balances - testValues1.collFinalBalance = contractsArray[0].collToken.balanceOf(nonWhitelistedUser); - testValues2.collFinalBalance = contractsArray[1].collToken.balanceOf(nonWhitelistedUser); - testValues3.collFinalBalance = contractsArray[2].collToken.balanceOf(nonWhitelistedUser); - testValues4.collFinalBalance = contractsArray[3].collToken.balanceOf(nonWhitelistedUser); - testValues5.collFinalBalance = contractsArray[4].collToken.balanceOf(nonWhitelistedUser); + testValues1.collFinalBalance = contractsArray[0].collToken.balanceOf( + nonWhitelistedUser + ); + testValues2.collFinalBalance = contractsArray[1].collToken.balanceOf( + nonWhitelistedUser + ); + testValues3.collFinalBalance = contractsArray[2].collToken.balanceOf( + nonWhitelistedUser + ); + testValues4.collFinalBalance = contractsArray[3].collToken.balanceOf( + nonWhitelistedUser + ); + testValues5.collFinalBalance = contractsArray[4].collToken.balanceOf( + nonWhitelistedUser + ); // first branch was not redeemed - assertApproxEqAbs(testValues1.collFinalBalance, testValues1.collInitialBalance, 1, "Wrong Collateral 1 balance"); + assertApproxEqAbs( + testValues1.collFinalBalance, + testValues1.collInitialBalance, + 1, + "Wrong Collateral 1 balance" + ); assertApproxEqAbs( testValues2.collFinalBalance - testValues2.collInitialBalance, - testValues2.redeemAmount * DECIMAL_PRECISION / testValues2.price - testValues2.fee, + (testValues2.redeemAmount * DECIMAL_PRECISION) / + testValues2.price - + testValues2.fee, 1e14, "Wrong Collateral 2 balance" ); assertApproxEqAbs( testValues3.collFinalBalance - testValues3.collInitialBalance, - testValues3.redeemAmount * DECIMAL_PRECISION / testValues3.price - testValues3.fee, + (testValues3.redeemAmount * DECIMAL_PRECISION) / + testValues3.price - + testValues3.fee, 1e13, "Wrong Collateral 3 balance" ); assertApproxEqAbs( testValues4.collFinalBalance - testValues4.collInitialBalance, - testValues4.redeemAmount * DECIMAL_PRECISION / testValues4.price - testValues4.fee, + (testValues4.redeemAmount * DECIMAL_PRECISION) / + testValues4.price - + testValues4.fee, 1e11, "Wrong Collateral 4 balance" ); assertApproxEqAbs( testValues5.collFinalBalance - testValues5.collInitialBalance, - testValues5.redeemAmount * DECIMAL_PRECISION / testValues5.price - testValues5.fee, + (testValues5.redeemAmount * DECIMAL_PRECISION) / + testValues5.price - + testValues5.fee, 1e11, "Wrong Collateral 5 balance" ); @@ -275,7 +458,7 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { ITroveManager[] memory validManager = new ITroveManager[](1); validManager[0] = troveManager; - // in constructor + // in constructor vm.expectRevert(CollateralRegistry.ZeroAddress.selector); new CollateralRegistry(boldToken, zeroAddressColl, validManager, A); @@ -284,7 +467,7 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { // in addCollaterals vm.startPrank(boldToken.owner()); - + vm.expectRevert(CollateralRegistry.ZeroAddress.selector); collateralRegistry.addNewCollaterals(zeroAddressColl, validManager); @@ -311,7 +494,10 @@ contract CollateralRegistryTest is MulticollateralTest, WhitelistTestSetup { assertEq(collateralRegistry.totalCollaterals(), 5); assertEq(address(collateralRegistry.getToken(4)), address(_tokens[0])); - assertEq(address(collateralRegistry.getTroveManager(4)), address(_troveManagers[0])); + assertEq( + address(collateralRegistry.getTroveManager(4)), + address(_troveManagers[0]) + ); // only owner can remove branch vm.expectRevert("Owned/not-owner"); diff --git a/contracts/test/multiCollateralWhitelist.t.sol b/contracts/test/multiCollateralWhitelist.t.sol index 8de30249d..275629dbf 100644 --- a/contracts/test/multiCollateralWhitelist.t.sol +++ b/contracts/test/multiCollateralWhitelist.t.sol @@ -5,7 +5,10 @@ pragma solidity 0.8.24; import "./TestContracts/WhitelistTestSetup.sol"; import {MulticollateralTest} from "./multicollateral.t.sol"; -contract MultiCollateralWhitelistedRedemptions is MulticollateralTest, WhitelistTestSetup { +contract MultiCollateralWhitelistedRedemptions is + MulticollateralTest, + WhitelistTestSetup +{ address[5] whitelistedUsers; address nonWhitelistedUser; @@ -21,9 +24,38 @@ contract MultiCollateralWhitelistedRedemptions is MulticollateralTest, Whitelist // whitelist all users involved in base tests whitelistedUsers = [A, B, C, D, E]; for (uint8 i = 0; i < 5; i++) { - _addToWhitelist(address(contractsArray[0].borrowerOperations), whitelistedUsers[i]); - _addToWhitelist(address(contractsArray[0].stabilityPool), whitelistedUsers[i]); - _addToWhitelist(address(contractsArray[0].troveManager), whitelistedUsers[i]); + _addToWhitelist( + address(contractsArray[0].borrowerOperations), + BorrowerOperations.openTrove.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(contractsArray[0].borrowerOperations), + BorrowerOperations + .openTroveAndJoinInterestBatchManager + .selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(contractsArray[0].borrowerOperations), + AddRemoveManagers.setRemoveManagerWithReceiver.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(contractsArray[0].stabilityPool), + IStabilityPool.provideToSP.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(contractsArray[0].troveManager), + TroveManager.urgentRedemption.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(contractsArray[0].troveManager), + TroveManager.redeemCollateral.selector, + whitelistedUsers[i] + ); } // set a non whitelisted address @@ -42,58 +74,128 @@ contract MultiCollateralWhitelistedRedemptions is MulticollateralTest, Whitelist uint256 redeemAmount = 1600e18; // First collateral unbacked Bold: 10k (SP empty) - but whitelisted - testValues1.troveId = openMulticollateralTroveNoHints100pctWithIndex(0, A, 0, 10e18, 10000e18, 5e16); + testValues1.troveId = openMulticollateralTroveNoHints100pctWithIndex( + 0, + A, + 0, + 10e18, + 10000e18, + 5e16 + ); // Second collateral unbacked Bold: 5k - testValues2.troveId = openMulticollateralTroveNoHints100pctWithIndex(1, A, 0, 100e18, 10000e18, 5e16); + testValues2.troveId = openMulticollateralTroveNoHints100pctWithIndex( + 1, + A, + 0, + 100e18, + 10000e18, + 5e16 + ); makeMulticollateralSPDepositAndClaim(1, A, 5000e18); // Third collateral unbacked Bold: 1k - testValues3.troveId = openMulticollateralTroveNoHints100pctWithIndex(2, A, 0, 10e18, 10000e18, 5e16); + testValues3.troveId = openMulticollateralTroveNoHints100pctWithIndex( + 2, + A, + 0, + 10e18, + 10000e18, + 5e16 + ); makeMulticollateralSPDepositAndClaim(2, A, 9000e18); // Fourth collateral unbacked Bold: 0 - testValues4.troveId = openMulticollateralTroveNoHints100pctWithIndex(3, A, 0, 10e18, 10000e18, 5e16); + testValues4.troveId = openMulticollateralTroveNoHints100pctWithIndex( + 3, + A, + 0, + 10e18, + 10000e18, + 5e16 + ); makeMulticollateralSPDepositAndClaim(3, A, 10000e18); // let time go by to reduce redemption rate (/16) vm.warp(block.timestamp + 1 days); // initial balances - testValues1.collInitialBalance = contractsArray[0].collToken.balanceOf(nonWhitelistedUser); - testValues2.collInitialBalance = contractsArray[1].collToken.balanceOf(nonWhitelistedUser); - testValues3.collInitialBalance = contractsArray[2].collToken.balanceOf(nonWhitelistedUser); - testValues4.collInitialBalance = contractsArray[3].collToken.balanceOf(nonWhitelistedUser); + testValues1.collInitialBalance = contractsArray[0].collToken.balanceOf( + nonWhitelistedUser + ); + testValues2.collInitialBalance = contractsArray[1].collToken.balanceOf( + nonWhitelistedUser + ); + testValues3.collInitialBalance = contractsArray[2].collToken.balanceOf( + nonWhitelistedUser + ); + testValues4.collInitialBalance = contractsArray[3].collToken.balanceOf( + nonWhitelistedUser + ); testValues1.price = contractsArray[0].priceFeed.getPrice(); testValues2.price = contractsArray[1].priceFeed.getPrice(); testValues3.price = contractsArray[2].priceFeed.getPrice(); testValues4.price = contractsArray[3].priceFeed.getPrice(); - testValues1.unbackedPortion = contractsArray[0].troveManager.getTroveEntireDebt(testValues1.troveId); - testValues2.unbackedPortion = contractsArray[1].troveManager.getTroveEntireDebt(testValues2.troveId) - 5000e18; - testValues3.unbackedPortion = contractsArray[2].troveManager.getTroveEntireDebt(testValues3.troveId) - 9000e18; - testValues4.unbackedPortion = contractsArray[3].troveManager.getTroveEntireDebt(testValues4.troveId) - 10000e18; + testValues1.unbackedPortion = contractsArray[0] + .troveManager + .getTroveEntireDebt(testValues1.troveId); + testValues2.unbackedPortion = + contractsArray[1].troveManager.getTroveEntireDebt( + testValues2.troveId + ) - + 5000e18; + testValues3.unbackedPortion = + contractsArray[2].troveManager.getTroveEntireDebt( + testValues3.troveId + ) - + 9000e18; + testValues4.unbackedPortion = + contractsArray[3].troveManager.getTroveEntireDebt( + testValues4.troveId + ) - + 10000e18; // branch 1 is not counted as it's skipped - uint256 totalUnbacked = testValues2.unbackedPortion + testValues3.unbackedPortion + testValues4.unbackedPortion; + uint256 totalUnbacked = testValues2.unbackedPortion + + testValues3.unbackedPortion + + testValues4.unbackedPortion; // testValues1.redeemAmount = redeemAmount * testValues1.unbackedPortion / totalUnbacked; // whitelisted branch - testValues2.redeemAmount = redeemAmount * testValues2.unbackedPortion / totalUnbacked; - testValues3.redeemAmount = redeemAmount * testValues3.unbackedPortion / totalUnbacked; - testValues4.redeemAmount = redeemAmount * testValues4.unbackedPortion / totalUnbacked; + testValues2.redeemAmount = + (redeemAmount * testValues2.unbackedPortion) / + totalUnbacked; + testValues3.redeemAmount = + (redeemAmount * testValues3.unbackedPortion) / + totalUnbacked; + testValues4.redeemAmount = + (redeemAmount * testValues4.unbackedPortion) / + totalUnbacked; // fees - uint256 fee = collateralRegistry.getEffectiveRedemptionFeeInBold(redeemAmount); + 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; + 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; // Check redemption rate assertApproxEqAbs( collateralRegistry.getRedemptionFeeWithDecay(redeemAmount), - redeemAmount * (INITIAL_BASE_RATE / 16 + REDEMPTION_FEE_FLOOR) / DECIMAL_PRECISION, + (redeemAmount * (INITIAL_BASE_RATE / 16 + REDEMPTION_FEE_FLOOR)) / + DECIMAL_PRECISION, 1e7, "Wrong redemption fee with decay" ); @@ -101,7 +203,11 @@ contract MultiCollateralWhitelistedRedemptions is MulticollateralTest, Whitelist // Transfer bold from A to nonWhitelistedUser for redemption vm.prank(A); boldToken.transfer(nonWhitelistedUser, 16000e18); - assertEq(boldToken.balanceOf(nonWhitelistedUser), 16000e18, "Wrong Bold balance before redemption"); + assertEq( + boldToken.balanceOf(nonWhitelistedUser), + 16000e18, + "Wrong Bold balance before redemption" + ); uint256 initialBoldSupply = boldToken.totalSupply(); @@ -111,38 +217,66 @@ contract MultiCollateralWhitelistedRedemptions is MulticollateralTest, Whitelist // Check redemption rate assertApproxEqAbs( collateralRegistry.getRedemptionRate(), - INITIAL_BASE_RATE / 16 + REDEMPTION_FEE_FLOOR + redeemAmount * DECIMAL_PRECISION / initialBoldSupply, + INITIAL_BASE_RATE / + 16 + + REDEMPTION_FEE_FLOOR + + (redeemAmount * DECIMAL_PRECISION) / + initialBoldSupply, 1e5, "Wrong redemption rate" ); // Check bold balance - assertApproxEqAbs(boldToken.balanceOf(nonWhitelistedUser), 14400e18, 10, "Wrong Bold balance after redemption"); + assertApproxEqAbs( + boldToken.balanceOf(nonWhitelistedUser), + 14400e18, + 10, + "Wrong Bold balance after redemption" + ); // Check collateral balances // final balances - testValues1.collFinalBalance = contractsArray[0].collToken.balanceOf(nonWhitelistedUser); - testValues2.collFinalBalance = contractsArray[1].collToken.balanceOf(nonWhitelistedUser); - testValues3.collFinalBalance = contractsArray[2].collToken.balanceOf(nonWhitelistedUser); - testValues4.collFinalBalance = contractsArray[3].collToken.balanceOf(nonWhitelistedUser); + testValues1.collFinalBalance = contractsArray[0].collToken.balanceOf( + nonWhitelistedUser + ); + testValues2.collFinalBalance = contractsArray[1].collToken.balanceOf( + nonWhitelistedUser + ); + testValues3.collFinalBalance = contractsArray[2].collToken.balanceOf( + nonWhitelistedUser + ); + testValues4.collFinalBalance = contractsArray[3].collToken.balanceOf( + nonWhitelistedUser + ); // first branch was not redeemed - assertApproxEqAbs(testValues1.collFinalBalance, testValues1.collInitialBalance, 1, "Wrong Collateral 1 balance"); + assertApproxEqAbs( + testValues1.collFinalBalance, + testValues1.collInitialBalance, + 1, + "Wrong Collateral 1 balance" + ); assertApproxEqAbs( testValues2.collFinalBalance - testValues2.collInitialBalance, - testValues2.redeemAmount * DECIMAL_PRECISION / testValues2.price - testValues2.fee, + (testValues2.redeemAmount * DECIMAL_PRECISION) / + testValues2.price - + testValues2.fee, 1e14, "Wrong Collateral 2 balance" ); assertApproxEqAbs( testValues3.collFinalBalance - testValues3.collInitialBalance, - testValues3.redeemAmount * DECIMAL_PRECISION / testValues3.price - testValues3.fee, + (testValues3.redeemAmount * DECIMAL_PRECISION) / + testValues3.price - + testValues3.fee, 1e13, "Wrong Collateral 3 balance" ); assertApproxEqAbs( testValues4.collFinalBalance - testValues4.collInitialBalance, - testValues4.redeemAmount * DECIMAL_PRECISION / testValues4.price - testValues4.fee, + (testValues4.redeemAmount * DECIMAL_PRECISION) / + testValues4.price - + testValues4.fee, 1e11, "Wrong Collateral 4 balance" ); diff --git a/contracts/test/troveNFT.t.sol b/contracts/test/troveNFT.t.sol index 400750159..008b5119d 100644 --- a/contracts/test/troveNFT.t.sol +++ b/contracts/test/troveNFT.t.sol @@ -146,10 +146,13 @@ contract troveNFTTest is WhitelistTestSetup { // whitelist users whitelistedUsers = [A, B, C, D, E]; for (uint8 i = 0; i < 5; i++) { - _addToWhitelist(address(borrowerOperations), whitelistedUsers[i]); - _addToWhitelist(address(stabilityPool), whitelistedUsers[i]); - _addToWhitelist(address(troveManager), whitelistedUsers[i]); - _addToWhitelist(address(troveNFTWETH), whitelistedUsers[i]); + _addToWhitelist(address(borrowerOperations), BorrowerOperations.openTrove.selector, whitelistedUsers[i]); + _addToWhitelist(address(borrowerOperations), BorrowerOperations.openTroveAndJoinInterestBatchManager.selector, whitelistedUsers[i]); + _addToWhitelist(address(borrowerOperations), AddRemoveManagers.setRemoveManagerWithReceiver.selector, whitelistedUsers[i]); + _addToWhitelist(address(stabilityPool), IStabilityPool.provideToSP.selector, whitelistedUsers[i]); + _addToWhitelist(address(troveManager), TroveManager.urgentRedemption.selector, whitelistedUsers[i]); + _addToWhitelist(address(troveManager), TroveManager.redeemCollateral.selector, whitelistedUsers[i]); + _addToWhitelist(address(troveNFTWETH), TroveNFT.transferFrom.selector, whitelistedUsers[i]); } // set a non whitelisted address diff --git a/contracts/test/whitelist.t.sol b/contracts/test/whitelist.t.sol index 4597a1440..b42d0c726 100644 --- a/contracts/test/whitelist.t.sol +++ b/contracts/test/whitelist.t.sol @@ -10,51 +10,94 @@ contract WhitelistTest is Test { address public owner; address public user; address public mockContract = address(123345); + bytes4 public mockFuncSig = bytes4(keccak256("mockFunc()")); + bytes4 public notWhitelistedFuncSig = + bytes4(keccak256("notWhitelistedFunc()")); function setUp() public { owner = makeAddr("owner"); user = makeAddr("user"); whitelistContract = new Whitelist(owner); + + vm.prank(owner); + whitelistContract.addWhitelistedFunc(mockContract, mockFuncSig); + } + + function test_addWhitelistedFunc() public { + assertFalse(whitelistContract.isWhitelistedFunc(mockContract, notWhitelistedFuncSig)); + assertTrue(whitelistContract.isWhitelisted(mockContract, notWhitelistedFuncSig, user)); + + vm.prank(owner); + whitelistContract.addWhitelistedFunc(mockContract, notWhitelistedFuncSig); + + assertTrue(whitelistContract.isWhitelistedFunc(mockContract, notWhitelistedFuncSig)); + assertFalse(whitelistContract.isWhitelisted(mockContract, notWhitelistedFuncSig, user)); + } + + function test_removeWhitelistedFunc() public { + test_addWhitelistedFunc(); + + vm.prank(owner); + whitelistContract.removeWhitelistedFunc(mockContract, notWhitelistedFuncSig); + + assertFalse(whitelistContract.isWhitelistedFunc(mockContract, notWhitelistedFuncSig)); + assertTrue(whitelistContract.isWhitelisted(mockContract, notWhitelistedFuncSig, user)); } function test_addWhitelist() public { - assertEq(whitelistContract.isWhitelisted(mockContract, user), false); + assertFalse( + whitelistContract.isWhitelisted(mockContract, mockFuncSig, user) + ); vm.prank(owner); - whitelistContract.addToWhitelist(mockContract, user); + whitelistContract.addToWhitelist(mockContract, mockFuncSig, user); + + assertTrue( + whitelistContract.isWhitelisted(mockContract, mockFuncSig, user) + ); + } - assertEq(whitelistContract.isWhitelisted(mockContract, user), true); + function test_addWhitelist_notWhitelistedFunc() public { + vm.startPrank(owner); + vm.expectRevert(Whitelist.FuncNotWhitelisted.selector); + whitelistContract.addToWhitelist(mockContract, notWhitelistedFuncSig, user); } function test_removeWhitelist() public { - assertEq(whitelistContract.isWhitelisted(mockContract, user), false); + assertFalse( + whitelistContract.isWhitelisted(mockContract, mockFuncSig, user) + ); vm.startPrank(owner); - whitelistContract.addToWhitelist(mockContract, user); + whitelistContract.addToWhitelist(mockContract, mockFuncSig, user); - assertEq(whitelistContract.isWhitelisted(mockContract, user), true); + assertTrue( + whitelistContract.isWhitelisted(mockContract, mockFuncSig, user) + ); - whitelistContract.removeFromWhitelist(mockContract, user); + whitelistContract.removeFromWhitelist(mockContract, mockFuncSig, user); - assertEq(whitelistContract.isWhitelisted(mockContract, user), false); + assertFalse( + whitelistContract.isWhitelisted(mockContract, mockFuncSig, user) + ); vm.stopPrank(); } function test_addWhitelist_onlyOwner() public { - vm.expectRevert(bytes("Owned/not-owner")); + vm.startPrank(user); - vm.prank(user); - whitelistContract.addToWhitelist(mockContract, user); + vm.expectRevert(bytes("Owned/not-owner")); + whitelistContract.addToWhitelist(mockContract, mockFuncSig, user); } function test_removeWhitelist_onlyOwner() public { vm.prank(owner); - whitelistContract.addToWhitelist(mockContract, user); + whitelistContract.addToWhitelist(mockContract, mockFuncSig, user); + vm.startPrank(user); vm.expectRevert(bytes("Owned/not-owner")); - vm.prank(user); - whitelistContract.removeFromWhitelist(mockContract, user); + whitelistContract.removeFromWhitelist(mockContract, mockFuncSig, user); } } diff --git a/contracts/test/whitelistRedemptions.t.sol b/contracts/test/whitelistRedemptions.t.sol index cc55b032f..5b42e3aaa 100644 --- a/contracts/test/whitelistRedemptions.t.sol +++ b/contracts/test/whitelistRedemptions.t.sol @@ -21,9 +21,38 @@ contract WhitelistedRedemptions is Redemptions, WhitelistTestSetup { // whitelist users whitelistedUsers = [A, B, C, D, E]; for (uint8 i = 0; i < 5; i++) { - _addToWhitelist(address(borrowerOperations), whitelistedUsers[i]); - _addToWhitelist(address(stabilityPool), whitelistedUsers[i]); - _addToWhitelist(address(troveManager), whitelistedUsers[i]); + _addToWhitelist( + address(borrowerOperations), + BorrowerOperations.openTrove.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(borrowerOperations), + BorrowerOperations + .openTroveAndJoinInterestBatchManager + .selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(borrowerOperations), + AddRemoveManagers.setRemoveManagerWithReceiver.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(stabilityPool), + IStabilityPool.provideToSP.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(troveManager), + TroveManager.urgentRedemption.selector, + whitelistedUsers[i] + ); + _addToWhitelist( + address(troveManager), + TroveManager.redeemCollateral.selector, + whitelistedUsers[i] + ); } // set a non whitelisted address @@ -34,7 +63,11 @@ contract WhitelistedRedemptions is Redemptions, WhitelistTestSetup { // all branch troves are skipped and remain untouched // redeemer bold balance is not burned function test_NonWhitelistedRedemption() public { - (uint256 coll,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); + ( + uint256 coll, + , + ABCDEF memory troveIDs + ) = _setupForRedemptionAscendingInterest(); uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); uint256 coll_A = troveManager.getTroveEntireColl(troveIDs.A); From 4f808735d504cbf0f96f4129dff8d5f57101dbee Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 23 Sep 2025 14:18:56 +0200 Subject: [PATCH 2/5] cleanup + whitelist for to --- contracts/src/Dependencies/BoldConverter.sol | 20 +- contracts/test/BoldConverter.t.sol | 283 ++++++++++++------- 2 files changed, 194 insertions(+), 109 deletions(-) diff --git a/contracts/src/Dependencies/BoldConverter.sol b/contracts/src/Dependencies/BoldConverter.sol index 149dd2c27..ef2a6cb99 100644 --- a/contracts/src/Dependencies/BoldConverter.sol +++ b/contracts/src/Dependencies/BoldConverter.sol @@ -11,6 +11,8 @@ import "./HasWhitelist.sol"; contract BoldConverter is Owned, HasWhitelist, ReentrancyGuard { uint256 public constant MAX_FEE = 10000; + bytes4 public constant DEPOSIT_SELECTOR = bytes4(keccak256("deposit(address,uint256,address)")); + bytes4 public constant WITHDRAW_SELECTOR = bytes4(keccak256("withdraw(address,uint256,address)")); IBoldToken public bvUSD; @@ -58,9 +60,16 @@ contract BoldConverter is Owned, HasWhitelist, ReentrancyGuard { ) public nonReentrant - checkWhitelisted(bytes4(keccak256("deposit(address,uint256,address)"))) + checkWhitelisted(DEPOSIT_SELECTOR) returns (uint256 boldAmount) { + if (msg.sender != to) { + IWhitelist _whitelist = whitelist; + if (address(_whitelist) != address(0)) { + _requireWhitelisted(_whitelist, DEPOSIT_SELECTOR, to); + } + } + Path memory path = _underlyingPaths[underlying]; require(path.underlyingReceiver != address(0), "Invalid path"); @@ -96,9 +105,16 @@ contract BoldConverter is Owned, HasWhitelist, ReentrancyGuard { ) public nonReentrant - checkWhitelisted(bytes4(keccak256("withdraw(address,uint256,address)"))) + checkWhitelisted(WITHDRAW_SELECTOR) returns (uint256 underlyingOut) { + if (msg.sender != to) { + IWhitelist _whitelist = whitelist; + if (address(_whitelist) != address(0)) { + _requireWhitelisted(_whitelist, WITHDRAW_SELECTOR, to); + } + } + Path memory path = _underlyingPaths[underlying]; require(path.underlyingReceiver != address(0), "Invalid path"); diff --git a/contracts/test/BoldConverter.t.sol b/contracts/test/BoldConverter.t.sol index 911e8db3b..4bd861604 100644 --- a/contracts/test/BoldConverter.t.sol +++ b/contracts/test/BoldConverter.t.sol @@ -27,16 +27,22 @@ contract TestToken is ERC20 { // test multiple paths contract StableCoinConverterTest is Test { + struct TestParams { + address from; + address to; + uint256 amount; + IERC20Metadata asset; + } + BoldConverter public converter; - IERC20Metadata public firstAsset; + IERC20Metadata public asset; IBoldToken public bvUSD; IWhitelist public whitelist; address public userA; - address public firstReceiver; - - address public converterAddr; + address public userB; + address public firstAssetReceiver; bytes4 public depositSelector = bytes4(keccak256("deposit(address,uint256,address)")); @@ -44,116 +50,124 @@ contract StableCoinConverterTest is Test { bytes4(keccak256("withdraw(address,uint256,address)")); function setUp() public { - firstAsset = IERC20Metadata(new TestToken(6, "USDC", "USDC")); + asset = IERC20Metadata(new TestToken(6, "USDC", "USDC")); bvUSD = IBoldToken(0x876aac7648D79f87245E73316eB2D100e75F3Df1); // katana fork userA = makeAddr("A"); - firstReceiver = makeAddr("Receiver"); + userB = makeAddr("B"); + firstAssetReceiver = makeAddr("Receiver"); BoldConverter.Path[] memory paths = new BoldConverter.Path[](1); - paths[0] = BoldConverter.Path(firstReceiver, 0, 100); + paths[0] = BoldConverter.Path(firstAssetReceiver, 0, 100); IERC20Metadata[] memory assets = new IERC20Metadata[](1); - assets[0] = firstAsset; + assets[0] = asset; converter = new BoldConverter(assets, paths, address(bvUSD)); - converterAddr = address(converter); whitelist = IWhitelist(address(new Whitelist(bvUSD.owner()))); converter.setWhitelist(whitelist); vm.startPrank(bvUSD.owner()); - bvUSD.setMinter(converterAddr, true); - bvUSD.setBurner(converterAddr, true); + bvUSD.setMinter(address(converter), true); + bvUSD.setBurner(address(converter), true); vm.stopPrank(); } // --- Helper functions --- function _deposit( - address who, - IERC20Metadata asset, - uint256 amount + address _from, + address _to, + uint256 _amount, + IERC20Metadata _asset ) internal returns (uint256 bvUSDOut) { - vm.startPrank(who); - asset.approve(converterAddr, amount); + vm.startPrank(_from); + _asset.approve(address(converter), _amount); - bvUSDOut = converter.deposit(asset, amount); + bvUSDOut = converter.deposit(_asset, _amount, _to); vm.stopPrank(); } // --- Deposit tests --- - function _testDeposit( - address user, - address receiver, - IERC20Metadata asset, - uint256 amount - ) internal { - uint256 assetDecimals = asset.decimals(); + function _testDeposit(TestParams memory params) internal { + address _assetReceiver = converter + .getPath(params.asset) + .underlyingReceiver; - deal(address(asset), user, amount); + deal(address(params.asset), params.from, params.amount); - uint256 receiverAssetBalanceBefore = asset.balanceOf(receiver); - uint256 userBvUSDBalanceBefore = bvUSD.balanceOf(user); - uint256 bvUSDSupplyBefore = bvUSD.totalSupply(); - uint256 userAssetBalanceBefore = asset.balanceOf(user); + uint256 assetReceiverBal0 = params.asset.balanceOf(_assetReceiver); + uint256 fromAssetBal0 = params.asset.balanceOf(params.from); + uint256 toBvUSDBal0 = bvUSD.balanceOf(params.to); + uint256 bvUSDSupply0 = bvUSD.totalSupply(); - if (amount == 0) { - vm.startPrank(user); - asset.approve(converterAddr, amount); + if (params.amount == 0) { + vm.startPrank(params.from); + params.asset.approve(address(converter), params.amount); vm.expectRevert("out = 0"); - converter.deposit(asset, amount); + converter.deposit(params.asset, params.amount, params.to); vm.stopPrank(); } else { // deposit - uint256 bvUSDOut = _deposit(user, asset, amount); - // Converter holds no assets + uint256 bvUSDOut = _deposit( + params.from, + params.to, + params.amount, + params.asset + ); + // No assets or bvUSD in converter assertEq( - asset.balanceOf(converterAddr), + params.asset.balanceOf(address(converter)), 0, - "Converter Asset balanace" + "Converter Asset balance" ); assertEq( - bvUSD.balanceOf(converterAddr), + bvUSD.balanceOf(address(converter)), 0, - "Converter bvUSD balanace" + "Converter bvUSD balance" ); - // receiver holds assets + // asset receiver got assets from sender assertEq( - asset.balanceOf(receiver), - receiverAssetBalanceBefore + amount, - "Receiver Asset balanace" + params.asset.balanceOf(_assetReceiver), + assetReceiverBal0 + params.amount, + "Asset receiver balance" ); // total supply increased by scaled amount - uint256 scaledbvUSDAmount = amount * 10 ** (18 - assetDecimals); assertEq( bvUSD.totalSupply(), - bvUSDSupplyBefore + scaledbvUSDAmount, + bvUSDSupply0 + + (params.amount * 10 ** (18 - params.asset.decimals())), "bvUSD supply" ); - // user receives bvUSD scaled amount + // to receives bvUSD scaled amount assertEq( - bvUSD.balanceOf(user), - userBvUSDBalanceBefore + bvUSDOut, + bvUSD.balanceOf(params.to), + toBvUSDBal0 + bvUSDOut, "bvUSD balance" ); - // user asset balance + // from asset balance decreased assertEq( - asset.balanceOf(user), - userAssetBalanceBefore - amount, - "User Asset balanace" + asset.balanceOf(params.from), + fromAssetBal0 - params.amount, + "User Asset balance" ); } } - function test_deposit_six_decimals(uint256 depositAmount) public { + function test_deposit(uint256 depositAmount) public { vm.assume(depositAmount < 1000000000000000000000); - _testDeposit(userA, firstReceiver, firstAsset, depositAmount); + _testDeposit(TestParams(userA, userA, depositAmount, asset)); + } + + function test_depositTo(uint256 depositAmount) public { + vm.assume(depositAmount < 1000000000000000000000); + _testDeposit(TestParams(userA, userB, depositAmount, asset)); } function test_deposit_invalidPath() public { @@ -167,73 +181,72 @@ contract StableCoinConverterTest is Test { // --- Withdraw tests --- - function _testWithdraw( - address user, - address receiver, - IERC20Metadata asset, - uint256 amount, - uint256 fee - ) internal { - uint256 assetDecimals = asset.decimals(); + function _testWithdraw(TestParams memory params, uint256 _fee) internal { + address _assetReceiver = converter + .getPath(params.asset) + .underlyingReceiver; + uint256 assetDecimals = params.asset.decimals(); - uint256 receiverAssetBalanceBefore = asset.balanceOf(receiver); - uint256 userBvUSDBalanceBefore = bvUSD.balanceOf(user); - uint256 bvUSDSupplyBefore = bvUSD.totalSupply(); - uint256 userAssetBalanceBefore = asset.balanceOf(user); + uint256 assetReceiverBal0 = params.asset.balanceOf(_assetReceiver); + uint256 toAssetBal0 = params.asset.balanceOf(params.to); + uint256 fromBvUSDBal0 = bvUSD.balanceOf(params.from); + uint256 bvUSDSupply0 = bvUSD.totalSupply(); // underlying receiver needs to approve converter - vm.prank(receiver); - asset.approve(converterAddr, amount); + vm.prank(_assetReceiver); + params.asset.approve(address(converter), params.amount); - vm.prank(user); - if ((assetDecimals != 18 && amount < 1e12) || amount == 0) { + vm.prank(params.from); + if ( + (assetDecimals != 18 && params.amount < 1e12) || params.amount == 0 + ) { vm.expectRevert("out = 0"); - converter.withdraw(asset, amount, user); + converter.withdraw(params.asset, params.amount, params.to); } else { - uint256 assetOut = converter.withdraw(asset, amount, user); - uint256 scaledWithdraw = amount / (10 ** (18 - assetDecimals)); - uint256 expectedFee = (scaledWithdraw * fee) / 10000; + uint256 assetOut = converter.withdraw(params.asset, params.amount, params.to); + uint256 scaledWithdraw = params.amount / (10 ** (18 - assetDecimals)); + uint256 expectedFee = (scaledWithdraw * _fee) / 10000; assertEq(assetOut, scaledWithdraw - expectedFee, "Fee amount"); // underlying receiver sent asset to user assertEq( - asset.balanceOf(receiver), - receiverAssetBalanceBefore - assetOut, + params.asset.balanceOf(_assetReceiver), + assetReceiverBal0 - assetOut, "Receiver asset balance" ); - // user received asset + // to received asset assertEq( - asset.balanceOf(userA), - userAssetBalanceBefore + assetOut, - "User asset balance" + params.asset.balanceOf(params.to), + toAssetBal0 + assetOut, + "To asset balance" ); - // user bvUSD + // from bvUSD balance decreased assertEq( - bvUSD.balanceOf(userA), - userBvUSDBalanceBefore - amount, - "bvUSD user balance" + bvUSD.balanceOf(params.from), + fromBvUSDBal0 - params.amount, + "From bvUSD balance" ); // bvUSD is burned assertEq( bvUSD.totalSupply(), - bvUSDSupplyBefore - amount, + bvUSDSupply0 - params.amount, "bvUSD supply" ); // Converter holds no assets assertEq( - asset.balanceOf(converterAddr), + params.asset.balanceOf(address(converter)), 0, - "Converter Asset balanace" + "Converter Asset balance" ); assertEq( - bvUSD.balanceOf(converterAddr), + bvUSD.balanceOf(address(converter)), 0, - "Converter bvUSD balanace" + "Converter bvUSD balance" ); } } @@ -245,20 +258,31 @@ contract StableCoinConverterTest is Test { vm.assume(depositAmount >= withdrawAmount); vm.assume(depositAmount < 1000000000000000000000); - _testDeposit(userA, firstReceiver, firstAsset, depositAmount); - _testWithdraw(userA, firstReceiver, firstAsset, withdrawAmount, 100); + _testDeposit(TestParams(userA, userA, depositAmount, asset)); + _testWithdraw(TestParams(userA, userA, withdrawAmount, asset), 100); + } + + function test_withdrawTo( + uint256 depositAmount, + uint256 withdrawAmount + ) public { + vm.assume(depositAmount >= withdrawAmount); + vm.assume(depositAmount < 1000000000000000000000); + + _testDeposit(TestParams(userA, userA, depositAmount, asset)); + _testWithdraw(TestParams(userA, userB, withdrawAmount, asset), 100); } function test_withdraw_noReceiverApproval() public { - deal(address(firstAsset), userA, 10e18); + deal(address(asset), userA, 10e18); // deposit - _deposit(userA, firstAsset, 10e18); + _deposit(userA, userA, 10e18, asset); // underlying asset holder does not approve assets to be pulled vm.expectRevert("ERC20: insufficient allowance"); vm.prank(userA); - converter.withdraw(firstAsset, 1e18, userA); + converter.withdraw(asset, 1e18, userA); } function test_withdraw_invalidPath() public { @@ -298,9 +322,8 @@ contract StableCoinConverterTest is Test { assertTrue(converter.isValidPath(newAsset)); - _testDeposit(userA, receiver2, newAsset, depositAmount); - - _testWithdraw(userA, receiver2, newAsset, withdrawAmount, 100); + _testDeposit(TestParams(userA, userA, depositAmount, newAsset)); + _testWithdraw(TestParams(userA, userA, withdrawAmount, newAsset), 100); } // --- Path management tests --- @@ -313,7 +336,7 @@ contract StableCoinConverterTest is Test { assertFalse(converter.isValidPath(newAsset)); BoldConverter.Path[] memory paths = new BoldConverter.Path[](1); - paths[0] = BoldConverter.Path(firstReceiver, 0, 0); + paths[0] = BoldConverter.Path(firstAssetReceiver, 0, 0); IERC20Metadata[] memory assets = new IERC20Metadata[](1); assets[0] = newAsset; @@ -335,7 +358,7 @@ contract StableCoinConverterTest is Test { assertFalse(converter.isValidPath(newAsset)); BoldConverter.Path[] memory paths = new BoldConverter.Path[](1); - paths[0] = BoldConverter.Path(firstReceiver, 0, 0); + paths[0] = BoldConverter.Path(firstAssetReceiver, 0, 0); IERC20Metadata[] memory assets = new IERC20Metadata[](1); assets[0] = newAsset; @@ -377,9 +400,8 @@ contract StableCoinConverterTest is Test { assertTrue(converter.isValidPath(newAsset)); - _testDeposit(userA, receiver2, newAsset, depositAmount); - - _testWithdraw(userA, receiver2, newAsset, withdrawAmount, 100); + _testDeposit(TestParams(userA, receiver2, depositAmount, newAsset)); + _testWithdraw(TestParams(userA, receiver2, withdrawAmount, newAsset), 100); } // --- Whitelist management tests --- @@ -403,7 +425,17 @@ contract StableCoinConverterTest is Test { whitelist.addToWhitelist(address(converter), depositSelector, userA); vm.stopPrank(); - _testDeposit(userA, firstReceiver, firstAsset, 1e18); + _testDeposit(TestParams(userA, userA, 1e18, asset)); + } + + function test_depositTo_whitelisted() public { + vm.startPrank(bvUSD.owner()); + whitelist.addWhitelistedFunc(address(converter), depositSelector); + whitelist.addToWhitelist(address(converter), depositSelector, userA); + whitelist.addToWhitelist(address(converter), depositSelector, userB); + vm.stopPrank(); + + _testDeposit(TestParams(userA, userB, 1e18, asset)); } function test_withdraw_whitelisted() public { @@ -412,8 +444,19 @@ contract StableCoinConverterTest is Test { whitelist.addToWhitelist(address(converter), withdrawSelector, userA); vm.stopPrank(); - _testDeposit(userA, firstReceiver, firstAsset, 1e18); - _testWithdraw(userA, firstReceiver, firstAsset, 1e18, 100); + _testDeposit(TestParams(userA, userA, 1e18, asset)); + _testWithdraw(TestParams(userA, userA, 1e18, asset), 100); + } + + function test_withdrawTo_whitelisted() public { + vm.startPrank(bvUSD.owner()); + whitelist.addWhitelistedFunc(address(converter), withdrawSelector); + whitelist.addToWhitelist(address(converter), withdrawSelector, userA); + whitelist.addToWhitelist(address(converter), withdrawSelector, userB); + vm.stopPrank(); + + _testDeposit(TestParams(userA, userA, 1e18, asset)); + _testWithdraw(TestParams(userA, userB, 1e18, asset), 100); } function test_deposit_notWhitelisted() public { @@ -425,7 +468,20 @@ contract StableCoinConverterTest is Test { vm.expectRevert( abi.encodeWithSelector(HasWhitelist.NotWhitelisted.selector, userA) ); - converter.deposit(firstAsset, 1e18); + converter.deposit(asset, 1e18); + } + + function test_depositTo_notWhitelisted() public { + vm.startPrank(bvUSD.owner()); + whitelist.addWhitelistedFunc(address(converter), depositSelector); + whitelist.addToWhitelist(address(converter), depositSelector, userA); + vm.stopPrank(); + + vm.startPrank(userA); + vm.expectRevert( + abi.encodeWithSelector(HasWhitelist.NotWhitelisted.selector, userB) + ); + converter.deposit(asset, 1e18, userB); } function test_withdraw_notWhitelisted() public { @@ -437,6 +493,19 @@ contract StableCoinConverterTest is Test { vm.expectRevert( abi.encodeWithSelector(HasWhitelist.NotWhitelisted.selector, userA) ); - converter.withdraw(firstAsset, 1e18, userA); + converter.withdraw(asset, 1e18, userA); + } + + function test_withdrawTo_notWhitelisted() public { + vm.startPrank(bvUSD.owner()); + whitelist.addWhitelistedFunc(address(converter), withdrawSelector); + whitelist.addToWhitelist(address(converter), withdrawSelector, userA); + vm.stopPrank(); + + vm.startPrank(userA); + vm.expectRevert( + abi.encodeWithSelector(HasWhitelist.NotWhitelisted.selector, userB) + ); + converter.withdraw(asset, 1e18, userB); } } From 73599646362d142d6191404c5a2333cc38e411b6 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 24 Sep 2025 10:32:34 +0200 Subject: [PATCH 3/5] txOrigin check --- contracts/src/Dependencies/BoldConverter.sol | 46 ++++--- contracts/test/BoldConverter.t.sol | 134 +++++++------------ 2 files changed, 73 insertions(+), 107 deletions(-) diff --git a/contracts/src/Dependencies/BoldConverter.sol b/contracts/src/Dependencies/BoldConverter.sol index ef2a6cb99..4d426f875 100644 --- a/contracts/src/Dependencies/BoldConverter.sol +++ b/contracts/src/Dependencies/BoldConverter.sol @@ -11,8 +11,10 @@ import "./HasWhitelist.sol"; contract BoldConverter is Owned, HasWhitelist, ReentrancyGuard { uint256 public constant MAX_FEE = 10000; - bytes4 public constant DEPOSIT_SELECTOR = bytes4(keccak256("deposit(address,uint256,address)")); - bytes4 public constant WITHDRAW_SELECTOR = bytes4(keccak256("withdraw(address,uint256,address)")); + bytes4 public constant DEPOSIT_SELECTOR = + bytes4(keccak256("deposit(address,uint256,address)")); + bytes4 public constant WITHDRAW_SELECTOR = + bytes4(keccak256("withdraw(address,uint256,address)")); IBoldToken public bvUSD; @@ -60,16 +62,9 @@ contract BoldConverter is Owned, HasWhitelist, ReentrancyGuard { ) public nonReentrant - checkWhitelisted(DEPOSIT_SELECTOR) + checkWhitelistedSenderAndOrigin(DEPOSIT_SELECTOR) returns (uint256 boldAmount) { - if (msg.sender != to) { - IWhitelist _whitelist = whitelist; - if (address(_whitelist) != address(0)) { - _requireWhitelisted(_whitelist, DEPOSIT_SELECTOR, to); - } - } - Path memory path = _underlyingPaths[underlying]; require(path.underlyingReceiver != address(0), "Invalid path"); @@ -105,16 +100,9 @@ contract BoldConverter is Owned, HasWhitelist, ReentrancyGuard { ) public nonReentrant - checkWhitelisted(WITHDRAW_SELECTOR) + checkWhitelistedSenderAndOrigin(WITHDRAW_SELECTOR) returns (uint256 underlyingOut) { - if (msg.sender != to) { - IWhitelist _whitelist = whitelist; - if (address(_whitelist) != address(0)) { - _requireWhitelisted(_whitelist, WITHDRAW_SELECTOR, to); - } - } - Path memory path = _underlyingPaths[underlying]; require(path.underlyingReceiver != address(0), "Invalid path"); @@ -198,4 +186,26 @@ contract BoldConverter is Owned, HasWhitelist, ReentrancyGuard { function setWhitelist(IWhitelist _whitelist) external onlyOwner { _setWhitelist(_whitelist); } + + modifier checkWhitelistedSenderAndOrigin(bytes4 funcSig) { + IWhitelist _whitelist = whitelist; + if (address(_whitelist) != address(0)) { + bool isWhitelisted = _whitelist.isWhitelisted( + address(this), + funcSig, + msg.sender + ); + if (!isWhitelisted && msg.sender != tx.origin) { + isWhitelisted = _whitelist.isWhitelisted( + address(this), + funcSig, + tx.origin + ); + } + if (!isWhitelisted) { + revert NotWhitelisted(msg.sender); + } + } + _; + } } diff --git a/contracts/test/BoldConverter.t.sol b/contracts/test/BoldConverter.t.sol index 4bd861604..9af7d4fae 100644 --- a/contracts/test/BoldConverter.t.sol +++ b/contracts/test/BoldConverter.t.sol @@ -29,6 +29,7 @@ contract TestToken is ERC20 { contract StableCoinConverterTest is Test { struct TestParams { address from; + address origin; address to; uint256 amount; IERC20Metadata asset; @@ -76,15 +77,12 @@ contract StableCoinConverterTest is Test { // --- Helper functions --- function _deposit( - address _from, - address _to, - uint256 _amount, - IERC20Metadata _asset + TestParams memory params ) internal returns (uint256 bvUSDOut) { - vm.startPrank(_from); - _asset.approve(address(converter), _amount); + vm.startPrank(params.from, params.origin); + params.asset.approve(address(converter), params.amount); - bvUSDOut = converter.deposit(_asset, _amount, _to); + bvUSDOut = converter.deposit(params.asset, params.amount, params.to); vm.stopPrank(); } @@ -103,7 +101,7 @@ contract StableCoinConverterTest is Test { uint256 bvUSDSupply0 = bvUSD.totalSupply(); if (params.amount == 0) { - vm.startPrank(params.from); + vm.startPrank(params.from, params.origin); params.asset.approve(address(converter), params.amount); vm.expectRevert("out = 0"); @@ -111,12 +109,7 @@ contract StableCoinConverterTest is Test { vm.stopPrank(); } else { // deposit - uint256 bvUSDOut = _deposit( - params.from, - params.to, - params.amount, - params.asset - ); + uint256 bvUSDOut = _deposit(params); // No assets or bvUSD in converter assertEq( params.asset.balanceOf(address(converter)), @@ -162,12 +155,12 @@ contract StableCoinConverterTest is Test { function test_deposit(uint256 depositAmount) public { vm.assume(depositAmount < 1000000000000000000000); - _testDeposit(TestParams(userA, userA, depositAmount, asset)); + _testDeposit(TestParams(userA, userA, userA, depositAmount, asset)); } function test_depositTo(uint256 depositAmount) public { vm.assume(depositAmount < 1000000000000000000000); - _testDeposit(TestParams(userA, userB, depositAmount, asset)); + _testDeposit(TestParams(userA, userA, userB, depositAmount, asset)); } function test_deposit_invalidPath() public { @@ -196,15 +189,20 @@ contract StableCoinConverterTest is Test { vm.prank(_assetReceiver); params.asset.approve(address(converter), params.amount); - vm.prank(params.from); + vm.prank(params.from, params.origin); if ( (assetDecimals != 18 && params.amount < 1e12) || params.amount == 0 ) { vm.expectRevert("out = 0"); converter.withdraw(params.asset, params.amount, params.to); } else { - uint256 assetOut = converter.withdraw(params.asset, params.amount, params.to); - uint256 scaledWithdraw = params.amount / (10 ** (18 - assetDecimals)); + uint256 assetOut = converter.withdraw( + params.asset, + params.amount, + params.to + ); + uint256 scaledWithdraw = params.amount / + (10 ** (18 - assetDecimals)); uint256 expectedFee = (scaledWithdraw * _fee) / 10000; assertEq(assetOut, scaledWithdraw - expectedFee, "Fee amount"); @@ -258,8 +256,11 @@ contract StableCoinConverterTest is Test { vm.assume(depositAmount >= withdrawAmount); vm.assume(depositAmount < 1000000000000000000000); - _testDeposit(TestParams(userA, userA, depositAmount, asset)); - _testWithdraw(TestParams(userA, userA, withdrawAmount, asset), 100); + _testDeposit(TestParams(userA, userA, userA, depositAmount, asset)); + _testWithdraw( + TestParams(userA, userA, userA, withdrawAmount, asset), + 100 + ); } function test_withdrawTo( @@ -269,15 +270,18 @@ contract StableCoinConverterTest is Test { vm.assume(depositAmount >= withdrawAmount); vm.assume(depositAmount < 1000000000000000000000); - _testDeposit(TestParams(userA, userA, depositAmount, asset)); - _testWithdraw(TestParams(userA, userB, withdrawAmount, asset), 100); + _testDeposit(TestParams(userA, userA, userA, depositAmount, asset)); + _testWithdraw( + TestParams(userA, userA, userB, withdrawAmount, asset), + 100 + ); } function test_withdraw_noReceiverApproval() public { deal(address(asset), userA, 10e18); // deposit - _deposit(userA, userA, 10e18, asset); + _deposit(TestParams(userA, userA, userA, 10e18, asset)); // underlying asset holder does not approve assets to be pulled vm.expectRevert("ERC20: insufficient allowance"); @@ -322,8 +326,11 @@ contract StableCoinConverterTest is Test { assertTrue(converter.isValidPath(newAsset)); - _testDeposit(TestParams(userA, userA, depositAmount, newAsset)); - _testWithdraw(TestParams(userA, userA, withdrawAmount, newAsset), 100); + _testDeposit(TestParams(userA, userA, userA, depositAmount, newAsset)); + _testWithdraw( + TestParams(userA, userA, userA, withdrawAmount, newAsset), + 100 + ); } // --- Path management tests --- @@ -377,33 +384,6 @@ contract StableCoinConverterTest is Test { converter.deletePaths(assets); } - function test_failing() public { - uint256 depositAmount = 489877346851431045; - uint256 withdrawAmount = 3; - - IERC20Metadata newAsset = IERC20Metadata( - new TestToken(18, "USDC2", "USDC2") - ); - - address receiver2 = makeAddr("receiverB"); - - assertFalse(converter.isValidPath(newAsset)); - - BoldConverter.Path[] memory paths = new BoldConverter.Path[](1); - paths[0] = BoldConverter.Path(receiver2, 0, 100); - - IERC20Metadata[] memory assets = new IERC20Metadata[](1); - assets[0] = newAsset; - - vm.prank(address(this)); - converter.setPaths(assets, paths); - - assertTrue(converter.isValidPath(newAsset)); - - _testDeposit(TestParams(userA, receiver2, depositAmount, newAsset)); - _testWithdraw(TestParams(userA, receiver2, withdrawAmount, newAsset), 100); - } - // --- Whitelist management tests --- function test_setWhitelist() public { @@ -425,17 +405,20 @@ contract StableCoinConverterTest is Test { whitelist.addToWhitelist(address(converter), depositSelector, userA); vm.stopPrank(); - _testDeposit(TestParams(userA, userA, 1e18, asset)); + _testDeposit(TestParams(userA, userA, userA, 1e18, asset)); } - function test_depositTo_whitelisted() public { + function test_deposit_whitelisted_origin() public { vm.startPrank(bvUSD.owner()); whitelist.addWhitelistedFunc(address(converter), depositSelector); - whitelist.addToWhitelist(address(converter), depositSelector, userA); whitelist.addToWhitelist(address(converter), depositSelector, userB); vm.stopPrank(); - _testDeposit(TestParams(userA, userB, 1e18, asset)); + vm.startPrank(userB); + asset.approve(address(converter), 1e18); + vm.stopPrank(); + + _testDeposit(TestParams(userA, userB, userB, 1e18, asset)); } function test_withdraw_whitelisted() public { @@ -444,19 +427,18 @@ contract StableCoinConverterTest is Test { whitelist.addToWhitelist(address(converter), withdrawSelector, userA); vm.stopPrank(); - _testDeposit(TestParams(userA, userA, 1e18, asset)); - _testWithdraw(TestParams(userA, userA, 1e18, asset), 100); + _testDeposit(TestParams(userA, userA, userA, 1e18, asset)); + _testWithdraw(TestParams(userA, userA, userA, 1e18, asset), 100); } - function test_withdrawTo_whitelisted() public { + function test_withdraw_whitelisted_origin() public { vm.startPrank(bvUSD.owner()); whitelist.addWhitelistedFunc(address(converter), withdrawSelector); - whitelist.addToWhitelist(address(converter), withdrawSelector, userA); whitelist.addToWhitelist(address(converter), withdrawSelector, userB); vm.stopPrank(); - _testDeposit(TestParams(userA, userA, 1e18, asset)); - _testWithdraw(TestParams(userA, userB, 1e18, asset), 100); + _testDeposit(TestParams(userA, userA, userA, 1e18, asset)); + _testWithdraw(TestParams(userA, userB, userB, 1e18, asset), 100); } function test_deposit_notWhitelisted() public { @@ -471,19 +453,6 @@ contract StableCoinConverterTest is Test { converter.deposit(asset, 1e18); } - function test_depositTo_notWhitelisted() public { - vm.startPrank(bvUSD.owner()); - whitelist.addWhitelistedFunc(address(converter), depositSelector); - whitelist.addToWhitelist(address(converter), depositSelector, userA); - vm.stopPrank(); - - vm.startPrank(userA); - vm.expectRevert( - abi.encodeWithSelector(HasWhitelist.NotWhitelisted.selector, userB) - ); - converter.deposit(asset, 1e18, userB); - } - function test_withdraw_notWhitelisted() public { vm.startPrank(bvUSD.owner()); whitelist.addWhitelistedFunc(address(converter), withdrawSelector); @@ -495,17 +464,4 @@ contract StableCoinConverterTest is Test { ); converter.withdraw(asset, 1e18, userA); } - - function test_withdrawTo_notWhitelisted() public { - vm.startPrank(bvUSD.owner()); - whitelist.addWhitelistedFunc(address(converter), withdrawSelector); - whitelist.addToWhitelist(address(converter), withdrawSelector, userA); - vm.stopPrank(); - - vm.startPrank(userA); - vm.expectRevert( - abi.encodeWithSelector(HasWhitelist.NotWhitelisted.selector, userB) - ); - converter.withdraw(asset, 1e18, userB); - } } From 192826e709533636c42fb6ddb9a03f94939b01e6 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 24 Sep 2025 11:47:44 +0200 Subject: [PATCH 4/5] deploy script --- contracts/script/DeployConverter.s.sol | 39 ++++++++++++++++++++ contracts/src/Dependencies/BoldConverter.sol | 12 +++--- 2 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 contracts/script/DeployConverter.s.sol diff --git a/contracts/script/DeployConverter.s.sol b/contracts/script/DeployConverter.s.sol new file mode 100644 index 000000000..f3be8e528 --- /dev/null +++ b/contracts/script/DeployConverter.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 +pragma solidity 0.8.24; + +import {Script, console} from "forge-std/Script.sol"; +import {BoldConverter, Path, IWhitelist, IBoldToken, IERC20Metadata} from "src/Dependencies/BoldConverter.sol"; + +contract DeployConverter is Script { + function run() public returns (BoldConverter converter) { + vm.startBroadcast(); + console.log("msg.sender:", msg.sender); + + IERC20Metadata[] memory underlyings = new IERC20Metadata[](1); + Path[] memory paths = new Path[](1); + + // Edit this + underlyings[0] = IERC20Metadata( + 0x203A662b0BD271A6ed5a60EdFbd04bFce608FD36 + ); // usdc + paths[0] = Path( + address(0x452DC676b4E377a76B4b3048eB3b511A0F1ec057), + 6, + 0 + ); + address bvUSD = 0x876aac7648D79f87245E73316eB2D100e75F3Df1; + address whitelist = 0x83BBAA022Cca1295a975EC101a073C44Ea336f79; + address owner = 0x452DC676b4E377a76B4b3048eB3b511A0F1ec057; + + converter = new BoldConverter( + underlyings, + paths, + bvUSD + ); + converter.setWhitelist(IWhitelist(whitelist)); + converter.nominateNewOwner(owner); + + vm.stopBroadcast(); + } +} diff --git a/contracts/src/Dependencies/BoldConverter.sol b/contracts/src/Dependencies/BoldConverter.sol index 4d426f875..4d97b0283 100644 --- a/contracts/src/Dependencies/BoldConverter.sol +++ b/contracts/src/Dependencies/BoldConverter.sol @@ -9,6 +9,12 @@ import {IBoldToken} from "../Interfaces/IBoldToken.sol"; import "./Owned.sol"; import "./HasWhitelist.sol"; +struct Path { + address underlyingReceiver; + uint256 underlyingDecimals; + uint256 withdrawalFee; +} + contract BoldConverter is Owned, HasWhitelist, ReentrancyGuard { uint256 public constant MAX_FEE = 10000; bytes4 public constant DEPOSIT_SELECTOR = @@ -18,12 +24,6 @@ contract BoldConverter is Owned, HasWhitelist, ReentrancyGuard { IBoldToken public bvUSD; - struct Path { - address underlyingReceiver; - uint256 underlyingDecimals; - uint256 withdrawalFee; - } - mapping(IERC20Metadata => Path) private _underlyingPaths; event NewPath(IERC20Metadata indexed underlying); From 50e0e2e9ceed44786d12e4737ed1c2d5a4ea907a Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 13 Oct 2025 14:20:21 +0200 Subject: [PATCH 5/5] Add deploy whitelist --- contracts/script/DeployConverter.s.sol | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/contracts/script/DeployConverter.s.sol b/contracts/script/DeployConverter.s.sol index f3be8e528..cd2e0f57b 100644 --- a/contracts/script/DeployConverter.s.sol +++ b/contracts/script/DeployConverter.s.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.24; import {Script, console} from "forge-std/Script.sol"; import {BoldConverter, Path, IWhitelist, IBoldToken, IERC20Metadata} from "src/Dependencies/BoldConverter.sol"; +import {Whitelist} from "src/Dependencies/Whitelist.sol"; contract DeployConverter is Script { function run() public returns (BoldConverter converter) { @@ -15,15 +16,15 @@ contract DeployConverter is Script { // Edit this underlyings[0] = IERC20Metadata( - 0x203A662b0BD271A6ed5a60EdFbd04bFce608FD36 + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 ); // usdc paths[0] = Path( address(0x452DC676b4E377a76B4b3048eB3b511A0F1ec057), 6, 0 ); - address bvUSD = 0x876aac7648D79f87245E73316eB2D100e75F3Df1; - address whitelist = 0x83BBAA022Cca1295a975EC101a073C44Ea336f79; + address bvUSD = 0x9BC2F611fa2196E097496B722f1CBCDfE2303855; + address whitelist = 0x788DbB1888a50e97837b9D06Fd70db107b082A12; address owner = 0x452DC676b4E377a76B4b3048eB3b511A0F1ec057; converter = new BoldConverter( @@ -37,3 +38,15 @@ contract DeployConverter is Script { vm.stopBroadcast(); } } + +contract DeployWhitelist is Script { + function run() public returns (Whitelist whitelist) { + vm.startBroadcast(); + console.log("msg.sender:", msg.sender); + address owner = address(0); + + whitelist = new Whitelist(owner); + + vm.stopBroadcast(); + } +}