From 40b03a7fe39fc3be363edcd3629a51801e2d8e5d Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Thu, 11 Dec 2025 15:05:00 -0500 Subject: [PATCH 01/13] Add redemption buffer funded on borrow and used in redemptions --- contracts/Interfaces/IRedemptionBuffer.sol | 18 +++++ contracts/RedemptionBuffer.sol | 78 ++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 contracts/Interfaces/IRedemptionBuffer.sol create mode 100644 contracts/RedemptionBuffer.sol diff --git a/contracts/Interfaces/IRedemptionBuffer.sol b/contracts/Interfaces/IRedemptionBuffer.sol new file mode 100644 index 0000000..6748158 --- /dev/null +++ b/contracts/Interfaces/IRedemptionBuffer.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; + +interface IRedemptionBuffer { + /// @notice Receive RBTC from BorrowerOperations when users open a Line of Credit + function deposit() external payable; + + /// @notice Withdraw RBTC to satisfy a ZUSD redemption + /// @dev Only callable by TroveManager + function withdrawForRedemption(address payable _to, uint256 _amount) external; + + /// @notice Governance-controlled distribution of RBTC to SOV stakers + /// @dev Only callable by the contract owner (timelock / Bitocracy executor) + function distributeToStakers(address payable _stakingContract, uint256 _amount) external; + + /// @return Current RBTC balance tracked by the buffer + function getBalance() external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/RedemptionBuffer.sol b/contracts/RedemptionBuffer.sol new file mode 100644 index 0000000..4861f67 --- /dev/null +++ b/contracts/RedemptionBuffer.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; + +import "./Dependencies/Ownable.sol"; +import "./Dependencies/SafeMath.sol"; +import "./Interfaces/IRedemptionBuffer.sol"; + +/** + * @dev Holds RBTC collected when Lines of Credit are opened. + * TroveManager uses this pool to satisfy ZUSD redemptions before touching troves. + * Governance (timelock / Bitocracy) can send excess RBTC to SOV stakers. + */ +contract RedemptionBuffer is Ownable, IRedemptionBuffer { + using SafeMath for uint256; + + address public borrowerOperations; + address public troveManager; + + uint256 public totalBufferedColl; // RBTC tracked by this contract + + modifier onlyBorrowerOps() { + require(msg.sender == borrowerOperations, "RB: caller is not BorrowerOperations"); + _; + } + + modifier onlyTroveManager() { + require(msg.sender == troveManager, "RB: caller is not TroveManager"); + _; + } + + function setAddresses(address _borrowerOps, address _troveManager) external onlyOwner { + require(_borrowerOps != address(0) && _troveManager != address(0), "RB: zero address"); + borrowerOperations = _borrowerOps; + troveManager = _troveManager; + } + + /// @dev Receives RBTC from BorrowerOperations when a user opens a Line of Credit. + function deposit() external payable override onlyBorrowerOps { + require(msg.value > 0, "RB: no value"); + totalBufferedColl = totalBufferedColl.add(msg.value); + } + + /// @dev Used by TroveManager to serve ZUSD redemptions from the buffer. + function withdrawForRedemption(address payable _to, uint256 _amount) + external + override + onlyTroveManager + { + require(_amount <= totalBufferedColl, "RB: insufficient buffer"); + totalBufferedColl = totalBufferedColl.sub(_amount); + + (bool success, ) = _to.call{ value: _amount }(""); + require(success, "RB: send failed"); + } + + /// @dev Governance-controlled drain of RBTC from the buffer to SOV stakers. + function distributeToStakers(address payable _stakingContract, uint256 _amount) + external + override + onlyOwner + { + require(_stakingContract != address(0), "RB: zero staking address"); + require(_amount <= totalBufferedColl, "RB: insufficient buffer"); + + totalBufferedColl = totalBufferedColl.sub(_amount); + + (bool success, ) = _stakingContract.call{ value: _amount }(""); + require(success, "RB: staking send failed"); + } + + /// @dev Returns the tracked RBTC balance of the buffer. + function getBalance() external view override returns (uint256) { + return totalBufferedColl; + } + + // Accept stray RBTC (e.g. selfdestruct), but do not count it in totalBufferedColl + receive() external payable {} +} \ No newline at end of file From fbd7a74baa04bde774be15fdf2196fee428a39f3 Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Thu, 11 Dec 2025 15:13:01 -0500 Subject: [PATCH 02/13] Add redemption buffer funded on borrow and used in redemptions --- contracts/BorrowerOperations.sol | 45 +++++-- contracts/BorrowerOperationsStorage.sol | 10 ++ .../Dependencies/TroveManagerRedeemOps.sol | 115 +++++++++++++++--- contracts/TroveManager.sol | 7 ++ contracts/TroveManagerStorage.sol | 4 + 5 files changed, 156 insertions(+), 25 deletions(-) diff --git a/contracts/BorrowerOperations.sol b/contracts/BorrowerOperations.sol index 8043271..ff11a70 100644 --- a/contracts/BorrowerOperations.sol +++ b/contracts/BorrowerOperations.sol @@ -16,6 +16,7 @@ import "./Dependencies/console.sol"; import "./BorrowerOperationsStorage.sol"; import "./Dependencies/Mynt/MyntLib.sol"; import "./Interfaces/IPermit2.sol"; +import "./Interfaces/IRedemptionBuffer.sol"; contract BorrowerOperations is LiquityBase, @@ -164,6 +165,14 @@ contract BorrowerOperations is emit MassetManagerAddressChanged(_massetManagerAddress); } + function setRedemptionBuffer(address _buffer, uint256 _rate) external onlyOwner { + require(_buffer != address(0), "BorrowerOps: zero buffer address"); + require(_rate <= DECIMAL_PRECISION, "BorrowerOps: rate > 100%"); + + redemptionBuffer = IRedemptionBuffer(_buffer); + redemptionBufferRate = _rate; + } + function openTrove( uint256 _maxFeePercentage, uint256 _ZUSDAmount, @@ -200,6 +209,15 @@ contract BorrowerOperations is ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, zusdToken); LocalVariables_openTrove memory vars; + // --- NEW: split collateral between ActivePool and RedemptionBuffer --- + + uint256 collSent = msg.value; + + uint256 bufferShare = collSent.mul(redemptionBufferRate).div(DECIMAL_PRECISION); + uint256 activeColl = collSent.sub(bufferShare); + + require(activeColl > 0, "BorrowerOps: active collateral must be > 0"); + vars.price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryMode(vars.price); @@ -224,15 +242,17 @@ contract BorrowerOperations is vars.compositeDebt = _getCompositeDebt(vars.netDebt); assert(vars.compositeDebt > 0); - vars.ICR = LiquityMath._computeCR(msg.value, vars.compositeDebt, vars.price); - vars.NICR = LiquityMath._computeNominalCR(msg.value, vars.compositeDebt); + // --- CHANGED: use activeColl instead of msg.value --- + + vars.ICR = LiquityMath._computeCR(activeColl, vars.compositeDebt, vars.price); + vars.NICR = LiquityMath._computeNominalCR(activeColl, vars.compositeDebt); if (isRecoveryMode) { _requireICRisAboveCCR(vars.ICR); } else { _requireICRisAboveMCR(vars.ICR); uint256 newTCR = _getNewTCRFromTroveChange( - msg.value, + activeColl, true, vars.compositeDebt, true, @@ -241,9 +261,9 @@ contract BorrowerOperations is _requireNewTCRisAboveCCR(newTCR); } - // Set the trove struct's properties + // Set the trove struct's properties (using activeColl) contractsCache.troveManager.setTroveStatus(msg.sender, 1); - contractsCache.troveManager.increaseTroveColl(msg.sender, msg.value); + contractsCache.troveManager.increaseTroveColl(msg.sender, activeColl); contractsCache.troveManager.increaseTroveDebt(msg.sender, vars.compositeDebt); contractsCache.troveManager.updateTroveRewardSnapshots(msg.sender); @@ -253,8 +273,16 @@ contract BorrowerOperations is vars.arrayIndex = contractsCache.troveManager.addTroveOwnerToArray(msg.sender); emit TroveCreated(msg.sender, vars.arrayIndex); - // Move the ether to the Active Pool, and mint the ZUSDAmount to the borrower - _activePoolAddColl(contractsCache.activePool, msg.value); + // Move the active collateral to the Active Pool + _activePoolAddColl(contractsCache.activePool, activeColl); + + // NEW: send bufferShare to RedemptionBuffer + if (bufferShare > 0) { + require(address(redemptionBuffer) != address(0), "BorrowerOps: redemption buffer not set"); + redemptionBuffer.deposit{ value: bufferShare }(); + } + + // Mint the ZUSDAmount to the borrower and gas comp to the Gas Pool _mintZusdAndIncreaseActivePoolDebt( contractsCache.activePool, contractsCache.zusdToken, @@ -262,7 +290,6 @@ contract BorrowerOperations is _ZUSDAmount, vars.netDebt ); - // Move the ZUSD gas compensation to the Gas Pool _mintZusdAndIncreaseActivePoolDebt( contractsCache.activePool, contractsCache.zusdToken, @@ -274,7 +301,7 @@ contract BorrowerOperations is emit TroveUpdated( msg.sender, vars.compositeDebt, - msg.value, + activeColl, vars.stake, BorrowerOperation.openTrove ); diff --git a/contracts/BorrowerOperationsStorage.sol b/contracts/BorrowerOperationsStorage.sol index 9e7d769..5f00a70 100644 --- a/contracts/BorrowerOperationsStorage.sol +++ b/contracts/BorrowerOperationsStorage.sol @@ -12,6 +12,7 @@ import "./Interfaces/IZEROStaking.sol"; import "./Interfaces/IFeeDistributor.sol"; import "./Dependencies/Ownable.sol"; import "./Dependencies/Mynt/IMassetManager.sol"; +import "./Interfaces/IRedemptionBuffer.sol"; contract BorrowerOperationsStorage is Ownable { string public constant NAME = "BorrowerOperations"; @@ -36,4 +37,13 @@ contract BorrowerOperationsStorage is Ownable { IMassetManager public massetManager; IFeeDistributor public feeDistributor; + + // --- Redemption buffer config --- + + // Redemption buffer contract used to hold protocol RBTC + IRedemptionBuffer internal redemptionBuffer; + + // Fraction of incoming RBTC sent to the buffer on openTrove. + // 1e18 == 100%, e.g. 1e17 == 10%. + uint256 internal redemptionBufferRate; } diff --git a/contracts/Dependencies/TroveManagerRedeemOps.sol b/contracts/Dependencies/TroveManagerRedeemOps.sol index 2686be6..66f40de 100644 --- a/contracts/Dependencies/TroveManagerRedeemOps.sol +++ b/contracts/Dependencies/TroveManagerRedeemOps.sol @@ -92,7 +92,15 @@ contract TroveManagerRedeemOps is TroveManagerBase { // Confirm redeemer's balance is less than total ZUSD supply assert(contractsCache.zusdToken.balanceOf(msg.sender) <= totals.totalZUSDSupplyAtStart); - totals.remainingZUSD = _ZUSDamount; + // --- Use RedemptionBuffer first, before touching troves --- + uint256 ethFromBuffer; + (totals.remainingZUSD, ethFromBuffer) = _redeemFromBuffer( + contractsCache, + _ZUSDamount, + totals.price, + msg.sender + ); + address currentBorrower; if ( @@ -139,7 +147,7 @@ contract TroveManagerRedeemOps is TroveManagerBase { _partialRedemptionHintNICR ); - if (singleRedemption.cancelledPartial) break; // Partial redemption was cancelled (out-of-date hint, or new net debt < minimum), therefore we could not redeem from the last Trove + if (singleRedemption.cancelledPartial) break; // Partial redemption was cancelled totals.totalZUSDToRedeem = totals.totalZUSDToRedeem.add(singleRedemption.ZUSDLot); totals.totalETHDrawn = totals.totalETHDrawn.add(singleRedemption.ETHLot); @@ -147,39 +155,55 @@ contract TroveManagerRedeemOps is TroveManagerBase { totals.remainingZUSD = totals.remainingZUSD.sub(singleRedemption.ZUSDLot); currentBorrower = nextUserToCheck; } - require(totals.totalETHDrawn > 0, "TroveManager: Unable to redeem any amount"); - // Decay the baseRate due to time passed, and then increase it according to the size of this redemption. + // total ETH redeemed = from buffer + from troves + uint256 totalETHDrawnInclBuffer = totals.totalETHDrawn.add(ethFromBuffer); + + // Require that *some* redemption actually happened (buffer or troves) + require(totalETHDrawnInclBuffer > 0, "TroveManager: Unable to redeem any amount"); + + // Decay the baseRate and then increase it according to the size of this redemption. // Use the saved total ZUSD supply value, from before it was reduced by the redemption. _updateBaseRateFromRedemption( - totals.totalETHDrawn, + totalETHDrawnInclBuffer, totals.price, totals.totalZUSDSupplyAtStart ); - // Calculate the ETH fee + // Calculate the ETH fee - only on trove-sourced ETH (buffer part can be fee-free) totals.ETHFee = _getRedemptionFee(totals.totalETHDrawn); - _requireUserAcceptsFee(totals.ETHFee, totals.totalETHDrawn, _maxFeePercentage); + // User cares about total ETH they receive vs fee + _requireUserAcceptsFee(totals.ETHFee, totalETHDrawnInclBuffer, _maxFeePercentage); // Send the ETH fee to the feeDistributorContract address - contractsCache.activePool.sendETH(address(feeDistributor), totals.ETHFee); - feeDistributor.distributeFees(); + if (totals.ETHFee > 0) { + contractsCache.activePool.sendETH(address(feeDistributor), totals.ETHFee); + feeDistributor.distributeFees(); + } + // ETH from ActivePool (troves) that goes to redeemer, after fee totals.ETHToSendToRedeemer = totals.totalETHDrawn.sub(totals.ETHFee); + // Total ZUSD cancelled (buffer + troves) = initial requested - remaining + uint256 totalZUSDCancelled = _ZUSDamount.sub(totals.remainingZUSD); + emit Redemption( _ZUSDamount, - totals.totalZUSDToRedeem, - totals.totalETHDrawn, + totalZUSDCancelled, + totalETHDrawnInclBuffer, totals.ETHFee ); - // Burn the total ZUSD that is cancelled with debt, and send the redeemed ETH to msg.sender - contractsCache.zusdToken.burn(msg.sender, totals.totalZUSDToRedeem); - // Update Active Pool ZUSD, and send ETH to account - contractsCache.activePool.decreaseZUSDDebt(totals.totalZUSDToRedeem); - contractsCache.activePool.sendETH(msg.sender, totals.ETHToSendToRedeemer); + // Burn the ZUSD that is cancelled with trove debt, and send the *trove* ETH to msg.sender + if (totals.totalZUSDToRedeem > 0) { + contractsCache.zusdToken.burn(msg.sender, totals.totalZUSDToRedeem); + contractsCache.activePool.decreaseZUSDDebt(totals.totalZUSDToRedeem); + } + // ETH from ActivePool → redeemer (buffer ETH already sent earlier) + if (totals.ETHToSendToRedeemer > 0) { + contractsCache.activePool.sendETH(msg.sender, totals.ETHToSendToRedeemer); + } } ///DLLR _owner can use Sovryn Mynt to convert DLLR to ZUSD, then use the Zero redemption mechanism to redeem ZUSD for RBTC, all in a single transaction @@ -259,6 +283,65 @@ contract TroveManagerRedeemOps is TroveManagerBase { nextTrove == address(0) || _getCurrentICR(nextTrove, _price) < liquityBaseParams.MCR(); } + /** + * @dev Try to satisfy part of a ZUSD redemption using the RedemptionBuffer. + * Burns ZUSD from the redeemer and decreases system ZUSD debt, while + * sending RBTC from the RedemptionBuffer to the redeemer. + * + * @param _contractsCache ActivePool + ZUSD token cache. + * @param _ZUSDAmount Total ZUSD the user requested to redeem. + * @param _price RBTC price (1e18 precision). + * @param _redeemer Address of the redeemer. + * + * @return remainingZUSD ZUSD left to redeem via troves + * @return ethFromBuffer RBTC amount sent from buffer to redeemer + */ + function _redeemFromBuffer( + ContractsCache memory _contractsCache, + uint256 _ZUSDAmount, + uint256 _price, + address _redeemer + ) internal returns (uint256 remainingZUSD, uint256 ethFromBuffer) { + remainingZUSD = _ZUSDAmount; + + // If buffer is not configured or amount is zero, do nothing + if (address(redemptionBuffer) == address(0) || _ZUSDAmount == 0) { + return (remainingZUSD, 0); + } + + uint256 bufferBal = redemptionBuffer.getBalance(); + if (bufferBal == 0) { + return (remainingZUSD, 0); + } + + // RBTC needed if the *entire* ZUSD amount was redeemed from this buffer + uint256 collNeededForFull = _ZUSDAmount.mul(DECIMAL_PRECISION).div(_price); + + uint256 collFromBuffer = collNeededForFull <= bufferBal + ? collNeededForFull + : bufferBal; + + // Corresponding ZUSD to burn, priced at the oracle price + uint256 zusdToBurn = collFromBuffer.mul(_price).div(DECIMAL_PRECISION); + if (zusdToBurn > remainingZUSD) { + zusdToBurn = remainingZUSD; + } + + if (zusdToBurn == 0) { + return (remainingZUSD, 0); + } + + // Burn ZUSD from redeemer and adjust system debt + _contractsCache.activePool.decreaseZUSDDebt(zusdToBurn); + _contractsCache.zusdToken.burn(_redeemer, zusdToBurn); + + // Send RBTC from buffer to redeemer + redemptionBuffer.withdrawForRedemption(payable(_redeemer), collFromBuffer); + + remainingZUSD = remainingZUSD.sub(zusdToBurn); + ethFromBuffer = collFromBuffer; + } + /// Redeem as much collateral as possible from _borrower's Trove in exchange for ZUSD up to _maxZUSDamount function _redeemCollateralFromTrove( ContractsCache memory _contractsCache, diff --git a/contracts/TroveManager.sol b/contracts/TroveManager.sol index cfcc2f0..eb8f6ee 100644 --- a/contracts/TroveManager.sol +++ b/contracts/TroveManager.sol @@ -17,6 +17,7 @@ import "./Dependencies/console.sol"; import "./Dependencies/TroveManagerBase.sol"; import "./TroveManagerStorage.sol"; import "./Interfaces/IPermit2.sol"; +import "./Interfaces/IRedemptionBuffer.sol"; contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { /** CONSTANT / IMMUTABLE VARIABLE ONLY */ @@ -117,6 +118,12 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { emit TroveManagerRedeemOpsAddressChanged(_troveManagerRedeemOps); } + // --- Redemption buffer config --- + function setRedemptionBuffer(address _buffer) external onlyOwner { + require(_buffer != address(0), "TroveManager: zero buffer address"); + redemptionBuffer = IRedemptionBuffer(_buffer); + } + // --- Getters --- function getTroveOwnersCount() external view override returns (uint256) { diff --git a/contracts/TroveManagerStorage.sol b/contracts/TroveManagerStorage.sol index 7fd6458..d540adf 100644 --- a/contracts/TroveManagerStorage.sol +++ b/contracts/TroveManagerStorage.sol @@ -12,6 +12,7 @@ import "./Interfaces/IFeeDistributor.sol"; import "./Dependencies/Ownable.sol"; import "./Dependencies/BaseMath.sol"; import "./Dependencies/console.sol"; +import "./Interfaces/IRedemptionBuffer.sol"; contract TroveManagerStorage is Ownable, BaseMath { string public constant NAME = "TroveManager"; @@ -99,4 +100,7 @@ contract TroveManagerStorage is Ownable, BaseMath { // Error trackers for the trove redistribution calculation uint256 public lastETHError_Redistribution; uint256 public lastZUSDDebtError_Redistribution; + + // Redemption buffer used to serve ZUSD redemptions before touching troves + IRedemptionBuffer internal redemptionBuffer; } From 7b25212a9af04e2c6b408b43a76b854288180b53 Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Thu, 11 Dec 2025 16:46:59 -0500 Subject: [PATCH 03/13] Wire redemption buffer distributions through FeeDistributor --- contracts/Interfaces/IRedemptionBuffer.sol | 2 +- contracts/RedemptionBuffer.sol | 38 ++++++++++++---------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/contracts/Interfaces/IRedemptionBuffer.sol b/contracts/Interfaces/IRedemptionBuffer.sol index 6748158..bf3e22e 100644 --- a/contracts/Interfaces/IRedemptionBuffer.sol +++ b/contracts/Interfaces/IRedemptionBuffer.sol @@ -11,7 +11,7 @@ interface IRedemptionBuffer { /// @notice Governance-controlled distribution of RBTC to SOV stakers /// @dev Only callable by the contract owner (timelock / Bitocracy executor) - function distributeToStakers(address payable _stakingContract, uint256 _amount) external; + function distributeToStakers(uint256 _amount) external; /// @return Current RBTC balance tracked by the buffer function getBalance() external view returns (uint256); diff --git a/contracts/RedemptionBuffer.sol b/contracts/RedemptionBuffer.sol index 4861f67..84dd70c 100644 --- a/contracts/RedemptionBuffer.sol +++ b/contracts/RedemptionBuffer.sol @@ -4,17 +4,14 @@ pragma solidity 0.6.11; import "./Dependencies/Ownable.sol"; import "./Dependencies/SafeMath.sol"; import "./Interfaces/IRedemptionBuffer.sol"; +import "./Interfaces/IFeeDistributor.sol"; -/** - * @dev Holds RBTC collected when Lines of Credit are opened. - * TroveManager uses this pool to satisfy ZUSD redemptions before touching troves. - * Governance (timelock / Bitocracy) can send excess RBTC to SOV stakers. - */ contract RedemptionBuffer is Ownable, IRedemptionBuffer { using SafeMath for uint256; address public borrowerOperations; address public troveManager; + IFeeDistributor public feeDistributor; uint256 public totalBufferedColl; // RBTC tracked by this contract @@ -28,10 +25,18 @@ contract RedemptionBuffer is Ownable, IRedemptionBuffer { _; } - function setAddresses(address _borrowerOps, address _troveManager) external onlyOwner { - require(_borrowerOps != address(0) && _troveManager != address(0), "RB: zero address"); + function setAddresses( + address _borrowerOps, + address _troveManager, + address _feeDistributor + ) external onlyOwner { + require(_borrowerOps != address(0), "RB: zero borrowerOps"); + require(_troveManager != address(0), "RB: zero troveManager"); + require(_feeDistributor != address(0), "RB: zero feeDistributor"); + borrowerOperations = _borrowerOps; troveManager = _troveManager; + feeDistributor = IFeeDistributor(_feeDistributor); } /// @dev Receives RBTC from BorrowerOperations when a user opens a Line of Credit. @@ -53,26 +58,23 @@ contract RedemptionBuffer is Ownable, IRedemptionBuffer { require(success, "RB: send failed"); } - /// @dev Governance-controlled drain of RBTC from the buffer to SOV stakers. - function distributeToStakers(address payable _stakingContract, uint256 _amount) - external - override - onlyOwner - { - require(_stakingContract != address(0), "RB: zero staking address"); + /// @dev Governance-controlled: send RBTC to FeeDistributor so it’s split like other ZERO fees. + function distributeToStakers(uint256 _amount) external override onlyOwner { + require(address(feeDistributor) != address(0), "RB: feeDistributor not set"); require(_amount <= totalBufferedColl, "RB: insufficient buffer"); totalBufferedColl = totalBufferedColl.sub(_amount); - (bool success, ) = _stakingContract.call{ value: _amount }(""); - require(success, "RB: staking send failed"); + // Same pattern as TroveManagerRedeemOps + (bool success, ) = address(feeDistributor).call{ value: _amount }(""); + require(success, "RB: send to feeDistributor failed"); + + feeDistributor.distributeFees(); } - /// @dev Returns the tracked RBTC balance of the buffer. function getBalance() external view override returns (uint256) { return totalBufferedColl; } - // Accept stray RBTC (e.g. selfdestruct), but do not count it in totalBufferedColl receive() external payable {} } \ No newline at end of file From 3e0978b06dec90c96530a3166356c31ae5dde8e5 Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Sun, 21 Dec 2025 14:29:02 -0500 Subject: [PATCH 04/13] Reworked redemption buffer fee to be on top of collateral --- contracts/BorrowerOperations.sol | 154 +++++++++++++++---- contracts/Interfaces/IBorrowerOperations.sol | 47 +++++- 2 files changed, 167 insertions(+), 34 deletions(-) diff --git a/contracts/BorrowerOperations.sol b/contracts/BorrowerOperations.sol index ff11a70..57d40a8 100644 --- a/contracts/BorrowerOperations.sol +++ b/contracts/BorrowerOperations.sol @@ -85,6 +85,8 @@ contract BorrowerOperations is event ZUSDTokenAddressChanged(address _zusdTokenAddress); event ZEROStakingAddressChanged(address _zeroStakingAddress); event MassetManagerAddressChanged(address _massetManagerAddress); + event RedemptionBufferAddressChanged(address _redemptionBufferAddress); + event RedemptionBufferRateChanged(uint256 _redemptionBufferRate); event TroveCreated(address indexed _borrower, uint256 arrayIndex); event TroveUpdated( @@ -165,12 +167,60 @@ contract BorrowerOperations is emit MassetManagerAddressChanged(_massetManagerAddress); } - function setRedemptionBuffer(address _buffer, uint256 _rate) external onlyOwner { + function setRedemptionBuffer(address _buffer) external override onlyOwner { require(_buffer != address(0), "BorrowerOps: zero buffer address"); - require(_rate <= DECIMAL_PRECISION, "BorrowerOps: rate > 100%"); - redemptionBuffer = IRedemptionBuffer(_buffer); + emit RedemptionBufferAddressChanged(_buffer); + } + + function setRedemptionBufferRate(uint256 _rate) external override onlyOwner { + require(_rate <= DECIMAL_PRECISION, "BorrowerOps: buffer rate too high"); + if (_rate > 0) { + require(address(redemptionBuffer) != address(0), "BorrowerOps: buffer not set"); + } redemptionBufferRate = _rate; + emit RedemptionBufferRateChanged(_rate); + } + + /// @notice Returns the configured RedemptionBuffer contract. + function getRedemptionBuffer() external view override returns (address) { + return address(redemptionBuffer); + } + + /// @notice Returns the configured redemption buffer rate (1e18 precision). + function getRedemptionBufferRate() external view override returns (uint256) { + return redemptionBufferRate; + } + + /// @notice Returns the extra RBTC (in wei) that must be sent on top of collateral + /// when opening a trove borrowing `_ZUSDAmount`. + /// @dev Uses priceFeed.fetchPrice() to match the exact pricing logic used by openTrove. + /// This function is NOT marked view because fetchPrice() is typically non-view in Liquity-style feeds. + /// UIs should call it using eth_call / staticcall. + function getRedemptionBufferFeeRBTC(uint256 _ZUSDAmount) external override returns (uint256) { + if (redemptionBufferRate == 0) { + return 0; + } + + // Optional: keep this require if you want misconfiguration to be loud. + require(address(redemptionBuffer) != address(0), "BorrowerOps: buffer not set"); + + uint256 price = priceFeed.fetchPrice(); + return _calcRedemptionBufferFeeRBTC(_ZUSDAmount, price); + } + + /// @notice View-only fee quote when caller supplies a price. + /// @dev Lets UIs avoid calling fetchPrice() from this contract. + function getRedemptionBufferFeeRBTCWithPrice(uint256 _ZUSDAmount, uint256 _price) + external + view + returns (uint256) + { + if (redemptionBufferRate == 0) { + return 0; + } + require(_price > 0, "BorrowerOps: invalid price"); + return _calcRedemptionBufferFeeRBTC(_ZUSDAmount, _price); } function openTrove( @@ -199,6 +249,29 @@ contract BorrowerOperations is } // --- Borrower Trove Operations --- + function _ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + if (a == 0) return 0; + return ((a - 1) / b) + 1; + } + + function _calcRedemptionBufferFeeRBTC(uint256 _zusdAmount, uint256 _price) + internal + view + returns (uint256) + { + if (redemptionBufferRate == 0) return 0; + + // Defensive: if you ever set a rate, buffer must be configured + require(address(redemptionBuffer) != address(0), "BorrowerOps: buffer not set"); + require(_price > 0, "BorrowerOps: invalid price"); + + // feeZUSD = ZUSD * rate / 1e18 + uint256 feeZUSD = _zusdAmount.mul(redemptionBufferRate).div(DECIMAL_PRECISION); + + // feeRBTC = ceil(feeZUSD * 1e18 / price) + return _ceilDiv(feeZUSD.mul(DECIMAL_PRECISION), _price); + } + function _openTrove( uint256 _maxFeePercentage, uint256 _ZUSDAmount, @@ -209,15 +282,6 @@ contract BorrowerOperations is ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, zusdToken); LocalVariables_openTrove memory vars; - // --- NEW: split collateral between ActivePool and RedemptionBuffer --- - - uint256 collSent = msg.value; - - uint256 bufferShare = collSent.mul(redemptionBufferRate).div(DECIMAL_PRECISION); - uint256 activeColl = collSent.sub(bufferShare); - - require(activeColl > 0, "BorrowerOps: active collateral must be > 0"); - vars.price = priceFeed.fetchPrice(); bool isRecoveryMode = _checkRecoveryMode(vars.price); @@ -242,17 +306,22 @@ contract BorrowerOperations is vars.compositeDebt = _getCompositeDebt(vars.netDebt); assert(vars.compositeDebt > 0); - // --- CHANGED: use activeColl instead of msg.value --- + // --- NEW: calculate buffer fee (RBTC) and keep trove collateral "as intended" --- + uint256 bufferFee = _calcRedemptionBufferFeeRBTC(_ZUSDAmount, vars.price); + require(msg.value > bufferFee, "BorrowerOps: insufficient RBTC for collateral+fee"); - vars.ICR = LiquityMath._computeCR(activeColl, vars.compositeDebt, vars.price); - vars.NICR = LiquityMath._computeNominalCR(activeColl, vars.compositeDebt); + uint256 coll = msg.value.sub(bufferFee); + require(coll > 0, "BorrowerOps: collateral must be > 0"); + + vars.ICR = LiquityMath._computeCR(coll, vars.compositeDebt, vars.price); + vars.NICR = LiquityMath._computeNominalCR(coll, vars.compositeDebt); if (isRecoveryMode) { _requireICRisAboveCCR(vars.ICR); } else { _requireICRisAboveMCR(vars.ICR); uint256 newTCR = _getNewTCRFromTroveChange( - activeColl, + coll, true, vars.compositeDebt, true, @@ -261,9 +330,9 @@ contract BorrowerOperations is _requireNewTCRisAboveCCR(newTCR); } - // Set the trove struct's properties (using activeColl) + // Set the trove struct's properties contractsCache.troveManager.setTroveStatus(msg.sender, 1); - contractsCache.troveManager.increaseTroveColl(msg.sender, activeColl); + contractsCache.troveManager.increaseTroveColl(msg.sender, coll); contractsCache.troveManager.increaseTroveDebt(msg.sender, vars.compositeDebt); contractsCache.troveManager.updateTroveRewardSnapshots(msg.sender); @@ -273,16 +342,15 @@ contract BorrowerOperations is vars.arrayIndex = contractsCache.troveManager.addTroveOwnerToArray(msg.sender); emit TroveCreated(msg.sender, vars.arrayIndex); - // Move the active collateral to the Active Pool - _activePoolAddColl(contractsCache.activePool, activeColl); + // Move collateral to Active Pool + _activePoolAddColl(contractsCache.activePool, coll); - // NEW: send bufferShare to RedemptionBuffer - if (bufferShare > 0) { - require(address(redemptionBuffer) != address(0), "BorrowerOps: redemption buffer not set"); - redemptionBuffer.deposit{ value: bufferShare }(); + // NEW: send the fee to the RedemptionBuffer + if (bufferFee > 0) { + redemptionBuffer.deposit{ value: bufferFee }(); } - // Mint the ZUSDAmount to the borrower and gas comp to the Gas Pool + // Mint the ZUSDAmount to the borrower _mintZusdAndIncreaseActivePoolDebt( contractsCache.activePool, contractsCache.zusdToken, @@ -290,6 +358,8 @@ contract BorrowerOperations is _ZUSDAmount, vars.netDebt ); + + // Move the ZUSD gas compensation to the Gas Pool _mintZusdAndIncreaseActivePoolDebt( contractsCache.activePool, contractsCache.zusdToken, @@ -301,7 +371,7 @@ contract BorrowerOperations is emit TroveUpdated( msg.sender, vars.compositeDebt, - activeColl, + coll, vars.stake, BorrowerOperation.openTrove ); @@ -338,7 +408,7 @@ contract BorrowerOperations is uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external override { + ) external payable override { _adjustTrove(msg.sender, 0, _ZUSDAmount, true, _upperHint, _lowerHint, _maxFeePercentage); } @@ -350,7 +420,7 @@ contract BorrowerOperations is uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external override returns (uint256) { + ) external payable override returns (uint256) { address thisAddress = address(this); uint256 balanceBefore = zusdToken.balanceOf(thisAddress); @@ -612,15 +682,29 @@ contract BorrowerOperations is vars.price = priceFeed.fetchPrice(); vars.isRecoveryMode = _checkRecoveryMode(vars.price); + // --- NEW: redemption buffer fee on ZUSD minting (debt increase) --- + uint256 collTopUp = msg.value; // actual collateral top-up (excludes fee) + uint256 bufferFee = 0; + if (_isDebtIncrease) { _requireValidMaxFeePercentage(_maxFeePercentage, vars.isRecoveryMode); _requireNonZeroDebtChange(_ZUSDChange); + + // fee is based on the *newly minted* ZUSD amount (same idea as openTrove) + bufferFee = _calcRedemptionBufferFeeRBTC(_ZUSDChange, vars.price); + + require(msg.value >= bufferFee, "BorrowerOps: insufficient RBTC for buffer fee"); + collTopUp = msg.value.sub(bufferFee); } - _requireSingularCollChange(_collWithdrawal); + + // If both are positive (excluding fee), revert + require(!(collTopUp > 0 && _collWithdrawal > 0), "BorrowerOps: cannot add and withdraw coll"); + _requireNonZeroAdjustment(_collWithdrawal, _ZUSDChange); _requireTroveisActive(contractsCache.troveManager, _borrower); - // Confirm the operation is either a borrower adjusting their own trove, or a pure ETH transfer from the Stability Pool to a trove + // Confirm the operation is either a borrower adjusting their own trove, + // or a pure ETH transfer from the Stability Pool to a trove assert( msg.sender == _borrower || (msg.sender == stabilityPoolAddress && msg.value > 0 && _ZUSDChange == 0) @@ -628,8 +712,9 @@ contract BorrowerOperations is contractsCache.troveManager.applyPendingRewards(_borrower); - // Get the collChange based on whether or not ETH was sent in the transaction - (vars.collChange, vars.isCollIncrease) = _getCollChange(msg.value, _collWithdrawal); + // Get the collChange based on whether or not ETH was sent in the transaction. + // IMPORTANT: use collTopUp (msg.value minus fee), not msg.value. + (vars.collChange, vars.isCollIncrease) = _getCollChange(collTopUp, _collWithdrawal); vars.netDebtChange = _ZUSDChange; @@ -717,6 +802,11 @@ contract BorrowerOperations is vars.netDebtChange, _tokensRecipient ); + + // --- NEW: send the buffer fee (RBTC) to RedemptionBuffer --- + if (_isDebtIncrease && bufferFee > 0) { + redemptionBuffer.deposit{ value: bufferFee }(); + } } function closeTrove() external override { diff --git a/contracts/Interfaces/IBorrowerOperations.sol b/contracts/Interfaces/IBorrowerOperations.sol index d6ed054..de024b9 100644 --- a/contracts/Interfaces/IBorrowerOperations.sol +++ b/contracts/Interfaces/IBorrowerOperations.sol @@ -4,6 +4,7 @@ pragma solidity 0.6.11; pragma experimental ABIEncoderV2; import "../Dependencies/Mynt/IMassetManager.sol"; +import "./IRedemptionBuffer.sol"; import { IPermit2, ISignatureTransfer } from "./IPermit2.sol"; /// Common interface for the Trove Manager. @@ -21,6 +22,10 @@ interface IBorrowerOperations { event SortedTrovesAddressChanged(address _sortedTrovesAddress); event ZUSDTokenAddressChanged(address _zusdTokenAddress); event ZEROStakingAddressChanged(address _zeroStakingAddress); + /// @notice Emitted when the RedemptionBuffer contract address is updated + event RedemptionBufferAddressChanged(address _redemptionBufferAddress); + /// @notice Emitted when the redemption buffer rate is updated + event RedemptionBufferRateChanged(uint256 _redemptionBufferRate); event TroveCreated(address indexed _borrower, uint256 arrayIndex); event TroveUpdated( @@ -65,6 +70,42 @@ interface IBorrowerOperations { address _zeroStakingAddress ) external; + /** + * @notice Sets the RedemptionBuffer contract address. + * @dev Callable only by owner (governance). The buffer receives RBTC fees when opening troves and is used + * to serve redemptions before touching troves. + * @param _buffer RedemptionBuffer contract address + */ + function setRedemptionBuffer(address _buffer) external; + + /** + * @notice Sets the redemption buffer rate used to calculate the RBTC fee-on-top when opening a trove. + * @dev Callable only by owner (governance). Rate uses 1e18 precision where 1e18 == 100%. + * @param _rate Redemption buffer rate (1e18 precision) + */ + function setRedemptionBufferRate(uint256 _rate) external; + + /** + * @notice Returns the configured RedemptionBuffer contract address. + * @return RedemptionBuffer contract address + */ + function getRedemptionBuffer() external view returns (address); + + /** + * @notice Returns the configured redemption buffer rate (1e18 precision). + * @return Redemption buffer rate (1e18 precision) + */ + function getRedemptionBufferRate() external view returns (uint256); + + /** + * @notice Quotes the extra RBTC (in wei) required on top of collateral when opening a trove borrowing `_ZUSDAmount`. + * @dev This function is intentionally NOT `view` because it calls `priceFeed.fetchPrice()` in the same way as openTrove. + * Frontends should call this using eth_call / staticcall (no transaction needed). + * @param _ZUSDAmount ZUSD amount the borrower wants to receive when opening the trove + * @return Extra RBTC amount (wei) that must be added on top of collateral as the redemption buffer fee + */ + function getRedemptionBufferFeeRBTC(uint256 _ZUSDAmount) external returns (uint256); + /** * @notice payable function that creates a Trove for the caller with the requested debt, and the Ether received as collateral. * Successful execution is conditional mainly on the resulting collateralization ratio which must exceed the minimum (110% in Normal Mode, 150% in Recovery Mode). @@ -129,6 +170,7 @@ interface IBorrowerOperations { * @notice issues `_amount` of ZUSD from the caller’s Trove to the caller. * Executes only if the Trove's collateralization ratio would remain above the minimum, and the resulting total collateralization ratio is above 150%. * The borrower has to provide a `_maxFeePercentage` that he/she is willing to accept in case of a fee slippage, i.e. when a redemption transaction is processed first, driving up the issuance fee. + * When increasing debt and redemptionBufferRate > 0, caller must send msg.value at least equal to the redemption buffer fee; quote via getRedemptionBufferFeeRBTC(). * @param _maxFee max fee percentage to acept in case of a fee slippage * @param _amount ZUSD amount to withdraw * @param _upperHint upper trove id hint @@ -139,15 +181,16 @@ interface IBorrowerOperations { uint256 _amount, address _upperHint, address _lowerHint - ) external; + ) external payable; /// Borrow (withdraw) ZUSD tokens from a trove: mint new ZUSD tokens to the owner and convert it to DLLR in one transaction + /// When increasing debt and redemptionBufferRate > 0, caller must send msg.value at least equal to the redemption buffer fee; quote via getRedemptionBufferFeeRBTC(). function withdrawZusdAndConvertToDLLR( uint256 _maxFeePercentage, uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external returns (uint256); + ) external payable returns (uint256); /// @notice repay `_amount` of ZUSD to the caller’s Trove, subject to leaving 50 debt in the Trove (which corresponds to the 50 ZUSD gas compensation). /// @param _amount ZUSD amount to repay From 577e5d087998f92ac2114ad087cdad5974c18397 Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Mon, 22 Dec 2025 01:17:15 -0500 Subject: [PATCH 05/13] Reworked redemption buffer fee to be on top of collateral --- contracts/BorrowerOperations.sol | 9 +- .../Dependencies/TroveManagerRedeemOps.sol | 406 ++++++++---------- contracts/FeeDistributor.sol | 55 ++- contracts/FeeDistributorStorage.sol | 3 + contracts/Interfaces/IBorrowerOperations.sol | 69 +-- contracts/Interfaces/IFeeDistributor.sol | 51 ++- contracts/Interfaces/IRedemptionBuffer.sol | 29 +- contracts/Interfaces/ITroveManager.sol | 11 +- contracts/RedemptionBuffer.sol | 17 +- contracts/TroveManager.sol | 3 +- 10 files changed, 354 insertions(+), 299 deletions(-) diff --git a/contracts/BorrowerOperations.sol b/contracts/BorrowerOperations.sol index 57d40a8..fc570c1 100644 --- a/contracts/BorrowerOperations.sol +++ b/contracts/BorrowerOperations.sol @@ -162,13 +162,14 @@ contract BorrowerOperations is emit ZEROStakingAddressChanged(_zeroStakingAddress); } - function setMassetManagerAddress(address _massetManagerAddress) external onlyOwner { + function setMassetManagerAddress(address _massetManagerAddress) external override onlyOwner { massetManager = IMassetManager(_massetManagerAddress); emit MassetManagerAddressChanged(_massetManagerAddress); } function setRedemptionBuffer(address _buffer) external override onlyOwner { require(_buffer != address(0), "BorrowerOps: zero buffer address"); + checkContract(_buffer); redemptionBuffer = IRedemptionBuffer(_buffer); emit RedemptionBufferAddressChanged(_buffer); } @@ -214,6 +215,7 @@ contract BorrowerOperations is function getRedemptionBufferFeeRBTCWithPrice(uint256 _ZUSDAmount, uint256 _price) external view + override returns (uint256) { if (redemptionBufferRate == 0) { @@ -976,6 +978,11 @@ contract BorrowerOperations is _burnZusdAndDecreaseActivePoolDebt(_activePool, _zusdToken, _borrower, _ZUSDChange); } + // Prevent 0-value external calls + if (_collChange == 0) { + return; + } + if (_isCollIncrease) { _activePoolAddColl(_activePool, _collChange); } else { diff --git a/contracts/Dependencies/TroveManagerRedeemOps.sol b/contracts/Dependencies/TroveManagerRedeemOps.sol index 66f40de..dff58fd 100644 --- a/contracts/Dependencies/TroveManagerRedeemOps.sol +++ b/contracts/Dependencies/TroveManagerRedeemOps.sol @@ -9,34 +9,19 @@ import "./TroveManagerBase.sol"; import "../Interfaces/IPermit2.sol"; /// This contract is designed to be used via delegatecall from the TroveManager contract -/// TroveManagerBase constructor param is bootsrap period when redemptions are not allowed contract TroveManagerRedeemOps is TroveManagerBase { - /** CONSTANT / IMMUTABLE VARIABLE ONLY */ IPermit2 public immutable permit2; - /** Send _ZUSDamount ZUSD to the system and redeem the corresponding amount of collateral from as many Troves as are needed to fill the redemption - request. Applies pending rewards 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, therefore they will be closed. - If the last Trove does have some remaining debt, it has a finite ICR, and the reinsertion could be anywhere in the list, therefore it requires a hint. - A frontend should use getRedemptionHints() to calculate what the ICR of this Trove will be after redemption, and pass a hint for its position - in the sortedTroves list along with the ICR value that the hint was found for. - - If another transaction modifies the list between calling getRedemptionHints() and passing the hints to redeemCollateral(), it - is very likely that the last (partially) redeemed Trove would end up with a different ICR than what the hint is for. In this case the - redemption will stop after the last completely redeemed Trove and the sender will keep the remaining ZUSD amount, which they can attempt - to redeem later. - */ + // Pack redemption hints/limits into one struct to reduce stack usage (Solidity 0.6.x stack-too-deep) + struct RedeemParams { + address firstRedemptionHint; + address upperPartialRedemptionHint; + address lowerPartialRedemptionHint; + uint256 partialRedemptionHintNICR; + uint256 maxIterations; + uint256 maxFeePercentage; + } - /** Constructor */ constructor(uint256 _bootstrapPeriod, address _permit2) public TroveManagerBase(_bootstrapPeriod) { permit2 = IPermit2(_permit2); } @@ -50,26 +35,19 @@ contract TroveManagerRedeemOps is TroveManagerBase { uint256 _maxIterations, uint256 _maxFeePercentage ) external { - _redeemCollateral( - _ZUSDamount, - _firstRedemptionHint, - _upperPartialRedemptionHint, - _lowerPartialRedemptionHint, - _partialRedemptionHintNICR, - _maxIterations, - _maxFeePercentage - ); + RedeemParams memory params = RedeemParams({ + firstRedemptionHint: _firstRedemptionHint, + upperPartialRedemptionHint: _upperPartialRedemptionHint, + lowerPartialRedemptionHint: _lowerPartialRedemptionHint, + partialRedemptionHintNICR: _partialRedemptionHintNICR, + maxIterations: _maxIterations, + maxFeePercentage: _maxFeePercentage + }); + + _redeemCollateral(_ZUSDamount, params); } - function _redeemCollateral( - uint256 _ZUSDamount, - address _firstRedemptionHint, - address _upperPartialRedemptionHint, - address _lowerPartialRedemptionHint, - uint256 _partialRedemptionHintNICR, - uint256 _maxIterations, - uint256 _maxFeePercentage - ) internal { + function _redeemCollateral(uint256 _ZUSDamount, RedeemParams memory _params) internal { ContractsCache memory contractsCache = ContractsCache( activePool, defaultPool, @@ -81,132 +59,154 @@ contract TroveManagerRedeemOps is TroveManagerBase { ); RedemptionTotals memory totals; - _requireValidMaxFeePercentage(_maxFeePercentage); + _requireValidMaxFeePercentage(_params.maxFeePercentage); _requireAfterBootstrapPeriod(); + totals.price = priceFeed.fetchPrice(); _requireTCRoverMCR(totals.price); _requireAmountGreaterThanZero(_ZUSDamount); _requireZUSDBalanceCoversRedemption(contractsCache.zusdToken, msg.sender, _ZUSDamount); totals.totalZUSDSupplyAtStart = getEntireSystemDebt(); - // Confirm redeemer's balance is less than total ZUSD supply assert(contractsCache.zusdToken.balanceOf(msg.sender) <= totals.totalZUSDSupplyAtStart); - // --- Use RedemptionBuffer first, before touching troves --- + // ------------------------------------------------------------ + // 1) Swap against RedemptionBuffer FIRST (ZUSD -> RBTC) + // - transfers ZUSD to FeeDistributor via transferFrom() + // - does NOT burn ZUSD + // - does NOT touch ActivePool debt + // ------------------------------------------------------------ uint256 ethFromBuffer; - (totals.remainingZUSD, ethFromBuffer) = _redeemFromBuffer( + uint256 zusdSwappedToFeeDistributor; + (totals.remainingZUSD, ethFromBuffer, zusdSwappedToFeeDistributor) = _swapFromBuffer( contractsCache, _ZUSDamount, totals.price, msg.sender ); - address currentBorrower; + // ------------------------------------------------------------ + // 2) Redeem remaining from troves (normal redemption path) + // Moved into helper to reduce stack usage. + // ------------------------------------------------------------ + if (totals.remainingZUSD > 0) { + _redeemFromTroves(contractsCache, totals, _params); + } - if ( - _isValidFirstRedemptionHint( - contractsCache.sortedTroves, - _firstRedemptionHint, - totals.price - ) - ) { - currentBorrower = _firstRedemptionHint; - } else { - currentBorrower = contractsCache.sortedTroves.getLast(); - // Find the first trove with ICR >= MCR - while ( - currentBorrower != address(0) && - _getCurrentICR(currentBorrower, totals.price) < liquityBaseParams.MCR() - ) { - currentBorrower = contractsCache.sortedTroves.getPrev(currentBorrower); - } + uint256 totalETHDrawnInclBuffer = totals.totalETHDrawn.add(ethFromBuffer); + require(totalETHDrawnInclBuffer > 0, "TroveManager: Unable to redeem any amount"); + + // ------------------------------------------------------------ + // 3) BaseRate update applies to buffer swaps too + // ------------------------------------------------------------ + _updateBaseRateFromRedemption(totalETHDrawnInclBuffer, totals.price, totals.totalZUSDSupplyAtStart); + + // ------------------------------------------------------------ + // 4) Redemption fee applies to BOTH sources (same formula) + // ------------------------------------------------------------ + uint256 ethFeeFromTroves = _getRedemptionFee(totals.totalETHDrawn); + uint256 ethFeeFromBuffer = _getRedemptionFee(ethFromBuffer); + totals.ETHFee = ethFeeFromTroves.add(ethFeeFromBuffer); + + _requireUserAcceptsFee(totals.ETHFee, totalETHDrawnInclBuffer, _params.maxFeePercentage); + + uint256 totalZUSDProcessed = _ZUSDamount.sub(totals.remainingZUSD); + + emit Redemption(_ZUSDamount, totalZUSDProcessed, totalETHDrawnInclBuffer, totals.ETHFee); + + // ------------------------------------------------------------ + // 5) Burn ONLY trove portion (buffer portion is a swap) + // ------------------------------------------------------------ + if (totals.totalZUSDToRedeem > 0) { + contractsCache.zusdToken.burn(msg.sender, totals.totalZUSDToRedeem); + contractsCache.activePool.decreaseZUSDDebt(totals.totalZUSDToRedeem); } - // Loop through the Troves starting from the one with lowest collateral ratio until _amount of ZUSD is exchanged for collateral - if (_maxIterations == 0) { - _maxIterations = uint256(-1); + // ------------------------------------------------------------ + // 6) Pay RBTC fees into FeeDistributor + // - trove fee from ActivePool + // - buffer fee directly from RedemptionBuffer + // ------------------------------------------------------------ + if (ethFeeFromTroves > 0) { + contractsCache.activePool.sendETH(address(feeDistributor), ethFeeFromTroves); } - while (currentBorrower != address(0) && totals.remainingZUSD > 0 && _maxIterations > 0) { - _maxIterations--; - // Save the address of the Trove preceding the current one, before potentially modifying the list - address nextUserToCheck = contractsCache.sortedTroves.getPrev(currentBorrower); - - _applyPendingRewards( - contractsCache.activePool, - contractsCache.defaultPool, - currentBorrower - ); - SingleRedemptionValues memory singleRedemption = _redeemCollateralFromTrove( - contractsCache, - currentBorrower, - totals.remainingZUSD, - totals.price, - _upperPartialRedemptionHint, - _lowerPartialRedemptionHint, - _partialRedemptionHintNICR - ); + if (ethFeeFromBuffer > 0) { + // IMPORTANT: FeeDistributor.receive() must accept RBTC from RedemptionBuffer + redemptionBuffer.withdrawForRedemption(payable(address(feeDistributor)), ethFeeFromBuffer); + } - if (singleRedemption.cancelledPartial) break; // Partial redemption was cancelled + // Distribute any ZUSD swapped into FeeDistributor + RBTC fees + if (zusdSwappedToFeeDistributor > 0 || totals.ETHFee > 0) { + feeDistributor.distributeFees(); + } - totals.totalZUSDToRedeem = totals.totalZUSDToRedeem.add(singleRedemption.ZUSDLot); - totals.totalETHDrawn = totals.totalETHDrawn.add(singleRedemption.ETHLot); + // ------------------------------------------------------------ + // 7) Send net RBTC to redeemer from each source + // ------------------------------------------------------------ + uint256 ethToSendFromTroves = totals.totalETHDrawn.sub(ethFeeFromTroves); + if (ethToSendFromTroves > 0) { + contractsCache.activePool.sendETH(msg.sender, ethToSendFromTroves); + } - totals.remainingZUSD = totals.remainingZUSD.sub(singleRedemption.ZUSDLot); - currentBorrower = nextUserToCheck; + uint256 ethToSendFromBuffer = ethFromBuffer.sub(ethFeeFromBuffer); + if (ethToSendFromBuffer > 0) { + redemptionBuffer.withdrawForRedemption(payable(msg.sender), ethToSendFromBuffer); } + } - // total ETH redeemed = from buffer + from troves - uint256 totalETHDrawnInclBuffer = totals.totalETHDrawn.add(ethFromBuffer); + // Helper extracted from _redeemCollateral to reduce stack depth in Solidity 0.6.x + function _redeemFromTroves( + ContractsCache memory _contractsCache, + RedemptionTotals memory _totals, + RedeemParams memory _params + ) internal { + address currentBorrower = address(0); - // Require that *some* redemption actually happened (buffer or troves) - require(totalETHDrawnInclBuffer > 0, "TroveManager: Unable to redeem any amount"); + if (_isValidFirstRedemptionHint(_contractsCache.sortedTroves, _params.firstRedemptionHint, _totals.price)) { + currentBorrower = _params.firstRedemptionHint; + } else { + currentBorrower = _contractsCache.sortedTroves.getLast(); + while ( + currentBorrower != address(0) && + _getCurrentICR(currentBorrower, _totals.price) < liquityBaseParams.MCR() + ) { + currentBorrower = _contractsCache.sortedTroves.getPrev(currentBorrower); + } + } - // Decay the baseRate and then increase it according to the size of this redemption. - // Use the saved total ZUSD supply value, from before it was reduced by the redemption. - _updateBaseRateFromRedemption( - totalETHDrawnInclBuffer, - totals.price, - totals.totalZUSDSupplyAtStart - ); + uint256 maxIterations = _params.maxIterations; + if (maxIterations == 0) { + maxIterations = uint256(-1); + } - // Calculate the ETH fee - only on trove-sourced ETH (buffer part can be fee-free) - totals.ETHFee = _getRedemptionFee(totals.totalETHDrawn); + while (currentBorrower != address(0) && _totals.remainingZUSD > 0 && maxIterations > 0) { + maxIterations--; - // User cares about total ETH they receive vs fee - _requireUserAcceptsFee(totals.ETHFee, totalETHDrawnInclBuffer, _maxFeePercentage); + address nextUserToCheck = _contractsCache.sortedTroves.getPrev(currentBorrower); - // Send the ETH fee to the feeDistributorContract address - if (totals.ETHFee > 0) { - contractsCache.activePool.sendETH(address(feeDistributor), totals.ETHFee); - feeDistributor.distributeFees(); - } + _applyPendingRewards(_contractsCache.activePool, _contractsCache.defaultPool, currentBorrower); - // ETH from ActivePool (troves) that goes to redeemer, after fee - totals.ETHToSendToRedeemer = totals.totalETHDrawn.sub(totals.ETHFee); + SingleRedemptionValues memory singleRedemption = _redeemCollateralFromTrove( + _contractsCache, + currentBorrower, + _totals.remainingZUSD, + _totals.price, + _params + ); - // Total ZUSD cancelled (buffer + troves) = initial requested - remaining - uint256 totalZUSDCancelled = _ZUSDamount.sub(totals.remainingZUSD); + if (singleRedemption.cancelledPartial) break; - emit Redemption( - _ZUSDamount, - totalZUSDCancelled, - totalETHDrawnInclBuffer, - totals.ETHFee - ); + _totals.totalZUSDToRedeem = _totals.totalZUSDToRedeem.add(singleRedemption.ZUSDLot); + _totals.totalETHDrawn = _totals.totalETHDrawn.add(singleRedemption.ETHLot); - // Burn the ZUSD that is cancelled with trove debt, and send the *trove* ETH to msg.sender - if (totals.totalZUSDToRedeem > 0) { - contractsCache.zusdToken.burn(msg.sender, totals.totalZUSDToRedeem); - contractsCache.activePool.decreaseZUSDDebt(totals.totalZUSDToRedeem); - } - // ETH from ActivePool → redeemer (buffer ETH already sent earlier) - if (totals.ETHToSendToRedeemer > 0) { - contractsCache.activePool.sendETH(msg.sender, totals.ETHToSendToRedeemer); + _totals.remainingZUSD = _totals.remainingZUSD.sub(singleRedemption.ZUSDLot); + currentBorrower = nextUserToCheck; } } - ///DLLR _owner can use Sovryn Mynt to convert DLLR to ZUSD, then use the Zero redemption mechanism to redeem ZUSD for RBTC, all in a single transaction + // ----- DLLR helpers unchanged ----- + function redeemCollateralViaDLLR( uint256 _dllrAmount, address _firstRedemptionHint, @@ -223,18 +223,19 @@ contract TroveManagerRedeemOps is TroveManagerBase { address(_zusdToken), _permitParams ); - _redeemCollateral( - _zusdAmount, - _firstRedemptionHint, - _upperPartialRedemptionHint, - _lowerPartialRedemptionHint, - _partialRedemptionHintNICR, - _maxIterations, - _maxFeePercentage - ); + + RedeemParams memory params = RedeemParams({ + firstRedemptionHint: _firstRedemptionHint, + upperPartialRedemptionHint: _upperPartialRedemptionHint, + lowerPartialRedemptionHint: _lowerPartialRedemptionHint, + partialRedemptionHintNICR: _partialRedemptionHintNICR, + maxIterations: _maxIterations, + maxFeePercentage: _maxFeePercentage + }); + + _redeemCollateral(_zusdAmount, params); } - ///DLLR _owner can use Sovryn Mynt to convert DLLR to ZUSD, then use the Zero redemption mechanism to redeem ZUSD for RBTC, all in a single transaction function redeemCollateralViaDllrWithPermit2( uint256 _dllrAmount, address _firstRedemptionHint, @@ -254,15 +255,16 @@ contract TroveManagerRedeemOps is TroveManagerBase { _signature ); - _redeemCollateral( - _zusdAmount, - _firstRedemptionHint, - _upperPartialRedemptionHint, - _lowerPartialRedemptionHint, - _partialRedemptionHintNICR, - _maxIterations, - _maxFeePercentage - ); + RedeemParams memory params = RedeemParams({ + firstRedemptionHint: _firstRedemptionHint, + upperPartialRedemptionHint: _upperPartialRedemptionHint, + lowerPartialRedemptionHint: _lowerPartialRedemptionHint, + partialRedemptionHintNICR: _partialRedemptionHintNICR, + maxIterations: _maxIterations, + maxFeePercentage: _maxFeePercentage + }); + + _redeemCollateral(_zusdAmount, params); } function _isValidFirstRedemptionHint( @@ -279,94 +281,73 @@ contract TroveManagerRedeemOps is TroveManagerBase { } address nextTrove = _sortedTroves.getNext(_firstRedemptionHint); - return - nextTrove == address(0) || _getCurrentICR(nextTrove, _price) < liquityBaseParams.MCR(); + return nextTrove == address(0) || _getCurrentICR(nextTrove, _price) < liquityBaseParams.MCR(); } /** - * @dev Try to satisfy part of a ZUSD redemption using the RedemptionBuffer. - * Burns ZUSD from the redeemer and decreases system ZUSD debt, while - * sending RBTC from the RedemptionBuffer to the redeemer. - * - * @param _contractsCache ActivePool + ZUSD token cache. - * @param _ZUSDAmount Total ZUSD the user requested to redeem. - * @param _price RBTC price (1e18 precision). - * @param _redeemer Address of the redeemer. - * - * @return remainingZUSD ZUSD left to redeem via troves - * @return ethFromBuffer RBTC amount sent from buffer to redeemer + * Swap ZUSD against RedemptionBuffer at oracle price. + * Uses transferFrom(redeemer -> FeeDistributor) (so redeemer must approve TroveManager). */ - function _redeemFromBuffer( + function _swapFromBuffer( ContractsCache memory _contractsCache, uint256 _ZUSDAmount, uint256 _price, address _redeemer - ) internal returns (uint256 remainingZUSD, uint256 ethFromBuffer) { + ) internal returns (uint256 remainingZUSD, uint256 ethFromBuffer, uint256 zusdSwappedToFeeDistributor) { remainingZUSD = _ZUSDAmount; - // If buffer is not configured or amount is zero, do nothing if (address(redemptionBuffer) == address(0) || _ZUSDAmount == 0) { - return (remainingZUSD, 0); + return (remainingZUSD, 0, 0); } uint256 bufferBal = redemptionBuffer.getBalance(); if (bufferBal == 0) { - return (remainingZUSD, 0); + return (remainingZUSD, 0, 0); } - // RBTC needed if the *entire* ZUSD amount was redeemed from this buffer - uint256 collNeededForFull = _ZUSDAmount.mul(DECIMAL_PRECISION).div(_price); - - uint256 collFromBuffer = collNeededForFull <= bufferBal - ? collNeededForFull - : bufferBal; + uint256 maxZusdFromBuffer = bufferBal.mul(_price).div(DECIMAL_PRECISION); + zusdSwappedToFeeDistributor = LiquityMath._min(_ZUSDAmount, maxZusdFromBuffer); - // Corresponding ZUSD to burn, priced at the oracle price - uint256 zusdToBurn = collFromBuffer.mul(_price).div(DECIMAL_PRECISION); - if (zusdToBurn > remainingZUSD) { - zusdToBurn = remainingZUSD; + if (zusdSwappedToFeeDistributor == 0) { + return (remainingZUSD, 0, 0); } - if (zusdToBurn == 0) { - return (remainingZUSD, 0); - } + ethFromBuffer = zusdSwappedToFeeDistributor.mul(DECIMAL_PRECISION).div(_price); - // Burn ZUSD from redeemer and adjust system debt - _contractsCache.activePool.decreaseZUSDDebt(zusdToBurn); - _contractsCache.zusdToken.burn(_redeemer, zusdToBurn); + // spender == TroveManager because this is delegatecall (address(this) is TroveManager) + require( + _contractsCache.zusdToken.allowance(_redeemer, address(this)) >= zusdSwappedToFeeDistributor, + "TroveManager: approve ZUSD allowance for buffer swap" + ); - // Send RBTC from buffer to redeemer - redemptionBuffer.withdrawForRedemption(payable(_redeemer), collFromBuffer); + require( + _contractsCache.zusdToken.transferFrom(_redeemer, address(feeDistributor), zusdSwappedToFeeDistributor), + "TroveManager: ZUSD transferFrom failed" + ); - remainingZUSD = remainingZUSD.sub(zusdToBurn); - ethFromBuffer = collFromBuffer; + remainingZUSD = remainingZUSD.sub(zusdSwappedToFeeDistributor); } - /// Redeem as much collateral as possible from _borrower's Trove in exchange for ZUSD up to _maxZUSDamount + // ----- original trove redemption logic unchanged (signature adjusted to use params struct) ----- + function _redeemCollateralFromTrove( ContractsCache memory _contractsCache, address _borrower, uint256 _maxZUSDamount, uint256 _price, - address _upperPartialRedemptionHint, - address _lowerPartialRedemptionHint, - uint256 _partialRedemptionHintNICR + RedeemParams memory _params ) internal returns (SingleRedemptionValues memory singleRedemption) { - // Determine the remaining amount (lot) to be redeemed, capped by the entire debt of the Trove minus the liquidation reserve singleRedemption.ZUSDLot = LiquityMath._min( _maxZUSDamount, Troves[_borrower].debt.sub(ZUSD_GAS_COMPENSATION) ); - // Get the ETHLot of equivalent value in USD singleRedemption.ETHLot = singleRedemption.ZUSDLot.mul(DECIMAL_PRECISION).div(_price); - // Decrease the debt and collateral of the current Trove according to the ZUSD lot and corresponding ETH to send uint256 newDebt = (Troves[_borrower].debt).sub(singleRedemption.ZUSDLot); uint256 newColl = (Troves[_borrower].coll).sub(singleRedemption.ETHLot); if (newDebt == ZUSD_GAS_COMPENSATION) { - // No debt left in the Trove (except for the liquidation reserve), therefore the trove gets closed _removeStake(_borrower); _closeTrove(_borrower, Status.closedByRedemption); _redeemCloseTrove(_contractsCache, _borrower, ZUSD_GAS_COMPENSATION, newColl); @@ -374,13 +355,7 @@ contract TroveManagerRedeemOps is TroveManagerBase { } else { uint256 newNICR = LiquityMath._computeNominalCR(newColl, newDebt); - /* - * If the provided hint is out of date, we bail since trying to reinsert without a good hint will almost - * certainly result in running out of gas. - * - * If the resultant net debt of the partial is less than the minimum, net debt we bail. - */ - if (newNICR != _partialRedemptionHintNICR || _getNetDebt(newDebt) < MIN_NET_DEBT) { + if (newNICR != _params.partialRedemptionHintNICR || _getNetDebt(newDebt) < MIN_NET_DEBT) { singleRedemption.cancelledPartial = true; return singleRedemption; } @@ -388,8 +363,8 @@ contract TroveManagerRedeemOps is TroveManagerBase { _contractsCache.sortedTroves.reInsert( _borrower, newNICR, - _upperPartialRedemptionHint, - _lowerPartialRedemptionHint + _params.upperPartialRedemptionHint, + _params.lowerPartialRedemptionHint ); Troves[_borrower].debt = newDebt; @@ -408,12 +383,6 @@ contract TroveManagerRedeemOps is TroveManagerBase { return singleRedemption; } - /** - This function has two impacts on the baseRate state variable: - 1) decays the baseRate based on time passed since last redemption or ZUSD borrowing operation. - then, - 2) increases the baseRate based on the amount redeemed, as a proportion of total supply - */ function _updateBaseRateFromRedemption( uint256 _ETHDrawn, uint256 _price, @@ -421,16 +390,12 @@ contract TroveManagerRedeemOps is TroveManagerBase { ) internal returns (uint256) { uint256 decayedBaseRate = _calcDecayedBaseRate(); - /* Convert the drawn ETH back to ZUSD at face value rate (1 ZUSD:1 USD), in order to get - * the fraction of total supply that was redeemed at face value. */ uint256 redeemedZUSDFraction = _ETHDrawn.mul(_price).div(_totalZUSDSupply); uint256 newBaseRate = decayedBaseRate.add(redeemedZUSDFraction.div(BETA)); - newBaseRate = LiquityMath._min(newBaseRate, DECIMAL_PRECISION); // cap baseRate at a maximum of 100% - //assert(newBaseRate <= DECIMAL_PRECISION); // This is already enforced in the line above - assert(newBaseRate > 0); // Base rate is always non-zero after redemption + newBaseRate = LiquityMath._min(newBaseRate, DECIMAL_PRECISION); + assert(newBaseRate > 0); - // Update the baseRate state variable baseRate = newBaseRate; emit BaseRateUpdated(newBaseRate); @@ -439,13 +404,6 @@ contract TroveManagerRedeemOps is TroveManagerBase { return newBaseRate; } - /** - Called when a full redemption occurs, and closes the trove. - The redeemer swaps (debt - liquidation reserve) ZUSD for (debt - liquidation reserve) worth of ETH, so the ZUSD liquidation reserve left corresponds to the remaining debt. - In order to close the trove, the ZUSD liquidation reserve is burned, and the corresponding debt is removed from the active pool. - The debt recorded on the trove's struct is zero'd elswhere, in _closeTrove. - Any surplus ETH left in the trove, is sent to the Coll surplus pool, and can be later claimed by the borrower. - */ function _redeemCloseTrove( ContractsCache memory _contractsCache, address _borrower, @@ -453,10 +411,8 @@ contract TroveManagerRedeemOps is TroveManagerBase { uint256 _ETH ) internal { _contractsCache.zusdToken.burn(gasPoolAddress, _ZUSD); - // Update Active Pool ZUSD, and send ETH to account _contractsCache.activePool.decreaseZUSDDebt(_ZUSD); - // send ETH from Active Pool to CollSurplus Pool _contractsCache.collSurplusPool.accountSurplus(_borrower, _ETH); _contractsCache.activePool.sendETH(address(_contractsCache.collSurplusPool), _ETH); } diff --git a/contracts/FeeDistributor.sol b/contracts/FeeDistributor.sol index 086b848..433ba5f 100644 --- a/contracts/FeeDistributor.sol +++ b/contracts/FeeDistributor.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT - pragma solidity 0.6.11; import "./Interfaces/IFeeDistributor.sol"; @@ -10,7 +9,6 @@ import "./Dependencies/SafeMath.sol"; contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor { using SafeMath for uint256; - // --- Events --- event FeeSharingCollectorAddressChanged(address _feeSharingCollectorAddress); event ZeroStakingAddressChanged(address _zeroStakingAddress); @@ -23,7 +21,7 @@ contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor event ZUSDDistributed(uint256 _zusdDistributedAmount); event RBTCistributed(uint256 _rbtcDistributedAmount); - // --- Dependency setters --- + event RedemptionBufferAddressChanged(address _redemptionBufferAddress); function setAddresses( address _feeSharingCollectorAddress, @@ -62,17 +60,48 @@ contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor emit ActivePoolAddressSet(_activePoolAddress); } + // -------------------------------------------------------------------- + // RedemptionBuffer wiring + // -------------------------------------------------------------------- + + /** + * @notice Getter required by IFeeDistributor. + * @dev Implemented explicitly (instead of relying on a public storage variable getter) + * to avoid multiple-inheritance conflicts in Solidity 0.6.x. + */ + function redemptionBufferAddress() external view override returns (address) { + return _redemptionBufferAddress; + } + + /** + * @notice Set RedemptionBuffer address. + * @dev Owner-only. This enables: + * - accepting RBTC sent from RedemptionBuffer (receive()) + * - optionally allowing RedemptionBuffer to call distributeFees() + */ + function setRedemptionBufferAddress(address _buffer) external override onlyOwner { + require(_buffer != address(0), "FeeDistributor: zero buffer"); + checkContract(_buffer); + + _redemptionBufferAddress = _buffer; + emit RedemptionBufferAddressChanged(_buffer); + } + function setFeeToFeeSharingCollector(uint256 FEE_TO_FEE_SHARING_COLLECTOR_) public onlyOwner { FEE_TO_FEE_SHARING_COLLECTOR = FEE_TO_FEE_SHARING_COLLECTOR_; } function distributeFees() public override { require( - msg.sender == address(borrowerOperations) || msg.sender == address(troveManager), + msg.sender == address(borrowerOperations) || + msg.sender == address(troveManager) || + msg.sender == _redemptionBufferAddress, "FeeDistributor: invalid caller" ); + uint256 zusdtoDistribute = zusdToken.balanceOf(address(this)); uint256 rbtcToDistribute = address(this).balance; + if (zusdtoDistribute != 0) { _distributeZUSD(zusdtoDistribute); } @@ -82,14 +111,13 @@ contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor } function _distributeZUSD(uint256 toDistribute) internal { - // Send fee to the FeeSharingCollector address uint256 feeToFeeSharingCollector = toDistribute.mul(FEE_TO_FEE_SHARING_COLLECTOR).div( LiquityMath.DECIMAL_PRECISION ); - zusdToken.approve(address(feeSharingCollector), feeToFeeSharingCollector); + zusdToken.approve(address(feeSharingCollector), feeToFeeSharingCollector); feeSharingCollector.transferTokens(address(zusdToken), uint96(feeToFeeSharingCollector)); - // Send fee to ZERO staking contract + uint256 feeToZeroStaking = toDistribute.sub(feeToFeeSharingCollector); if (feeToZeroStaking != 0) { require( @@ -98,32 +126,35 @@ contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor ); zeroStaking.increaseF_ZUSD(feeToZeroStaking); } + emit ZUSDDistributed(toDistribute); } function _distributeRBTC(uint256 toDistribute) internal { - // Send fee to the feeSharingCollector address uint256 feeToFeeSharingCollector = toDistribute.mul(FEE_TO_FEE_SHARING_COLLECTOR).div( LiquityMath.DECIMAL_PRECISION ); feeSharingCollector.transferRBTC{ value: feeToFeeSharingCollector }(); - // Send the ETH fee to the ZERO staking contract uint256 feeToZeroStaking = toDistribute.sub(feeToFeeSharingCollector); if (feeToZeroStaking != 0) { (bool success, ) = address(zeroStaking).call{ value: feeToZeroStaking }(""); require(success, "FeeDistributor: sending ETH failed"); zeroStaking.increaseF_ETH(feeToZeroStaking); } + emit RBTCistributed(toDistribute); } - function _requireCallerIsActivePool() internal view { - require(msg.sender == activePoolAddress, "FeeDistributor: caller is not ActivePool"); + function _requireCallerIsActivePoolOrRedemptionBuffer() internal view { + require( + msg.sender == activePoolAddress || msg.sender == _redemptionBufferAddress, + "FeeDistributor: invalid RBTC sender" + ); } receive() external payable { - _requireCallerIsActivePool(); + _requireCallerIsActivePoolOrRedemptionBuffer(); } } diff --git a/contracts/FeeDistributorStorage.sol b/contracts/FeeDistributorStorage.sol index df86c68..d9a0160 100644 --- a/contracts/FeeDistributorStorage.sol +++ b/contracts/FeeDistributorStorage.sol @@ -31,4 +31,7 @@ contract FeeDistributorStorage is Ownable { //pct of fees sent to feeSharingCollector address uint256 public FEE_TO_FEE_SHARING_COLLECTOR; + + // Buffer address so we can accept RBTC from it and optionally let it call distributeFees() + address internal _redemptionBufferAddress; } diff --git a/contracts/Interfaces/IBorrowerOperations.sol b/contracts/Interfaces/IBorrowerOperations.sol index de024b9..369df8d 100644 --- a/contracts/Interfaces/IBorrowerOperations.sol +++ b/contracts/Interfaces/IBorrowerOperations.sol @@ -7,7 +7,13 @@ import "../Dependencies/Mynt/IMassetManager.sol"; import "./IRedemptionBuffer.sol"; import { IPermit2, ISignatureTransfer } from "./IPermit2.sol"; -/// Common interface for the Trove Manager. +/// @title IBorrowerOperations +/// @notice External interface for BorrowerOperations (opening/adjusting/closing troves). +/// @dev +/// Redemptions are handled by TroveManager/TroveManagerRedeemOps, but BorrowerOperations is +/// responsible for issuing debt and collecting: +/// - the normal ZUSD borrowing fee (paid in ZUSD and minted to FeeDistributor) +/// - the RedemptionBuffer fee (paid in RBTC and deposited into RedemptionBuffer) interface IBorrowerOperations { // --- Events --- @@ -22,9 +28,11 @@ interface IBorrowerOperations { event SortedTrovesAddressChanged(address _sortedTrovesAddress); event ZUSDTokenAddressChanged(address _zusdTokenAddress); event ZEROStakingAddressChanged(address _zeroStakingAddress); - /// @notice Emitted when the RedemptionBuffer contract address is updated + /// @notice Emitted when the Mynt/mAsset manager is configured. + event MassetManagerAddressChanged(address _massetManagerAddress); + /// @notice Emitted when the RedemptionBuffer contract address is updated. event RedemptionBufferAddressChanged(address _redemptionBufferAddress); - /// @notice Emitted when the redemption buffer rate is updated + /// @notice Emitted when the redemption buffer rate is updated. event RedemptionBufferRateChanged(uint256 _redemptionBufferRate); event TroveCreated(address indexed _borrower, uint256 arrayIndex); @@ -70,42 +78,45 @@ interface IBorrowerOperations { address _zeroStakingAddress ) external; - /** - * @notice Sets the RedemptionBuffer contract address. - * @dev Callable only by owner (governance). The buffer receives RBTC fees when opening troves and is used - * to serve redemptions before touching troves. - * @param _buffer RedemptionBuffer contract address - */ + /// @notice Sets the Mynt/mAsset manager used for NUE/DLLR flows. + /// @dev Owner/governance only in implementation. + function setMassetManagerAddress(address _massetManagerAddress) external; + + // --- RedemptionBuffer configuration --- + + /// @notice Sets the RedemptionBuffer contract address. + /// @dev Owner/governance only in implementation. function setRedemptionBuffer(address _buffer) external; - /** - * @notice Sets the redemption buffer rate used to calculate the RBTC fee-on-top when opening a trove. - * @dev Callable only by owner (governance). Rate uses 1e18 precision where 1e18 == 100%. - * @param _rate Redemption buffer rate (1e18 precision) - */ + /// @notice Sets the redemption buffer rate used to compute the RBTC fee. + /// @dev 1e18 precision; 1e18 == 100%. Owner/governance only in implementation. function setRedemptionBufferRate(uint256 _rate) external; - /** - * @notice Returns the configured RedemptionBuffer contract address. - * @return RedemptionBuffer contract address - */ + /// @notice Returns the configured RedemptionBuffer contract address. function getRedemptionBuffer() external view returns (address); - /** - * @notice Returns the configured redemption buffer rate (1e18 precision). - * @return Redemption buffer rate (1e18 precision) - */ + /// @notice Returns the configured redemption buffer rate (1e18 precision). function getRedemptionBufferRate() external view returns (uint256); - /** - * @notice Quotes the extra RBTC (in wei) required on top of collateral when opening a trove borrowing `_ZUSDAmount`. - * @dev This function is intentionally NOT `view` because it calls `priceFeed.fetchPrice()` in the same way as openTrove. - * Frontends should call this using eth_call / staticcall (no transaction needed). - * @param _ZUSDAmount ZUSD amount the borrower wants to receive when opening the trove - * @return Extra RBTC amount (wei) that must be added on top of collateral as the redemption buffer fee - */ + // --- RedemptionBuffer fee quoting --- + + /// @notice Quotes the extra RBTC (wei) required on top of collateral when borrowing `_ZUSDAmount`. + /// @dev NOT view in many Liquity forks because priceFeed.fetchPrice() is non-view. + /// Frontends should call via eth_call/staticcall. function getRedemptionBufferFeeRBTC(uint256 _ZUSDAmount) external returns (uint256); + /// @notice View-only quote that uses a caller-supplied price. + /// @dev Lets frontends avoid calling BorrowerOperations.getRedemptionBufferFeeRBTC() + /// if they already have a price from elsewhere. The implementation should mirror the + /// exact arithmetic (including rounding) used by openTrove/adjustTrove. + /// @param _ZUSDAmount Amount of new ZUSD debt being minted (not including borrowing fee). + /// @param _price Oracle price in 1e18 precision (RBTC/USD or RBTC/ZUSD face-value model). + /// @return feeRBTC Extra RBTC (wei) required as the redemption buffer fee. + function getRedemptionBufferFeeRBTCWithPrice(uint256 _ZUSDAmount, uint256 _price) + external + view + returns (uint256 feeRBTC); + /** * @notice payable function that creates a Trove for the caller with the requested debt, and the Ether received as collateral. * Successful execution is conditional mainly on the resulting collateralization ratio which must exceed the minimum (110% in Normal Mode, 150% in Recovery Mode). diff --git a/contracts/Interfaces/IFeeDistributor.sol b/contracts/Interfaces/IFeeDistributor.sol index 713e976..d44ed74 100644 --- a/contracts/Interfaces/IFeeDistributor.sol +++ b/contracts/Interfaces/IFeeDistributor.sol @@ -1,10 +1,18 @@ // SPDX-License-Identifier: MIT - pragma solidity 0.6.11; -/// Common interface for Fee Distributor. +/// @title IFeeDistributor +/// @notice FeeDistributor receives protocol fees (ZUSD and/or RBTC) and forwards them to +/// FeeSharingCollector / ZEROStaking according to protocol configuration. +/// @dev +/// - BorrowerOperations sends ZUSD borrowing fees here and then calls distributeFees(). +/// - TroveManager sends RBTC redemption fees here and then calls distributeFees(). +/// - With the RedemptionBuffer "swap" model, TroveManagerRedeemOps may: +/// * transfer ZUSD from redeemer -> FeeDistributor (ERC20 transferFrom) +/// * send RBTC redemption fee from RedemptionBuffer -> FeeDistributor +/// and then call distributeFees(). interface IFeeDistributor { - // --- Events --- + // --- Events (mirrors FeeDistributor.sol) --- event FeeSharingCollectorAddressChanged(address _feeSharingCollectorAddress); event ZeroStakingAddressChanged(address _zeroStakingAddress); @@ -14,22 +22,19 @@ interface IFeeDistributor { event ZUSDTokenAddressChanged(address _zusdTokenAddress); event ActivePoolAddressSet(address _activePoolAddress); + /// @notice Emitted when ZUSD is distributed to collectors/stakers. event ZUSDDistributed(uint256 _zusdDistributedAmount); + + /// @notice Emitted when RBTC is distributed to collectors/stakers. + /// @dev Note: name preserved (typo) for backwards compatibility with existing deployments/logs. event RBTCistributed(uint256 _rbtcDistributedAmount); - // --- Functions --- - - /** - * @notice Called only once on init, to set addresses of other Zero contracts. Callable only by owner - * @dev initializer function, checks addresses are contracts - * @param _feeSharingCollectorAddress FeeSharingCollector address - * @param _zeroStakingAddress ZEROStaking contract address - * @param _borrowerOperationsAddress borrowerOperations contract address - * @param _troveManagerAddress TroveManager contract address - * @param _wrbtcAddress wrbtc ERC20 contract address - * @param _zusdTokenAddress ZUSDToken contract address - * @param _activePoolAddress ActivePool contract address - */ + /// @notice Emitted when the RedemptionBuffer address is configured. + event RedemptionBufferAddressChanged(address _redemptionBufferAddress); + + // --- Admin/initializer functions --- + + /// @notice Called once on init to wire core contracts. function setAddresses( address _feeSharingCollectorAddress, address _zeroStakingAddress, @@ -40,5 +45,19 @@ interface IFeeDistributor { address _activePoolAddress ) external; + /// @notice Sets the RedemptionBuffer contract address. + /// @dev Needed if FeeDistributor.receive() should accept RBTC directly from RedemptionBuffer + /// (e.g. when redemption fees on buffer-swaps are paid from the buffer). + function setRedemptionBufferAddress(address _redemptionBufferAddress) external; + + // --- Core function --- + + /// @notice Distributes any ZUSD and/or RBTC currently held by FeeDistributor. + /// @dev In your implementation this is permissioned (BO/TroveManager/(optional) RedemptionBuffer). function distributeFees() external; + + // --- Views / getters --- + + /// @notice Getter for the configured RedemptionBuffer address (public var in implementation). + function redemptionBufferAddress() external view returns (address); } diff --git a/contracts/Interfaces/IRedemptionBuffer.sol b/contracts/Interfaces/IRedemptionBuffer.sol index bf3e22e..a9054da 100644 --- a/contracts/Interfaces/IRedemptionBuffer.sol +++ b/contracts/Interfaces/IRedemptionBuffer.sol @@ -1,18 +1,33 @@ // SPDX-License-Identifier: MIT pragma solidity 0.6.11; +/// @title IRedemptionBuffer +/// @notice Minimal interface for the protocol RBTC "buffer" used during redemptions. +/// @dev +/// - BorrowerOperations deposits RBTC (collected as a fee when debt is issued). +/// - TroveManager withdraws RBTC to satisfy redemptions. +/// - Governance may send RBTC to FeeDistributor (or elsewhere) via distributeToStakers(). +/// +/// IMPORTANT DESIGN NOTE: +/// This interface does NOT prescribe whether redeemer ZUSD is burned or transferred (swap-style). +/// That policy lives in TroveManagerRedeemOps. The buffer just holds/sends RBTC. interface IRedemptionBuffer { - /// @notice Receive RBTC from BorrowerOperations when users open a Line of Credit + /// @notice Receive RBTC into the buffer. + /// @dev Expected caller: BorrowerOperations (onlyBorrowerOps in implementation). function deposit() external payable; - /// @notice Withdraw RBTC to satisfy a ZUSD redemption - /// @dev Only callable by TroveManager + /// @notice Withdraw RBTC from the buffer. + /// @dev Expected caller: TroveManager (onlyTroveManager in implementation). + /// @param _to Destination to receive RBTC (redeemer, FeeDistributor, etc.). + /// @param _amount Amount of RBTC (wei) to send. function withdrawForRedemption(address payable _to, uint256 _amount) external; - /// @notice Governance-controlled distribution of RBTC to SOV stakers - /// @dev Only callable by the contract owner (timelock / Bitocracy executor) + /// @notice Governance-controlled distribution of RBTC held in the buffer. + /// @dev Typically used to send RBTC to FeeDistributor so it gets split to stakers. + /// @param _amount Amount of RBTC (wei) to distribute. function distributeToStakers(uint256 _amount) external; - /// @return Current RBTC balance tracked by the buffer + /// @notice Returns the RBTC balance tracked by the buffer. + /// @dev Implementation usually tracks an internal accounting variable (e.g. totalBufferedColl). function getBalance() external view returns (uint256); -} \ No newline at end of file +} diff --git a/contracts/Interfaces/ITroveManager.sol b/contracts/Interfaces/ITroveManager.sol index bb0c442..70e7da2 100644 --- a/contracts/Interfaces/ITroveManager.sol +++ b/contracts/Interfaces/ITroveManager.sol @@ -11,7 +11,12 @@ import "./IZEROStaking.sol"; import "../Dependencies/Mynt/IMassetManager.sol"; import { IPermit2, ISignatureTransfer } from "./IPermit2.sol"; -/// Common interface for the Trove Manager. +/// @title ITroveManager +/// @notice External interface for TroveManager (liquidations/redemptions/system accounting). +/// @dev +/// RedemptionBuffer integration: +/// - TroveManager (via TroveManagerRedeemOps) may pull RBTC out of RedemptionBuffer to +/// satisfy redemptions and/or pay redemption fees to FeeDistributor. interface ITroveManager is ILiquityBase { // --- Events --- @@ -106,6 +111,10 @@ interface ITroveManager is ILiquityBase { function setTroveManagerRedeemOps(address _troveManagerRedeemOps) external; + /// @notice Configures the RedemptionBuffer contract address used during redemptions. + /// @dev Owner/governance only in implementation. + function setRedemptionBuffer(address _buffer) external; + /// @return Trove owners count function getTroveOwnersCount() external view returns (uint256); diff --git a/contracts/RedemptionBuffer.sol b/contracts/RedemptionBuffer.sol index 84dd70c..483a242 100644 --- a/contracts/RedemptionBuffer.sol +++ b/contracts/RedemptionBuffer.sol @@ -13,7 +13,7 @@ contract RedemptionBuffer is Ownable, IRedemptionBuffer { address public troveManager; IFeeDistributor public feeDistributor; - uint256 public totalBufferedColl; // RBTC tracked by this contract + uint256 public totalBufferedColl; modifier onlyBorrowerOps() { require(msg.sender == borrowerOperations, "RB: caller is not BorrowerOperations"); @@ -39,13 +39,11 @@ contract RedemptionBuffer is Ownable, IRedemptionBuffer { feeDistributor = IFeeDistributor(_feeDistributor); } - /// @dev Receives RBTC from BorrowerOperations when a user opens a Line of Credit. function deposit() external payable override onlyBorrowerOps { require(msg.value > 0, "RB: no value"); totalBufferedColl = totalBufferedColl.add(msg.value); } - /// @dev Used by TroveManager to serve ZUSD redemptions from the buffer. function withdrawForRedemption(address payable _to, uint256 _amount) external override @@ -58,23 +56,28 @@ contract RedemptionBuffer is Ownable, IRedemptionBuffer { require(success, "RB: send failed"); } - /// @dev Governance-controlled: send RBTC to FeeDistributor so it’s split like other ZERO fees. function distributeToStakers(uint256 _amount) external override onlyOwner { require(address(feeDistributor) != address(0), "RB: feeDistributor not set"); require(_amount <= totalBufferedColl, "RB: insufficient buffer"); totalBufferedColl = totalBufferedColl.sub(_amount); - // Same pattern as TroveManagerRedeemOps (bool success, ) = address(feeDistributor).call{ value: _amount }(""); require(success, "RB: send to feeDistributor failed"); + // With the FeeDistributor patch above, this can now succeed. feeDistributor.distributeFees(); } + function syncBalance() external onlyOwner { + totalBufferedColl = address(this).balance; + } + function getBalance() external view override returns (uint256) { return totalBufferedColl; } - receive() external payable {} -} \ No newline at end of file + receive() external payable { + totalBufferedColl = totalBufferedColl.add(msg.value); + } +} diff --git a/contracts/TroveManager.sol b/contracts/TroveManager.sol index eb8f6ee..d101404 100644 --- a/contracts/TroveManager.sol +++ b/contracts/TroveManager.sol @@ -119,8 +119,9 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { } // --- Redemption buffer config --- - function setRedemptionBuffer(address _buffer) external onlyOwner { + function setRedemptionBuffer(address _buffer) external override onlyOwner { require(_buffer != address(0), "TroveManager: zero buffer address"); + checkContract(_buffer); redemptionBuffer = IRedemptionBuffer(_buffer); } From baa886180d1c5694dbecfa2314af7019db553195 Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Mon, 22 Dec 2025 01:43:24 -0500 Subject: [PATCH 06/13] Reworked redemption buffer fee to be on top of collateral --- .../Dependencies/TroveManagerRedeemOps.sol | 288 +++++++++++++++++- 1 file changed, 276 insertions(+), 12 deletions(-) diff --git a/contracts/Dependencies/TroveManagerRedeemOps.sol b/contracts/Dependencies/TroveManagerRedeemOps.sol index dff58fd..eb1b80c 100644 --- a/contracts/Dependencies/TroveManagerRedeemOps.sol +++ b/contracts/Dependencies/TroveManagerRedeemOps.sol @@ -8,24 +8,88 @@ import "../Interfaces/IBorrowerOperations.sol"; import "./TroveManagerBase.sol"; import "../Interfaces/IPermit2.sol"; -/// This contract is designed to be used via delegatecall from the TroveManager contract +/// @notice Redemption operations module for TroveManager, intended to be executed via delegatecall. +/// @dev +/// - This contract is designed to be used via delegatecall from the TroveManager contract. +/// That means `address(this)` will be the TroveManager at runtime (important for allowance checks, etc). +/// - TroveManagerBase constructor param is the bootstrap period when redemptions are not allowed. contract TroveManagerRedeemOps is TroveManagerBase { + /** CONSTANT / IMMUTABLE VARIABLE ONLY */ + /// @notice Permit2 contract reference used by DLLR->ZUSD redemption path. IPermit2 public immutable permit2; - // Pack redemption hints/limits into one struct to reduce stack usage (Solidity 0.6.x stack-too-deep) + // --------------------------------------------------------------------- + // Stack-too-deep mitigation + // --------------------------------------------------------------------- + // Pack redemption hints/limits into one struct to reduce stack usage (Solidity 0.6.x stack-too-deep). + // NOTE: Packing in a struct changes *only* internal plumbing (fewer stack vars), + // not the external API or redemption semantics. struct RedeemParams { + /// @dev Hint for the first trove to start redeeming from (must be valid or ignored). address firstRedemptionHint; + + /// @dev Reinsert hints for the *last* (partially) redeemed trove, if any. + /// Only needed when we do a partial redemption that leaves the last trove with debt > gas compensation. address upperPartialRedemptionHint; address lowerPartialRedemptionHint; + + /// @dev The NICR the frontend calculated for the partially redeemed trove using getRedemptionHints(). + /// If it no longer matches at execution time, we cancel the partial redemption to avoid OOG. uint256 partialRedemptionHintNICR; + + /// @dev Loop cap. If 0, treated as "no cap" (uint256(-1)). uint256 maxIterations; + + /// @dev Max fee percentage user is willing to accept for this redemption. uint256 maxFeePercentage; } + /** Constructor */ constructor(uint256 _bootstrapPeriod, address _permit2) public TroveManagerBase(_bootstrapPeriod) { permit2 = IPermit2(_permit2); } + /** + @notice + Send `_ZUSDamount` ZUSD to the system and redeem the corresponding amount of collateral + from as many Troves as are needed to fill the redemption request. + + @dev High level behavior (ORIGINAL semantics, now with an added "buffer-first" stage): + ------------------------------------------------------------------------- + A) Apply pending rewards to a Trove before reducing its debt and collateral. + B) Walk troves from the lowest collateral ratio upward (subject to MCR constraint), + redeeming until the requested ZUSD amount is exhausted or we hit a stop condition. + C) Most troves redeemed-from will end up closed (debt -> liquidation reserve only). + D) The last trove may be partially redeemed; reinsertion requires correct sorted list hints. + + Gas & iteration notes (from original implementation): + ------------------------------------------------------------------------- + - If `_ZUSDamount` is very large, this function can run out of gas, especially if traversed troves are small. + This can be avoided by splitting the total amount into appropriate chunks and calling multiple times. + - Param `_maxIterations` can cap the trove loop (if it’s zero, it will be ignored). This helps frontends + avoid OOG without knowing the exact “topology” of the trove list, and avoids hard-coding a cap in the contract. + + Partial redemption / hint correctness (from original implementation): + ------------------------------------------------------------------------- + - If the last trove does have some remaining debt, it has a finite ICR; reinsertion could be anywhere, + therefore it requires a hint. + - A frontend should use getRedemptionHints() to calculate the ICR/NICR after redemption and pass a hint. + - If another transaction modifies the list between getRedemptionHints() and this call, it is likely + that the partially redeemed trove’s NICR changes. In that case the partial redemption is cancelled: + redemption stops after the last completely redeemed trove, and the sender keeps the remaining ZUSD. + + NEW addition in this version: RedemptionBuffer "buffer-first" swap stage + ------------------------------------------------------------------------- + - Before redeeming from troves, we attempt to swap ZUSD for RBTC directly from `redemptionBuffer` + at the oracle price (1 ZUSD = 1 USD face value, and ETH/RBTC valued by `_price`). + - This stage: + * transfers ZUSD to FeeDistributor via transferFrom (so user must approve TroveManager) + * does NOT burn ZUSD + * does NOT decrease ActivePool ZUSD debt + * later withdraws RBTC from RedemptionBuffer to the user (minus fee) + - Any remainder (if the buffer is insufficient) is redeemed from troves in the traditional manner. + - Fees/baseRate are applied consistently across BOTH sources. + */ function redeemCollateral( uint256 _ZUSDamount, address _firstRedemptionHint, @@ -47,6 +111,18 @@ contract TroveManagerRedeemOps is TroveManagerBase { _redeemCollateral(_ZUSDamount, params); } + /** + * @dev Internal redemption entrypoint. + * + * The redemption flow is now split into clearly delineated stages: + * 1) Validate inputs, system state, bootstrap period, and balances + * 2) Swap against RedemptionBuffer first (ZUSD -> RBTC) at oracle price + * 3) Redeem remainder from troves (classic redemption) + * 4) Update baseRate from total RBTC drawn (troves + buffer) + * 5) Compute and enforce redemption fee (troves + buffer) + * 6) Apply burns/debt changes ONLY for trove portion (buffer is a swap) + * 7) Pay fees to FeeDistributor, distribute fees, and send net RBTC to redeemer + */ function _redeemCollateral(uint256 _ZUSDamount, RedeemParams memory _params) internal { ContractsCache memory contractsCache = ContractsCache( activePool, @@ -59,15 +135,23 @@ contract TroveManagerRedeemOps is TroveManagerBase { ); RedemptionTotals memory totals; + // --- Requirements / guards (same intent as original) --- _requireValidMaxFeePercentage(_params.maxFeePercentage); _requireAfterBootstrapPeriod(); + // Fetch oracle price once and use consistently across buffer and trove stages. totals.price = priceFeed.fetchPrice(); + + // Redemption is only valid if system is healthy enough (TCR over MCR). _requireTCRoverMCR(totals.price); + _requireAmountGreaterThanZero(_ZUSDamount); _requireZUSDBalanceCoversRedemption(contractsCache.zusdToken, msg.sender, _ZUSDamount); + // Capture the total system debt at the start for baseRate math. totals.totalZUSDSupplyAtStart = getEntireSystemDebt(); + + // Confirm redeemer's balance is <= total ZUSD supply (sanity check; mirrors original intent). assert(contractsCache.zusdToken.balanceOf(msg.sender) <= totals.totalZUSDSupplyAtStart); // ------------------------------------------------------------ @@ -75,6 +159,11 @@ contract TroveManagerRedeemOps is TroveManagerBase { // - transfers ZUSD to FeeDistributor via transferFrom() // - does NOT burn ZUSD // - does NOT touch ActivePool debt + // + // Rationale: + // - If the buffer has RBTC liquidity, we can satisfy redemptions without + // walking troves (cheaper gas; less list traversal). + // - Remaining amount (if any) continues through classic trove redemption. // ------------------------------------------------------------ uint256 ethFromBuffer; uint256 zusdSwappedToFeeDistributor; @@ -87,22 +176,40 @@ contract TroveManagerRedeemOps is TroveManagerBase { // ------------------------------------------------------------ // 2) Redeem remaining from troves (normal redemption path) - // Moved into helper to reduce stack usage. + // + // Note: + // - Only the trove portion affects: ZUSD burn, ActivePool debt decrease, + // trove closures/updates, reward application, etc. // ------------------------------------------------------------ if (totals.remainingZUSD > 0) { _redeemFromTroves(contractsCache, totals, _params); } + // Total RBTC drawn from both sources. uint256 totalETHDrawnInclBuffer = totals.totalETHDrawn.add(ethFromBuffer); + + // If we got nothing from buffer and nothing from troves, revert (matches original spirit). require(totalETHDrawnInclBuffer > 0, "TroveManager: Unable to redeem any amount"); // ------------------------------------------------------------ // 3) BaseRate update applies to buffer swaps too + // + // The baseRate is used for redemption fee calculation, and historically + // it responds to redemption demand. Buffer swaps are economically + // equivalent demand, so they are included in the baseRate update. // ------------------------------------------------------------ - _updateBaseRateFromRedemption(totalETHDrawnInclBuffer, totals.price, totals.totalZUSDSupplyAtStart); + _updateBaseRateFromRedemption( + totalETHDrawnInclBuffer, + totals.price, + totals.totalZUSDSupplyAtStart + ); // ------------------------------------------------------------ // 4) Redemption fee applies to BOTH sources (same formula) + // + // NOTE: + // - We compute fee independently on each RBTC amount and sum. + // - Rounding will be consistent with the existing _getRedemptionFee() logic. // ------------------------------------------------------------ uint256 ethFeeFromTroves = _getRedemptionFee(totals.totalETHDrawn); uint256 ethFeeFromBuffer = _getRedemptionFee(ethFromBuffer); @@ -110,12 +217,22 @@ contract TroveManagerRedeemOps is TroveManagerBase { _requireUserAcceptsFee(totals.ETHFee, totalETHDrawnInclBuffer, _params.maxFeePercentage); + // Total ZUSD "processed" by this redemption attempt (buffer swap + trove redemption). + // If we stop early (partial cancellation or iteration cap), some ZUSD remains with the user. uint256 totalZUSDProcessed = _ZUSDamount.sub(totals.remainingZUSD); emit Redemption(_ZUSDamount, totalZUSDProcessed, totalETHDrawnInclBuffer, totals.ETHFee); // ------------------------------------------------------------ // 5) Burn ONLY trove portion (buffer portion is a swap) + // + // Original behavior: + // - burn the redeemed ZUSD + // - decrease ActivePool debt by same amount + // + // New behavior: + // - trove path: unchanged + // - buffer path: does not burn and does not change ActivePool debt // ------------------------------------------------------------ if (totals.totalZUSDToRedeem > 0) { contractsCache.zusdToken.burn(msg.sender, totals.totalZUSDToRedeem); @@ -126,23 +243,36 @@ contract TroveManagerRedeemOps is TroveManagerBase { // 6) Pay RBTC fees into FeeDistributor // - trove fee from ActivePool // - buffer fee directly from RedemptionBuffer + // + // This split reflects where the RBTC originated: + // - Trove redemption draws collateral from ActivePool. + // - Buffer swap draws collateral from RedemptionBuffer. // ------------------------------------------------------------ if (ethFeeFromTroves > 0) { contractsCache.activePool.sendETH(address(feeDistributor), ethFeeFromTroves); } if (ethFeeFromBuffer > 0) { - // IMPORTANT: FeeDistributor.receive() must accept RBTC from RedemptionBuffer + // IMPORTANT: + // - FeeDistributor.receive() (or fallback) must accept RBTC from RedemptionBuffer. + // - withdrawForRedemption is expected to transfer RBTC out of the buffer. redemptionBuffer.withdrawForRedemption(payable(address(feeDistributor)), ethFeeFromBuffer); } - // Distribute any ZUSD swapped into FeeDistributor + RBTC fees + // Distribute any ZUSD swapped into FeeDistributor + any RBTC fees paid in. + // (If nothing was sent, skip to save gas.) if (zusdSwappedToFeeDistributor > 0 || totals.ETHFee > 0) { feeDistributor.distributeFees(); } // ------------------------------------------------------------ // 7) Send net RBTC to redeemer from each source + // + // IMPORTANT: + // - Trove portion comes from ActivePool + // - Buffer portion comes from RedemptionBuffer + // + // Net = drawn - fee, per source. // ------------------------------------------------------------ uint256 ethToSendFromTroves = totals.totalETHDrawn.sub(ethFeeFromTroves); if (ethToSendFromTroves > 0) { @@ -155,7 +285,24 @@ contract TroveManagerRedeemOps is TroveManagerBase { } } - // Helper extracted from _redeemCollateral to reduce stack depth in Solidity 0.6.x + // --------------------------------------------------------------------- + // Trove loop helper + // --------------------------------------------------------------------- + + /** + * @dev Helper extracted from _redeemCollateral to reduce stack depth in Solidity 0.6.x. + * + * Core responsibilities (classic redemption logic): + * - Determine a valid starting trove: + * * if hint is valid -> start there + * * else scan from the tail for first trove with ICR >= MCR + * - Iterate through troves, applying pending rewards then redeeming from each + * - Stop if: + * * remainingZUSD becomes 0, or + * * we run out of troves, or + * * we hit maxIterations, or + * * partial redemption must be cancelled due to stale hints/min net debt + */ function _redeemFromTroves( ContractsCache memory _contractsCache, RedemptionTotals memory _totals, @@ -163,9 +310,11 @@ contract TroveManagerRedeemOps is TroveManagerBase { ) internal { address currentBorrower = address(0); + // Use the provided first hint iff it is still valid at current price. if (_isValidFirstRedemptionHint(_contractsCache.sortedTroves, _params.firstRedemptionHint, _totals.price)) { currentBorrower = _params.firstRedemptionHint; } else { + // Otherwise start from the last trove and find first with ICR >= MCR. currentBorrower = _contractsCache.sortedTroves.getLast(); while ( currentBorrower != address(0) && @@ -175,18 +324,24 @@ contract TroveManagerRedeemOps is TroveManagerBase { } } + // Loop through the Troves starting from the one with lowest collateral ratio (that is still >= MCR) + // until the requested ZUSD is exchanged for collateral or we hit the iteration cap. uint256 maxIterations = _params.maxIterations; if (maxIterations == 0) { + // 0 means "uncapped" to preserve original UX. maxIterations = uint256(-1); } while (currentBorrower != address(0) && _totals.remainingZUSD > 0 && maxIterations > 0) { maxIterations--; + // Save the address of the Trove preceding the current one, before potentially modifying the list. address nextUserToCheck = _contractsCache.sortedTroves.getPrev(currentBorrower); + // Apply any pending redistribution rewards (affects debt/coll, thus affects ICR/NICR). _applyPendingRewards(_contractsCache.activePool, _contractsCache.defaultPool, currentBorrower); + // Redeem from this trove (full close or partial). SingleRedemptionValues memory singleRedemption = _redeemCollateralFromTrove( _contractsCache, currentBorrower, @@ -195,6 +350,9 @@ contract TroveManagerRedeemOps is TroveManagerBase { _params ); + // Partial redemption was cancelled (out-of-date hint, or new net debt < minimum), + // therefore we could not redeem from the last Trove. + // We STOP here to avoid a likely OOG reinsertion attempt. if (singleRedemption.cancelledPartial) break; _totals.totalZUSDToRedeem = _totals.totalZUSDToRedeem.add(singleRedemption.ZUSDLot); @@ -205,8 +363,13 @@ contract TroveManagerRedeemOps is TroveManagerBase { } } - // ----- DLLR helpers unchanged ----- + // --------------------------------------------------------------------- + // DLLR redemption helpers + // --------------------------------------------------------------------- + /// @notice + /// DLLR owner can use Sovryn Mynt to convert DLLR to ZUSD, then use the redemption mechanism + /// to redeem ZUSD for RBTC, all in a single transaction. function redeemCollateralViaDLLR( uint256 _dllrAmount, address _firstRedemptionHint, @@ -217,6 +380,7 @@ contract TroveManagerRedeemOps is TroveManagerBase { uint256 _maxFeePercentage, IMassetManager.PermitParams calldata _permitParams ) external { + // Convert DLLR -> ZUSD (with permit), then proceed with standard redemption using the obtained ZUSD. uint256 _zusdAmount = MyntLib.redeemZusdFromDllrWithPermit( IBorrowerOperations(borrowerOperationsAddress).getMassetManager(), _dllrAmount, @@ -236,6 +400,9 @@ contract TroveManagerRedeemOps is TroveManagerBase { _redeemCollateral(_zusdAmount, params); } + /// @notice + /// DLLR owner can use Sovryn Mynt to convert DLLR to ZUSD, then redeem ZUSD for RBTC, all in one tx. + /// This variant uses Permit2 for token authorization. function redeemCollateralViaDllrWithPermit2( uint256 _dllrAmount, address _firstRedemptionHint, @@ -247,6 +414,7 @@ contract TroveManagerRedeemOps is TroveManagerBase { ISignatureTransfer.PermitTransferFrom memory _permit, bytes calldata _signature ) external { + // Convert DLLR -> ZUSD using Permit2, then proceed with standard redemption. uint256 _zusdAmount = MyntLib.redeemZusdFromDllrWithPermit2( IBorrowerOperations(borrowerOperationsAddress).getMassetManager(), address(_zusdToken), @@ -267,6 +435,20 @@ contract TroveManagerRedeemOps is TroveManagerBase { _redeemCollateral(_zusdAmount, params); } + // --------------------------------------------------------------------- + // Hint validation + // --------------------------------------------------------------------- + + /** + * @dev Returns true if `_firstRedemptionHint` is still a valid first redemption point at `_price`. + * + * Validity conditions: + * - hint is non-zero + * - hint exists in the sorted list + * - hinted trove has ICR >= MCR + * - the next trove (higher ICR) is either non-existent or has ICR < MCR + * (i.e., hint is "the first redeemable trove" when scanning from low ICR upwards) + */ function _isValidFirstRedemptionHint( ISortedTroves _sortedTroves, address _firstRedemptionHint, @@ -284,9 +466,29 @@ contract TroveManagerRedeemOps is TroveManagerBase { return nextTrove == address(0) || _getCurrentICR(nextTrove, _price) < liquityBaseParams.MCR(); } + // --------------------------------------------------------------------- + // RedemptionBuffer swap stage + // --------------------------------------------------------------------- + /** - * Swap ZUSD against RedemptionBuffer at oracle price. - * Uses transferFrom(redeemer -> FeeDistributor) (so redeemer must approve TroveManager). + * @notice Swap ZUSD against RedemptionBuffer at oracle price (ZUSD -> RBTC). + * @dev + * - Uses transferFrom(redeemer -> FeeDistributor) (so redeemer must approve TroveManager). + * - Does not burn ZUSD (unlike trove redemption). + * - Does not update ActivePool ZUSD debt (unlike trove redemption). + * + * Accounting model: + * - The buffer holds RBTC. We compute how much ZUSD that RBTC can cover at the current oracle price. + * - We take min(requestedZUSD, maxZUSDFromBuffer), transfer that ZUSD to FeeDistributor, + * and "reserve" the corresponding RBTC amount to withdraw later in _redeemCollateral(). + * + * Delegatecall nuance: + * - This contract is executed via delegatecall, so `address(this)` is the TroveManager. + * Therefore, the allowance required is allowance(redeemer, TroveManager). + * + * Rounding note: + * - Conversion uses integer division. Dust due to rounding is expected and should be consistent + * with existing fee and redemption math across the system. */ function _swapFromBuffer( ContractsCache memory _contractsCache, @@ -296,40 +498,55 @@ contract TroveManagerRedeemOps is TroveManagerBase { ) internal returns (uint256 remainingZUSD, uint256 ethFromBuffer, uint256 zusdSwappedToFeeDistributor) { remainingZUSD = _ZUSDAmount; + // If buffer is not configured or nothing requested, do nothing. if (address(redemptionBuffer) == address(0) || _ZUSDAmount == 0) { return (remainingZUSD, 0, 0); } + // Read current RBTC balance in buffer. If empty, do nothing. uint256 bufferBal = redemptionBuffer.getBalance(); if (bufferBal == 0) { return (remainingZUSD, 0, 0); } + // Maximum ZUSD that the buffer can satisfy at current price: + // bufferBal (RBTC) * price (USD/RBTC) / 1e18 => USD value in "ZUSD terms" uint256 maxZusdFromBuffer = bufferBal.mul(_price).div(DECIMAL_PRECISION); + + // We can only swap up to the requested amount. zusdSwappedToFeeDistributor = LiquityMath._min(_ZUSDAmount, maxZusdFromBuffer); if (zusdSwappedToFeeDistributor == 0) { return (remainingZUSD, 0, 0); } + // Corresponding RBTC amount to withdraw later: + // ZUSD * 1e18 / price ethFromBuffer = zusdSwappedToFeeDistributor.mul(DECIMAL_PRECISION).div(_price); // spender == TroveManager because this is delegatecall (address(this) is TroveManager) + // Require explicit allowance to make the UX failure mode clear. require( _contractsCache.zusdToken.allowance(_redeemer, address(this)) >= zusdSwappedToFeeDistributor, "TroveManager: approve ZUSD allowance for buffer swap" ); + // Transfer the swapped ZUSD directly into FeeDistributor. + // This ZUSD is not burned; it is intended for distribution via the fee mechanism. require( _contractsCache.zusdToken.transferFrom(_redeemer, address(feeDistributor), zusdSwappedToFeeDistributor), "TroveManager: ZUSD transferFrom failed" ); + // Remaining amount proceeds to trove redemption. remainingZUSD = remainingZUSD.sub(zusdSwappedToFeeDistributor); } - // ----- original trove redemption logic unchanged (signature adjusted to use params struct) ----- + // --------------------------------------------------------------------- + // Original trove redemption logic (signature adjusted to use params struct) + // --------------------------------------------------------------------- + /// @dev Redeem as much collateral as possible from `_borrower`'s Trove in exchange for ZUSD up to `_maxZUSDamount`. function _redeemCollateralFromTrove( ContractsCache memory _contractsCache, address _borrower, @@ -337,17 +554,22 @@ contract TroveManagerRedeemOps is TroveManagerBase { uint256 _price, RedeemParams memory _params ) internal returns (SingleRedemptionValues memory singleRedemption) { + // Determine the remaining amount (lot) to be redeemed, capped by: + // (entire trove debt - liquidation reserve) singleRedemption.ZUSDLot = LiquityMath._min( _maxZUSDamount, Troves[_borrower].debt.sub(ZUSD_GAS_COMPENSATION) ); + // Get the ETHLot of equivalent value in USD (1 ZUSD : 1 USD face value). singleRedemption.ETHLot = singleRedemption.ZUSDLot.mul(DECIMAL_PRECISION).div(_price); + // Decrease the debt and collateral of the current Trove according to the ZUSD lot and corresponding ETH to send. uint256 newDebt = (Troves[_borrower].debt).sub(singleRedemption.ZUSDLot); uint256 newColl = (Troves[_borrower].coll).sub(singleRedemption.ETHLot); if (newDebt == ZUSD_GAS_COMPENSATION) { + // No debt left in the Trove (except for the liquidation reserve), therefore the trove gets closed. _removeStake(_borrower); _closeTrove(_borrower, Status.closedByRedemption); _redeemCloseTrove(_contractsCache, _borrower, ZUSD_GAS_COMPENSATION, newColl); @@ -355,11 +577,21 @@ contract TroveManagerRedeemOps is TroveManagerBase { } else { uint256 newNICR = LiquityMath._computeNominalCR(newColl, newDebt); + /* + * If the provided hint is out of date, we bail since trying to reinsert without a good hint will almost + * certainly result in running out of gas. + * + * If the resultant net debt of the partial is less than the minimum net debt, we bail. + * + * This “cancel partial redemption” behavior is intentional and mirrors the original design: + * it protects users from OOG and makes frontends responsible for providing fresh hints. + */ if (newNICR != _params.partialRedemptionHintNICR || _getNetDebt(newDebt) < MIN_NET_DEBT) { singleRedemption.cancelledPartial = true; return singleRedemption; } + // Reinsert into the sorted list at the correct newNICR position using user-provided hints. _contractsCache.sortedTroves.reInsert( _borrower, newNICR, @@ -367,6 +599,7 @@ contract TroveManagerRedeemOps is TroveManagerBase { _params.lowerPartialRedemptionHint ); + // Persist updated trove state. Troves[_borrower].debt = newDebt; Troves[_borrower].coll = newColl; _updateStakeAndTotalStakes(_borrower); @@ -383,6 +616,17 @@ contract TroveManagerRedeemOps is TroveManagerBase { return singleRedemption; } + /** + @dev + This function has two impacts on the baseRate state variable: + 1) decays the baseRate based on time passed since last redemption or ZUSD borrowing operation. + then, + 2) increases the baseRate based on the amount redeemed, as a proportion of total supply. + + @param _ETHDrawn Total RBTC drawn by this redemption (in this version: may include buffer + troves). + @param _price Oracle price used to convert RBTC value back to ZUSD face value. + @param _totalZUSDSupply Total system debt captured at the start of redemption. + */ function _updateBaseRateFromRedemption( uint256 _ETHDrawn, uint256 _price, @@ -390,12 +634,17 @@ contract TroveManagerRedeemOps is TroveManagerBase { ) internal returns (uint256) { uint256 decayedBaseRate = _calcDecayedBaseRate(); + /* Convert the drawn ETH back to ZUSD at face value rate (1 ZUSD:1 USD), in order to get + * the fraction of total supply that was redeemed at face value. */ uint256 redeemedZUSDFraction = _ETHDrawn.mul(_price).div(_totalZUSDSupply); uint256 newBaseRate = decayedBaseRate.add(redeemedZUSDFraction.div(BETA)); - newBaseRate = LiquityMath._min(newBaseRate, DECIMAL_PRECISION); + newBaseRate = LiquityMath._min(newBaseRate, DECIMAL_PRECISION); // cap baseRate at a maximum of 100% + + // Base rate is always non-zero after redemption assert(newBaseRate > 0); + // Update the baseRate state variable baseRate = newBaseRate; emit BaseRateUpdated(newBaseRate); @@ -404,6 +653,19 @@ contract TroveManagerRedeemOps is TroveManagerBase { return newBaseRate; } + /** + @dev + Called when a full redemption occurs, and closes the trove. + + The redeemer swaps (debt - liquidation reserve) ZUSD for (debt - liquidation reserve) worth of ETH, + so the ZUSD liquidation reserve left corresponds to the remaining debt. + + In order to close the trove: + - the ZUSD liquidation reserve is burned (from gasPool), + - the corresponding debt is removed from the Active Pool, + - the trove’s debt/coll struct is zero'd elsewhere (in _closeTrove), + - any surplus ETH left in the trove is sent to the CollSurplusPool and can later be claimed by the borrower. + */ function _redeemCloseTrove( ContractsCache memory _contractsCache, address _borrower, @@ -411,8 +673,10 @@ contract TroveManagerRedeemOps is TroveManagerBase { uint256 _ETH ) internal { _contractsCache.zusdToken.burn(gasPoolAddress, _ZUSD); + // Update Active Pool ZUSD, and send ETH to account _contractsCache.activePool.decreaseZUSDDebt(_ZUSD); + // Send ETH from Active Pool to CollSurplus Pool (tracked for borrower to claim). _contractsCache.collSurplusPool.accountSurplus(_borrower, _ETH); _contractsCache.activePool.sendETH(address(_contractsCache.collSurplusPool), _ETH); } From 51a2011a629840d8087c7a0bd7c14d7a8c44f14e Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Mon, 22 Dec 2025 02:00:58 -0500 Subject: [PATCH 07/13] Reworked redemption buffer fee to be on top of collateral --- contracts/RedemptionBuffer.sol | 192 +++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/contracts/RedemptionBuffer.sol b/contracts/RedemptionBuffer.sol index 483a242..dd912ad 100644 --- a/contracts/RedemptionBuffer.sol +++ b/contracts/RedemptionBuffer.sol @@ -6,25 +6,109 @@ import "./Dependencies/SafeMath.sol"; import "./Interfaces/IRedemptionBuffer.sol"; import "./Interfaces/IFeeDistributor.sol"; +/** + * @title RedemptionBuffer + * @notice + * A simple RBTC/ETH holding contract used as a "buffer" liquidity source for redemptions. + * + * Conceptually, this contract acts like a small collateral pool that can be tapped by TroveManager + * during redemptions (or used to forward collateral to FeeDistributor / stakers). + * + * Important high-level roles: + * - borrowerOperations: allowed to deposit collateral into this buffer (system-controlled inflows) + * - troveManager: allowed to withdraw collateral from this buffer for redemptions (system-controlled outflows) + * - owner: allowed to configure addresses and optionally forward buffer collateral to stakers + * + * @dev + * - This contract tracks collateral using `totalBufferedColl`, not by reading `address(this).balance` each time. + * Under normal operation those should match, but they can diverge if ETH/RBTC is forced into this contract + * (e.g., via SELFDESTRUCT), which is why `syncBalance()` exists. + * - ETH here represents RBTC on Rootstock deployments, but the EVM semantics are identical. + * - Uses SafeMath because Solidity 0.6.x does not have built-in overflow/underflow checking. + * + * SECURITY NOTES: + * - withdrawForRedemption() sends ETH via low-level call. State is updated *before* the call, + * which is the correct “checks-effects-interactions” order. However, the recipient `_to` + * could be a contract and could attempt re-entrancy *through TroveManager*. + * The onlyTroveManager gate prevents direct reentry into this contract, but it does not + * prevent the recipient from calling TroveManager again, which could call back here. + * The system is typically protected by TroveManager’s own nonReentrant / flow constraints. + * - setAddresses() can be called multiple times by the owner. This is powerful admin control. + * Many deployments prefer “one-time initialization” patterns; here it is intentionally mutable. + */ contract RedemptionBuffer is Ownable, IRedemptionBuffer { using SafeMath for uint256; + // --------------------------------------------------------------------- + // System wiring (privileged counterpart contracts) + // --------------------------------------------------------------------- + + /// @notice Contract allowed to deposit collateral into the buffer. + /// @dev Typically a core system module (BorrowerOperations) that accumulates collateral destined for the buffer. address public borrowerOperations; + + /// @notice Contract allowed to withdraw collateral from the buffer for redemption execution. + /// @dev Typically TroveManager (or TroveManager via delegatecall modules) pulls collateral out to pay redeemers/fees. address public troveManager; + + /// @notice FeeDistributor that can receive RBTC/ETH and distribute it (and potentially paired ZUSD flows). + /// @dev This is an interface reference to the current configured FeeDistributor. IFeeDistributor public feeDistributor; + // --------------------------------------------------------------------- + // Accounting + // --------------------------------------------------------------------- + + /** + * @notice Total amount of collateral (RBTC/ETH) that this buffer considers “available”. + * + * @dev + * - This is updated on: + * * deposit() (restricted inflow) + * * receive() (unrestricted inflow, e.g. direct transfers) + * * withdrawForRedemption() (restricted outflow) + * * distributeToStakers() (restricted outflow) + * * syncBalance() (admin repair) + * + * - Ideally: totalBufferedColl == address(this).balance + * But the EVM allows ETH to be forcibly sent to a contract without calling receive() + * (e.g., via selfdestruct), so syncBalance() exists to reconcile. + */ uint256 public totalBufferedColl; + // --------------------------------------------------------------------- + // Access control modifiers + // --------------------------------------------------------------------- + + /// @dev Restricts function access to BorrowerOperations only. modifier onlyBorrowerOps() { require(msg.sender == borrowerOperations, "RB: caller is not BorrowerOperations"); _; } + /// @dev Restricts function access to TroveManager only. modifier onlyTroveManager() { require(msg.sender == troveManager, "RB: caller is not TroveManager"); _; } + // --------------------------------------------------------------------- + // Admin configuration + // --------------------------------------------------------------------- + + /** + * @notice Configure core counterpart addresses. + * @dev onlyOwner — typically the deployer / governance multisig. + * + * Requirements: + * - none of the addresses may be zero + * + * Operational note: + * - This function does not emit an event (by design in your snippet). + * In many deployments, emitting an event is useful for indexers/audits. + * - This function can be called multiple times. This implies the owner can + * rotate system contracts (upgrades/migrations). + */ function setAddresses( address _borrowerOps, address _troveManager, @@ -39,29 +123,99 @@ contract RedemptionBuffer is Ownable, IRedemptionBuffer { feeDistributor = IFeeDistributor(_feeDistributor); } + // --------------------------------------------------------------------- + // Inflows + // --------------------------------------------------------------------- + + /** + * @notice Deposit collateral into the buffer. + * @dev + * - Only BorrowerOperations may call this. This prevents arbitrary users + * from “pretending” to be part of system inflows. (Though allowing public + * deposits would not steal funds — it would only add buffer liquidity.) + * - Payable: msg.value is the RBTC/ETH amount deposited. + * + * Accounting: + * - Increments totalBufferedColl by msg.value. + */ function deposit() external payable override onlyBorrowerOps { require(msg.value > 0, "RB: no value"); totalBufferedColl = totalBufferedColl.add(msg.value); } + // --------------------------------------------------------------------- + // Outflows used during redemption execution + // --------------------------------------------------------------------- + + /** + * @notice Withdraw collateral from the buffer to fulfill a redemption payment or fee payment. + * + * @dev + * - Only TroveManager may call this. In your updated TroveManagerRedeemOps flow, + * this is used to: + * * send net RBTC to redeemers for the "buffer portion" of a redemption, and/or + * * send the "buffer fee" portion to FeeDistributor. + * - Uses low-level call to forward ETH/RBTC and bubble success/failure. + * + * Security / correctness: + * - Checks buffer sufficiency first. + * - Updates state (totalBufferedColl) before external call (CEI pattern). + * - Reentrancy considerations: + * * Direct reentry into this function is blocked unless msg.sender == troveManager. + * * However, the recipient `_to` could be a contract and could call TroveManager again, + * potentially causing another withdraw call. The system generally relies on TroveManager’s + * own execution invariants / reentrancy protections to make this safe. + */ function withdrawForRedemption(address payable _to, uint256 _amount) external override onlyTroveManager { require(_amount <= totalBufferedColl, "RB: insufficient buffer"); + + // Effects: decrement internal accounting first. totalBufferedColl = totalBufferedColl.sub(_amount); + // Interaction: send RBTC/ETH out. (bool success, ) = _to.call{ value: _amount }(""); require(success, "RB: send failed"); } + // --------------------------------------------------------------------- + // Optional outflow to stakers / FeeDistributor + // --------------------------------------------------------------------- + + /** + * @notice Push a portion of the buffer collateral into FeeDistributor, then trigger distribution. + * + * @dev + * - onlyOwner: this is an admin/governance action, not a redemption action. + * - This function is useful if the system wants to periodically route buffer collateral + * to stakers (or other fee recipients) without waiting for redemptions. + * + * Requirements: + * - feeDistributor must be configured + * - `_amount` must be available in the buffer + * + * Flow: + * 1) decrement buffer accounting + * 2) send ETH/RBTC to feeDistributor (low-level call) + * 3) call feeDistributor.distributeFees() + * + * NOTE: + * - The low-level call sends plain ETH. FeeDistributor must have a receive/fallback + * that accepts ETH; otherwise this reverts. + * - The comment “With the FeeDistributor patch above, this can now succeed.” + * indicates FeeDistributor was modified to properly accept ETH and/or distribution semantics. + */ function distributeToStakers(uint256 _amount) external override onlyOwner { require(address(feeDistributor) != address(0), "RB: feeDistributor not set"); require(_amount <= totalBufferedColl, "RB: insufficient buffer"); + // Effects: decrement internal accounting first. totalBufferedColl = totalBufferedColl.sub(_amount); + // Interaction: transfer RBTC/ETH to FeeDistributor. (bool success, ) = address(feeDistributor).call{ value: _amount }(""); require(success, "RB: send to feeDistributor failed"); @@ -69,14 +223,52 @@ contract RedemptionBuffer is Ownable, IRedemptionBuffer { feeDistributor.distributeFees(); } + // --------------------------------------------------------------------- + // Admin maintenance / accounting reconciliation + // --------------------------------------------------------------------- + + /** + * @notice Force internal accounting to match actual on-chain balance. + * @dev onlyOwner. + * + * Why this exists: + * - ETH can be forced into a contract without triggering receive() + * (e.g. via SELFDESTRUCT). In such a case, address(this).balance increases + * but totalBufferedColl does not, causing getBalance() to underreport. + * + * When to use: + * - Typically only during migrations, after unexpected forced transfers, + * or as part of operational “sanity repair” procedures. + */ function syncBalance() external onlyOwner { totalBufferedColl = address(this).balance; } + // --------------------------------------------------------------------- + // Views + // --------------------------------------------------------------------- + + /** + * @notice Returns the buffer’s tracked collateral balance. + * @dev This returns totalBufferedColl, not necessarily address(this).balance (see syncBalance()). + */ function getBalance() external view override returns (uint256) { return totalBufferedColl; } + // --------------------------------------------------------------------- + // Fallback receive hook + // --------------------------------------------------------------------- + + /** + * @notice Accept direct ETH/RBTC transfers. + * + * @dev + * - This is intentionally NOT restricted. Anyone can top up the buffer, + * which can only help the system (it increases available redemption liquidity). + * - The accounting variable is incremented by msg.value. + * - If ETH is forced in (SELFDESTRUCT), this function is not executed; use syncBalance(). + */ receive() external payable { totalBufferedColl = totalBufferedColl.add(msg.value); } From 3fb26522e1054b846b78126abf868ee367365499 Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Mon, 22 Dec 2025 02:49:19 -0500 Subject: [PATCH 08/13] Reworked redemption buffer fee to be on top of collateral --- contracts/FeeDistributor.sol | 13 +++++++++---- contracts/Proxy/BorrowerOperationsScript.sol | 12 ++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/contracts/FeeDistributor.sol b/contracts/FeeDistributor.sol index 433ba5f..34a913e 100644 --- a/contracts/FeeDistributor.sol +++ b/contracts/FeeDistributor.sol @@ -9,6 +9,7 @@ import "./Dependencies/SafeMath.sol"; contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor { using SafeMath for uint256; + // --- Events --- event FeeSharingCollectorAddressChanged(address _feeSharingCollectorAddress); event ZeroStakingAddressChanged(address _zeroStakingAddress); @@ -23,6 +24,8 @@ contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor event RedemptionBufferAddressChanged(address _redemptionBufferAddress); + // --- Dependency setters --- + function setAddresses( address _feeSharingCollectorAddress, address _zeroStakingAddress, @@ -111,13 +114,15 @@ contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor } function _distributeZUSD(uint256 toDistribute) internal { + // Send fee to the FeeSharingCollector address uint256 feeToFeeSharingCollector = toDistribute.mul(FEE_TO_FEE_SHARING_COLLECTOR).div( LiquityMath.DECIMAL_PRECISION ); - zusdToken.approve(address(feeSharingCollector), feeToFeeSharingCollector); - feeSharingCollector.transferTokens(address(zusdToken), uint96(feeToFeeSharingCollector)); + feeSharingCollector.transferTokens(address(zusdToken), uint96(feeToFeeSharingCollector)); + + // Send fee to ZERO staking contract uint256 feeToZeroStaking = toDistribute.sub(feeToFeeSharingCollector); if (feeToZeroStaking != 0) { require( @@ -126,24 +131,24 @@ contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor ); zeroStaking.increaseF_ZUSD(feeToZeroStaking); } - emit ZUSDDistributed(toDistribute); } function _distributeRBTC(uint256 toDistribute) internal { + // Send fee to the feeSharingCollector address uint256 feeToFeeSharingCollector = toDistribute.mul(FEE_TO_FEE_SHARING_COLLECTOR).div( LiquityMath.DECIMAL_PRECISION ); feeSharingCollector.transferRBTC{ value: feeToFeeSharingCollector }(); + // Send the ETH fee to the ZERO staking contract uint256 feeToZeroStaking = toDistribute.sub(feeToFeeSharingCollector); if (feeToZeroStaking != 0) { (bool success, ) = address(zeroStaking).call{ value: feeToZeroStaking }(""); require(success, "FeeDistributor: sending ETH failed"); zeroStaking.increaseF_ETH(feeToZeroStaking); } - emit RBTCistributed(toDistribute); } diff --git a/contracts/Proxy/BorrowerOperationsScript.sol b/contracts/Proxy/BorrowerOperationsScript.sol index e8a1d3e..e6fb40e 100644 --- a/contracts/Proxy/BorrowerOperationsScript.sol +++ b/contracts/Proxy/BorrowerOperationsScript.sol @@ -45,4 +45,16 @@ contract BorrowerOperationsScript is CheckContract { function claimCollateral() external { borrowerOperations.claimCollateral(); } + + function getRedemptionBufferFeeRBTC(uint256 _ZUSDAmount) external { + borrowerOperations.getRedemptionBufferFeeRBTC(_ZUSDAmount); + } + + function getRedemptionBufferFeeRBTCWithPrice(uint256 _ZUSDAmount, uint256 _price) external { + borrowerOperations.getRedemptionBufferFeeRBTCWithPrice(_ZUSDAmount, _price); + } + + function getRedemptionBufferRate() external { + borrowerOperations.getRedemptionBufferRate(); + } } From 3460d176ef8ed9e855bbd46d4819b6dbab06d653 Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Mon, 22 Dec 2025 13:55:15 -0500 Subject: [PATCH 09/13] Added reentrancy logic --- contracts/BorrowerOperations.sol | 48 +++++++++++++-------- contracts/BorrowerOperationsStorage.sol | 6 +++ contracts/Dependencies/TroveManagerBase.sol | 14 ++++++ contracts/TroveManager.sol | 42 ++++++++++-------- contracts/TroveManagerStorage.sol | 6 +++ 5 files changed, 80 insertions(+), 36 deletions(-) diff --git a/contracts/BorrowerOperations.sol b/contracts/BorrowerOperations.sol index fc570c1..560f817 100644 --- a/contracts/BorrowerOperations.sol +++ b/contracts/BorrowerOperations.sol @@ -27,6 +27,19 @@ contract BorrowerOperations is /** CONSTANT / IMMUTABLE VARIABLE ONLY */ IPermit2 public immutable permit2; + // --------------------------------------------------------------------- + // Reentrancy guard + // --------------------------------------------------------------------- + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + modifier nonReentrant() { + require(_reentrancyStatus != _ENTERED, "BorrowerOps: reentrant call"); + _reentrancyStatus = _ENTERED; + _; + _reentrancyStatus = _NOT_ENTERED; + } + /* --- Variable container structs --- Used to hold, return and assign variables inside a function, in order to avoid the error: @@ -101,6 +114,7 @@ contract BorrowerOperations is /** Constructor */ constructor(address _permit2) public { permit2 = IPermit2(_permit2); + _reentrancyStatus = _NOT_ENTERED; } // --- Dependency setters --- @@ -230,7 +244,7 @@ contract BorrowerOperations is uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external payable override { + ) external payable override nonReentrant { _openTrove(_maxFeePercentage, _ZUSDAmount, _upperHint, _lowerHint, msg.sender); } @@ -239,7 +253,7 @@ contract BorrowerOperations is uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external payable override { + ) external payable override nonReentrant { require(address(massetManager) != address(0), "Masset address not set"); _openTrove(_maxFeePercentage, _ZUSDAmount, _upperHint, _lowerHint, address(this)); @@ -381,7 +395,7 @@ contract BorrowerOperations is } /// Send ETH as collateral to a trove - function addColl(address _upperHint, address _lowerHint) external payable override { + function addColl(address _upperHint, address _lowerHint) external payable override nonReentrant { _adjustTrove(msg.sender, 0, 0, false, _upperHint, _lowerHint, 0); } @@ -390,7 +404,7 @@ contract BorrowerOperations is address _borrower, address _upperHint, address _lowerHint - ) external payable override { + ) external payable override nonReentrant { _requireCallerIsStabilityPool(); _adjustTrove(_borrower, 0, 0, false, _upperHint, _lowerHint, 0); } @@ -400,7 +414,7 @@ contract BorrowerOperations is uint256 _collWithdrawal, address _upperHint, address _lowerHint - ) external override { + ) external override nonReentrant { _adjustTrove(msg.sender, _collWithdrawal, 0, false, _upperHint, _lowerHint, 0); } @@ -410,7 +424,7 @@ contract BorrowerOperations is uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external payable override { + ) external payable override nonReentrant { _adjustTrove(msg.sender, 0, _ZUSDAmount, true, _upperHint, _lowerHint, _maxFeePercentage); } @@ -422,7 +436,7 @@ contract BorrowerOperations is uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external payable override returns (uint256) { + ) external payable override nonReentrant returns (uint256) { address thisAddress = address(this); uint256 balanceBefore = zusdToken.balanceOf(thisAddress); @@ -451,7 +465,7 @@ contract BorrowerOperations is uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external override { + ) external override nonReentrant { _adjustTrove(msg.sender, 0, _ZUSDAmount, false, _upperHint, _lowerHint, 0); } @@ -461,7 +475,7 @@ contract BorrowerOperations is address _upperHint, address _lowerHint, IMassetManager.PermitParams calldata _permitParams - ) external override { + ) external override nonReentrant { _adjustNueTrove(0, 0, _dllrAmount, false, _upperHint, _lowerHint, _permitParams); } @@ -472,7 +486,7 @@ contract BorrowerOperations is address _lowerHint, ISignatureTransfer.PermitTransferFrom memory _permit, bytes calldata _signature - ) external override { + ) external override nonReentrant { _adjustNueTroveWithPermit2(0, 0, _dllrAmount, false, _upperHint, _lowerHint, _permit, _signature); } @@ -483,7 +497,7 @@ contract BorrowerOperations is bool _isDebtIncrease, address _upperHint, address _lowerHint - ) external payable override { + ) external payable override nonReentrant { _adjustTrove( msg.sender, _collWithdrawal, @@ -504,7 +518,7 @@ contract BorrowerOperations is address _upperHint, address _lowerHint, IMassetManager.PermitParams calldata _permitParams - ) external payable override { + ) external payable override nonReentrant { _adjustNueTrove( _maxFeePercentage, _collWithdrawal, @@ -526,7 +540,7 @@ contract BorrowerOperations is address _lowerHint, ISignatureTransfer.PermitTransferFrom memory _permit, bytes calldata _signature - ) external payable override { + ) external payable override nonReentrant { _adjustNueTroveWithPermit2( _maxFeePercentage, _collWithdrawal, @@ -811,11 +825,11 @@ contract BorrowerOperations is } } - function closeTrove() external override { + function closeTrove() external override nonReentrant { _closeTrove(); } - function closeNueTrove(IMassetManager.PermitParams calldata _permitParams) external override { + function closeNueTrove(IMassetManager.PermitParams calldata _permitParams) external override nonReentrant { require(address(massetManager) != address(0), "Masset address not set"); uint256 debt = troveManager.getTroveDebt(msg.sender); @@ -829,7 +843,7 @@ contract BorrowerOperations is _closeTrove(); } - function closeNueTroveWithPermit2(ISignatureTransfer.PermitTransferFrom memory _permit, bytes calldata _signature) external override { + function closeNueTroveWithPermit2(ISignatureTransfer.PermitTransferFrom memory _permit, bytes calldata _signature) external override nonReentrant { require(address(massetManager) != address(0), "Masset address not set"); uint256 debt = troveManager.getTroveDebt(msg.sender); @@ -894,7 +908,7 @@ contract BorrowerOperations is /** * Claim remaining collateral from a redemption or from a liquidation with ICR > MCR in Recovery Mode */ - function claimCollateral() external override { + function claimCollateral() external override nonReentrant { // send ETH from CollSurplus Pool to owner collSurplusPool.claimColl(msg.sender); } diff --git a/contracts/BorrowerOperationsStorage.sol b/contracts/BorrowerOperationsStorage.sol index 5f00a70..abadea5 100644 --- a/contracts/BorrowerOperationsStorage.sol +++ b/contracts/BorrowerOperationsStorage.sol @@ -46,4 +46,10 @@ contract BorrowerOperationsStorage is Ownable { // Fraction of incoming RBTC sent to the buffer on openTrove. // 1e18 == 100%, e.g. 1e17 == 10%. uint256 internal redemptionBufferRate; + + // --------------------------------------------------------------------- + // Reentrancy guard (BorrowerOperations only) + // --------------------------------------------------------------------- + // 0 = uninitialized, 1 = not entered, 2 = entered + uint256 internal _reentrancyStatus; } diff --git a/contracts/Dependencies/TroveManagerBase.sol b/contracts/Dependencies/TroveManagerBase.sol index b3ee6c4..9cc111f 100644 --- a/contracts/Dependencies/TroveManagerBase.sol +++ b/contracts/Dependencies/TroveManagerBase.sol @@ -25,6 +25,19 @@ contract TroveManagerBase is LiquityBase, TroveManagerStorage { */ uint256 public constant BETA = 2; + // --------------------------------------------------------------------- + // Reentrancy guard + // --------------------------------------------------------------------- + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + modifier nonReentrant() { + require(_reentrancyStatus != _ENTERED, "TroveManager: reentrant call"); + _reentrancyStatus = _ENTERED; + _; + _reentrancyStatus = _NOT_ENTERED; + } + /** --- Variable container structs for liquidations --- @@ -152,6 +165,7 @@ contract TroveManagerBase is LiquityBase, TroveManagerStorage { constructor(uint256 _bootstrapPeriod) public { BOOTSTRAP_PERIOD = _bootstrapPeriod; + _reentrancyStatus = _NOT_ENTERED; // init guard } /// Return the current collateral ratio (ICR) of a given Trove. Takes a trove's pending coll and debt rewards from redistributions into account. diff --git a/contracts/TroveManager.sol b/contracts/TroveManager.sol index d101404..61a2437 100644 --- a/contracts/TroveManager.sol +++ b/contracts/TroveManager.sol @@ -140,12 +140,12 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { // --- Trove Liquidation functions --- /// Single liquidation function. Closes the trove if its ICR is lower than the minimum collateral ratio. - function liquidate(address _borrower) external override { + function liquidate(address _borrower) external override nonReentrant { _requireTroveIsActive(_borrower); address[] memory borrowers = new address[](1); borrowers[0] = _borrower; - batchLiquidateTroves(borrowers); + _batchLiquidateTroves(borrowers); } // --- Inner single liquidation functions --- @@ -399,7 +399,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { * Liquidate a sequence of troves. Closes a maximum number of n under-collateralized Troves, * starting from the one with the lowest collateral ratio in the system, and moving upwards */ - function liquidateTroves(uint256 _n) external override { + function liquidateTroves(uint256 _n) external override nonReentrant { ContractsCache memory contractsCache = ContractsCache( activePool, defaultPool, @@ -602,7 +602,11 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { /** * Attempt to liquidate a custom list of troves provided by the caller. */ - function batchLiquidateTroves(address[] memory _troveArray) public override { + function batchLiquidateTroves(address[] memory _troveArray) public override nonReentrant { + _batchLiquidateTroves(_troveArray); + } + + function _batchLiquidateTroves(address[] memory _troveArray) internal { require(_troveArray.length != 0, "TroveManager: Calldata address array must not be empty"); IActivePool activePoolCached = activePool; @@ -853,13 +857,13 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { return NICR; } - function applyPendingRewards(address _borrower) external override { + function applyPendingRewards(address _borrower) external override nonReentrant { _requireCallerIsBorrowerOperations(); return _applyPendingRewards(activePool, defaultPool, _borrower); } /// Update borrower's snapshots of L_ETH and L_ZUSDDebt to reflect the current values - function updateTroveRewardSnapshots(address _borrower) external override { + function updateTroveRewardSnapshots(address _borrower) external override nonReentrant { _requireCallerIsBorrowerOperations(); return _updateTroveRewardSnapshots(_borrower); } @@ -888,12 +892,12 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { coll = coll.add(pendingETHReward); } - function removeStake(address _borrower) external override { + function removeStake(address _borrower) external override nonReentrant { _requireCallerIsBorrowerOperations(); return _removeStake(_borrower); } - function updateStakeAndTotalStakes(address _borrower) external override returns (uint256) { + function updateStakeAndTotalStakes(address _borrower) external override nonReentrant returns (uint256) { _requireCallerIsBorrowerOperations(); return _updateStakeAndTotalStakes(_borrower); } @@ -945,7 +949,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { _activePool.sendETH(address(_defaultPool), _coll); } - function closeTrove(address _borrower) external override { + function closeTrove(address _borrower) external override nonReentrant { _requireCallerIsBorrowerOperations(); return _closeTrove(_borrower, Status.closedByOwner); } @@ -974,7 +978,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { } /// Push the owner's address to the Trove owners list, and record the corresponding array index on the Trove struct - function addTroveOwnerToArray(address _borrower) external override returns (uint256 index) { + function addTroveOwnerToArray(address _borrower) external override nonReentrant returns (uint256 index) { _requireCallerIsBorrowerOperations(); return _addTroveOwnerToArray(_borrower); } @@ -1066,7 +1070,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { } /// Updates the baseRate state variable based on time elapsed since the last redemption or ZUSD borrowing operation. - function decayBaseRateFromBorrowing() external override { + function decayBaseRateFromBorrowing() external override nonReentrant { _requireCallerIsBorrowerOperations(); uint256 decayedBaseRate = _calcDecayedBaseRate(); @@ -1100,7 +1104,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { // --- Trove property setters, called by BorrowerOperations --- - function setTroveStatus(address _borrower, uint256 _num) external override { + function setTroveStatus(address _borrower, uint256 _num) external override nonReentrant { _requireCallerIsBorrowerOperations(); Troves[_borrower].status = Status(_num); } @@ -1108,7 +1112,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function increaseTroveColl( address _borrower, uint256 _collIncrease - ) external override returns (uint256) { + ) external override nonReentrant returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newColl = Troves[_borrower].coll.add(_collIncrease); Troves[_borrower].coll = newColl; @@ -1118,7 +1122,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function decreaseTroveColl( address _borrower, uint256 _collDecrease - ) external override returns (uint256) { + ) external override nonReentrant returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newColl = Troves[_borrower].coll.sub(_collDecrease); Troves[_borrower].coll = newColl; @@ -1128,7 +1132,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function increaseTroveDebt( address _borrower, uint256 _debtIncrease - ) external override returns (uint256) { + ) external override nonReentrant returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newDebt = Troves[_borrower].debt.add(_debtIncrease); Troves[_borrower].debt = newDebt; @@ -1138,7 +1142,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function decreaseTroveDebt( address _borrower, uint256 _debtDecrease - ) external override returns (uint256) { + ) external override nonReentrant returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newDebt = Troves[_borrower].debt.sub(_debtDecrease); Troves[_borrower].debt = newDebt; @@ -1178,7 +1182,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { uint256 _partialRedemptionHintNICR, uint256 _maxIterations, uint256 _maxFeePercentage - ) external override { + ) external override nonReentrant { (bool success, bytes memory returndata) = troveManagerRedeemOps.delegatecall(msg.data); require(success, string(returndata)); } @@ -1195,7 +1199,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { uint256 _maxIterations, uint256 _maxFeePercentage, IMassetManager.PermitParams calldata _permitParams - ) external override { + ) external override nonReentrant { (bool success, bytes memory returndata) = troveManagerRedeemOps.delegatecall(msg.data); require(success, string(returndata)); } @@ -1213,7 +1217,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { uint256 _maxFeePercentage, ISignatureTransfer.PermitTransferFrom memory _permit, bytes calldata _signature - ) external override { + ) external override nonReentrant { (bool success, bytes memory returndata) = troveManagerRedeemOps.delegatecall(msg.data); require(success, string(returndata)); } diff --git a/contracts/TroveManagerStorage.sol b/contracts/TroveManagerStorage.sol index d540adf..49a0011 100644 --- a/contracts/TroveManagerStorage.sol +++ b/contracts/TroveManagerStorage.sol @@ -103,4 +103,10 @@ contract TroveManagerStorage is Ownable, BaseMath { // Redemption buffer used to serve ZUSD redemptions before touching troves IRedemptionBuffer internal redemptionBuffer; + + // --------------------------------------------------------------------- + // Reentrancy guard (shared across TroveManager + delegatecall modules) + // --------------------------------------------------------------------- + // 0 = uninitialized, 1 = not entered, 2 = entered + uint256 internal _reentrancyStatus; } From 6127122df63997ce9f7284de42c8ee97093c86af Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Mon, 22 Dec 2025 14:47:34 -0500 Subject: [PATCH 10/13] Added reentrancy logic --- contracts/Dependencies/TroveManagerBase.sol | 5 ++++ contracts/TroveManager.sol | 33 +++++++++++---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/contracts/Dependencies/TroveManagerBase.sol b/contracts/Dependencies/TroveManagerBase.sol index 9cc111f..5121481 100644 --- a/contracts/Dependencies/TroveManagerBase.sol +++ b/contracts/Dependencies/TroveManagerBase.sol @@ -38,6 +38,11 @@ contract TroveManagerBase is LiquityBase, TroveManagerStorage { _reentrancyStatus = _NOT_ENTERED; } + modifier requireNotEntered() { + require(_reentrancyStatus != _ENTERED, "TroveManager: locked"); + _; + } + /** --- Variable container structs for liquidations --- diff --git a/contracts/TroveManager.sol b/contracts/TroveManager.sol index 61a2437..9d84a6f 100644 --- a/contracts/TroveManager.sol +++ b/contracts/TroveManager.sol @@ -602,7 +602,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { /** * Attempt to liquidate a custom list of troves provided by the caller. */ - function batchLiquidateTroves(address[] memory _troveArray) public override nonReentrant { + function batchLiquidateTroves(address[] calldata _troveArray) external override nonReentrant { _batchLiquidateTroves(_troveArray); } @@ -734,9 +734,10 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { singleLiquidation.debtToOffset ); vars.entireSystemDebt = vars.entireSystemDebt.sub(singleLiquidation.debtToOffset); - vars.entireSystemColl = vars.entireSystemColl.sub( - singleLiquidation.collToSendToSP - ); + // FIX: match behavior in _getTotalsFromLiquidateTrovesSequence_RecoveryMode() by subtracting collSurplus + vars.entireSystemColl = vars.entireSystemColl + .sub(singleLiquidation.collToSendToSP) + .sub(singleLiquidation.collSurplus); // Add liquidation values to their respective running totals totals = _addLiquidationValuesToTotals(totals, singleLiquidation); @@ -857,13 +858,13 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { return NICR; } - function applyPendingRewards(address _borrower) external override nonReentrant { + function applyPendingRewards(address _borrower) external override requireNotEntered { _requireCallerIsBorrowerOperations(); return _applyPendingRewards(activePool, defaultPool, _borrower); } /// Update borrower's snapshots of L_ETH and L_ZUSDDebt to reflect the current values - function updateTroveRewardSnapshots(address _borrower) external override nonReentrant { + function updateTroveRewardSnapshots(address _borrower) external override requireNotEntered { _requireCallerIsBorrowerOperations(); return _updateTroveRewardSnapshots(_borrower); } @@ -892,12 +893,12 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { coll = coll.add(pendingETHReward); } - function removeStake(address _borrower) external override nonReentrant { + function removeStake(address _borrower) external override requireNotEntered { _requireCallerIsBorrowerOperations(); return _removeStake(_borrower); } - function updateStakeAndTotalStakes(address _borrower) external override nonReentrant returns (uint256) { + function updateStakeAndTotalStakes(address _borrower) external override requireNotEntered returns (uint256) { _requireCallerIsBorrowerOperations(); return _updateStakeAndTotalStakes(_borrower); } @@ -949,7 +950,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { _activePool.sendETH(address(_defaultPool), _coll); } - function closeTrove(address _borrower) external override nonReentrant { + function closeTrove(address _borrower) external override requireNotEntered { _requireCallerIsBorrowerOperations(); return _closeTrove(_borrower, Status.closedByOwner); } @@ -978,7 +979,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { } /// Push the owner's address to the Trove owners list, and record the corresponding array index on the Trove struct - function addTroveOwnerToArray(address _borrower) external override nonReentrant returns (uint256 index) { + function addTroveOwnerToArray(address _borrower) external override requireNotEntered returns (uint256 index) { _requireCallerIsBorrowerOperations(); return _addTroveOwnerToArray(_borrower); } @@ -1070,7 +1071,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { } /// Updates the baseRate state variable based on time elapsed since the last redemption or ZUSD borrowing operation. - function decayBaseRateFromBorrowing() external override nonReentrant { + function decayBaseRateFromBorrowing() external override requireNotEntered { _requireCallerIsBorrowerOperations(); uint256 decayedBaseRate = _calcDecayedBaseRate(); @@ -1104,7 +1105,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { // --- Trove property setters, called by BorrowerOperations --- - function setTroveStatus(address _borrower, uint256 _num) external override nonReentrant { + function setTroveStatus(address _borrower, uint256 _num) external override requireNotEntered { _requireCallerIsBorrowerOperations(); Troves[_borrower].status = Status(_num); } @@ -1112,7 +1113,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function increaseTroveColl( address _borrower, uint256 _collIncrease - ) external override nonReentrant returns (uint256) { + ) external override requireNotEntered returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newColl = Troves[_borrower].coll.add(_collIncrease); Troves[_borrower].coll = newColl; @@ -1122,7 +1123,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function decreaseTroveColl( address _borrower, uint256 _collDecrease - ) external override nonReentrant returns (uint256) { + ) external override requireNotEntered returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newColl = Troves[_borrower].coll.sub(_collDecrease); Troves[_borrower].coll = newColl; @@ -1132,7 +1133,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function increaseTroveDebt( address _borrower, uint256 _debtIncrease - ) external override nonReentrant returns (uint256) { + ) external override requireNotEntered returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newDebt = Troves[_borrower].debt.add(_debtIncrease); Troves[_borrower].debt = newDebt; @@ -1142,7 +1143,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function decreaseTroveDebt( address _borrower, uint256 _debtDecrease - ) external override nonReentrant returns (uint256) { + ) external override requireNotEntered returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newDebt = Troves[_borrower].debt.sub(_debtDecrease); Troves[_borrower].debt = newDebt; From d16608b23ef66961ebe098adb90476ae32da981d Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Wed, 24 Dec 2025 13:27:51 -0500 Subject: [PATCH 11/13] Tests & TroveManagerBase fix --- contracts/BorrowerOperations.sol | 4 +- contracts/Dependencies/TroveManagerBase.sol | 3 + contracts/Interfaces/IBorrowerOperations.sol | 4 +- contracts/Interfaces/ITroveManager.sol | 2 +- contracts/TroveManager.sol | 2 +- tests/js/BorrowerOperationsTest.js | 1228 +++++++++++------ tests/js/CollSurplusPool.js | 17 +- tests/js/ProxyBorrowerWrappersScript.js | 66 +- tests/js/StabilityPoolTest.js | 4 +- tests/js/StabilityPool_SPWithdrawalTest.js | 188 +-- .../js/StabilityPool_SPWithdrawalToCDPTest.js | 165 ++- tests/js/TroveManagerTest.js | 288 +++- tests/js/TroveManager_RecoveryModeTest.js | 18 +- utils/js/deploymentHelpers.js | 18 + utils/js/proxyHelpers.js | 4 + utils/js/testHelpers.js | 727 +++++++++- utils/js/truffleDeploymentHelpers.js | 14 +- 17 files changed, 2100 insertions(+), 652 deletions(-) diff --git a/contracts/BorrowerOperations.sol b/contracts/BorrowerOperations.sol index 560f817..b583427 100644 --- a/contracts/BorrowerOperations.sol +++ b/contracts/BorrowerOperations.sol @@ -181,7 +181,7 @@ contract BorrowerOperations is emit MassetManagerAddressChanged(_massetManagerAddress); } - function setRedemptionBuffer(address _buffer) external override onlyOwner { + function setRedemptionBufferAddress(address _buffer) external override onlyOwner { require(_buffer != address(0), "BorrowerOps: zero buffer address"); checkContract(_buffer); redemptionBuffer = IRedemptionBuffer(_buffer); @@ -198,7 +198,7 @@ contract BorrowerOperations is } /// @notice Returns the configured RedemptionBuffer contract. - function getRedemptionBuffer() external view override returns (address) { + function getRedemptionBufferAddress() external view override returns (address) { return address(redemptionBuffer); } diff --git a/contracts/Dependencies/TroveManagerBase.sol b/contracts/Dependencies/TroveManagerBase.sol index 5121481..46f239f 100644 --- a/contracts/Dependencies/TroveManagerBase.sol +++ b/contracts/Dependencies/TroveManagerBase.sol @@ -370,6 +370,9 @@ contract TroveManagerBase is LiquityBase, TroveManagerStorage { uint256 _redemptionRate, uint256 _ETHDrawn ) internal pure returns (uint256) { + if (_ETHDrawn == 0) { + return 0; // This prevents a revert if for instance no eth is drawn from the redemption buffer or no eth is drawn from troves. + } uint256 redemptionFee = _redemptionRate.mul(_ETHDrawn).div(DECIMAL_PRECISION); require( redemptionFee < _ETHDrawn, diff --git a/contracts/Interfaces/IBorrowerOperations.sol b/contracts/Interfaces/IBorrowerOperations.sol index 369df8d..8507cf8 100644 --- a/contracts/Interfaces/IBorrowerOperations.sol +++ b/contracts/Interfaces/IBorrowerOperations.sol @@ -86,14 +86,14 @@ interface IBorrowerOperations { /// @notice Sets the RedemptionBuffer contract address. /// @dev Owner/governance only in implementation. - function setRedemptionBuffer(address _buffer) external; + function setRedemptionBufferAddress(address _buffer) external; /// @notice Sets the redemption buffer rate used to compute the RBTC fee. /// @dev 1e18 precision; 1e18 == 100%. Owner/governance only in implementation. function setRedemptionBufferRate(uint256 _rate) external; /// @notice Returns the configured RedemptionBuffer contract address. - function getRedemptionBuffer() external view returns (address); + function getRedemptionBufferAddress() external view returns (address); /// @notice Returns the configured redemption buffer rate (1e18 precision). function getRedemptionBufferRate() external view returns (uint256); diff --git a/contracts/Interfaces/ITroveManager.sol b/contracts/Interfaces/ITroveManager.sol index 70e7da2..2ac602f 100644 --- a/contracts/Interfaces/ITroveManager.sol +++ b/contracts/Interfaces/ITroveManager.sol @@ -113,7 +113,7 @@ interface ITroveManager is ILiquityBase { /// @notice Configures the RedemptionBuffer contract address used during redemptions. /// @dev Owner/governance only in implementation. - function setRedemptionBuffer(address _buffer) external; + function setRedemptionBufferAddress(address _buffer) external; /// @return Trove owners count function getTroveOwnersCount() external view returns (uint256); diff --git a/contracts/TroveManager.sol b/contracts/TroveManager.sol index 9d84a6f..118364c 100644 --- a/contracts/TroveManager.sol +++ b/contracts/TroveManager.sol @@ -119,7 +119,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { } // --- Redemption buffer config --- - function setRedemptionBuffer(address _buffer) external override onlyOwner { + function setRedemptionBufferAddress(address _buffer) external override onlyOwner { require(_buffer != address(0), "TroveManager: zero buffer address"); checkContract(_buffer); redemptionBuffer = IRedemptionBuffer(_buffer); diff --git a/tests/js/BorrowerOperationsTest.js b/tests/js/BorrowerOperationsTest.js index 0fbb096..2be3339 100644 --- a/tests/js/BorrowerOperationsTest.js +++ b/tests/js/BorrowerOperationsTest.js @@ -71,6 +71,7 @@ contract("BorrowerOperations", async accounts => { let massetManager; let nueMockToken; let permit2; + let redemptionBuffer; let contracts; @@ -81,6 +82,9 @@ contract("BorrowerOperations", async accounts => { th.getActualDebtFromComposite(compositeDebt, contracts); const openTrove = async params => th.openTrove(contracts, params); const openNueTrove = async params => th.openNueTrove(contracts, params); + const withdrawZUSD = async params => th.withdrawZUSD(contracts, params); + const adjustTrove = async params => th.adjustTrove(contracts, params); + const adjustNueTrove = async params => th.adjustNueTrove(contracts, params); const getTroveEntireColl = async trove => th.getTroveEntireColl(contracts, trove); const getTroveEntireDebt = async trove => th.getTroveEntireDebt(contracts, trove); const getTroveStake = async trove => th.getTroveStake(contracts, trove); @@ -167,6 +171,7 @@ contract("BorrowerOperations", async accounts => { borrowerOperations = contracts.borrowerOperations; massetManager = contracts.massetManager; //hintHelpers = contracts.hintHelpers; + redemptionBuffer = contracts.redemptionBuffer; zeroStaking = ZEROContracts.zeroStaking; zeroToken = ZEROContracts.zeroToken; @@ -213,7 +218,7 @@ contract("BorrowerOperations", async accounts => { }); it("addColl(): Increases the activePool ETH and raw ether balance by correct amount", async () => { - const { collateral: aliceColl } = await openTrove({ + const { collateral: aliceColl, bufferFee: aliceBufferFee } = await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: alice } }); @@ -230,6 +235,8 @@ contract("BorrowerOperations", async accounts => { const activePool_RawEther_After = toBN(await web3.eth.getBalance(activePool.address)); assert.isTrue(activePool_ETH_After.eq(aliceColl.add(toBN(dec(1, 16))))); assert.isTrue(activePool_RawEther_After.eq(aliceColl.add(toBN(dec(1, 16))))); + const bufferBal = toBN(await redemptionBuffer.getBalance()); + assert.isTrue(bufferBal.eq(aliceBufferFee)); }); it("addColl(), active Trove: adds the correct collateral amount to the Trove", async () => { @@ -895,8 +902,14 @@ contract("BorrowerOperations", async accounts => { // 2 hours pass th.fastForwardTime(7200, web3.currentProvider); - // D withdraws ZUSD - await borrowerOperations.withdrawZUSD(th._100pct, dec(1, 16), A, A, { from: D }); + // D withdraws ZUSD (helper will attach the RBTC buffer fee as msg.value) + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(1, 16), + upperHint: A, + lowerHint: A, + extraParams: { from: D } + }); // Check baseRate has decreased const baseRate_2 = await troveManager.baseRate(); @@ -906,7 +919,13 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(3600, web3.currentProvider); // E withdraws ZUSD - await borrowerOperations.withdrawZUSD(th._100pct, dec(1, 16), A, A, { from: E }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(1, 16), + upperHint: A, + lowerHint: A, + extraParams: { from: E } + }); const baseRate_3 = await troveManager.baseRate(); assert.isTrue(baseRate_3.lt(baseRate_2)); @@ -1095,34 +1114,62 @@ contract("BorrowerOperations", async accounts => { // Attempt with maxFee > 5% const moreThan5pct = "50000000000000001"; - const tx1 = await borrowerOperations.withdrawZUSD(moreThan5pct, dec(1, 16), A, A, { from: A }); + const { tx: tx1 } = await withdrawZUSD({ + maxFeePercentage: moreThan5pct, + zusdAmount: dec(1, 16), + upperHint: A, + lowerHint: A, + extraParams: { from: A } + }); assert.isTrue(tx1.receipt.status); baseRate = await troveManager.baseRate(); // expect 5% base rate assert.equal(baseRate, dec(5, 16)); // Attempt with maxFee = 5% - const tx2 = await borrowerOperations.withdrawZUSD(dec(5, 16), dec(1, 16), A, A, { from: B }); + const { tx: tx2 } = await withdrawZUSD({ + maxFeePercentage: dec(5, 16), + zusdAmount: dec(1, 16), + upperHint: A, + lowerHint: A, + extraParams: { from: B } + }); assert.isTrue(tx2.receipt.status); baseRate = await troveManager.baseRate(); // expect 5% base rate assert.equal(baseRate, dec(5, 16)); // Attempt with maxFee 10% - const tx3 = await borrowerOperations.withdrawZUSD(dec(1, 17), dec(1, 16), A, A, { from: C }); + const { tx: tx3 } = await withdrawZUSD({ + maxFeePercentage: dec(1, 17), + zusdAmount: dec(1, 16), + upperHint: A, + lowerHint: A, + extraParams: { from: C } + }); assert.isTrue(tx3.receipt.status); baseRate = await troveManager.baseRate(); // expect 5% base rate assert.equal(baseRate, dec(5, 16)); // Attempt with maxFee 37.659% - const tx4 = await borrowerOperations.withdrawZUSD(dec(37659, 13), dec(1, 16), A, A, { - from: D + const { tx: tx4 } = await withdrawZUSD({ + maxFeePercentage: dec(37659, 13), + zusdAmount: dec(1, 16), + upperHint: A, + lowerHint: A, + extraParams: { from: D } }); assert.isTrue(tx4.receipt.status); // Attempt with maxFee 100% - const tx5 = await borrowerOperations.withdrawZUSD(dec(1, 18), dec(1, 16), A, A, { from: E }); + const { tx: tx5 } = await withdrawZUSD({ + maxFeePercentage: dec(1, 18), + zusdAmount: dec(1, 16), + upperHint: A, + lowerHint: A, + extraParams: { from: E } + }); assert.isTrue(tx5.receipt.status); }); @@ -1163,7 +1210,13 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(7200, web3.currentProvider); // D withdraws ZUSD - await borrowerOperations.withdrawZUSD(th._100pct, dec(37, 16), A, A, { from: D }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(37, 16), + upperHint: A, + lowerHint: A, + extraParams: { from: D } + }); // Check baseRate is still 0 const baseRate_2 = await troveManager.baseRate(); @@ -1173,7 +1226,13 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(3600, web3.currentProvider); // E opens trove - await borrowerOperations.withdrawZUSD(th._100pct, dec(12, 16), A, A, { from: E }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(12, 16), + upperHint: A, + lowerHint: A, + extraParams: { from: E } + }); const baseRate_3 = await troveManager.baseRate(); assert.equal(baseRate_3, "0"); @@ -1212,7 +1271,13 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(10, web3.currentProvider); // Borrower C triggers a fee - await borrowerOperations.withdrawZUSD(th._100pct, dec(1, 16), C, C, { from: C }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(1, 16), + upperHint: C, + lowerHint: C, + extraParams: { from: C } + }); const lastFeeOpTime_2 = await troveManager.lastFeeOperationTime(); @@ -1228,7 +1293,13 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(toBN(timeNow).sub(lastFeeOpTime_1).gte(60)); // Borrower C triggers a fee - await borrowerOperations.withdrawZUSD(th._100pct, dec(1, 16), C, C, { from: C }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(1, 16), + upperHint: C, + lowerHint: C, + extraParams: { from: C } + }); const lastFeeOpTime_3 = await troveManager.lastFeeOperationTime(); @@ -1267,13 +1338,25 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(30, web3.currentProvider); // Borrower C triggers a fee, before decay interval has passed - await borrowerOperations.withdrawZUSD(th._100pct, dec(1, 16), C, C, { from: C }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(1, 16), + upperHint: C, + lowerHint: C, + extraParams: { from: C } + }); // 30 seconds pass th.fastForwardTime(30, web3.currentProvider); // Borrower C triggers another fee - await borrowerOperations.withdrawZUSD(th._100pct, dec(1, 16), C, C, { from: C }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(1, 16), + upperHint: C, + lowerHint: C, + extraParams: { from: C } + }); // Check base rate has decreased even though Borrower tried to stop it decaying const baseRate_2 = await troveManager.baseRate(); @@ -1324,7 +1407,13 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(7200, web3.currentProvider); // D withdraws ZUSD - await borrowerOperations.withdrawZUSD(th._100pct, dec(37, 16), C, C, { from: D }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(37, 16), + upperHint: C, + lowerHint: C, + extraParams: { from: D } + }); // All the fees are sent to SOV holders const zeroStaking_ZUSDBalance_After = await zusdToken.balanceOf(zeroStaking.address); @@ -1375,13 +1464,13 @@ contract("BorrowerOperations", async accounts => { // D withdraws ZUSD const withdrawal_D = toBN(dec(37, 16)); - const withdrawalTx = await borrowerOperations.withdrawZUSD( - th._100pct, - toBN(dec(37, 16)), - D, - D, - { from: D } - ); + const { tx: withdrawalTx } = await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: withdrawal_D, + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); const emittedFee = toBN(th.getZUSDFeeFromZUSDBorrowingEvent(withdrawalTx)); assert.isTrue(emittedFee.gt(toBN("0"))); @@ -1441,7 +1530,13 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(7200, web3.currentProvider); // D withdraws ZUSD - await borrowerOperations.withdrawZUSD(th._100pct, toBN(dec(37, 16)), D, D, { from: D }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: toBN(dec(37, 16)), + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); // Check ZERO contract ZUSD fees-per-unit-staked hasn't increased const F_ZUSD_After = await zeroStaking.F_ZUSD(); @@ -1495,7 +1590,13 @@ contract("BorrowerOperations", async accounts => { // D withdraws ZUSD const D_ZUSDRequest = toBN(dec(37, 18)); - await borrowerOperations.withdrawZUSD(th._100pct, D_ZUSDRequest, D, D, { from: D }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: D_ZUSDRequest, + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); // All the fees are sent to SOV holders const zeroStaking_ZUSDBalance_After = await zusdToken.balanceOf(zeroStaking.address); @@ -1545,7 +1646,13 @@ contract("BorrowerOperations", async accounts => { assert.equal(F_ZUSD_Before, "0"); // D withdraws ZUSD - await borrowerOperations.withdrawZUSD(th._100pct, dec(37, 16), D, D, { from: D }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(37, 16), + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); // Check ZERO ZUSD balance after > 0 const F_ZUSD_After = await zeroStaking.F_ZUSD(); @@ -1586,7 +1693,13 @@ contract("BorrowerOperations", async accounts => { // D withdraws ZUSD const D_ZUSDRequest = toBN(dec(37, 16)); - await borrowerOperations.withdrawZUSD(th._100pct, dec(37, 16), D, D, { from: D }); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(37, 16), + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); // Check D's ZUSD balance now equals their requested ZUSD const D_ZUSDBalanceAfter = await zusdToken.balanceOf(D); @@ -1600,21 +1713,25 @@ contract("BorrowerOperations", async accounts => { await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: bob } }); // Bob successfully withdraws ZUSD - const txBob = await borrowerOperations.withdrawZUSD(th._100pct, dec(100, 16), bob, bob, { - from: bob + const { tx: txBob } = await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(100, 16), + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob } }); assert.isTrue(txBob.receipt.status); // Carol with no active trove attempts to withdraw ZUSD try { - const txCarol = await borrowerOperations.withdrawZUSD( - th._100pct, - dec(100, 16), - carol, - carol, - { from: carol } - ); - assert.isFalse(txCarol.receipt.status); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(100, 16), + upperHint: carol, + lowerHint: carol, + extraParams: { from: carol } + }); + assert.fail("Expected revert"); } catch (err) { assert.include(err.message, "revert"); } @@ -1629,13 +1746,25 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(txBob.receipt.status); // Alice attempts to withdraw 0 ZUSD - try { + /*try { const txAlice = await borrowerOperations.withdrawZUSD(th._100pct, 0, alice, alice, { from: alice }); assert.isFalse(txAlice.receipt.status); } catch (err) { assert.include(err.message, "revert"); + }*/ + try { + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(100, 16), + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }); + assert.fail("Expected revert"); + } catch (err) { + assert.include(err.message, "revert"); } }); @@ -1647,8 +1776,12 @@ contract("BorrowerOperations", async accounts => { assert.isFalse(await th.checkRecoveryMode(contracts)); // Withdrawal possible when recoveryMode == false - const txAlice = await borrowerOperations.withdrawZUSD(th._100pct, dec(100, 16), alice, alice, { - from: alice + const { tx: txAlice } = await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(100, 16), + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } }); assert.isTrue(txAlice.receipt.status); @@ -1658,8 +1791,14 @@ contract("BorrowerOperations", async accounts => { //Check ZUSD withdrawal impossible when recoveryMode == true try { - const txBob = await borrowerOperations.withdrawZUSD(th._100pct, 1, bob, bob, { from: bob }); - assert.isFalse(txBob.receipt.status); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(1, 0), + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob } + }); + assert.fail("Expected revert"); } catch (err) { assert.include(err.message, "revert"); } @@ -1671,8 +1810,14 @@ contract("BorrowerOperations", async accounts => { // Bob tries to withdraw ZUSD that would bring his ICR < MCR try { - const txBob = await borrowerOperations.withdrawZUSD(th._100pct, 1, bob, bob, { from: bob }); - assert.isFalse(txBob.receipt.status); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(1, 0), + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob } + }); + assert.fail("Expected revert"); } catch (err) { assert.include(err.message, "revert"); } @@ -1692,10 +1837,14 @@ contract("BorrowerOperations", async accounts => { // Bob attempts to withdraw 1 ZUSD. // System TCR would be: ((3+3) * 100 ) / (200+201) = 600/401 = 149.62%, i.e. below CCR of 150%. try { - const txBob = await borrowerOperations.withdrawZUSD(th._100pct, dec(1, 16), bob, bob, { - from: bob + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(1, 16), + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob } }); - assert.isFalse(txBob.receipt.status); + assert.fail("Expected revert"); } catch (err) { assert.include(err.message, "revert"); } @@ -1713,10 +1862,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue((await th.getTCR(contracts)).lt(toBN(dec(15, 17)))); try { - const txData = await borrowerOperations.withdrawZUSD(th._100pct, "200", alice, alice, { - from: alice + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: "200", + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } }); - assert.isFalse(txData.receipt.status); + assert.fail("Expected revert"); } catch (err) { assert.include(err.message, "revert"); } @@ -1729,13 +1882,13 @@ contract("BorrowerOperations", async accounts => { const aliceDebtBefore = await getTroveEntireDebt(alice); assert.isTrue(aliceDebtBefore.gt(toBN(0))); - await borrowerOperations.withdrawZUSD( - th._100pct, - await getNetBorrowingAmount(100), - alice, - alice, - { from: alice } - ); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: await getNetBorrowingAmount(100), + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }); // check after const aliceDebtAfter = await getTroveEntireDebt(alice); @@ -1755,13 +1908,13 @@ contract("BorrowerOperations", async accounts => { const activePool_ZUSD_Before = await activePool.getZUSDDebt(); assert.isTrue(activePool_ZUSD_Before.eq(aliceDebtBefore)); - await borrowerOperations.withdrawZUSD( - th._100pct, - await getNetBorrowingAmount(dec(10000, 16)), - alice, - alice, - { from: alice } - ); + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: await getNetBorrowingAmount(dec(10000, 16)), + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }); // check after const activePool_ZUSD_After = await activePool.getZUSDDebt(); @@ -1781,8 +1934,12 @@ contract("BorrowerOperations", async accounts => { const alice_ZUSDTokenBalance_Before = await zusdToken.balanceOf(alice); assert.isTrue(alice_ZUSDTokenBalance_Before.gt(toBN("0"))); - await borrowerOperations.withdrawZUSD(th._100pct, dec(10000, 16), alice, alice, { - from: alice + await withdrawZUSD({ + maxFeePercentage: th._100pct, + zusdAmount: dec(10000, 16), + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } }); // check after @@ -2192,9 +2349,14 @@ contract("BorrowerOperations", async accounts => { const collTopUp = 1; await assertRevert( - borrowerOperations.adjustTrove(th._100pct, 0, ZUSDRepayment, false, alice, alice, { - from: alice, - value: collTopUp + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: ZUSDRepayment, + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: collTopUp } }), "BorrowerOps: An operation that would result in ICR < MCR is not permitted" ); @@ -2208,17 +2370,38 @@ contract("BorrowerOperations", async accounts => { }); await assertRevert( - borrowerOperations.adjustTrove(0, 0, dec(1, 16), true, A, A, { from: A, value: dec(2, 16) }), + adjustTrove({ + maxFeePercentage: 0, + collWithdrawal: 0, + zusdAmount: dec(1, 16), + isDebtIncrease: true, + upperHint: A, + lowerHint: A, + extraParams: { from: A, value: dec(2, 16) } + }), "Max fee percentage must be between 0.5% and 100%" ); await assertRevert( - borrowerOperations.adjustTrove(1, 0, dec(1, 16), true, A, A, { from: A, value: dec(2, 16) }), + adjustTrove({ + maxFeePercentage: 1, + collWithdrawal: 0, + zusdAmount: dec(1, 16), + isDebtIncrease: true, + upperHint: A, + lowerHint: A, + extraParams: { from: A, value: dec(2, 16) } + }), "Max fee percentage must be between 0.5% and 100%" ); await assertRevert( - borrowerOperations.adjustTrove("4999999999999999", 0, dec(1, 18), true, A, A, { - from: A, - value: dec(2, 16) + adjustTrove({ + maxFeePercentage: "4999999999999999", + collWithdrawal: 0, + zusdAmount: dec(1, 18), + isDebtIncrease: true, + upperHint: A, + lowerHint: A, + extraParams: { from: A, value: dec(2, 16) } }), "Max fee percentage must be between 0.5% and 100%" ); @@ -2239,21 +2422,36 @@ contract("BorrowerOperations", async accounts => { await priceFeed.setPrice(dec(120, 18)); assert.isTrue(await th.checkRecoveryMode(contracts)); - await borrowerOperations.adjustTrove(0, 0, dec(1, 7), true, A, A, { - from: A, - value: dec(300, 16) + await adjustTrove({ + maxFeePercentage: 0, + collWithdrawal: 0, + zusdAmount: dec(1, 7), + isDebtIncrease: true, + upperHint: A, + lowerHint: A, + extraParams: { from: A, value: dec(300, 16) } }); await priceFeed.setPrice(dec(1, 18)); assert.isTrue(await th.checkRecoveryMode(contracts)); - await borrowerOperations.adjustTrove(1, 0, dec(1, 7), true, A, A, { - from: A, - value: dec(30000, 18) + await adjustTrove({ + maxFeePercentage: 1, + collWithdrawal: 0, + zusdAmount: dec(1, 7), + isDebtIncrease: true, + upperHint: A, + lowerHint: A, + extraParams: { from: A, value: dec(30000, 18) } }); await priceFeed.setPrice(dec(1, 16)); assert.isTrue(await th.checkRecoveryMode(contracts)); - await borrowerOperations.adjustTrove("4999999999999999", 0, dec(1, 9), true, A, A, { - from: A, - value: dec(3000000, 16) + await adjustTrove({ + maxFeePercentage: "4999999999999999", + collWithdrawal: 0, + zusdAmount: dec(1, 9), + isDebtIncrease: true, + upperHint: A, + lowerHint: A, + extraParams: { from: A, value: dec(3000000, 16) } }); }); @@ -2296,8 +2494,16 @@ contract("BorrowerOperations", async accounts => { // 2 hours pass th.fastForwardTime(7200, web3.currentProvider); - // D adjusts trove - await borrowerOperations.adjustTrove(th._100pct, 0, dec(37, 16), true, D, D, { from: D }); + // D adjusts trove (borrow more ZUSD) + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(37, 16), + isDebtIncrease: true, + upperHint: D, + lowerHint: D, + extraParams: { from: D } // helper will auto-set value = bufferFee + }); // Check baseRate has decreased const baseRate_2 = await troveManager.baseRate(); @@ -2306,8 +2512,16 @@ contract("BorrowerOperations", async accounts => { // 1 hour passes th.fastForwardTime(3600, web3.currentProvider); - // E adjusts trove - await borrowerOperations.adjustTrove(th._100pct, 0, dec(37, 13), true, E, E, { from: D }); + // E adjusts trove (borrow more ZUSD) + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(37, 13), + isDebtIncrease: true, + upperHint: E, + lowerHint: E, + extraParams: { from: E } // (FIX this should be E; adjustTrove adjusts msg.sender’s trove) + }); const baseRate_3 = await troveManager.baseRate(); assert.isTrue(baseRate_3.lt(baseRate_2)); @@ -2350,9 +2564,14 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(7200, web3.currentProvider); // D adjusts trove with 0 debt - await borrowerOperations.adjustTrove(th._100pct, 0, 0, false, D, D, { - from: D, - value: dec(1, 16) + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: 0, + isDebtIncrease: false, + upperHint: D, + lowerHint: D, + extraParams: { from: D, value: dec(1, 16) } }); // Check baseRate has not decreased @@ -2380,7 +2599,15 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(7200, web3.currentProvider); // D adjusts trove - await borrowerOperations.adjustTrove(th._100pct, 0, dec(37, 18), true, D, D, { from: D }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(37, 18), + isDebtIncrease: true, + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); // Check baseRate is still 0 const baseRate_2 = await troveManager.baseRate(); @@ -2390,7 +2617,15 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(3600, web3.currentProvider); // E adjusts trove - await borrowerOperations.adjustTrove(th._100pct, 0, dec(37, 15), true, E, E, { from: D }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(37, 15), + isDebtIncrease: true, + upperHint: E, + lowerHint: E, + extraParams: { from: E } // <- also fixes the bug + }); const baseRate_3 = await troveManager.baseRate(); assert.equal(baseRate_3, "0"); @@ -2428,7 +2663,15 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(10, web3.currentProvider); // Borrower C triggers a fee - await borrowerOperations.adjustTrove(th._100pct, 0, dec(1, 18), true, C, C, { from: C }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(1, 18), + isDebtIncrease: true, + upperHint: C, + lowerHint: C, + extraParams: { from: C } + }); const lastFeeOpTime_2 = await troveManager.lastFeeOperationTime(); @@ -2444,7 +2687,15 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(toBN(timeNow).sub(lastFeeOpTime_1).gte(60)); // Borrower C triggers a fee - await borrowerOperations.adjustTrove(th._100pct, 0, dec(1, 18), true, C, C, { from: C }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(1, 18), + isDebtIncrease: true, + upperHint: C, + lowerHint: C, + extraParams: { from: C } + }); const lastFeeOpTime_3 = await troveManager.lastFeeOperationTime(); @@ -2480,13 +2731,29 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(baseRate_1.gt(toBN("0"))); // Borrower C triggers a fee, before decay interval of 1 minute has passed - await borrowerOperations.adjustTrove(th._100pct, 0, dec(1, 18), true, C, C, { from: C }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(1, 18), + isDebtIncrease: true, + upperHint: C, + lowerHint: C, + extraParams: { from: C } + }); // 1 minute passes th.fastForwardTime(60, web3.currentProvider); // Borrower C triggers another fee - await borrowerOperations.adjustTrove(th._100pct, 0, dec(1, 18), true, C, C, { from: C }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(1, 18), + isDebtIncrease: true, + upperHint: C, + lowerHint: C, + extraParams: { from: C } + }); // Check base rate has decreased even though Borrower tried to stop it decaying const baseRate_2 = await troveManager.baseRate(); @@ -2588,15 +2855,15 @@ contract("BorrowerOperations", async accounts => { const withdrawal_D = toBN(dec(37, 18)); // D withdraws ZUSD - const adjustmentTx = await borrowerOperations.adjustTrove( - th._100pct, - 0, - withdrawal_D, - true, - D, - D, - { from: D } - ); + const { tx: adjustmentTx } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: withdrawal_D, + isDebtIncrease: true, + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); const emittedFee = toBN(th.getZUSDFeeFromZUSDBorrowingEvent(adjustmentTx)); assert.isTrue(emittedFee.gt(toBN("0"))); @@ -2652,7 +2919,15 @@ contract("BorrowerOperations", async accounts => { th.fastForwardTime(7200, web3.currentProvider); // D adjusts trove - await borrowerOperations.adjustTrove(th._100pct, 0, dec(37, 18), true, D, D, { from: D }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(37, 18), + isDebtIncrease: true, + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); // Check ZERO contract ZUSD fees-per-unit-staked hasn't increased const F_ZUSD_After = await zeroStaking.F_ZUSD(); @@ -2706,7 +2981,15 @@ contract("BorrowerOperations", async accounts => { // D adjusts trove const ZUSDRequest_D = toBN(dec(40, 18)); - await borrowerOperations.adjustTrove(th._100pct, 0, ZUSDRequest_D, true, D, D, { from: D }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: ZUSDRequest_D, + isDebtIncrease: true, + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); // All the fees are sent to SOV holders const zeroStaking_ZUSDBalance_After = await zusdToken.balanceOf(zeroStaking.address); @@ -2752,7 +3035,15 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(zeroStaking_ZUSDBalance_Before.eq(toBN("0"))); // D adjusts trove - await borrowerOperations.adjustTrove(th._100pct, 0, dec(37, 18), true, D, D, { from: D }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(37, 18), + isDebtIncrease: true, + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); // Check staking ZUSD balance after = staking balance before const zeroStaking_ZUSDBalance_After = await zusdToken.balanceOf(zeroStaking.address); @@ -2802,7 +3093,15 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(F_ZUSD_Before.eq(toBN("0"))); // D adjusts trove - await borrowerOperations.adjustTrove(th._100pct, 0, dec(37, 18), true, D, D, { from: D }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(37, 18), + isDebtIncrease: true, + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); // All the fees are sent to SOV holders const F_ZUSD_After = await zeroStaking.F_ZUSD(); @@ -2848,7 +3147,15 @@ contract("BorrowerOperations", async accounts => { // D adjusts trove const ZUSDRequest_D = toBN(dec(40, 18)); - await borrowerOperations.adjustTrove(th._100pct, 0, ZUSDRequest_D, true, D, D, { from: D }); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: ZUSDRequest_D, + isDebtIncrease: true, + upperHint: D, + lowerHint: D, + extraParams: { from: D } + }); // Check D's ZUSD balance increased by their requested ZUSD const ZUSDBalanceAfter = await zusdToken.balanceOf(D); @@ -2868,21 +3175,26 @@ contract("BorrowerOperations", async accounts => { }); // Alice coll and debt increase(+1 ETH, +50ZUSD) - await borrowerOperations.adjustTrove(th._100pct, 0, dec(50, 16), true, alice, alice, { - from: alice, - value: dec(1, 16) + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(50, 16), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(1, 16) } }); try { - const txCarol = await borrowerOperations.adjustTrove( - th._100pct, - 0, - dec(50, 16), - true, - carol, - carol, - { from: carol, value: dec(1, 16) } - ); + const { tx: txCarol } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(50, 16), + isDebtIncrease: true, + upperHint: carol, + lowerHint: carol, + extraParams: { from: carol, value: dec(1, 16) } + }); assert.isFalse(txCarol.receipt.status); } catch (err) { assert.include(err.message, "revert"); @@ -2903,15 +3215,15 @@ contract("BorrowerOperations", async accounts => { assert.isFalse(await th.checkRecoveryMode(contracts)); - const txAlice = await borrowerOperations.adjustTrove( - th._100pct, - 0, - dec(50, 16), - true, - alice, - alice, - { from: alice, value: dec(1, 16) } - ); + const { tx: txAlice } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(50, 16), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(1, 16) } + }); assert.isTrue(txAlice.receipt.status); await priceFeed.setPrice(dec(120, 18)); // trigger drop in ETH price @@ -2920,15 +3232,15 @@ contract("BorrowerOperations", async accounts => { try { // collateral withdrawal should also fail - const txAlice = await borrowerOperations.adjustTrove( - th._100pct, - dec(1, 16), - 0, - false, - alice, - alice, - { from: alice } - ); + const { tx: txAlice } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: dec(1, 16), + zusdAmount: 0, + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }); assert.isFalse(txAlice.receipt.status); } catch (err) { assert.include(err.message, "revert"); @@ -2936,15 +3248,15 @@ contract("BorrowerOperations", async accounts => { try { // debt increase should fail - const txBob = await borrowerOperations.adjustTrove( - th._100pct, - 0, - dec(50, 16), - true, - bob, - bob, - { from: bob } - ); + const { tx: txBob } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(50, 16), + isDebtIncrease: true, + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob } + }); assert.isFalse(txBob.receipt.status); } catch (err) { assert.include(err.message, "revert"); @@ -2952,15 +3264,15 @@ contract("BorrowerOperations", async accounts => { try { // debt increase that's also a collateral increase should also fail, if ICR will be worse off - const txBob = await borrowerOperations.adjustTrove( - th._100pct, - 0, - dec(111, 18), - true, - bob, - bob, - { from: bob, value: dec(1, 16) } - ); + const { tx: txBob } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(111, 18), + isDebtIncrease: true, + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob, value: dec(1, 16) } + }); assert.isFalse(txBob.receipt.status); } catch (err) { assert.include(err.message, "revert"); @@ -2987,8 +3299,14 @@ contract("BorrowerOperations", async accounts => { // Alice attempts an adjustment that repays half her debt BUT withdraws 1 wei collateral, and fails await assertRevert( - borrowerOperations.adjustTrove(th._100pct, 1, dec(5000, 18), false, alice, alice, { - from: alice + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 1, + zusdAmount: dec(5000, 18), + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } }), "BorrowerOps: Collateral withdrawal not permitted Recovery Mode" ); @@ -3031,9 +3349,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(newICR.gt(ICR_A) && newICR.lt(CCR)); await assertRevert( - borrowerOperations.adjustTrove(th._100pct, 0, debtIncrease, true, alice, alice, { - from: alice, - value: collIncrease + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: debtIncrease, + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: collIncrease } }), "BorrowerOps: Operation must leave trove with ICR >= CCR" ); @@ -3081,9 +3404,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(newICR_A.lt(ICR_A) && newICR_A.gt(CCR)); await assertRevert( - borrowerOperations.adjustTrove(th._100pct, 0, aliceDebtIncrease, true, alice, alice, { - from: alice, - value: aliceCollIncrease + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: aliceDebtIncrease, + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: aliceCollIncrease } }), "BorrowerOps: Cannot decrease your Trove's ICR in Recovery Mode" ); @@ -3110,9 +3438,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(newICR_B.lt(ICR_B)); await assertRevert( - borrowerOperations.adjustTrove(th._100pct, 0, bobDebtIncrease, true, bob, bob, { - from: bob, - value: bobCollIncrease + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: bobDebtIncrease, + isDebtIncrease: true, + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob, value: bobCollIncrease } }), " BorrowerOps: Operation must leave trove with ICR >= CCR" ); @@ -3156,15 +3489,15 @@ contract("BorrowerOperations", async accounts => { // Check new ICR would be > 150% assert.isTrue(newICR.gt(CCR)); - const tx = await borrowerOperations.adjustTrove( - th._100pct, - 0, - debtIncrease, - true, - alice, - alice, - { from: alice, value: collIncrease } - ); + const { tx: tx } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: debtIncrease, + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: collIncrease } + }); assert.isTrue(tx.receipt.status); const actualNewICR = await troveManager.getCurrentICR(alice, price); @@ -3209,15 +3542,15 @@ contract("BorrowerOperations", async accounts => { // Check new ICR would be > old ICR assert.isTrue(newICR.gt(initialICR)); - const tx = await borrowerOperations.adjustTrove( - th._100pct, - 0, - debtIncrease, - true, - alice, - alice, - { from: alice, value: collIncrease } - ); + const { tx: tx } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: debtIncrease, + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: collIncrease } + }); assert.isTrue(tx.receipt.status); const actualNewICR = await troveManager.getCurrentICR(alice, price); @@ -3250,15 +3583,15 @@ contract("BorrowerOperations", async accounts => { // All the fees are sent to SOV holders assert.isTrue(zeroStakingZUSDBalanceBefore.eq(toBN("0"))); - const txAlice = await borrowerOperations.adjustTrove( - th._100pct, - 0, - dec(50, 16), - true, - alice, - alice, - { from: alice, value: dec(100, "ether") } - ); + const { tx: txAlice } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(50, 16), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(100, "ether") } + }); assert.isTrue(txAlice.receipt.status); // Check emitted fee = 0 @@ -3287,15 +3620,15 @@ contract("BorrowerOperations", async accounts => { // Bob attempts an operation that would bring the TCR below the CCR try { - const txBob = await borrowerOperations.adjustTrove( - th._100pct, - 0, - dec(1, 18), - true, - bob, - bob, - { from: bob } - ); + const { tx: txBob } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(1, 18), + isDebtIncrease: true, + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob } + }); assert.isFalse(txBob.receipt.status); } catch (err) { assert.include(err.message, "revert"); @@ -3319,9 +3652,14 @@ contract("BorrowerOperations", async accounts => { // Bob attempts an adjustment that would repay 1 wei more than his debt await assertRevert( - borrowerOperations.adjustTrove(th._100pct, 0, remainingDebt.add(toBN(1)), false, bob, bob, { - from: bob, - value: dec(1, 16) + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: remainingDebt.add(toBN(1)), + isDebtIncrease: false, + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob, value: dec(1, 16) } }), "revert" ); @@ -3336,15 +3674,15 @@ contract("BorrowerOperations", async accounts => { // Carol attempts an adjustment that would withdraw 1 wei more than her ETH try { - const txCarol = await borrowerOperations.adjustTrove( - th._100pct, - carolColl.add(toBN(1)), - 0, - true, - carol, - carol, - { from: carol } - ); + const { tx: txCarol } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: carolColl.add(toBN(1)), + zusdAmount: 0, + isDebtIncrease: true, + upperHint: carol, + lowerHint: carol, + extraParams: { from: carol } + }); assert.isFalse(txCarol.receipt.status); } catch (err) { assert.include(err.message, "revert"); @@ -3374,15 +3712,15 @@ contract("BorrowerOperations", async accounts => { // Bob attempts to increase debt by 100 ZUSD and 1 ether, i.e. a change that constitutes a 100% ratio of coll:debt. // Since his ICR prior is 110%, this change would reduce his ICR below MCR. try { - const txBob = await borrowerOperations.adjustTrove( - th._100pct, - 0, - dec(100, 16), - true, - bob, - bob, - { from: bob, value: dec(1, 16) } - ); + const { tx: txBob } = await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(100, 16), + isDebtIncrease: true, + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob, value: dec(1, 16) } + }); assert.isFalse(txBob.receipt.status); } catch (err) { assert.include(err.message, "revert"); @@ -3403,9 +3741,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(aliceCollBefore.eq(activePoolCollBefore)); // Alice adjusts trove. No coll change, and a debt increase (+50ZUSD) - await borrowerOperations.adjustTrove(th._100pct, 0, dec(50, 16), true, alice, alice, { - from: alice, - value: 0 + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(50, 16), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: 0 } }); const aliceCollAfter = await getTroveEntireColl(alice); @@ -3456,9 +3799,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(aliceDebtBefore.eq(activePoolDebtBefore)); // Alice adjusts trove. Coll change, no debt change - await borrowerOperations.adjustTrove(th._100pct, 0, 0, false, alice, alice, { - from: alice, - value: dec(1, 16) + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: 0, + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(1, 16) } }); const aliceDebtAfter = await getTroveEntireDebt(alice); @@ -3491,9 +3839,14 @@ contract("BorrowerOperations", async accounts => { // Alice adjusts trove. Coll and debt increase(+1 ETH, +50ZUSD) const increaseAmount = await getNetBorrowingAmount(dec(50, 16)); const permission = await signERC2612Permit(alice_signer, nueMockToken.address, alice_signer.address, borrowerOperations.address, increaseAmount.toString()); - await borrowerOperations.adjustNueTrove(th._100pct, 0, increaseAmount, true, alice, alice, permission, { - from: alice, - value: dec(1, 16) + await adjustNueTrove({ + collWithdrawal: 0, + zusdAmount: increaseAmount, + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + permitParams: permission, + extraParams: { from: alice, value: dec(1, 16) } }); const debtAfter = await getTroveEntireDebt(alice); @@ -3525,15 +3878,15 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(collBefore.gt(toBN("0"))); // Alice adjusts trove. Coll and debt increase(+1 ETH, +50ZUSD) - await borrowerOperations.adjustTrove( - th._100pct, - 0, - await getNetBorrowingAmount(dec(50, 16)), - true, - alice, - alice, - { from: alice, value: dec(1, 16) } - ); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: await getNetBorrowingAmount(dec(50, 16)), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(1, 16) } + }); const debtAfter = await getTroveEntireDebt(alice); const collAfter = await getTroveEntireColl(alice); @@ -3673,15 +4026,15 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(collBefore.gt(toBN("0"))); // Alice adjusts trove coll and debt decrease (-0.5 ETH, -50ZUSD) - await borrowerOperations.adjustTrove( - th._100pct, - dec(500, "finney"), - dec(50, 16), - false, - alice, - alice, - { from: alice } - ); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: dec(500, "finney"), + zusdAmount: dec(50, 16), + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }); const debtAfter = await getTroveEntireDebt(alice); const collAfter = await getTroveEntireColl(alice); @@ -3709,9 +4062,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(collBefore.gt(toBN("0"))); // Alice adjusts trove - coll increase and debt decrease (+0.5 ETH, -50ZUSD) - await borrowerOperations.adjustTrove(th._100pct, 0, dec(50, 16), false, alice, alice, { - from: alice, - value: dec(500, "finney") + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(50, 16), + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(500, "finney") } }); const debtAfter = await getTroveEntireDebt(alice); @@ -3740,15 +4098,15 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(collBefore.gt(toBN("0"))); // Alice adjusts trove - coll decrease and debt increase (0.1 ETH, 10ZUSD) - await borrowerOperations.adjustTrove( - th._100pct, - dec(1, 17), - await getNetBorrowingAmount(dec(1, 18)), - true, - alice, - alice, - { from: alice } - ); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: dec(1, 17), + zusdAmount: await getNetBorrowingAmount(dec(1, 18)), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }); const debtAfter = await getTroveEntireDebt(alice); const collAfter = await getTroveEntireColl(alice); @@ -3776,9 +4134,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(totalStakesBefore.gt(toBN("0"))); // Alice adjusts trove - coll and debt increase (+1 ETH, +50 ZUSD) - await borrowerOperations.adjustTrove(th._100pct, 0, dec(50, 16), true, alice, alice, { - from: alice, - value: dec(1, 16) + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(50, 16), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(1, 16) } }); const stakeAfter = await troveManager.getTroveStake(alice); @@ -3807,15 +4170,15 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(totalStakesBefore.gt(toBN("0"))); // Alice adjusts trove - coll decrease and debt decrease - await borrowerOperations.adjustTrove( - th._100pct, - dec(500, "finney"), - dec(50, 16), - false, - alice, - alice, - { from: alice } - ); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: dec(500, "finney"), + zusdAmount: dec(50, 16), + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }); const stakeAfter = await troveManager.getTroveStake(alice); const totalStakesAfter = await troveManager.totalStakes(); @@ -3841,15 +4204,15 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(alice_ZUSDTokenBalance_Before.gt(toBN("0"))); // Alice adjusts trove - coll decrease and debt decrease - await borrowerOperations.adjustTrove( - th._100pct, - dec(100, "finney"), - dec(10, 18), - false, - alice, - alice, - { from: alice } - ); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: dec(100, "finney"), + zusdAmount: dec(10, 18), + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }); // check after const alice_ZUSDTokenBalance_After = await zusdToken.balanceOf(alice); @@ -3875,9 +4238,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(alice_ZUSDTokenBalance_Before.gt(toBN("0"))); // Alice adjusts trove - coll increase and debt increase - await borrowerOperations.adjustTrove(th._100pct, 0, dec(100, 16), true, alice, alice, { - from: alice, - value: dec(1, 16) + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(100, 16), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(1, 16) } }); // check after @@ -3906,15 +4274,15 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(activePool_RawEther_Before.gt(toBN("0"))); // Alice adjusts trove - coll decrease and debt decrease - await borrowerOperations.adjustTrove( - th._100pct, - dec(100, "finney"), - dec(10, 18), - false, - alice, - alice, - { from: alice } - ); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: dec(100, "finney"), + zusdAmount: dec(10, 18), + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }); const activePool_ETH_After = await activePool.getETH(); const activePool_RawEther_After = toBN(await web3.eth.getBalance(activePool.address)); @@ -3941,9 +4309,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(activePool_RawEther_Before.gt(toBN("0"))); // Alice adjusts trove - coll increase and debt increase - await borrowerOperations.adjustTrove(th._100pct, 0, dec(100, 16), true, alice, alice, { - from: alice, - value: dec(1, 16) + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(100, 16), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(1, 16) } }); const activePool_ETH_After = await activePool.getETH(); @@ -3969,9 +4342,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(activePool_ZUSDDebt_Before.gt(toBN("0"))); // Alice adjusts trove - coll increase and debt decrease - await borrowerOperations.adjustTrove(th._100pct, 0, dec(30, 18), false, alice, alice, { - from: alice, - value: dec(1, 16) + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(30, 18), + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(1, 16) } }); const activePool_ZUSDDebt_After = await activePool.getZUSDDebt(); @@ -3994,15 +4372,15 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(activePool_ZUSDDebt_Before.gt(toBN("0"))); // Alice adjusts trove - coll increase and debt increase - await borrowerOperations.adjustTrove( - th._100pct, - 0, - await getNetBorrowingAmount(dec(100, 16)), - true, - alice, - alice, - { from: alice, value: dec(1, 16) } - ); + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: await getNetBorrowingAmount(dec(100, 16)), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(1, 16) } + }); const activePool_ZUSDDebt_After = await activePool.getZUSDDebt(); @@ -4033,8 +4411,14 @@ contract("BorrowerOperations", async accounts => { assert.isTrue(isInSortedList_Before); await assertRevert( - borrowerOperations.adjustTrove(th._100pct, aliceColl, aliceDebt, true, alice, alice, { - from: alice + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: aliceColl, + zusdAmount: aliceDebt, + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } }), "BorrowerOps: An operation that would result in ICR < MCR is not permitted" ); @@ -4053,7 +4437,15 @@ contract("BorrowerOperations", async accounts => { }); await assertRevert( - borrowerOperations.adjustTrove(th._100pct, 0, 0, true, alice, alice, { from: alice }), + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: 0, + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }), "BorrowerOps: Debt increase requires non-zero debtChange" ); }); @@ -4071,9 +4463,14 @@ contract("BorrowerOperations", async accounts => { }); await assertRevert( - borrowerOperations.adjustTrove(th._100pct, dec(1, 16), dec(100, 16), true, alice, alice, { - from: alice, - value: dec(3, "ether") + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: dec(1, 16), + zusdAmount: dec(100, 16), + isDebtIncrease: true, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: dec(3, "ether") } }), "BorrowerOperations: Cannot withdraw and add coll" ); @@ -4087,7 +4484,15 @@ contract("BorrowerOperations", async accounts => { }); await assertRevert( - borrowerOperations.adjustTrove(th._100pct, 0, 0, false, alice, alice, { from: alice }), + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: 0, + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } + }), "BorrowerOps: There must be either a collateral change or a debt change" ); }); @@ -4108,20 +4513,26 @@ contract("BorrowerOperations", async accounts => { // Requested coll withdrawal > coll in the trove await assertRevert( - borrowerOperations.adjustTrove(th._100pct, aliceColl.add(toBN(1)), 0, false, alice, alice, { - from: alice + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: aliceColl.add(toBN(1)), + zusdAmount: 0, + isDebtIncrease: false, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice } }) ); await assertRevert( - borrowerOperations.adjustTrove( - th._100pct, - aliceColl.add(toBN(dec(37, "ether"))), - 0, - false, - bob, - bob, - { from: bob } - ) + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: aliceColl.add(toBN(dec(37, "ether"))), + zusdAmount: 0, + isDebtIncrease: false, + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob } + }) ); }); @@ -4145,18 +4556,19 @@ contract("BorrowerOperations", async accounts => { const B_ZUSDBal = await zusdToken.balanceOf(B); assert.isTrue(B_ZUSDBal.lt(bobDebt)); - const repayZUSDPromise_B = borrowerOperations.adjustTrove( - th._100pct, - 0, - bobDebt, - false, - B, - B, - { from: B } - ); - // B attempts to repay all his debt - await assertRevert(repayZUSDPromise_B, "revert"); + await assertRevert( + adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: bobDebt, + isDebtIncrease: false, + upperHint: B, + lowerHint: B, + extraParams: { from: B } + }), + "revert" + ); }); // --- Internal _adjustTrove() --- @@ -4882,8 +5294,14 @@ contract("BorrowerOperations", async accounts => { ); // whale adjusts trove, pulling their rewards out of DefaultPool - await borrowerOperations.adjustTrove(th._100pct, 0, dec(1, 18), true, whale, whale, { - from: whale + await adjustTrove({ + maxFeePercentage: th._100pct, + collWithdrawal: 0, + zusdAmount: dec(1, 18), + isDebtIncrease: true, + upperHint: whale, + lowerHint: whale, + extraParams: { from: whale } }); // Close Bob's trove. Expect DefaultPool coll and debt to drop to 0, since closing pulls his rewards out. @@ -6584,13 +7002,19 @@ contract("BorrowerOperations", async accounts => { const troveColl = toBN(dec(1000, "ether")); const troveTotalDebt = toBN(dec(100000, 18)); const troveZUSDAmount = await getOpenTroveZUSDAmount(troveTotalDebt); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, alice, alice, { - from: alice, - value: troveColl + await openTrove({ + maxFeePercentage: th._100pct, + zusdAmount: troveZUSDAmount, + upperHint: alice, + lowerHint: alice, + extraParams: { from: alice, value: troveColl } // helper adds bufferFee on top }); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, bob, bob, { - from: bob, - value: troveColl + await openTrove({ + maxFeePercentage: th._100pct, + zusdAmount: troveZUSDAmount, + upperHint: bob, + lowerHint: bob, + extraParams: { from: bob, value: troveColl } // helper adds bufferFee on top }); await priceFeed.setPrice(dec(100, 18)); @@ -6630,13 +7054,13 @@ contract("BorrowerOperations", async accounts => { const troveColl = toBN(dec(1000, "ether")); const troveTotalDebt = toBN(dec(100000, 18)); const troveZUSDAmount = await getOpenTroveZUSDAmount(troveTotalDebt); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, alice, alice, { - from: alice, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: alice, value: troveColl } // helper adds bufferFee on top }); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, bob, bob, { - from: bob, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: bob, value: troveColl } // helper adds bufferFee on top }); await priceFeed.setPrice(dec(100, 18)); @@ -6676,13 +7100,13 @@ contract("BorrowerOperations", async accounts => { const troveColl = toBN(dec(1000, "ether")); const troveTotalDebt = toBN(dec(100000, 18)); const troveZUSDAmount = await getOpenTroveZUSDAmount(troveTotalDebt); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, alice, alice, { - from: alice, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: alice, value: troveColl } // helper adds bufferFee on top }); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, bob, bob, { - from: bob, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: bob, value: troveColl } // helper adds bufferFee on top }); await priceFeed.setPrice(dec(100, 18)); @@ -6721,13 +7145,13 @@ contract("BorrowerOperations", async accounts => { const troveColl = toBN(dec(1000, "ether")); const troveTotalDebt = toBN(dec(100000, 18)); const troveZUSDAmount = await getOpenTroveZUSDAmount(troveTotalDebt); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, alice, alice, { - from: alice, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: alice, value: troveColl } // helper adds bufferFee on top }); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, bob, bob, { - from: bob, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: bob, value: troveColl } // helper adds bufferFee on top }); await priceFeed.setPrice(dec(100, 18)); @@ -6767,13 +7191,13 @@ contract("BorrowerOperations", async accounts => { const troveColl = toBN(dec(1000, 16)); const troveTotalDebt = toBN(dec(100000, 16)); const troveZUSDAmount = await getOpenTroveZUSDAmount(troveTotalDebt); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, alice, alice, { - from: alice, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: alice, value: troveColl } // helper adds bufferFee on top }); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, bob, bob, { - from: bob, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: bob, value: troveColl } // helper adds bufferFee on top }); await priceFeed.setPrice(dec(100, 18)); @@ -6814,13 +7238,13 @@ contract("BorrowerOperations", async accounts => { const troveColl = toBN(dec(1000, 16)); const troveTotalDebt = toBN(dec(100000, 16)); const troveZUSDAmount = await getOpenTroveZUSDAmount(troveTotalDebt); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, alice, alice, { - from: alice, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: alice, value: troveColl } // helper adds bufferFee on top }); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, bob, bob, { - from: bob, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: bob, value: troveColl } // helper adds bufferFee on top }); await priceFeed.setPrice(dec(100, 18)); @@ -6861,13 +7285,13 @@ contract("BorrowerOperations", async accounts => { const troveColl = toBN(dec(1000, "ether")); const troveTotalDebt = toBN(dec(100000, 18)); const troveZUSDAmount = await getOpenTroveZUSDAmount(troveTotalDebt); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, alice, alice, { - from: alice, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: alice, value: troveColl } // helper adds bufferFee on top }); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, bob, bob, { - from: bob, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: bob, value: troveColl } // helper adds bufferFee on top }); await priceFeed.setPrice(dec(100, 18)); @@ -6908,13 +7332,13 @@ contract("BorrowerOperations", async accounts => { const troveColl = toBN(dec(1000, "ether")); const troveTotalDebt = toBN(dec(100000, 18)); const troveZUSDAmount = await getOpenTroveZUSDAmount(troveTotalDebt); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, alice, alice, { - from: alice, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: alice, value: troveColl } // helper adds bufferFee on top }); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, bob, bob, { - from: bob, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: bob, value: troveColl } // helper adds bufferFee on top }); await priceFeed.setPrice(dec(100, 18)); @@ -6955,13 +7379,13 @@ contract("BorrowerOperations", async accounts => { const troveColl = toBN(dec(1000, "ether")); const troveTotalDebt = toBN(dec(100000, 18)); const troveZUSDAmount = await getOpenTroveZUSDAmount(troveTotalDebt); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, alice, alice, { - from: alice, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: alice, value: troveColl } // helper adds bufferFee on top }); - await borrowerOperations.openTrove(th._100pct, troveZUSDAmount, bob, bob, { - from: bob, - value: troveColl + await openTrove({ + zusdAmount: troveZUSDAmount, + extraParams: { from: bob, value: troveColl } // helper adds bufferFee on top }); await priceFeed.setPrice(dec(100, 18)); @@ -7002,9 +7426,9 @@ contract("BorrowerOperations", async accounts => { const nonPayable = await NonPayable.new(); // we need 2 troves to be able to close 1 and have 1 remaining in the system - await borrowerOperations.openTrove(th._100pct, dec(100000, 18), alice, alice, { - from: alice, - value: dec(1000, 18) + await openTrove({ + zusdAmount: dec(100000, 18), + extraParams: { from: alice, value: dec(1000, 18) } // helper adds bufferFee on top }); // Alice sends ZUSD to NonPayable so its ZUSD balance covers its debt diff --git a/tests/js/CollSurplusPool.js b/tests/js/CollSurplusPool.js index 9bfb326..4234d96 100644 --- a/tests/js/CollSurplusPool.js +++ b/tests/js/CollSurplusPool.js @@ -73,7 +73,12 @@ contract('CollSurplusPool', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // At ETH:USD = 100, this redemption should leave 1 ether of coll surplus - await th.redeemCollateralAndGetTxObject(A, contracts, B_netDebt); + // In order to have the desired test result, we need to redeem all of the RedemptionBuffer balance along with our intended redemption amount: + const bufferBal = toBN(await contracts.redemptionBuffer.getBalance()); // important: match contract logic + const maxZusdFromBuffer = bufferBal.mul(price).div(mv._1e18BN); + // Redeem enough so that AFTER the buffer swap, there's still `redeemAmount` left to redeem from troves: + const totalToRedeem = toBN(B_netDebt).add(maxZusdFromBuffer); + await th.redeemCollateralAndGetTxObject(A, contracts, totalToRedeem); const ETH_2 = await collSurplusPool.getETH(); th.assertIsApproximatelyEqual(ETH_2, B_coll.sub(B_netDebt.mul(mv._1e18BN).div(price))); @@ -98,14 +103,20 @@ contract('CollSurplusPool', async accounts => { const B_zusdAmount = toBN(dec(3000, 18)); const B_netDebt = await th.getAmountWithBorrowingFee(contracts, B_zusdAmount); const openTroveData = th.getTransactionData('openTrove(uint256,uint256,address,address)', ['0xde0b6b3a7640000', web3.utils.toHex(B_zusdAmount), B, B]); - await nonPayable.forward(borrowerOperations.address, openTroveData, { value: B_coll }); + const B_bufFee = await th.getRedemptionBufferFeeRBTC(contracts, B_zusdAmount, nonPayable.address); // get the redemption buffer fee for B + await nonPayable.forward(borrowerOperations.address, openTroveData, { value: B_coll.add(B_bufFee) }); // add the buffer fee on top of the collateral await openTrove({ extraZUSDAmount: B_netDebt, extraParams: { from: A, value: dec(3000, 'ether') } }); // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // At ETH:USD = 100, this redemption should leave 1 ether of coll surplus for B - await th.redeemCollateralAndGetTxObject(A, contracts, B_netDebt); + // In order to have the desired test result, we need to redeem all of the RedemptionBuffer balance along with our intended redemption amount: + const bufferBal = toBN(await contracts.redemptionBuffer.getBalance()); // important: match contract logic + const maxZusdFromBuffer = bufferBal.mul(price).div(mv._1e18BN); + // Redeem enough so that AFTER the buffer swap, there's still `redeemAmount` left to redeem from troves: + const totalToRedeem = toBN(B_netDebt).add(maxZusdFromBuffer); + await th.redeemCollateralAndGetTxObject(A, contracts, totalToRedeem); const ETH_2 = await collSurplusPool.getETH(); th.assertIsApproximatelyEqual(ETH_2, B_coll.sub(B_netDebt.mul(mv._1e18BN).div(price))); diff --git a/tests/js/ProxyBorrowerWrappersScript.js b/tests/js/ProxyBorrowerWrappersScript.js index 0301353..d4a37cd 100644 --- a/tests/js/ProxyBorrowerWrappersScript.js +++ b/tests/js/ProxyBorrowerWrappersScript.js @@ -157,7 +157,7 @@ contract('BorrowerWrappers', async accounts => { await openTrove({ ICR: toBN(dec(2, 18)), extraParams: { from: whale } }); // alice opens Trove - const { zusdAmount, collateral } = await openTrove({ ICR: toBN(dec(15, 17)), extraParams: { from: alice } }); + const { requestedZUSDAmount, collateral, bufferFee } = await openTrove({ ICR: toBN(dec(15, 17)), extraParams: { from: alice } }); const proxyAddress = borrowerWrappers.getProxyAddressFromUser(alice); assert.equal(await web3.eth.getBalance(proxyAddress), '0'); @@ -167,21 +167,22 @@ contract('BorrowerWrappers', async accounts => { // alice claims collateral and re-opens the trove await assertRevert( - borrowerWrappers.claimCollateralAndOpenTrove(th._100pct, zusdAmount, alice, alice, { from: alice }), + borrowerWrappers.claimCollateralAndOpenTrove(th._100pct, requestedZUSDAmount, alice, alice, { from: alice }), 'CollSurplusPool: No collateral available to claim' ); // check everything remain the same assert.equal(await web3.eth.getBalance(proxyAddress), '0'); th.assertIsApproximatelyEqual(await collSurplusPool.getCollateral(proxyAddress), '0'); - th.assertIsApproximatelyEqual(await zusdToken.balanceOf(proxyAddress), zusdAmount); + th.assertIsApproximatelyEqual(await zusdToken.balanceOf(proxyAddress), requestedZUSDAmount); assert.equal(await troveManager.getTroveStatus(proxyAddress), 1); th.assertIsApproximatelyEqual(await troveManager.getTroveColl(proxyAddress), collateral); }); it('claimCollateralAndOpenTrove(): without sending any value', async () => { + const price = await priceFeed.getPrice(); // alice opens Trove - const { zusdAmount, netDebt: redeemAmount, collateral } = await openTrove({ extraZUSDAmount: 0, ICR: toBN(dec(3, 18)), extraParams: { from: alice } }); + const { requestedZUSDAmount, netDebt: redeemAmount, collateral, bufferFee } = await openTrove({ extraZUSDAmount: 0, ICR: toBN(dec(3, 18)), extraParams: { from: alice } }); // Whale opens Trove await openTrove({ extraZUSDAmount: redeemAmount, ICR: toBN(dec(5, 18)), extraParams: { from: whale } }); @@ -192,28 +193,37 @@ contract('BorrowerWrappers', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // whale redeems 150 ZUSD - await th.redeemCollateral(whale, contracts, redeemAmount); + //await th.redeemCollateral(whale, contracts, redeemAmount); + + // In order to have the desired test result, we need to redeem all of the RedemptionBuffer balance along with our intended redemption amount: + const bufferBal = toBN(await contracts.redemptionBuffer.getBalance()); // important: match contract logic + const maxZusdFromBuffer = bufferBal.mul(price).div(mv._1e18BN); + // Redeem enough so that AFTER the buffer swap, there's still `redeemAmount` left to redeem from troves: + const totalToRedeem = toBN(redeemAmount).add(maxZusdFromBuffer); + await th.redeemCollateral(whale, contracts, totalToRedeem); assert.equal(await web3.eth.getBalance(proxyAddress), '0'); // surplus: 5 - 150/200 - const price = await priceFeed.getPrice(); const expectedSurplus = collateral.sub(redeemAmount.mul(mv._1e18BN).div(price)); th.assertIsApproximatelyEqual(await collSurplusPool.getCollateral(proxyAddress), expectedSurplus); assert.equal(await troveManager.getTroveStatus(proxyAddress), 4); // closed by redemption // alice claims collateral and re-opens the trove - await borrowerWrappers.claimCollateralAndOpenTrove(th._100pct, zusdAmount, alice, alice, { from: alice }); + // We need to set value to the RedemptionBuffer fee for the internal openTrove() call. + const reopenBufFee = await th.getRedemptionBufferFeeRBTC(contracts, requestedZUSDAmount, alice); + await borrowerWrappers.claimCollateralAndOpenTrove(th._100pct, requestedZUSDAmount, alice, alice, { from: alice, value: reopenBufFee }); assert.equal(await web3.eth.getBalance(proxyAddress), '0'); th.assertIsApproximatelyEqual(await collSurplusPool.getCollateral(proxyAddress), '0'); - th.assertIsApproximatelyEqual(await zusdToken.balanceOf(proxyAddress), zusdAmount.mul(toBN(2))); + th.assertIsApproximatelyEqual(await zusdToken.balanceOf(proxyAddress), requestedZUSDAmount.mul(toBN(2))); assert.equal(await troveManager.getTroveStatus(proxyAddress), 1); th.assertIsApproximatelyEqual(await troveManager.getTroveColl(proxyAddress), expectedSurplus); }); it('claimCollateralAndOpenTrove(): sending value in the transaction', async () => { + const price = await priceFeed.getPrice(); // alice opens Trove - const { zusdAmount, netDebt: redeemAmount, collateral } = await openTrove({ extraParams: { from: alice } }); + const { requestedZUSDAmount, netDebt: redeemAmount, collateral } = await openTrove({ extraParams: { from: alice } }); // Whale opens Trove await openTrove({ extraZUSDAmount: redeemAmount, ICR: toBN(dec(2, 18)), extraParams: { from: whale } }); @@ -224,21 +234,27 @@ contract('BorrowerWrappers', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // whale redeems 150 ZUSD - await th.redeemCollateral(whale, contracts, redeemAmount); + //await th.redeemCollateral(whale, contracts, redeemAmount); + const bufferBal = toBN(await contracts.redemptionBuffer.getBalance()); // important: match contract logic + const maxZusdFromBuffer = bufferBal.mul(price).div(mv._1e18BN); + // Redeem enough so that AFTER the buffer swap, there's still `redeemAmount` left to redeem from troves: + const totalToRedeem = toBN(redeemAmount).add(maxZusdFromBuffer); + await th.redeemCollateral(whale, contracts, totalToRedeem); assert.equal(await web3.eth.getBalance(proxyAddress), '0'); // surplus: 5 - 150/200 - const price = await priceFeed.getPrice(); const expectedSurplus = collateral.sub(redeemAmount.mul(mv._1e18BN).div(price)); th.assertIsApproximatelyEqual(await collSurplusPool.getCollateral(proxyAddress), expectedSurplus); assert.equal(await troveManager.getTroveStatus(proxyAddress), 4); // closed by redemption // alice claims collateral and re-opens the trove - await borrowerWrappers.claimCollateralAndOpenTrove(th._100pct, zusdAmount, alice, alice, { from: alice, value: collateral }); + // We need to set value to the RedemptionBuffer fee for the internal openTrove() call. + const reopenBufFee = await th.getRedemptionBufferFeeRBTC(contracts, requestedZUSDAmount, alice); + await borrowerWrappers.claimCollateralAndOpenTrove(th._100pct, requestedZUSDAmount, alice, alice, { from: alice, value: collateral.add(reopenBufFee) }); assert.equal(await web3.eth.getBalance(proxyAddress), '0'); th.assertIsApproximatelyEqual(await collSurplusPool.getCollateral(proxyAddress), '0'); - th.assertIsApproximatelyEqual(await zusdToken.balanceOf(proxyAddress), zusdAmount.mul(toBN(2))); + th.assertIsApproximatelyEqual(await zusdToken.balanceOf(proxyAddress), requestedZUSDAmount.mul(toBN(2))); assert.equal(await troveManager.getTroveStatus(proxyAddress), 1); th.assertIsApproximatelyEqual(await troveManager.getTroveColl(proxyAddress), expectedSurplus.add(collateral)); }); @@ -286,7 +302,7 @@ contract('BorrowerWrappers', async accounts => { await stabilityPool.provideToSP(aliceDeposit, ZERO_ADDRESS, { from: alice }); // Defaulter Trove opened - const { zusdAmount, netDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); + const { requestedZUSDAmount: zusdAmount, netDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); // price drops: defaulters' Troves fall below MCR, alice and whale Trove remain active const price = toBN(dec(100, 18)); @@ -387,7 +403,7 @@ contract('BorrowerWrappers', async accounts => { await zeroStaking.stake(dec(150, 18), { from: alice }); // Defaulter Trove opened - const { zusdAmount, netDebt, totalDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); + const { requestedZUSDAmount, netDebt, totalDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); @@ -425,8 +441,8 @@ contract('BorrowerWrappers', async accounts => { await zeroStaking.stake(dec(150, 18), { from: alice }); // Defaulter Trove opened - const { zusdAmount, netDebt, totalDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); - const borrowingFee = netDebt.sub(zusdAmount); + const { requestedZUSDAmount, netDebt, totalDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); + const borrowingFee = netDebt.sub(requestedZUSDAmount); // Alice ZUSD gain is ((150/2000) * borrowingFee) const expectedZUSDGain_A = borrowingFee.mul(toBN(dec(150, 18))).div(toBN(dec(2000, 18))); @@ -488,8 +504,8 @@ contract('BorrowerWrappers', async accounts => { const feeSharingCollectorZUSDBalanceBefore = await zusdToken.balanceOf(feeSharingCollector); // Defaulter Trove opened - const { zusdAmount, netDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); - const borrowingFee = netDebt.sub(zusdAmount); + const { requestedZUSDAmount, netDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); + const borrowingFee = netDebt.sub(requestedZUSDAmount); // 100% sent to feeSharingCollector address const borrowingFeeToFeeSharingCollector = borrowingFee.mul(toBN(dec(100, 16))).div(mv._1e18BN); const feeSharingCollectorZUSDBalanceAfter = await zusdToken.balanceOf(feeSharingCollector); @@ -513,7 +529,7 @@ contract('BorrowerWrappers', async accounts => { const redeemedAmount = toBN(dec(100, 18)); const feeSharingCollectorBalanceBefore = await wrbtcToken.balanceOf(feeSharingCollector); await th.redeemCollateral(whale, contracts, redeemedAmount); - const feeSharingCollectorBalanceAfter = web3.utils.toBN(await web3.eth.getBalance(feeSharingCollector)); + const feeSharingCollectorBalanceAfter = toBN(await web3.eth.getBalance(feeSharingCollector)); // Alice ETH gain is ((150/2000) * (redemption fee over redeemedAmount) / price) const redemptionFee = await troveManager.getRedemptionFeeWithDecay(redeemedAmount); @@ -610,8 +626,8 @@ contract('BorrowerWrappers', async accounts => { const feeSharingCollectorZUSDBalanceBefore = await zusdToken.balanceOf(feeSharingCollector); // Defaulter Trove opened - const { zusdAmount, netDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); - const borrowingFee = netDebt.sub(zusdAmount); + const { requestedZUSDAmount, netDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); + const borrowingFee = netDebt.sub(requestedZUSDAmount); // 100% sent to feeSharingCollector address const borrowingFeeToFeeSharingCollector = borrowingFee.mul(toBN(dec(100, 16))).div(mv._1e18BN); @@ -688,8 +704,8 @@ contract('BorrowerWrappers', async accounts => { const feeSharingCollectorZUSDBalanceBefore = await zusdToken.balanceOf(feeSharingCollector); // Defaulter Trove opened - const { zusdAmount, netDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); - const borrowingFee = netDebt.sub(zusdAmount); + const { requestedZUSDAmount, netDebt, collateral } = await openTrove({ ICR: toBN(dec(210, 16)), extraParams: { from: defaulter_1 } }); + const borrowingFee = netDebt.sub(requestedZUSDAmount); // 100% sent to feeSharingCollector address const borrowingFeeToFeeSharingCollector = borrowingFee.mul(toBN(dec(100, 16))).div(mv._1e18BN); @@ -706,7 +722,7 @@ contract('BorrowerWrappers', async accounts => { const redeemedAmount = toBN(dec(100, 18)); const feeSharingCollectorBalanceBefore = await wrbtcToken.balanceOf(feeSharingCollector); await th.redeemCollateral(whale, contracts, redeemedAmount); - const feeSharingCollectorBalanceAfter = web3.utils.toBN(await web3.eth.getBalance(feeSharingCollector)); + const feeSharingCollectorBalanceAfter = toBN(await web3.eth.getBalance(feeSharingCollector)); // Alice ETH gain is ((150/2000) * (redemption fee over redeemedAmount) / price) const redemptionFee = await troveManager.getRedemptionFeeWithDecay(redeemedAmount); diff --git a/tests/js/StabilityPoolTest.js b/tests/js/StabilityPoolTest.js index 25ebcdb..cbc5219 100644 --- a/tests/js/StabilityPoolTest.js +++ b/tests/js/StabilityPoolTest.js @@ -2054,7 +2054,9 @@ contract('StabilityPool', async accounts => { await priceFeed.setPrice(dec(200, 18)); // Bob issues a further 5000 ZUSD from his trove - await borrowerOperations.withdrawZUSD(th._100pct, dec(5000, 18), bob, bob, { from: bob }); + const requestedZUSDAmount = dec(5000, 18); + const reopenBufFee = await th.getRedemptionBufferFeeRBTC(contracts, requestedZUSDAmount, bob); + await borrowerOperations.withdrawZUSD(th._100pct, requestedZUSDAmount, bob, bob, { from: bob, value: reopenBufFee }); // Expect Alice's ZUSD balance increase be very close to 8333.3333333333333333 ZUSD await stabilityPool.withdrawFromSP(dec(10000, 18), { from: alice }); diff --git a/tests/js/StabilityPool_SPWithdrawalTest.js b/tests/js/StabilityPool_SPWithdrawalTest.js index f79ed1e..3b63fd4 100644 --- a/tests/js/StabilityPool_SPWithdrawalTest.js +++ b/tests/js/StabilityPool_SPWithdrawalTest.js @@ -53,6 +53,21 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' const getOpenTroveZUSDAmount = async (totalDebt) => th.getOpenTroveZUSDAmount(contracts, totalDebt); + const defaulterOpenTrove = async (debt, coll, from) => { + const zusdAmount = await getOpenTroveZUSDAmount(debt); + const bufFee = await th.getRedemptionBufferFeeRBTC(contracts, zusdAmount, from); + await borrowerOperations.openTrove( + th._100pct, + zusdAmount, + from, + from, + { + from: from, + value: toBN(coll).add(bufFee) + } + ); + }; + describe("Stability Pool Withdrawal", async () => { before(async () => { @@ -103,7 +118,7 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulter opens trove with 200% ICR and 10k ZUSD net debt - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -142,8 +157,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -182,9 +197,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -226,8 +241,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: '50000000000000000000' }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(7000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: '70000000000000000000' }); + await defaulterOpenTrove(dec(5000, 18), '50000000000000000000', defaulter_1); + await defaulterOpenTrove(dec(7000, 18), '70000000000000000000', defaulter_2); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -268,9 +283,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: '50000000000000000000' }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(6000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: '60000000000000000000' }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(7000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: '70000000000000000000' }); + await defaulterOpenTrove(dec(5000, 18), '50000000000000000000', defaulter_1); + await defaulterOpenTrove(dec(6000, 18), '60000000000000000000', defaulter_2); + await defaulterOpenTrove(dec(7000, 18), '70000000000000000000', defaulter_3); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -314,8 +329,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(30000, 18), ZERO_ADDRESS, { from: carol }); // 2 Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -356,9 +371,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(30000, 18), ZERO_ADDRESS, { from: carol }); // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -410,9 +425,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' Defaulter 2: 5000 ZUSD & 50 ETH Defaulter 3: 46700 ZUSD & 500 ETH */ - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('207000000000000000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(2160, 18) }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5, 21)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(50, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('46700000000000000000000'), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(500, 'ether') }); + await defaulterOpenTrove('207000000000000000000000', dec(2160, 18), defaulter_1); + await defaulterOpenTrove(dec(5, 21), dec(50, 'ether'), defaulter_2); + await defaulterOpenTrove('46700000000000000000000', dec(500, 'ether'), defaulter_3); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -457,9 +472,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -512,10 +527,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -577,10 +592,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' Defaulter 3: 5000 ZUSD, 50 ETH Defaulter 4: 40000 ZUSD, 400 ETH */ - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(25000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: '250000000000000000000' }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: '50000000000000000000' }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(40000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(400, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(25000, 18), '250000000000000000000', defaulter_2); + await defaulterOpenTrove(dec(5000, 18), '50000000000000000000', defaulter_3); + await defaulterOpenTrove(dec(40000, 18), dec(400, 'ether'), defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -635,10 +650,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -705,10 +720,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' Defaulter 3: 30000 ZUSD Defaulter 4: 5000 ZUSD */ - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(30000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(300, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: '50000000000000000000' }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(30000, 18), dec(300, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(5000, 18), '50000000000000000000', defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -764,10 +779,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open troves - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: '50000000000000000000' }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(5000, 18), '50000000000000000000', defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -833,8 +848,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // 2 Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -900,10 +915,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // 4 Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -992,8 +1007,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // 2 Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1055,9 +1070,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: alice }); // Defaulter 1,2,3 withdraw 10000 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1094,10 +1109,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(100000, 18)), whale, whale, { from: whale, value: dec(100000, 'ether') }); // 4 Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(200, 'ether') }); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -1203,12 +1218,12 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: alice }); // Defaulter 1 withdraws 'almost' 10000 ZUSD: 9999.99991 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999999910000000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); + await defaulterOpenTrove('9999999910000000000000', dec(100, 'ether'), defaulter_1); assert.equal(await stabilityPool.currentScale(), '0'); // Defaulter 2 withdraws 9900 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(9900, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(60, 'ether') }); + await defaulterOpenTrove(dec(9900, 18), dec(60, 'ether'), defaulter_2); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1257,10 +1272,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: alice }); // Defaulter 1 withdraws 'almost' 10k ZUSD. - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999999910000000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - // Defaulter 2 withdraws 59400 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('59400000000000000000000'), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(330, 'ether') }); + await defaulterOpenTrove('9999999910000000000000', dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove('59400000000000000000000', dec(330, 'ether'), defaulter_2); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1336,8 +1350,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: alice }); // Defaulter 1 and default 2 each withdraw 9999.999999999 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(99999, 17)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(99999, 17)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(99999, 17), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(99999, 17), dec(100, 'ether'), defaulter_2); // price drops by 50%: defaulter 1 ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -1388,8 +1402,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: alice }); // Defaulter 1 and default 2 withdraw up to debt of 9999.9 ZUSD and 59999.4 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999900000000000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('59999400000000000000000'), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(600, 'ether') }); + await defaulterOpenTrove('9999900000000000000000', dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove('59999400000000000000000', dec(600, 'ether'), defaulter_2); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1448,7 +1462,7 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(100000, 18)), whale, whale, { from: whale, value: dec(100000, 'ether') }); // Defaulters 1 withdraws 9999.9999999 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999999999900000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); + await defaulterOpenTrove('9999999999900000000000', dec(100, 'ether'), defaulter_1); // Price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1482,10 +1496,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(100000, 18)), whale, whale, { from: whale, value: dec(100000, 'ether') }); // Defaulters 1-4 each withdraw 9999.9 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999900000000000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999900000000000000000'), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999900000000000000000'), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999900000000000000000'), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(100, 'ether') }); + await defaulterOpenTrove('9999900000000000000000', dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove('9999900000000000000000', dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove('9999900000000000000000', dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove('9999900000000000000000', dec(100, 'ether'), defaulter_4); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1559,9 +1573,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(100000, 18)), whale, whale, { from: whale, value: dec(100000, 'ether') }); // Defaulters 1-3 each withdraw 24100, 24300, 24500 ZUSD (inc gas comp) - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(24100, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(24300, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(24500, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(200, 'ether') }); + await defaulterOpenTrove(dec(24100, 18), dec(200, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(24300, 18), dec(200, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(24500, 18), dec(200, 'ether'), defaulter_3); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1685,11 +1699,11 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(100000, 18)), whale, whale, { from: whale, value: dec(100000, 'ether') }); // Defaulters 1-5 each withdraw up to debt of 9999.9999999 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(99999, 17)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(99999, 17)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(99999, 17)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(99999, 17)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(99999, 17)), defaulter_5, defaulter_5, { from: defaulter_5, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(99999, 17), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(99999, 17), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(99999, 17), dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(99999, 17), dec(100, 'ether'), defaulter_4); + await defaulterOpenTrove(dec(99999, 17), dec(100, 'ether'), defaulter_5); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1769,7 +1783,7 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulter opens trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(1, 36)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(1, 27) }); + await defaulterOpenTrove(dec(1, 36), dec(1, 27), defaulter_1); // ETH:USD price drops to $1 billion per ETH await priceFeed.setPrice(dec(1, 27)); @@ -1824,7 +1838,19 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulter opens trove with 50e-7 ETH and 5000 ZUSD. 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: '5000000000000' }); + //await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: '5000000000000' }); + const defaulterZUSDAmount_1 = await getOpenTroveZUSDAmount(dec(5000, 18)); + const defaulterBufFee_1 = await th.getRedemptionBufferFeeRBTC(contracts, defaulterZUSDAmount_1, defaulter_1); + await borrowerOperations.openTrove( + th._100pct, + defaulterZUSDAmount_1, + defaulter_1, + defaulter_1, + { + from: defaulter_1, + value: toBN('5000000000000').add(defaulterBufFee_1) + } + ); // ETH:USD price drops to $1 billion per ETH await priceFeed.setPrice(dec(1, 27)); diff --git a/tests/js/StabilityPool_SPWithdrawalToCDPTest.js b/tests/js/StabilityPool_SPWithdrawalToCDPTest.js index e37b16e..d7a0744 100644 --- a/tests/js/StabilityPool_SPWithdrawalToCDPTest.js +++ b/tests/js/StabilityPool_SPWithdrawalToCDPTest.js @@ -53,6 +53,21 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' const getOpenTroveZUSDAmount = async (totalDebt) => th.getOpenTroveZUSDAmount(contracts, totalDebt); + const defaulterOpenTrove = async (debt, coll, from) => { + const zusdAmount = await getOpenTroveZUSDAmount(debt); + const bufFee = await th.getRedemptionBufferFeeRBTC(contracts, zusdAmount, from); + await borrowerOperations.openTrove( + th._100pct, + zusdAmount, + from, + from, + { + from: from, + value: toBN(coll).add(bufFee) + } + ); + }; + describe("Stability Pool Withdrawal", async () => { before(async () => { @@ -112,7 +127,7 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulter opens trove with 200% ICR and 10k ZUSD net debt - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -156,8 +171,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -201,9 +216,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -250,8 +265,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: '50000000000000000000' }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(7000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: '70000000000000000000' }); + await defaulterOpenTrove(dec(5000, 18), '50000000000000000000', defaulter_1); + await defaulterOpenTrove(dec(7000, 18), '70000000000000000000', defaulter_2); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -297,9 +312,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: '50000000000000000000' }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(6000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: '60000000000000000000' }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(7000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: '70000000000000000000' }); + await defaulterOpenTrove(dec(5000, 18), '50000000000000000000', defaulter_1); + await defaulterOpenTrove(dec(6000, 18), '60000000000000000000', defaulter_2); + await defaulterOpenTrove(dec(7000, 18), '70000000000000000000', defaulter_3); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -348,8 +363,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(30000, 18), ZERO_ADDRESS, { from: carol }); // 2 Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -395,9 +410,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(30000, 18), ZERO_ADDRESS, { from: carol }); // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -454,9 +469,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' Defaulter 2: 5000 ZUSD & 50 ETH Defaulter 3: 46700 ZUSD & 500 ETH */ - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('207000000000000000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(2160, 18) }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5, 21)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(50, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('46700000000000000000000'), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(500, 'ether') }); + await defaulterOpenTrove('207000000000000000000000', dec(2160, 18), defaulter_1); + await defaulterOpenTrove(dec(5, 21), dec(50, 'ether'), defaulter_2); + await defaulterOpenTrove('46700000000000000000000', dec(500, 'ether'), defaulter_3); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -507,9 +522,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -568,10 +583,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -639,10 +654,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' Defaulter 3: 5000 ZUSD, 50 ETH Defaulter 4: 40000 ZUSD, 400 ETH */ - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(25000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: '250000000000000000000' }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: '50000000000000000000' }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(40000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(400, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(25000, 18), '250000000000000000000', defaulter_2); + await defaulterOpenTrove(dec(5000, 18), '50000000000000000000', defaulter_3); + await defaulterOpenTrove(dec(40000, 18), dec(400, 'ether'), defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -703,10 +718,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -778,10 +793,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' Defaulter 3: 30000 ZUSD Defaulter 4: 5000 ZUSD */ - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(30000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(300, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: '50000000000000000000' }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(30000, 18), dec(300, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(5000, 18), '50000000000000000000', defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -842,10 +857,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulters open troves - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: '50000000000000000000' }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(5000, 18), '50000000000000000000', defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -917,8 +932,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // 2 Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -990,10 +1005,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // 4 Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -1089,8 +1104,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // 2 Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1158,9 +1173,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: alice }); // Defaulter 1,2,3 withdraw 10000 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(10000, 18), dec(100, 'ether'), defaulter_3); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1207,10 +1222,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), ZERO_ADDRESS, ZERO_ADDRESS, { from: graham, value: dec(10000, 'ether') }); // 4 Defaulters open trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(20000, 18)), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(200, 'ether') }); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_3); + await defaulterOpenTrove(dec(20000, 18), dec(200, 'ether'), defaulter_4); // price drops by 50%: defaulter ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -1319,12 +1334,12 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: alice }); // Defaulter 1 withdraws 'almost' 10000 ZUSD: 9999.99991 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999999910000000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); + await defaulterOpenTrove('9999999910000000000000', dec(100, 'ether'), defaulter_1); assert.equal(await stabilityPool.currentScale(), '0'); // Defaulter 2 withdraws 9900 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(9900, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(60, 'ether') }); + await defaulterOpenTrove(dec(9900, 18), dec(60, 'ether'), defaulter_2); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1379,10 +1394,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: alice }); // Defaulter 1 withdraws 'almost' 10k ZUSD. - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999999910000000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); + await defaulterOpenTrove('9999999910000000000000', dec(100, 'ether'), defaulter_1); // Defaulter 2 withdraws 59400 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('59400000000000000000000'), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(330, 'ether') }); + await defaulterOpenTrove('59400000000000000000000', dec(330, 'ether'), defaulter_2); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1462,8 +1477,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: alice }); // Defaulter 1 and default 2 each withdraw 9999.999999999 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(99999, 17)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(99999, 17)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); + await defaulterOpenTrove(dec(99999, 17), dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(99999, 17), dec(100, 'ether'), defaulter_2); // price drops by 50%: defaulter 1 ICR falls to 100% await priceFeed.setPrice(dec(100, 18)); @@ -1519,8 +1534,8 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await stabilityPool.provideToSP(dec(10000, 18), ZERO_ADDRESS, { from: alice }); // Defaulter 1 and default 2 withdraw up to debt of 9999.9 ZUSD and 59999.4 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999900000000000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('59999400000000000000000'), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(600, 'ether') }); + await defaulterOpenTrove('9999900000000000000000', dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove('59999400000000000000000', dec(600, 'ether'), defaulter_2); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1584,7 +1599,7 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), ZERO_ADDRESS, ZERO_ADDRESS, { from: dennis, value: dec(10000, 'ether') }); // Defaulters 1 withdraws 9999.9999999 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999999999900000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); + await defaulterOpenTrove('9999999999900000000000', dec(100, 'ether'), defaulter_1); // Price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1623,10 +1638,10 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), ZERO_ADDRESS, ZERO_ADDRESS, { from: dennis, value: dec(10000, 'ether') }); // Defaulters 1-4 each withdraw 9999.9 ZUSD - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999900000000000000000'), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999900000000000000000'), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999900000000000000000'), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(100, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount('9999900000000000000000'), defaulter_4, defaulter_4, { from: defaulter_4, value: dec(100, 'ether') }); + await defaulterOpenTrove('9999900000000000000000', dec(100, 'ether'), defaulter_1); + await defaulterOpenTrove('9999900000000000000000', dec(100, 'ether'), defaulter_2); + await defaulterOpenTrove('9999900000000000000000', dec(100, 'ether'), defaulter_3); + await defaulterOpenTrove('9999900000000000000000', dec(100, 'ether'), defaulter_4); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1707,9 +1722,9 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(10000, 18)), ZERO_ADDRESS, ZERO_ADDRESS, { from: F, value: dec(10000, 'ether') }); // Defaulters 1-3 each withdraw 24100, 24300, 24500 ZUSD (inc gas comp) - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(24100, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(24300, 18)), defaulter_2, defaulter_2, { from: defaulter_2, value: dec(200, 'ether') }); - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(24500, 18)), defaulter_3, defaulter_3, { from: defaulter_3, value: dec(200, 'ether') }); + await defaulterOpenTrove(dec(24100, 18), dec(200, 'ether'), defaulter_1); + await defaulterOpenTrove(dec(24300, 18), dec(200, 'ether'), defaulter_2); + await defaulterOpenTrove(dec(24500, 18), dec(200, 'ether'), defaulter_3); // price drops by 50% await priceFeed.setPrice(dec(100, 18)); @@ -1844,7 +1859,7 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulter opens trove with 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(1, 36)), defaulter_1, defaulter_1, { from: defaulter_1, value: dec(1, 27) }); + await defaulterOpenTrove(dec(1, 36), dec(1, 27), defaulter_1); // ETH:USD price drops to $1 billion per ETH await priceFeed.setPrice(dec(1, 27)); @@ -1899,7 +1914,7 @@ contract('StabilityPool - Withdrawal of stability deposit - Reward calculations' } // Defaulter opens trove with 50e-7 ETH and 5000 ZUSD. 200% ICR - await borrowerOperations.openTrove(th._100pct, await getOpenTroveZUSDAmount(dec(5000, 18)), defaulter_1, defaulter_1, { from: defaulter_1, value: '5000000000000' }); + await defaulterOpenTrove(dec(5000, 18), '5000000000000', defaulter_1); // ETH:USD price drops to $1 billion per ETH await priceFeed.setPrice(dec(1, 27)); diff --git a/tests/js/TroveManagerTest.js b/tests/js/TroveManagerTest.js index 003b598..3cada95 100644 --- a/tests/js/TroveManagerTest.js +++ b/tests/js/TroveManagerTest.js @@ -64,6 +64,61 @@ contract('TroveManager', async accounts => { const openNueTrove = async (params) => th.openNueTrove(contracts, params); const withdrawZUSD = async (params) => th.withdrawZUSD(contracts, params); + const drainRedemptionBufferWithWhale = async () => { + const price = toBN(await priceFeed.getPrice()); + + const bufferBalRBTC = toBN(await contracts.redemptionBuffer.getBalance()); // RBTC wei + const drainZUSD = bufferBalRBTC.mul(price).div(mv._1e18BN); // max ZUSD buffer can cover (floor) + + // If buffer can't cover even 1 wei of ZUSD at this price, nothing to do + if (drainZUSD.eq(toBN(0))) return; + + // Fund whale with enough ZUSD to perform the drain (we don't care about Alice/Bob/Carol wallet balances in this test) + let needed = drainZUSD.sub(toBN(await zusdToken.balanceOf(whale))); + if (needed.gt(toBN(0))) { + for (const src of [alice, bob, carol]) { + const srcBal = toBN(await zusdToken.balanceOf(src)); + if (srcBal.eq(toBN(0))) continue; + + const send = srcBal.gte(needed) ? needed : srcBal; + await zusdToken.transfer(whale, send, { from: src }); + needed = needed.sub(send); + if (needed.eq(toBN(0))) break; + } + } + + // Sanity: whale must have enough now + const whaleBal = toBN(await zusdToken.balanceOf(whale)); + assert(whaleBal.gte(drainZUSD), `whale doesn't have enough ZUSD to drain buffer: have=${whaleBal} need=${drainZUSD}`); + + await zusdToken.approve(troveManager.address, drainZUSD, { from: whale }); + + // Hints (even if Troves won't be touched, some implementations still validate hints) + const { firstRedemptionHint, partialRedemptionHintNICR } = + await hintHelpers.getRedemptionHints(drainZUSD, price, 0); + + const { 0: upperHint, 1: lowerHint } = + await sortedTroves.findInsertPosition(partialRedemptionHintNICR, whale, whale); + + // Drain: this should be buffer-only (remaining-to-redeem from Troves becomes 0) + await troveManager.redeemCollateral( + drainZUSD, + firstRedemptionHint, + upperHint, + lowerHint, + partialRedemptionHintNICR, + 0, + th._100pct, + { from: whale, gasPrice: 0 } + ); + + // Verify buffer is effectively empty at this price (capacity == 0 ZUSD) + const bufferAfterRBTC = toBN(await contracts.redemptionBuffer.getBalance()); + const bufferAfterMaxZUSD = bufferAfterRBTC.mul(price).div(mv._1e18BN); + assert.equal(bufferAfterMaxZUSD.toString(), "0", "buffer still has redeemable capacity"); + }; + + before(async () => { contracts = await deploymentHelper.deployLiquityCore(); permit2 = contracts.permit2; @@ -875,9 +930,9 @@ contract('TroveManager', async accounts => { it("liquidate(): does not alter the liquidated user's token balance", async () => { await openTrove({ ICR: toBN(dec(10, 18)), extraParams: { from: whale } }); - const { zusdAmount: A_zusdAmount } = await openTrove({ ICR: toBN(dec(2, 18)), extraZUSDAmount: toBN(dec(300, 18)), extraParams: { from: alice } }); - const { zusdAmount: B_zusdAmount } = await openTrove({ ICR: toBN(dec(2, 18)), extraZUSDAmount: toBN(dec(200, 18)), extraParams: { from: bob } }); - const { zusdAmount: C_zusdAmount } = await openTrove({ ICR: toBN(dec(2, 18)), extraZUSDAmount: toBN(dec(100, 18)), extraParams: { from: carol } }); + const { requestedZUSDAmount: A_zusdAmount } = await openTrove({ ICR: toBN(dec(2, 18)), extraZUSDAmount: toBN(dec(300, 18)), extraParams: { from: alice } }); + const { requestedZUSDAmount: B_zusdAmount } = await openTrove({ ICR: toBN(dec(2, 18)), extraZUSDAmount: toBN(dec(200, 18)), extraParams: { from: bob } }); + const { requestedZUSDAmount: C_zusdAmount } = await openTrove({ ICR: toBN(dec(2, 18)), extraZUSDAmount: toBN(dec(100, 18)), extraParams: { from: carol } }); await priceFeed.setPrice(dec(100, 18)); @@ -2407,6 +2462,9 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + const nonce = th.generateNonce(); const deadline = th.toDeadline(1000 * 60 * 60 * 60 * 24 * 28 ); const permitTransferFrom = { @@ -2429,6 +2487,13 @@ contract('TroveManager', async accounts => { // Dennis redeems 20 ZUSD // Don't pay for gas, as it makes it easier to calculate the received Ether + await contracts.zusdToken.approve( + troveManager.address, + redemptionAmount.toString(), + { from: dennis, + gasPrice: 0 + } + ); const redemptionTx = await troveManager.redeemCollateralViaDllrWithPermit2( redemptionAmount.toString(), firstRedemptionHint, @@ -2519,11 +2584,22 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + // get ERC2612 permission from alice for stability pool to spend DLLR amount const permission = await signERC2612Permit(dennis_signer, nueMockToken.address, dennis_signer.address, troveManager.address, redemptionAmount.toString()); // Dennis redeems 20 ZUSD // Don't pay for gas, as it makes it easier to calculate the received Ether + await contracts.zusdToken.approve( + troveManager.address, + redemptionAmount.toString(), + { + from: dennis, + gasPrice: 0 + } + ); const redemptionTx = await troveManager.redeemCollateralViaDLLR( redemptionAmount.toString(), firstRedemptionHint, @@ -2607,8 +2683,19 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + // Dennis redeems 20 ZUSD // Don't pay for gas, as it makes it easier to calculate the received Ether + await contracts.zusdToken.approve( + troveManager.address, + redemptionAmount.toString(), + { + from: dennis, + gasPrice: 0 + } + ); const redemptionTx = await troveManager.redeemCollateral( redemptionAmount, firstRedemptionHint, @@ -2687,8 +2774,19 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + // Dennis redeems 20 ZUSD // Don't pay for gas, as it makes it easier to calculate the received Ether + await contracts.zusdToken.approve( + troveManager.address, + redemptionAmount.toString(), + { + from: dennis, + gasPrice: 0 + } + ); const redemptionTx = await troveManager.redeemCollateral( redemptionAmount, ZERO_ADDRESS, // invalid first hint @@ -2767,8 +2865,19 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + // Dennis redeems 20 ZUSD // Don't pay for gas, as it makes it easier to calculate the received Ether + await contracts.zusdToken.approve( + troveManager.address, + redemptionAmount.toString(), + { + from: dennis, + gasPrice: 0 + } + ); const redemptionTx = await troveManager.redeemCollateral( redemptionAmount, erin, // invalid first hint, it doesn’t have a trove @@ -2853,8 +2962,19 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + // Dennis redeems 20 ZUSD // Don't pay for gas, as it makes it easier to calculate the received Ether + await contracts.zusdToken.approve( + troveManager.address, + redemptionAmount.toString(), + { + from: dennis, + gasPrice: 0 + } + ); const redemptionTx = await troveManager.redeemCollateral( redemptionAmount, erin, // invalid trove, below MCR @@ -2912,12 +3032,23 @@ contract('TroveManager', async accounts => { // --- TEST --- // open trove from redeemer. Redeemer has highest ICR (100ETH, 100 ZUSD), 20000% - const { zusdAmount: F_zusdAmount } = await openTrove({ ICR: toBN(dec(200, 18)), extraZUSDAmount: redemptionAmount.mul(toBN(2)), extraParams: { from: flyn } }); + const { requestedZUSDAmount: F_zusdAmount } = await openTrove({ ICR: toBN(dec(200, 18)), extraZUSDAmount: redemptionAmount.mul(toBN(2)), extraParams: { from: flyn } }); // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + // Flyn redeems collateral + await contracts.zusdToken.approve( + troveManager.address, + redemptionAmount.toString(), + { + from: flyn, + gasPrice: 0 + } + ); await troveManager.redeemCollateral(redemptionAmount, alice, alice, alice, 0, 0, th._100pct, { from: flyn }); // Check Flyn's redemption has reduced his balance from 100 to (100-60) = 40 ZUSD @@ -2969,12 +3100,23 @@ contract('TroveManager', async accounts => { // --- TEST --- // open trove from redeemer. Redeemer has highest ICR (100ETH, 100 ZUSD), 20000% - const { zusdAmount: F_zusdAmount } = await openTrove({ ICR: toBN(dec(200, 18)), extraZUSDAmount: redemptionAmount.mul(toBN(2)), extraParams: { from: flyn } }); + const { requestedZUSDAmount: F_zusdAmount } = await openTrove({ ICR: toBN(dec(200, 18)), extraZUSDAmount: redemptionAmount.mul(toBN(2)), extraParams: { from: flyn } }); // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + // Flyn redeems collateral with only two iterations + await contracts.zusdToken.approve( + troveManager.address, + attemptedRedemptionAmount.toString(), + { + from: flyn, + gasPrice: 0 + } + ); await troveManager.redeemCollateral(attemptedRedemptionAmount, alice, alice, alice, 0, 2, th._100pct, { from: flyn }); // Check Flyn's redemption has reduced his balance from 100 to (100-40) = 60 ZUSD @@ -3012,6 +3154,8 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); // ZUSD redemption is 55000 US const ZUSDRedemption = dec(55000, 18); @@ -3040,6 +3184,8 @@ contract('TroveManager', async accounts => { // Skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); // ZUSD redemption is 49900 ZUSD const ZUSDRedemption = dec(49900, 18); // await zusdToken.balanceOf(B) //dec(59800, 18) @@ -3079,6 +3225,11 @@ contract('TroveManager', async accounts => { // --- TEST --- + // skip bootstrapping phase + await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + const { firstRedemptionHint, partialRedemptionHintNICR @@ -3108,6 +3259,14 @@ contract('TroveManager', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Alice redeems 1 ZUSD from Carol's Trove + await contracts.zusdToken.approve( + troveManager.address, + frontRunRedepmtion.toString(), + { + from: alice, + gasPrice: 0 + } + ); await troveManager.redeemCollateral( frontRunRedepmtion, firstRedemptionHint, @@ -3120,6 +3279,14 @@ contract('TroveManager', async accounts => { } // Dennis tries to redeem 20 ZUSD + await contracts.zusdToken.approve( + troveManager.address, + redemptionAmount.toString(), + { + from: dennis, + gasPrice: 0 + } + ); const redemptionTx = await troveManager.redeemCollateral( redemptionAmount, firstRedemptionHint, @@ -3179,7 +3346,17 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + await contracts.zusdToken.approve( + troveManager.address, + amount.toString(), + { + from: carol, + gasPrice: 0 + } + ); const redemptionTx = await troveManager.redeemCollateral( amount, alice, @@ -3212,7 +3389,7 @@ contract('TroveManager', async accounts => { // --- SETUP --- const { netDebt: A_debt } = await openTrove({ ICR: toBN(dec(13, 18)), extraParams: { from: alice } }); - const { zusdAmount: B_zusdAmount, totalDebt: B_totalDebt } = await openTrove({ ICR: toBN(dec(133, 16)), extraZUSDAmount: A_debt, extraParams: { from: bob } }); + const { requestedZUSDAmount: B_zusdAmount, totalDebt: B_totalDebt } = await openTrove({ ICR: toBN(dec(133, 16)), extraZUSDAmount: A_debt, extraParams: { from: bob } }); await zusdToken.transfer(carol, B_zusdAmount, { from: bob }); @@ -3224,7 +3401,17 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + await contracts.zusdToken.approve( + troveManager.address, + A_debt.toString(), + { + from: carol, + gasPrice: 0 + } + ); await troveManager.redeemCollateral( A_debt, alice, @@ -3272,7 +3459,17 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + await contracts.zusdToken.approve( + troveManager.address, + redemptionAmount.toString(), + { + from: dennis, + gasPrice: 0 + } + ); const tx = await troveManager.redeemCollateral( redemptionAmount, carol, // try to trick redeemCollateral by passing a hint that doesn't exactly point to the @@ -3334,6 +3531,11 @@ contract('TroveManager', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Erin attempts to redeem with _amount = 0 + await contracts.zusdToken.approve( + troveManager.address, + toBN("1").toString(), + { from: erin } + ); const redemptionTxPromise = troveManager.redeemCollateral(0, erin, erin, erin, 0, 0, th._100pct, { from: erin }); await assertRevert(redemptionTxPromise, "TroveManager: Amount must be greater than zero"); }); @@ -3568,6 +3770,8 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); // Erin attempts to redeem 400 ZUSD const { @@ -3581,6 +3785,14 @@ contract('TroveManager', async accounts => { erin ); + await contracts.zusdToken.approve( + troveManager.address, + dec(400,18), + { + from: erin, + gasPrice: 0 + } + ); await troveManager.redeemCollateral( dec(400, 18), firstRedemptionHint, @@ -3588,7 +3800,8 @@ contract('TroveManager', async accounts => { lowerPartialRedemptionHint, partialRedemptionHintNICR, 0, th._100pct, - { from: erin }); + { from: erin } + ); // Check activePool debt reduced by 400 ZUSD const activePool_debt_after = await activePool.getZUSDDebt(); @@ -3639,6 +3852,8 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); // Erin tries to redeem 1000 ZUSD try { @@ -3793,7 +4008,17 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + await contracts.zusdToken.approve( + troveManager.address, + _120_ZUSD.toString(), + { + from: erin, + gasPrice: 0 + } + ); const redemption_1 = await troveManager.redeemCollateral( _120_ZUSD, firstRedemptionHint, @@ -3824,6 +4049,14 @@ contract('TroveManager', async accounts => { flyn ); + await contracts.zusdToken.approve( + troveManager.address, + _373_ZUSD.toString(), + { + from: flyn, + gasPrice: 0 + } + ); const redemption_2 = await troveManager.redeemCollateral( _373_ZUSD, firstRedemptionHint, @@ -3853,6 +4086,14 @@ contract('TroveManager', async accounts => { graham ); + await contracts.zusdToken.approve( + troveManager.address, + _950_ZUSD.toString(), + { + from: graham, + gasPrice: 0 + } + ); const redemption_3 = await troveManager.redeemCollateral( _950_ZUSD, firstRedemptionHint, @@ -3935,6 +4176,8 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); // Bob attempts to redeem his ill-gotten 101 ZUSD, from a system that has 100 ZUSD outstanding debt try { @@ -4301,6 +4544,9 @@ contract('TroveManager', async accounts => { const B_balanceBefore = toBN(await web3.eth.getBalance(B)); const C_balanceBefore = toBN(await web3.eth.getBalance(C)); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); + // whale redeems 360 ZUSD. Expect this to fully redeem A, B, C, and partially redeem D. await th.redeemCollateral(whale, contracts, redemptionAmount); @@ -4405,6 +4651,8 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); // whale redeems ZUSD. Expect this to fully redeem A, B, C, and partially redeem 15 ZUSD from D. const redemptionTx = await th.redeemCollateralAndGetTxObject(whale, contracts, redemptionAmount, th._100pct, { gasPrice: 0 }); @@ -4481,9 +4729,9 @@ contract('TroveManager', async accounts => { const B_surplus = B_collBefore.sub(B_netDebt.mul(mv._1e18BN).div(price)); const C_surplus = C_collBefore.sub(C_netDebt.mul(mv._1e18BN).div(price)); - const { collateral: A_coll } = await openTrove({ ICR: toBN(dec(200, 16)), extraZUSDAmount: dec(100, 18), extraParams: { from: A } }); - const { collateral: B_coll } = await openTrove({ ICR: toBN(dec(190, 16)), extraZUSDAmount: dec(100, 18), extraParams: { from: B } }); - const { collateral: C_coll } = await openTrove({ ICR: toBN(dec(180, 16)), extraZUSDAmount: dec(100, 18), extraParams: { from: C } }); + const { collateral: A_coll, bufferFee: A_buffFee } = await openTrove({ ICR: toBN(dec(200, 16)), extraZUSDAmount: dec(100, 18), extraParams: { from: A } }); + const { collateral: B_coll, bufferFee: B_buffFee } = await openTrove({ ICR: toBN(dec(190, 16)), extraZUSDAmount: dec(100, 18), extraParams: { from: B } }); + const { collateral: C_coll, bufferFee: C_buffFee } = await openTrove({ ICR: toBN(dec(180, 16)), extraZUSDAmount: dec(100, 18), extraParams: { from: C } }); const A_collAfter = await troveManager.getTroveColl(A); const B_collAfter = await troveManager.getTroveColl(B); @@ -4512,7 +4760,7 @@ contract('TroveManager', async accounts => { it('redeemCollateral(): reverts if fee eats up all returned collateral', async () => { // --- SETUP --- - const { zusdAmount } = await openTrove({ ICR: toBN(dec(200, 16)), extraZUSDAmount: dec(1, 24), extraParams: { from: alice } }); + const { requestedZUSDAmount: zusdAmount } = await openTrove({ ICR: toBN(dec(200, 16)), extraZUSDAmount: dec(1, 24), extraParams: { from: alice } }); await openTrove({ ICR: toBN(dec(150, 16)), extraParams: { from: bob } }); const price = await priceFeed.getPrice(); @@ -4522,6 +4770,8 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // Drain the redemption buffer so this test will complete successufully + await drainRedemptionBufferWithWhale(); // keep redeeming until we get the base rate to the ceiling of 100% for (let i = 0; i < 2; i++) { @@ -4532,6 +4782,14 @@ contract('TroveManager', async accounts => { } = await hintHelpers.getRedemptionHints(zusdAmount, price, 0); // Don't pay for gas, as it makes it easier to calculate the received Ether + await contracts.zusdToken.approve( + troveManager.address, + zusdAmount.toString(), + { + from: alice, + gasPrice: 0 + } + ); const redemptionTx = await troveManager.redeemCollateral( zusdAmount, firstRedemptionHint, @@ -4554,6 +4812,14 @@ contract('TroveManager', async accounts => { partialRedemptionHintNICR } = await hintHelpers.getRedemptionHints(zusdAmount, price, 0); + await contracts.zusdToken.approve( + troveManager.address, + zusdAmount.toString(), + { + from: alice, + gasPrice: 0 + } + ); await assertRevert( troveManager.redeemCollateral( zusdAmount, diff --git a/tests/js/TroveManager_RecoveryModeTest.js b/tests/js/TroveManager_RecoveryModeTest.js index 3572759..7d0b328 100644 --- a/tests/js/TroveManager_RecoveryModeTest.js +++ b/tests/js/TroveManager_RecoveryModeTest.js @@ -1570,9 +1570,9 @@ contract('TroveManager - in Recovery Mode', async accounts => { it("liquidate(): does not alter the liquidated user's token balance", async () => { await openTrove({ ICR: toBN(dec(220, 16)), extraZUSDAmount: dec(1000, 18), extraParams: { from: whale } }); - const { zusdAmount: A_zusdAmount } = await openTrove({ ICR: toBN(dec(200, 16)), extraZUSDAmount: dec(300, 18), extraParams: { from: alice } }); - const { zusdAmount: B_zusdAmount } = await openTrove({ ICR: toBN(dec(200, 16)), extraZUSDAmount: dec(200, 18), extraParams: { from: bob } }); - const { zusdAmount: C_zusdAmount } = await openTrove({ ICR: toBN(dec(206, 16)), extraZUSDAmount: dec(100, 18), extraParams: { from: carol } }); + const { requestedZUSDAmount: A_zusdAmount } = await openTrove({ ICR: toBN(dec(200, 16)), extraZUSDAmount: dec(300, 18), extraParams: { from: alice } }); + const { requestedZUSDAmount: B_zusdAmount } = await openTrove({ ICR: toBN(dec(200, 16)), extraZUSDAmount: dec(200, 18), extraParams: { from: bob } }); + const { requestedZUSDAmount: C_zusdAmount } = await openTrove({ ICR: toBN(dec(206, 16)), extraZUSDAmount: dec(100, 18), extraParams: { from: carol } }); await priceFeed.setPrice(dec(105, 18)); @@ -2470,9 +2470,9 @@ contract('TroveManager - in Recovery Mode', async accounts => { await openTrove({ ICR: toBN(dec(300, 16)), extraParams: { from: whale } }); // D, E, F open troves that will fall below MCR when price drops to 100 - const { zusdAmount: zusdAmountD } = await openTrove({ ICR: toBN(dec(200, 16)), extraParams: { from: dennis } }); - const { zusdAmount: zusdAmountE } = await openTrove({ ICR: toBN(dec(133, 16)), extraParams: { from: erin } }); - const { zusdAmount: zusdAmountF } = await openTrove({ ICR: toBN(dec(111, 16)), extraParams: { from: freddy } }); + const { requestedZUSDAmount: zusdAmountD } = await openTrove({ ICR: toBN(dec(200, 16)), extraParams: { from: dennis } }); + const { requestedZUSDAmount: zusdAmountE } = await openTrove({ ICR: toBN(dec(133, 16)), extraParams: { from: erin } }); + const { requestedZUSDAmount: zusdAmountF } = await openTrove({ ICR: toBN(dec(111, 16)), extraParams: { from: freddy } }); // Check list size is 4 assert.equal((await sortedTroves.getSize()).toString(), '4'); @@ -2507,11 +2507,11 @@ contract('TroveManager - in Recovery Mode', async accounts => { it("liquidateTroves(): Liquidating troves at 100 < ICR < 110 with SP deposits correctly impacts their SP deposit and ETH gain", async () => { // Whale provides ZUSD to the SP - const { zusdAmount: W_zusdAmount } = await openTrove({ ICR: toBN(dec(300, 16)), extraZUSDAmount: dec(4000, 18), extraParams: { from: whale } }); + const { requestedZUSDAmount: W_zusdAmount } = await openTrove({ ICR: toBN(dec(300, 16)), extraZUSDAmount: dec(4000, 18), extraParams: { from: whale } }); await stabilityPool.provideToSP(W_zusdAmount, ZERO_ADDRESS, { from: whale }); - const { zusdAmount: A_zusdAmount, totalDebt: A_totalDebt, collateral: A_coll } = await openTrove({ ICR: toBN(dec(191, 16)), extraZUSDAmount: dec(40, 18), extraParams: { from: alice } }); - const { zusdAmount: B_zusdAmount, totalDebt: B_totalDebt, collateral: B_coll } = await openTrove({ ICR: toBN(dec(200, 16)), extraZUSDAmount: dec(240, 18), extraParams: { from: bob } }); + const { requestedZUSDAmount: A_zusdAmount, totalDebt: A_totalDebt, collateral: A_coll } = await openTrove({ ICR: toBN(dec(191, 16)), extraZUSDAmount: dec(40, 18), extraParams: { from: alice } }); + const { requestedZUSDAmount: B_zusdAmount, totalDebt: B_totalDebt, collateral: B_coll } = await openTrove({ ICR: toBN(dec(200, 16)), extraZUSDAmount: dec(240, 18), extraParams: { from: bob } }); const { totalDebt: C_totalDebt, collateral: C_coll } = await openTrove({ ICR: toBN(dec(209, 16)), extraParams: { from: carol } }); // A, B provide to the SP diff --git a/utils/js/deploymentHelpers.js b/utils/js/deploymentHelpers.js index bc2b1b3..2095df2 100644 --- a/utils/js/deploymentHelpers.js +++ b/utils/js/deploymentHelpers.js @@ -3,6 +3,7 @@ const SortedTroves = artifacts.require("./SortedTroves.sol"); const LiquityBaseParams = artifacts.require("./LiquityBaseParams.sol"); const TroveManagerRedeemOps = artifacts.require("./Dependencies/TroveManagerRedeemOps.sol"); const TroveManager = artifacts.require("./TroveManager.sol"); +const RedemptionBuffer = artifacts.require("./RedemptionBuffer.sol"); const PriceFeedTestnet = artifacts.require("./PriceFeedTestnet.sol"); const PriceFeedSovryn = artifacts.require("./PriceFeedSovrynTester.sol"); const ZUSDToken = artifacts.require("./ZUSDToken.sol"); @@ -107,6 +108,7 @@ class DeploymentHelper { const liquityBaseParams = await LiquityBaseParams.new(); const troveManagerRedeemOps = await TroveManagerRedeemOps.new(TWO_WEEKS, permit2.address); const troveManager = await TroveManager.new(TWO_WEEKS, permit2.address); + const redemptionBuffer = await RedemptionBuffer.new(); const activePool = await ActivePool.new(); const stabilityPool = await StabilityPool.new(permit2.address); const gasPool = await GasPool.new(); @@ -132,6 +134,7 @@ class DeploymentHelper { LiquityBaseParams.setAsDeployed(liquityBaseParams); TroveManagerRedeemOps.setAsDeployed(troveManagerRedeemOps); TroveManager.setAsDeployed(troveManager); + RedemptionBuffer.setAsDeployed(redemptionBuffer); ActivePool.setAsDeployed(activePool); StabilityPool.setAsDeployed(stabilityPool); GasPool.setAsDeployed(gasPool); @@ -151,6 +154,7 @@ class DeploymentHelper { liquityBaseParams, troveManagerRedeemOps, troveManager, + redemptionBuffer, activePool, stabilityPool, gasPool, @@ -175,6 +179,7 @@ class DeploymentHelper { testerContracts.priceFeedTestnet = await PriceFeedTestnet.new(); testerContracts.priceFeedSovryn = await PriceFeedSovryn.new(); testerContracts.sortedTroves = await SortedTroves.new(); + testerContracts.redemptionBuffer = await RedemptionBuffer.new(); // Actual tester contracts testerContracts.communityIssuance = await CommunityIssuanceTester.new(); testerContracts.activePool = await ActivePoolTester.new(); @@ -272,6 +277,7 @@ class DeploymentHelper { const liquityBaseParams = await LiquityBaseParams.new(); const troveManagerRedeemOps = await TroveManagerRedeemOps.new(TWO_WEEKS, permit2.address); const troveManager = await TroveManager.new(TWO_WEEKS); + const redemptionBuffer = await RedemptionBuffer.new(); const activePool = await ActivePool.new(); const stabilityPool = await StabilityPool.new(permit2.address); const gasPool = await GasPool.new(); @@ -296,6 +302,7 @@ class DeploymentHelper { liquityBaseParams, troveManagerRedeemOps, troveManager, + redemptionBuffer, activePool, stabilityPool, gasPool, @@ -453,6 +460,7 @@ class DeploymentHelper { _zeroStakingAddress: ZEROContracts.zeroStaking.address } ); + await contracts.troveManager.setRedemptionBufferAddress(contracts.redemptionBuffer.address); // set contracts in BorrowerOperations await contracts.borrowerOperations.setAddresses( @@ -469,6 +477,15 @@ class DeploymentHelper { contracts.zusdToken.address, ZEROContracts.zeroStaking.address ); + await contracts.borrowerOperations.setRedemptionBufferAddress(contracts.redemptionBuffer.address); + await contracts.borrowerOperations.setRedemptionBufferRate(web3.utils.toBN('25000000000000000')); // 2.5% in 1e18 precision + + // set contracts in RedemptionBuffer + await contracts.redemptionBuffer.setAddresses( + contracts.borrowerOperations.address, + contracts.troveManager.address, + contracts.feeDistributor.address + ); // set contracts in FeeDistributor await contracts.feeDistributor.setAddresses( @@ -480,6 +497,7 @@ class DeploymentHelper { contracts.zusdToken.address, contracts.activePool.address ); + await contracts.feeDistributor.setRedemptionBufferAddress(contracts.redemptionBuffer.address); // set contracts in the Pools await contracts.stabilityPool.setAddresses( diff --git a/utils/js/proxyHelpers.js b/utils/js/proxyHelpers.js index ec696ae..fb6a06b 100644 --- a/utils/js/proxyHelpers.js +++ b/utils/js/proxyHelpers.js @@ -123,6 +123,10 @@ class BorrowerOperationsProxy extends Proxy { return this.forwardFunction(params, 'claimRedeemedCollateral(address)') } + async getRedemptionBufferFeeRBTC(...params) { + return this.forwardFunction(params, 'getRedemptionBufferFeeRBTC(uint256)') + } + async getNewTCRFromTroveChange(...params) { return this.proxyFunction('getNewTCRFromTroveChange', params) } diff --git a/utils/js/testHelpers.js b/utils/js/testHelpers.js index a1dee4e..b27d589 100644 --- a/utils/js/testHelpers.js +++ b/utils/js/testHelpers.js @@ -549,6 +549,64 @@ class TestHelper { return { newColl, newDebt }; } + + static _getBorrowerOpsTruffleInstance(contracts) { + const bo = contracts.borrowerOperations; + + // If it's the Proxy wrapper, it has forwardFunction() and a .contract pointing to the real Truffle instance + if (bo && typeof bo.forwardFunction === "function" && bo.contract) { + return bo.contract; + } + + // Otherwise it's already the Truffle contract instance + return bo; + } + + static _getBorrowerOpsCallFrom(contracts, from) { + const bo = contracts.borrowerOperations; + + // In proxy tests, you may want msg.sender == DSProxy during eth_call + if (bo && typeof bo.getProxyAddressFromUser === "function") { + return bo.getProxyAddressFromUser(from); + } + + return from; + } + + static _getTroveManagerTruffleInstance(contracts) { + const tm = contracts.troveManager; + + // Proxy wrapper has forwardFunction() and .contract points to the real Truffle instance + if (tm && typeof tm.forwardFunction === "function" && tm.contract) { + return tm.contract; + } + + // Already the Truffle instance + return tm; + } + + + static async getRedemptionBufferFeeRBTC(contracts, zusdAmount, from) { + const bo = this._getBorrowerOpsTruffleInstance(contracts); + if (!bo || !bo.getRedemptionBufferFeeRBTC) return this.toBN(0); + + const callFrom = from && from !== this.ZERO_ADDRESS ? this._getBorrowerOpsCallFrom(contracts, from) : undefined; + const opts = callFrom ? { from: callFrom } : {}; + + return this.toBN(await bo.getRedemptionBufferFeeRBTC.call(zusdAmount, opts)); + } + + static _getBorrowerAddress(contracts, user) { + // If this is a proxy wrapper, map to DSProxy + if (contracts.borrowerOperations && typeof contracts.borrowerOperations.getProxyAddressFromUser === "function") { + return contracts.borrowerOperations.getProxyAddressFromUser(user); + } + return user; + } + + + + // --- BorrowerOperations gas functions --- static async openTrove_allAccounts(accounts, contracts, ETHAmount, ZUSDAmount) { @@ -752,45 +810,86 @@ class TestHelper { static async openTrove( contracts, - { maxFeePercentage, extraZUSDAmount, upperHint, lowerHint, ICR, extraParams } + { maxFeePercentage, zusdAmount, extraZUSDAmount, upperHint, lowerHint, ICR, extraParams } ) { + // Can't specify both + if (zusdAmount !== undefined && extraZUSDAmount !== undefined) { + throw new Error("openTrove helper: specify either zusdAmount OR extraZUSDAmount, not both"); + } + if (!maxFeePercentage) maxFeePercentage = this._100pct; + if (!extraZUSDAmount) extraZUSDAmount = this.toBN(0); else if (typeof extraZUSDAmount == "string") extraZUSDAmount = this.toBN(extraZUSDAmount); + if (!upperHint) upperHint = this.ZERO_ADDRESS; if (!lowerHint) lowerHint = this.ZERO_ADDRESS; + if (!extraParams) extraParams = {}; + if (!extraParams.from) throw new Error("openTrove helper: extraParams.from is required"); + + const from = extraParams.from; + const MIN_DEBT = ( await this.getNetBorrowingAmount(contracts, await contracts.borrowerOperations.MIN_NET_DEBT()) ).add(this.toBN(1)); // add 1 to avoid rounding issues - const zusdAmount = MIN_DEBT.add(extraZUSDAmount); - if (!ICR && !extraParams.value) ICR = this.toBN(this.dec(15, 17)); - // 150% + // Decide requested ZUSD amount + let requestedZUSDAmount; + if (zusdAmount !== undefined) { + requestedZUSDAmount = typeof zusdAmount === "string" ? this.toBN(zusdAmount) : this.toBN(zusdAmount); + } else { + if (!extraZUSDAmount) extraZUSDAmount = this.toBN(0); + else if (typeof extraZUSDAmount === "string") extraZUSDAmount = this.toBN(extraZUSDAmount); + + requestedZUSDAmount = MIN_DEBT.add(extraZUSDAmount); + } + + if (!ICR && !extraParams.value) ICR = this.toBN(this.dec(15, 17)); // 150% else if (typeof ICR == "string") ICR = this.toBN(ICR); - const totalDebt = await this.getOpenTroveTotalDebt(contracts, zusdAmount); + const totalDebt = await this.getOpenTroveTotalDebt(contracts, requestedZUSDAmount); const netDebt = await this.getActualDebtFromComposite(totalDebt, contracts); + // ----------------------------- + // NEW: compute intended trove collateral + buffer fee-on-top + // ----------------------------- + + // 1) intended collateral that should end up in the trove / ActivePool (NOT including buffer fee) + let collateral; if (ICR) { const price = await contracts.priceFeedTestnet.getPrice(); - extraParams.value = ICR.mul(totalDebt).div(price); + collateral = ICR.mul(totalDebt).div(price); + } else { + // If caller supplied a value without ICR, treat it as intended trove collateral + // (previously msg.value == collateral; now we add fee on top) + collateral = + typeof extraParams.value == "string" ? this.toBN(extraParams.value) : this.toBN(extraParams.value); } + // 2) quote the buffer fee for this borrow amount + const bufferFee = await this.getRedemptionBufferFeeRBTC(contracts, requestedZUSDAmount, from); + + // 3) send collateral + fee + const totalValue = collateral.add(bufferFee); + extraParams.value = totalValue; + const tx = await contracts.borrowerOperations.openTrove( maxFeePercentage, - zusdAmount, + requestedZUSDAmount, upperHint, lowerHint, extraParams ); return { - zusdAmount, + requestedZUSDAmount, netDebt, totalDebt, ICR, - collateral: extraParams.value, + collateral, // trove collateral (what ends up in ActivePool / Trove) + bufferFee, // fee paid to RedemptionBuffer + totalValue, // msg.value actually sent (collateral + fee) tx }; } @@ -800,28 +899,50 @@ class TestHelper { { maxFeePercentage, extraZUSDAmount, upperHint, lowerHint, ICR, extraParams } ) { if (!maxFeePercentage) maxFeePercentage = this._100pct; + if (!extraZUSDAmount) extraZUSDAmount = this.toBN(0); else if (typeof extraZUSDAmount == "string") extraZUSDAmount = this.toBN(extraZUSDAmount); + if (!upperHint) upperHint = this.ZERO_ADDRESS; if (!lowerHint) lowerHint = this.ZERO_ADDRESS; + if (!extraParams) extraParams = {}; + if (!extraParams.from) throw new Error("openNueTrove helper: extraParams.from is required"); + const from = extraParams.from; + const MIN_DEBT = ( await this.getNetBorrowingAmount(contracts, await contracts.borrowerOperations.MIN_NET_DEBT()) ).add(this.toBN(1)); // add 1 to avoid rounding issues + const zusdAmount = MIN_DEBT.add(extraZUSDAmount); - if (!ICR && !extraParams.value) ICR = this.toBN(this.dec(15, 17)); - // 150% + if (!ICR && !extraParams.value) ICR = this.toBN(this.dec(15, 17)); // 150% else if (typeof ICR == "string") ICR = this.toBN(ICR); const totalDebt = await this.getOpenTroveTotalDebt(contracts, zusdAmount); const netDebt = await this.getActualDebtFromComposite(totalDebt, contracts); + // ----------------------------- + // NEW: compute intended trove collateral + buffer fee-on-top + // ----------------------------- + + // 1) intended collateral that should end up in the trove (NOT including buffer fee) + let collateral; if (ICR) { const price = await contracts.priceFeedTestnet.getPrice(); - extraParams.value = ICR.mul(totalDebt).div(price); + collateral = ICR.mul(totalDebt).div(price); + } else { + // If caller supplied a value without ICR, treat it as intended trove collateral + collateral = this.toBN(extraParams.value); } + // 2) quote the buffer fee for this borrow amount (non-view -> use .call()) + const bufferFee = await this.getRedemptionBufferFeeRBTC(contracts, zusdAmount, from); + + // 3) send collateral + fee + const totalValue = collateral.add(bufferFee); + extraParams.value = totalValue; + const tx = await contracts.borrowerOperations.openNueTrove( maxFeePercentage, zusdAmount, @@ -835,11 +956,365 @@ class TestHelper { netDebt, totalDebt, ICR, - collateral: extraParams.value, + collateral, // trove collateral (what ends up in ActivePool / Trove) + bufferFee, // fee paid to RedemptionBuffer + totalValue, // msg.value actually sent (collateral + fee) tx }; } + /** + * adjustTrove(): wrapper around BorrowerOperations.adjustTrove that: + * - optionally targets an ICR (for debt increase) + * - automatically adds the RedemptionBuffer fee to msg.value when minting new ZUSD + * + * Notes: + * - extraParams.value is treated as the *collateral top-up* (the amount that should end up in the trove), + * NOT including the buffer fee. + * - If you are only increasing debt and not topping up collateral, omit extraParams.value and the helper + * will still send msg.value = bufferFee so the tx doesn't revert. + */ + static async adjustTrove( + contracts, + { maxFeePercentage, collWithdrawal, zusdAmount, ICR, isDebtIncrease, upperHint, lowerHint, extraParams } + ) { + if (!extraParams) extraParams = {}; + if (maxFeePercentage === undefined || maxFeePercentage === null) maxFeePercentage = this._100pct; + maxFeePercentage = this.toBN(maxFeePercentage); + if (upperHint === undefined || upperHint === null) upperHint = this.ZERO_ADDRESS; + if (lowerHint === undefined || lowerHint === null) lowerHint = this.ZERO_ADDRESS; + if (collWithdrawal === undefined) collWithdrawal = this.toBN(0); + else collWithdrawal = this.toBN(collWithdrawal); + + // default + if (zusdAmount === undefined) zusdAmount = this.toBN(0); + else zusdAmount = (typeof zusdAmount === "string") ? this.toBN(zusdAmount) : this.toBN(zusdAmount); + + // Default bool: infer from zusdAmount if not explicitly passed + if (isDebtIncrease === undefined) isDebtIncrease = zusdAmount.gt(this.toBN(0)); + + // Collateral topup intended for the trove (excluding buffer fee) + let collTopUp = extraParams.value ? this.toBN(extraParams.value) : this.toBN(0); + + // If targeting an ICR, compute the required ZUSD amount (debt increase only) + let increasedTotalDebt = this.toBN(0); + if (ICR) { + assert(isDebtIncrease, "ICR targeting only makes sense for debt increases"); + assert(extraParams.from, "A 'from' account is needed"); + + ICR = (typeof ICR === "string") ? this.toBN(ICR) : this.toBN(ICR); + + const borrower = this._getBorrowerAddress(contracts, extraParams.from); + const { debt, coll } = await contracts.troveManager.getEntireDebtAndColl(borrower); + + const price = await contracts.priceFeedTestnet.getPrice(); + + // newColl = existing coll + topup - withdrawal + const newColl = coll.add(collTopUp).sub(collWithdrawal); + + const targetDebt = newColl.mul(price).div(ICR); + assert(targetDebt.gt(debt), "ICR is already greater than or equal to target"); + + increasedTotalDebt = targetDebt.sub(debt); + zusdAmount = await this.getNetBorrowingAmount(contracts, increasedTotalDebt); + } + + // If debt increase, compute (a) borrowing-fee-inclusive debt increase and (b) buffer fee in RBTC + let bufferFee = this.toBN(0); + + if (isDebtIncrease) { + // total debt increase that hits the trove struct includes borrowing fee + if (increasedTotalDebt.eq(this.toBN(0))) { + increasedTotalDebt = await this.getAmountWithBorrowingFee(contracts, zusdAmount); + } + + // IMPORTANT: call options MUST NOT include `value` (getRedemptionBufferFeeRBTC is nonpayable) + const fromForCall = extraParams.from ? extraParams.from : this.ZERO_ADDRESS; + bufferFee = await this.getRedemptionBufferFeeRBTC(contracts, zusdAmount, fromForCall); + + // msg.value must cover buffer fee + optional collateral top-up + extraParams.value = collTopUp.add(bufferFee); + } else { + // no buffer fee needed + extraParams.value = collTopUp; + } + + const tx = await contracts.borrowerOperations.adjustTrove( + maxFeePercentage, + collWithdrawal, + zusdAmount, + isDebtIncrease, + upperHint, + lowerHint, + extraParams + ); + + return { + tx, + zusdAmount, + increasedTotalDebt, + collTopUp, + bufferFee + }; + } + + // ------------------------------------------------------------------------- + // NUE / DLLR helpers + // ------------------------------------------------------------------------- + + static _emptyMassetPermitParams() { + return { + deadline: 0, + v: 0, + r: "0x" + "0".repeat(64), + s: "0x" + "0".repeat(64) + }; + } + + static _emptyPermit2PermitTransferFrom() { + // ISignatureTransfer.PermitTransferFrom: + // { permitted: { token, amount }, nonce, deadline } + return { + permitted: { token: this.ZERO_ADDRESS, amount: 0 }, + nonce: 0, + deadline: 0 + }; + } + + static _normalizePermit2PermitTransferFrom(permit) { + // Web3/Truffle is happiest with tuple-arrays. Support both object + tuple input. + if (Array.isArray(permit)) return permit; + + const token = permit.permitted.token; + const amount = permit.permitted.amount; + const nonce = permit.nonce; + const deadline = permit.deadline; + + return [[token, amount], nonce, deadline]; + } + + /** + * adjustNueTrove(): wrapper around BorrowerOperations.adjustNueTrove. + * + * IMPORTANT SEMANTICS (based on your contract): + * - if isDebtIncrease == true: `_ZUSDChange` is the ZUSD amount to mint (then converted to DLLR) + * - if isDebtIncrease == false: `_ZUSDChange` is treated as a DLLR amount by the contract (repay via DLLR) + * + * This helper: + * - adds RedemptionBuffer fee to msg.value for debt increases + * - treats extraParams.value as collateral top-up *excluding* buffer fee + */ + static async adjustNueTrove( + contracts, + { + maxFeePercentage, + collWithdrawal, + zusdAmount, // for debt increase + dllrAmount, // for repayment (isDebtIncrease=false) + ICR, + isDebtIncrease, + upperHint, + lowerHint, + permitParams, + extraParams + } + ) { + if (!extraParams) extraParams = {}; + if (!maxFeePercentage) maxFeePercentage = this._100pct; + if (!upperHint) upperHint = this.ZERO_ADDRESS; + if (!lowerHint) lowerHint = this.ZERO_ADDRESS; + if (collWithdrawal === undefined) collWithdrawal = this.toBN(0); + else collWithdrawal = this.toBN(collWithdrawal); + + // default permit params (unused in borrow path, required by ABI) + if (!permitParams) permitParams = this._emptyMassetPermitParams(); + + // Determine the "amount" argument passed to the contract + let amount; + if (isDebtIncrease === undefined) { + // infer: if zusdAmount provided -> increase; else if dllrAmount provided -> repay + isDebtIncrease = !!zusdAmount; + } + + if (isDebtIncrease) { + // debt increase uses zusdAmount + if (zusdAmount === undefined) zusdAmount = this.toBN(0); + zusdAmount = (typeof zusdAmount === "string") ? this.toBN(zusdAmount) : this.toBN(zusdAmount); + + // Collateral topup intended for trove (excluding buffer fee) + let collTopUp = extraParams.value ? this.toBN(extraParams.value) : this.toBN(0); + + // ICR targeting supported (borrow only) + let increasedTotalDebt = this.toBN(0); + if (ICR) { + assert(extraParams.from, "A 'from' account is needed"); + ICR = (typeof ICR === "string") ? this.toBN(ICR) : this.toBN(ICR); + + const borrower = this._getBorrowerAddress(contracts, extraParams.from); + const { debt, coll } = await contracts.troveManager.getEntireDebtAndColl(borrower); + + const price = await contracts.priceFeedTestnet.getPrice(); + const newColl = coll.add(collTopUp).sub(collWithdrawal); + + const targetDebt = newColl.mul(price).div(ICR); + assert(targetDebt.gt(debt), "ICR is already greater than or equal to target"); + + increasedTotalDebt = targetDebt.sub(debt); + zusdAmount = await this.getNetBorrowingAmount(contracts, increasedTotalDebt); + } + + // compute buffer fee based on minted ZUSD + const fromForCall = extraParams.from ? extraParams.from : this.ZERO_ADDRESS; + const bufferFee = await this.getRedemptionBufferFeeRBTC(contracts, zusdAmount, fromForCall); + + + extraParams.value = collTopUp.add(bufferFee); + + amount = zusdAmount; + + const tx = await contracts.borrowerOperations.adjustNueTrove( + maxFeePercentage, + collWithdrawal, + amount, + true, + upperHint, + lowerHint, + permitParams, + extraParams + ); + + return { tx, zusdAmount: amount, collTopUp, bufferFee }; + } else { + // repayment path uses dllrAmount (contract treats `_ZUSDChange` as DLLR amount in this case) + if (dllrAmount === undefined) { + // allow reusing zusdAmount field as the "repay amount" if caller didn't supply dllrAmount + dllrAmount = zusdAmount; + } + if (dllrAmount === undefined) dllrAmount = this.toBN(0); + dllrAmount = (typeof dllrAmount === "string") ? this.toBN(dllrAmount) : this.toBN(dllrAmount); + + // no buffer fee + extraParams.value = extraParams.value ? this.toBN(extraParams.value) : this.toBN(0); + + amount = dllrAmount; + + const tx = await contracts.borrowerOperations.adjustNueTrove( + maxFeePercentage, + collWithdrawal, + amount, + false, + upperHint, + lowerHint, + permitParams, + extraParams + ); + + return { tx, dllrAmount: amount }; + } + } + + /** + * adjustNueTroveWithPermit2(): same logic as adjustNueTrove, but uses Permit2 parameters. + * For debt increases, permit/signature are unused but still required by ABI. + */ + static async adjustNueTroveWithPermit2( + contracts, + { + maxFeePercentage, + collWithdrawal, + zusdAmount, + dllrAmount, + ICR, + isDebtIncrease, + upperHint, + lowerHint, + permit, + signature, + extraParams + } + ) { + if (!extraParams) extraParams = {}; + if (!maxFeePercentage) maxFeePercentage = this._100pct; + if (!upperHint) upperHint = this.ZERO_ADDRESS; + if (!lowerHint) lowerHint = this.ZERO_ADDRESS; + if (collWithdrawal === undefined) collWithdrawal = this.toBN(0); + else collWithdrawal = this.toBN(collWithdrawal); + + if (!permit) permit = this._emptyPermit2PermitTransferFrom(); + if (!signature) signature = "0x"; + + const permitTuple = this._normalizePermit2PermitTransferFrom(permit); + + // Determine increase vs repay + if (isDebtIncrease === undefined) isDebtIncrease = !!zusdAmount; + + if (isDebtIncrease) { + if (zusdAmount === undefined) zusdAmount = this.toBN(0); + zusdAmount = (typeof zusdAmount === "string") ? this.toBN(zusdAmount) : this.toBN(zusdAmount); + + let collTopUp = extraParams.value ? this.toBN(extraParams.value) : this.toBN(0); + + // ICR targeting supported (borrow only) + if (ICR) { + assert(extraParams.from, "A 'from' account is needed"); + ICR = (typeof ICR === "string") ? this.toBN(ICR) : this.toBN(ICR); + + const borrower = this._getBorrowerAddress(contracts, extraParams.from); + const { debt, coll } = await contracts.troveManager.getEntireDebtAndColl(borrower); + + const price = await contracts.priceFeedTestnet.getPrice(); + const newColl = coll.add(collTopUp).sub(collWithdrawal); + + const targetDebt = newColl.mul(price).div(ICR); + assert(targetDebt.gt(debt), "ICR is already greater than or equal to target"); + + const increasedTotalDebt = targetDebt.sub(debt); + zusdAmount = await this.getNetBorrowingAmount(contracts, increasedTotalDebt); + } + + const fromForCall = extraParams.from ? extraParams.from : this.ZERO_ADDRESS; + const bufferFee = await this.getRedemptionBufferFeeRBTC(contracts, zusdAmount, fromForCall); + + extraParams.value = collTopUp.add(bufferFee); + + const tx = await contracts.borrowerOperations.adjustNueTroveWithPermit2( + maxFeePercentage, + collWithdrawal, + zusdAmount, + true, + upperHint, + lowerHint, + permitTuple, + signature, + extraParams + ); + + return { tx, zusdAmount, collTopUp, bufferFee }; + } else { + if (dllrAmount === undefined) dllrAmount = zusdAmount; + if (dllrAmount === undefined) dllrAmount = this.toBN(0); + dllrAmount = (typeof dllrAmount === "string") ? this.toBN(dllrAmount) : this.toBN(dllrAmount); + + extraParams.value = extraParams.value ? this.toBN(extraParams.value) : this.toBN(0); + + const tx = await contracts.borrowerOperations.adjustNueTroveWithPermit2( + maxFeePercentage, + collWithdrawal, + dllrAmount, + false, + upperHint, + lowerHint, + permitTuple, + signature, + extraParams + ); + + return { tx, dllrAmount }; + } + } + + // If you call withdrawZUSD with no extraParams.value, it will send msg.value = bufferFee automatically. + // If you include extraParams.value, it becomes a collateral top-up, and the helper sends collTopUp + bufferFee. static async withdrawZUSD( contracts, { maxFeePercentage, zusdAmount, ICR, upperHint, lowerHint, extraParams } @@ -847,26 +1322,58 @@ class TestHelper { if (!maxFeePercentage) maxFeePercentage = this._100pct; if (!upperHint) upperHint = this.ZERO_ADDRESS; if (!lowerHint) lowerHint = this.ZERO_ADDRESS; + if (!extraParams) extraParams = {}; + + // --- normalize inputs to BN (dec(...) returns string) --- + if (zusdAmount && typeof zusdAmount === "string") zusdAmount = this.toBN(zusdAmount); + if (ICR && typeof ICR === "string") ICR = this.toBN(ICR); + if (extraParams.value && typeof extraParams.value === "string") extraParams.value = this.toBN(extraParams.value); assert( !(zusdAmount && ICR) && (zusdAmount || ICR), "Specify either zusd amount or target ICR, but not both" ); + const collTopUp = extraParams.value ? this.toBN(extraParams.value) : this.toBN(0); + let increasedTotalDebt; if (ICR) { assert(extraParams.from, "A from account is needed"); - const { debt, coll } = await contracts.troveManager.getEntireDebtAndColl(extraParams.from); - const price = await contracts.priceFeedTestnet.getPrice(); - const targetDebt = coll.mul(price).div(ICR); - assert(targetDebt > debt, "ICR is already greater than or equal to target"); - increasedTotalDebt = targetDebt.sub(debt); + + const borrower = this._getBorrowerAddress(contracts, extraParams.from); + const { debt, coll } = await contracts.troveManager.getEntireDebtAndColl(borrower); + + const price = this.toBN(await contracts.priceFeedTestnet.getPrice()); + + const debtBN = this.toBN(debt); + const collBN = this.toBN(coll); + + const effectiveColl = collBN.add(collTopUp); + const targetDebt = effectiveColl.mul(price).div(ICR); + + assert(targetDebt.gt(debtBN), "ICR is already greater than or equal to target"); + + increasedTotalDebt = targetDebt.sub(debtBN); zusdAmount = await this.getNetBorrowingAmount(contracts, increasedTotalDebt); } else { + // zusdAmount is BN here because we normalized it above increasedTotalDebt = await this.getAmountWithBorrowingFee(contracts, zusdAmount); } - await contracts.borrowerOperations.withdrawZUSD( + // Quote buffer fee and send it on top + /*let bufferFee = this.toBN(0); + if (contracts.borrowerOperations.getRedemptionBufferFeeRBTC) { + const feeCallOpts = extraParams.from ? { from: extraParams.from } : {}; + bufferFee = this.toBN( + await contracts.borrowerOperations.getRedemptionBufferFeeRBTC.call(zusdAmount, feeCallOpts) + ); + }*/ + const bufferFee = await this.getRedemptionBufferFeeRBTC(contracts, zusdAmount, extraParams.from); + + const totalValue = collTopUp.add(bufferFee); + extraParams.value = totalValue; + + const tx = await contracts.borrowerOperations.withdrawZUSD( maxFeePercentage, zusdAmount, upperHint, @@ -874,56 +1381,102 @@ class TestHelper { extraParams ); - return { - zusdAmount, - increasedTotalDebt - }; + return { zusdAmount, increasedTotalDebt, collTopUp, bufferFee, totalValue, tx }; } - static async withdrawZusdAndConvertToDLLR(contracts, { maxFeePercentage, zusdAmount, ICR, upperHint, lowerHint, extraParams }) { - //TODO: implement + static async withdrawZusdAndConvertToDLLR( + contracts, + { maxFeePercentage, zusdAmount, ICR, upperHint, lowerHint, extraParams } + ) { if (!maxFeePercentage) maxFeePercentage = this._100pct; if (!upperHint) upperHint = this.ZERO_ADDRESS; if (!lowerHint) lowerHint = this.ZERO_ADDRESS; + if (!extraParams) extraParams = {}; + + // --- normalize inputs to BN (dec(...) returns string) --- + if (zusdAmount && typeof zusdAmount === "string") zusdAmount = this.toBN(zusdAmount); + if (ICR && typeof ICR === "string") ICR = this.toBN(ICR); + if (extraParams.value && typeof extraParams.value === "string") extraParams.value = this.toBN(extraParams.value); assert( !(zusdAmount && ICR) && (zusdAmount || ICR), "Specify either zusd amount or target ICR, but not both" ); + const collTopUp = extraParams.value ? this.toBN(extraParams.value) : this.toBN(0); + let increasedTotalDebt; + if (ICR) { assert(extraParams.from, "A 'from' account is needed"); - const { debt, coll } = await contracts.troveManager.getEntireDebtAndColl(extraParams.from); + if (typeof ICR == "string") ICR = this.toBN(ICR); + + const borrower = this._getBorrowerAddress(contracts, extraParams.from); + const { debt, coll } = await contracts.troveManager.getEntireDebtAndColl(borrower); + const price = await contracts.priceFeedTestnet.getPrice(); - const targetDebt = coll.mul(price).div(ICR); - assert(targetDebt > debt, "ICR is already greater than or equal to target"); - increasedTotalDebt = targetDebt.sub(debt); + + const effectiveColl = this.toBN(coll).add(collTopUp); + const targetDebt = effectiveColl.mul(price).div(ICR); + + assert(targetDebt.gt(this.toBN(debt)), "ICR is already greater than or equal to target"); + + increasedTotalDebt = targetDebt.sub(this.toBN(debt)); zusdAmount = await this.getNetBorrowingAmount(contracts, increasedTotalDebt); } else { increasedTotalDebt = await this.getAmountWithBorrowingFee(contracts, zusdAmount); } - //TODO: fix callStatic - + // --- NEW: redemption buffer fee on ZUSD minting --- + const bufferFee = await this.getRedemptionBufferFeeRBTC(contracts, zusdAmount, extraParams.from); + + + const totalValue = collTopUp.add(bufferFee); + + // Use ethers for return value (DLLR amount) const { ethers } = hre; + + // Pick signer based on extraParams.from if provided + const signers = await ethers.getSigners(); + let signer = signers[0]; + + if (extraParams.from) { + const want = extraParams.from.toLowerCase(); + for (const s of signers) { + const addr = (s.address ? s.address : await s.getAddress()).toLowerCase(); + if (addr === want) { + signer = s; + break; + } + } + } + const ethersBorrowerOperations = await ethers.getContractAt( "BorrowerOperationsTester", - contracts.borrowerOperations.address, (await ethers.getSigners())[1] + contracts.borrowerOperations.address, + signer ); + + // Build ethers overrides: remove `from` (ethers signer already sets it) + const overrides = { ...extraParams }; + delete overrides.from; + overrides.value = totalValue.toString(); + + // NOTE: ethers v6 uses `.staticCall(...)` const dllrAmount = await ethersBorrowerOperations.withdrawZusdAndConvertToDLLR.staticCall( - maxFeePercentage, + maxFeePercentage.toString(), zusdAmount.toString(), upperHint, lowerHint, - extraParams + overrides ); await ethersBorrowerOperations.withdrawZusdAndConvertToDLLR( - maxFeePercentage, + maxFeePercentage.toString(), zusdAmount.toString(), upperHint, lowerHint, - extraParams + overrides ); return { @@ -932,6 +1485,9 @@ class TestHelper { lowerHint, zusdAmount, increasedTotalDebt, + collTopUp, + bufferFee, + totalValue, dllrAmount }; } @@ -1430,14 +1986,14 @@ class TestHelper { for (const redeemer of accounts) { const randZUSDAmount = this.randAmountInWei(min, max); - await this.performRedemptionTx(redeemer, price, contracts, randZUSDAmount); + const tx = await this.performRedemptionTx(redeemer, price, contracts, randZUSDAmount); const gas = this.gasUsed(tx); gasCostList.push(gas); } return this.getGasMetrics(gasCostList); } - static async performRedemptionTx(redeemer, price, contracts, ZUSDAmount, maxFee = 0) { + /*static async performRedemptionTx(redeemer, price, contracts, ZUSDAmount, maxFee = 0) { const redemptionhint = await contracts.hintHelpers.getRedemptionHints(ZUSDAmount, price, 0); const firstRedemptionHint = redemptionhint[0]; @@ -1459,6 +2015,14 @@ class TestHelper { approxPartialRedemptionHint ); + // Ensure TroveManager can transferFrom redeemer for the buffer stage + await contracts.zusdToken.approve( + contracts.troveManager.address, + ZUSDAmount, + { from: redeemer } + ); + + const tx = await contracts.troveManager.redeemCollateral( ZUSDAmount, firstRedemptionHint, @@ -1471,8 +2035,97 @@ class TestHelper { ); return tx; + }*/ + + static async performRedemptionTx(redeemer, price, contracts, ZUSDAmount, maxFee = 0) { + const toBN = web3.utils.toBN; + + const amountBN = toBN(ZUSDAmount); + const priceBN = toBN(price); + + // ------------------------------------------------------------------ + // Mirror TroveManagerRedeemOps._swapFromBuffer() to know "remainingZUSD" + // ------------------------------------------------------------------ + let bufferBal = toBN("0"); + if (contracts.redemptionBuffer && contracts.redemptionBuffer.getBalance) { + bufferBal = toBN(await contracts.redemptionBuffer.getBalance()); + } else if (contracts.redemptionBuffer && contracts.redemptionBuffer.address) { + bufferBal = toBN(await web3.eth.getBalance(contracts.redemptionBuffer.address)); + } + + const DECIMAL_PRECISION = MoneyValues._1e18BN; // IMPORTANT: correct 1e18 BN + + const maxZusdFromBuffer = bufferBal.mul(priceBN).div(DECIMAL_PRECISION); + const zusdFromBuffer = amountBN.lt(maxZusdFromBuffer) ? amountBN : maxZusdFromBuffer; + const zusdFromTroves = amountBN.sub(zusdFromBuffer); + + // ------------------------------------------------------------------ + // Approve ONLY the buffer portion (transferFrom in _swapFromBuffer) + // ------------------------------------------------------------------ + if (zusdFromBuffer.gt(toBN("0"))) { + // Optional safety: reset allowance first for non-standard ERC20s + await contracts.zusdToken.approve(contracts.troveManager.address, 0, { from: redeemer }); + + await contracts.zusdToken.approve( + contracts.troveManager.address, + zusdFromBuffer, + { from: redeemer } + ); + } + + // ------------------------------------------------------------------ + // Redemption hints MUST be computed for the TROVE portion (remainingZUSD) + // ------------------------------------------------------------------ + let firstRedemptionHint = this.ZERO_ADDRESS; + let partialRedemptionNewICR = toBN("0"); + let upperHint = this.ZERO_ADDRESS; + let lowerHint = this.ZERO_ADDRESS; + + if (zusdFromTroves.gt(toBN("0"))) { + const redemptionhint = await contracts.hintHelpers.getRedemptionHints( + zusdFromTroves, + priceBN, + 0 + ); + + firstRedemptionHint = redemptionhint[0]; + partialRedemptionNewICR = redemptionhint[1]; + + const { hintAddress: approxPartialRedemptionHint, latestRandomSeed } = + await contracts.hintHelpers.getApproxHint( + partialRedemptionNewICR, + 50, + this.latestRandomSeed + ); + + this.latestRandomSeed = latestRandomSeed; + + const exactPartialRedemptionHint = await contracts.sortedTroves.findInsertPosition( + partialRedemptionNewICR, + approxPartialRedemptionHint, + approxPartialRedemptionHint + ); + + upperHint = exactPartialRedemptionHint[0]; + lowerHint = exactPartialRedemptionHint[1]; + } + + // IMPORTANT: still pass ORIGINAL amountBN to redeemCollateral() + return contracts.troveManager.redeemCollateral( + amountBN, + firstRedemptionHint, + upperHint, + lowerHint, + partialRedemptionNewICR, + 0, + maxFee, + { from: redeemer, gasPrice: 0 } + ); } + + + // --- Composite functions --- static async makeTrovesIncreasingICR(accounts, contracts) { diff --git a/utils/js/truffleDeploymentHelpers.js b/utils/js/truffleDeploymentHelpers.js index ac5c5bb..7741e4a 100644 --- a/utils/js/truffleDeploymentHelpers.js +++ b/utils/js/truffleDeploymentHelpers.js @@ -8,6 +8,7 @@ const DefaultPool = artifacts.require("./DefaultPool.sol"); const StabilityPool = artifacts.require("./StabilityPool.sol") const FunctionCaller = artifacts.require("./FunctionCaller.sol") const BorrowerOperations = artifacts.require("./BorrowerOperations.sol") +const RedemptionBuffer = artifacts.require("./RedemptionBuffer.sol") const Permit2 = artifacts.require("Permit2"); const deployLiquity = async () => { @@ -20,6 +21,7 @@ const deployLiquity = async () => { const defaultPool = await DefaultPool.new() const functionCaller = await FunctionCaller.new() const borrowerOperations = await BorrowerOperations.new(permit2.address) + const redemptionBuffer = await RedemptionBuffer.new() const zusdToken = await ZUSDToken.new() await zusdToken.initialize( troveManager.address, @@ -35,6 +37,7 @@ const deployLiquity = async () => { StabilityPool.setAsDeployed(stabilityPool) FunctionCaller.setAsDeployed(functionCaller) BorrowerOperations.setAsDeployed(borrowerOperations) + RedemptionBuffer.setAsDeployed(redemptionBuffer) const contracts = { priceFeedTestnet, @@ -45,7 +48,8 @@ const deployLiquity = async () => { stabilityPool, defaultPool, functionCaller, - borrowerOperations + borrowerOperations, + redemptionBuffer } return contracts } @@ -60,7 +64,8 @@ const getAddresses = (contracts) => { StabilityPool: contracts.stabilityPool.address, ActivePool: contracts.activePool.address, DefaultPool: contracts.defaultPool.address, - FunctionCaller: contracts.functionCaller.address + FunctionCaller: contracts.functionCaller.address, + RedemptionBuffer: contracts.redemptionBuffer.address } } @@ -84,6 +89,7 @@ const connectContracts = async (contracts, addresses) => { await contracts.troveManager.setDefaultPool(addresses.DefaultPool) await contracts.troveManager.setStabilityPool(addresses.StabilityPool) await contracts.troveManager.setBorrowerOperations(addresses.BorrowerOperations) + await contracts.troveManager.setRedemptionBufferAddress(addresses.RedemptionBuffer) // set contracts in BorrowerOperations await contracts.borrowerOperations.setSortedTroves(addresses.SortedTroves) @@ -91,6 +97,7 @@ const connectContracts = async (contracts, addresses) => { await contracts.borrowerOperations.setActivePool(addresses.ActivePool) await contracts.borrowerOperations.setDefaultPool(addresses.DefaultPool) await contracts.borrowerOperations.setTroveManager(addresses.TroveManager) + await contracts.borrowerOperations.setRedemptionBufferAddress(addresses.RedemptionBuffer) // set contracts in the Pools await contracts.stabilityPool.setActivePoolAddress(addresses.ActivePool) @@ -101,6 +108,9 @@ const connectContracts = async (contracts, addresses) => { await contracts.defaultPool.setStabilityPoolAddress(addresses.StabilityPool) await contracts.defaultPool.setActivePoolAddress(addresses.ActivePool) + + await contracts.redemptionBuffer.setTroveManager(addresses.TroveManager) + await contracts.redemptionBuffer.setBorrowerOperations(addresses.BorrowerOperations) } const connectEchidnaProxy = async (echidnaProxy, addresses) => { From 6cdcb41bdf85b278ce0836a97323555ceb8b2c9f Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Thu, 25 Dec 2025 14:56:49 -0500 Subject: [PATCH 12/13] Tests fix --- contracts/Proxy/BorrowerWrappersScript.sol | 55 +++++++++++-- tests/js/ProxyBorrowerWrappersScript.js | 7 +- tests/js/TroveManagerTest.js | 81 ++++++++++++------- .../js/TroveManager_LiquidationRewardsTest.js | 8 +- tests/js/TroveManager_RecoveryModeTest.js | 31 ++++++- utils/js/proxyHelpers.js | 4 + utils/js/testHelpers.js | 6 +- 7 files changed, 143 insertions(+), 49 deletions(-) diff --git a/contracts/Proxy/BorrowerWrappersScript.sol b/contracts/Proxy/BorrowerWrappersScript.sol index a06b677..b5fc58d 100644 --- a/contracts/Proxy/BorrowerWrappersScript.sol +++ b/contracts/Proxy/BorrowerWrappersScript.sol @@ -81,25 +81,63 @@ contract BorrowerWrappersScript is BorrowerOperationsScript, ETHTransferScript, borrowerOperations.openTrove{ value: totalCollateral }(_maxFee, _ZUSDAmount, _upperHint, _lowerHint); } - function claimSPRewardsAndRecycle(uint _maxFee, address _upperHint, address _lowerHint) external { + function claimSPRewardsAndRecycle( + uint _maxFee, + address _upperHint, + address _lowerHint + ) external payable { uint collBalanceBefore = address(this).balance; uint zeroBalanceBefore = zeroToken.balanceOf(address(this)); - // Claim rewards + // Claim rewards (ETH gain + ZERO) stabilityPool.withdrawFromSP(0); uint collBalanceAfter = address(this).balance; uint zeroBalanceAfter = zeroToken.balanceOf(address(this)); + + // IMPORTANT: this is only the ETH gain from SP (msg.value cancels out because it was already in balanceBefore) uint claimedCollateral = collBalanceAfter.sub(collBalanceBefore); - // Add claimed ETH to trove, get more ZUSD and stake it into the Stability Pool if (claimedCollateral > 0) { _requireUserHasTrove(address(this)); - uint ZUSDAmount = _getNetZUSDAmount(claimedCollateral); - borrowerOperations.adjustTrove{ value: claimedCollateral }(_maxFee, 0, ZUSDAmount, true, _upperHint, _lowerHint); - // Provide withdrawn ZUSD to Stability Pool - if (ZUSDAmount > 0) { - stabilityPool.provideToSP(ZUSDAmount, address(0)); + + uint zusdAmount = _getNetZUSDAmount(claimedCollateral); + + // --- NEW: buffer fee on debt increase must be paid on top --- + uint bufferFee = 0; + if (zusdAmount > 0) { + // Prefer WithPrice so we control the price input and avoid extra fetchPrice side effects. + // getRedemptionBufferFeeRBTC(zusdAmount) would work as well. + uint p = priceFeed.fetchPrice(); + bufferFee = borrowerOperations.getRedemptionBufferFeeRBTCWithPrice(zusdAmount, p); + } + + require(msg.value >= bufferFee, "BorrowerWrappers: insufficient ETH for buffer fee"); + + borrowerOperations.adjustTrove{ value: claimedCollateral.add(bufferFee) }( + _maxFee, + 0, + zusdAmount, + true, + _upperHint, + _lowerHint + ); + + if (zusdAmount > 0) { + stabilityPool.provideToSP(zusdAmount, address(0)); + } + + // Refund any extra ETH sent (keeps proxy ETH balance unchanged) + uint refund = msg.value.sub(bufferFee); + if (refund > 0) { + (bool ok, ) = msg.sender.call{ value: refund }(""); + require(ok, "BorrowerWrappers: refund failed"); + } + } else { + // If no ETH gain, don't trap ETH in the proxy + if (msg.value > 0) { + (bool ok, ) = msg.sender.call{ value: msg.value }(""); + require(ok, "BorrowerWrappers: refund failed"); } } @@ -110,6 +148,7 @@ contract BorrowerWrappersScript is BorrowerOperationsScript, ETHTransferScript, } } + function claimStakingGainsAndRecycle(uint _maxFee, address _upperHint, address _lowerHint) external { uint collBalanceBefore = address(this).balance; uint zusdBalanceBefore = zusdToken.balanceOf(address(this)); diff --git a/tests/js/ProxyBorrowerWrappersScript.js b/tests/js/ProxyBorrowerWrappersScript.js index d4a37cd..0e6da6a 100644 --- a/tests/js/ProxyBorrowerWrappersScript.js +++ b/tests/js/ProxyBorrowerWrappersScript.js @@ -345,11 +345,14 @@ contract('BorrowerWrappers', async accounts => { // ie. 0.06 * 787,084.753044 = 47,225.0851826 const expectedZEROGain_A = toBN('47225085182600000000000'); - await priceFeed.setPrice(price.mul(toBN(2))); + const priceNow = price.mul(toBN(2)); + await priceFeed.setPrice(priceNow); + + const bufferFee = await borrowerOperations.getRedemptionBufferFeeRBTCWithPrice(netDebtChange, priceNow); // Alice claims SP rewards and puts them back in the system through the proxy const proxyAddress = borrowerWrappers.getProxyAddressFromUser(alice); - await borrowerWrappers.claimSPRewardsAndRecycle(th._100pct, alice, alice, { from: alice }); + await borrowerWrappers.claimSPRewardsAndRecycle(th._100pct, alice, alice, { from: alice, value: bufferFee }); const ethBalanceAfter = await web3.eth.getBalance(borrowerOperations.getProxyAddressFromUser(alice)); const troveCollAfter = await troveManager.getTroveColl(alice); diff --git a/tests/js/TroveManagerTest.js b/tests/js/TroveManagerTest.js index 3cada95..493ae8b 100644 --- a/tests/js/TroveManagerTest.js +++ b/tests/js/TroveManagerTest.js @@ -65,60 +65,70 @@ contract('TroveManager', async accounts => { const withdrawZUSD = async (params) => th.withdrawZUSD(contracts, params); const drainRedemptionBufferWithWhale = async () => { + await emptyRedemptionBufferAsOwner(); + /*// Use the same price source your tests are using const price = toBN(await priceFeed.getPrice()); const bufferBalRBTC = toBN(await contracts.redemptionBuffer.getBalance()); // RBTC wei - const drainZUSD = bufferBalRBTC.mul(price).div(mv._1e18BN); // max ZUSD buffer can cover (floor) + const drainZUSD = bufferBalRBTC.mul(price).div(mv._1e18BN); // max ZUSD buffer can cover (floor) - // If buffer can't cover even 1 wei of ZUSD at this price, nothing to do if (drainZUSD.eq(toBN(0))) return; - // Fund whale with enough ZUSD to perform the drain (we don't care about Alice/Bob/Carol wallet balances in this test) - let needed = drainZUSD.sub(toBN(await zusdToken.balanceOf(whale))); + // --- Fund whale with enough ZUSD (scan all accounts, not just alice/bob/carol) --- + let whaleBal = toBN(await zusdToken.balanceOf(whale)); + let needed = drainZUSD.sub(whaleBal); + if (needed.gt(toBN(0))) { - for (const src of [alice, bob, carol]) { + const all = await web3.eth.getAccounts(); + + for (const src of all) { + if (src === whale) continue; + const srcBal = toBN(await zusdToken.balanceOf(src)); if (srcBal.eq(toBN(0))) continue; const send = srcBal.gte(needed) ? needed : srcBal; await zusdToken.transfer(whale, send, { from: src }); + needed = needed.sub(send); if (needed.eq(toBN(0))) break; } } - // Sanity: whale must have enough now - const whaleBal = toBN(await zusdToken.balanceOf(whale)); - assert(whaleBal.gte(drainZUSD), `whale doesn't have enough ZUSD to drain buffer: have=${whaleBal} need=${drainZUSD}`); + whaleBal = toBN(await zusdToken.balanceOf(whale)); + assert.isTrue( + whaleBal.gte(drainZUSD), + `whale doesn't have enough ZUSD to drain buffer: have=${whaleBal.toString()} need=${drainZUSD.toString()}` + ); - await zusdToken.approve(troveManager.address, drainZUSD, { from: whale }); + // --- Drain: use the updated redemption helper so allowance/hints match buffer-first redeem --- + await th.redeemCollateralAndGetTxObject(whale, contracts, drainZUSD, th._100pct); - // Hints (even if Troves won't be touched, some implementations still validate hints) - const { firstRedemptionHint, partialRedemptionHintNICR } = - await hintHelpers.getRedemptionHints(drainZUSD, price, 0); + // Optional sanity: after drain, buffer should not be able to cover even 1 wei of ZUSD + const bufferAfter = toBN(await contracts.redemptionBuffer.getBalance()); + const maxZusdAfter = bufferAfter.mul(price).div(mv._1e18BN); + assert.equal(maxZusdAfter.toString(), "0");*/ + }; - const { 0: upperHint, 1: lowerHint } = - await sortedTroves.findInsertPosition(partialRedemptionHintNICR, whale, whale); + const emptyRedemptionBufferAsOwner = async () => { + const bufferBal = toBN(await contracts.redemptionBuffer.getBalance()); + if (bufferBal.eq(toBN(0))) return; - // Drain: this should be buffer-only (remaining-to-redeem from Troves becomes 0) - await troveManager.redeemCollateral( - drainZUSD, - firstRedemptionHint, - upperHint, - lowerHint, - partialRedemptionHintNICR, - 0, - th._100pct, - { from: whale, gasPrice: 0 } + // RedemptionBuffer is Ownable — use its current owner (often `owner` or `multisig`) + const rbOwner = await contracts.redemptionBuffer.getOwner(); + + await contracts.redemptionBuffer.distributeToStakers( + bufferBal, + { from: rbOwner, gasPrice: 0 } ); - // Verify buffer is effectively empty at this price (capacity == 0 ZUSD) - const bufferAfterRBTC = toBN(await contracts.redemptionBuffer.getBalance()); - const bufferAfterMaxZUSD = bufferAfterRBTC.mul(price).div(mv._1e18BN); - assert.equal(bufferAfterMaxZUSD.toString(), "0", "buffer still has redeemable capacity"); + // sanity + const afterBal = toBN(await contracts.redemptionBuffer.getBalance()); + assert.isTrue(afterBal.eq(toBN(0)), "buffer not fully emptied"); }; + before(async () => { contracts = await deploymentHelper.deployLiquityCore(); permit2 = contracts.permit2; @@ -4587,6 +4597,9 @@ contract('TroveManager', async accounts => { const baseRate = await troveManager.baseRate(); assert.equal(baseRate, '0'); + // NEW: ensure the buffer can't satisfy part of the redemption + await emptyRedemptionBufferAsOwner(); + // whale redeems ZUSD. Expect this to fully redeem A, B, C, and partially redeem D. await th.redeemCollateral(whale, contracts, redemptionAmount); @@ -4770,11 +4783,13 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); - // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + // Empty the buffer without touching any user's ZUSD balance + await emptyRedemptionBufferAsOwner(); // keep redeeming until we get the base rate to the ceiling of 100% for (let i = 0; i < 2; i++) { + // Ensure buffer is empty BEFORE the redemption (adjust/open below refill it) + await emptyRedemptionBufferAsOwner(); // Find hints for redeeming const { firstRedemptionHint, @@ -4804,9 +4819,13 @@ contract('TroveManager', async accounts => { ); await openTrove({ ICR: toBN(dec(150, 16)), extraParams: { from: bob } }); - await borrowerOperations.adjustTrove(th._100pct, 0, zusdAmount, true, alice, alice, { from: alice, value: zusdAmount.mul(mv._1e18BN).div(price) }); + const bufferFee = await borrowerOperations.getRedemptionBufferFeeRBTCWithPrice(zusdAmount, price); + await borrowerOperations.adjustTrove(th._100pct, 0, zusdAmount, true, alice, alice, { from: alice, value: zusdAmount.mul(mv._1e18BN).div(price).add(bufferFee) }); } + // Ensure buffer is empty before the final redemption attempt too + await emptyRedemptionBufferAsOwner(); + const { firstRedemptionHint, partialRedemptionHintNICR diff --git a/tests/js/TroveManager_LiquidationRewardsTest.js b/tests/js/TroveManager_LiquidationRewardsTest.js index 093b924..679d62f 100644 --- a/tests/js/TroveManager_LiquidationRewardsTest.js +++ b/tests/js/TroveManager_LiquidationRewardsTest.js @@ -559,7 +559,9 @@ contract('TroveManager - Redistribution reward calculations', async accounts => await borrowerOperations.addColl(bob, bob, { from: bob, value: addedColl }); // Alice withdraws ZUSD - await borrowerOperations.withdrawZUSD(th._100pct, await getNetBorrowingAmount(A_totalDebt), alice, alice, { from: alice }); + const borrowAmount = await getNetBorrowingAmount(A_totalDebt); + const buffFee = await th.getRedemptionBufferFeeRBTC(contracts, borrowAmount, alice); + await borrowerOperations.withdrawZUSD(th._100pct, borrowAmount, alice, alice, { from: alice, value: buffFee }); // Price drops to 100 $/E await priceFeed.setPrice(dec(100, 18)); @@ -897,7 +899,9 @@ contract('TroveManager - Redistribution reward calculations', async accounts => await borrowerOperations.withdrawColl(withdrawnColl, bob, bob, { from: bob }); // Alice withdraws ZUSD - await borrowerOperations.withdrawZUSD(th._100pct, await getNetBorrowingAmount(A_totalDebt), alice, alice, { from: alice }); + const borrowAmount = await getNetBorrowingAmount(A_totalDebt); + const buffFee = await th.getRedemptionBufferFeeRBTC(contracts, borrowAmount, alice); + await borrowerOperations.withdrawZUSD(th._100pct, await getNetBorrowingAmount(A_totalDebt), alice, alice, { from: alice, value: buffFee }); // Price drops to 100 $/E await priceFeed.setPrice(dec(100, 18)); diff --git a/tests/js/TroveManager_RecoveryModeTest.js b/tests/js/TroveManager_RecoveryModeTest.js index 7d0b328..4b1011d 100644 --- a/tests/js/TroveManager_RecoveryModeTest.js +++ b/tests/js/TroveManager_RecoveryModeTest.js @@ -52,6 +52,24 @@ contract('TroveManager - in Recovery Mode', async accounts => { const getNetBorrowingAmount = async (debtWithFee) => th.getNetBorrowingAmount(contracts, debtWithFee); const openTrove = async (params) => th.openTrove(contracts, params); + const emptyRedemptionBufferAsOwner = async () => { + const bufferBal = toBN(await contracts.redemptionBuffer.getBalance()); + if (bufferBal.eq(toBN(0))) return; + + // RedemptionBuffer is Ownable — use its current owner (often `owner` or `multisig`) + const rbOwner = await contracts.redemptionBuffer.getOwner(); + + await contracts.redemptionBuffer.distributeToStakers( + bufferBal, + { from: rbOwner, gasPrice: 0 } + ); + + // sanity + const afterBal = toBN(await contracts.redemptionBuffer.getBalance()); + assert.isTrue(afterBal.eq(toBN(0)), "buffer not fully emptied"); + }; + + before(async () => { contracts = await deploymentHelper.deployLiquityCore(); contracts.troveManager = await TroveManagerTester.new(contracts.permit2.address); @@ -1633,7 +1651,7 @@ contract('TroveManager - in Recovery Mode', async accounts => { await troveManager.liquidate(bob, { from: owner }); // check Bob’s collateral surplus: 5.76 * 100 - 480 * 1.1 - const bob_remainingCollateral = B_coll.sub(B_totalDebt.mul(th.toBN(dec(11, 17))).div(price)); + const bob_remainingCollateral = B_coll.sub(B_totalDebt.mul(toBN(dec(11, 17))).div(price)); th.assertIsApproximatelyEqual(await collSurplusPool.getCollateral(bob), bob_remainingCollateral); // can claim collateral const bob_balanceBefore = th.toBN(await web3.eth.getBalance(bob)); @@ -1649,15 +1667,17 @@ contract('TroveManager - in Recovery Mode', async accounts => { await priceFeed.setPrice('200000000000000000000'); const { collateral: B_coll_2, netDebt: B_netDebt_2 } = await openTrove({ ICR: toBN(dec(150, 16)), extraZUSDAmount: dec(480, 18), extraParams: { from: bob, value: bob_remainingCollateral } }); const { collateral: D_coll } = await openTrove({ ICR: toBN(dec(266, 16)), extraZUSDAmount: B_netDebt_2, extraParams: { from: dennis } }); + // NEW: force redemption to come from troves (not the buffer) + await emptyRedemptionBufferAsOwner(); await th.redeemCollateral(dennis, contracts, B_netDebt_2); price = await priceFeed.getPrice(); const bob_surplus = B_coll_2.sub(B_netDebt_2.mul(mv._1e18BN).div(price)); th.assertIsApproximatelyEqual(await collSurplusPool.getCollateral(bob), bob_surplus); // can claim collateral - const bob_balanceBefore_2 = th.toBN(await web3.eth.getBalance(bob)); + const bob_balanceBefore_2 = toBN(await web3.eth.getBalance(bob)); await borrowerOperations.claimCollateral({ from: bob, gasPrice: 0 }); - const bob_balanceAfter_2 = th.toBN(await web3.eth.getBalance(bob)); - th.assertIsApproximatelyEqual(bob_balanceAfter_2, bob_balanceBefore_2.add(th.toBN(bob_surplus))); + const bob_balanceAfter_2 = toBN(await web3.eth.getBalance(bob)); + th.assertIsApproximatelyEqual(bob_balanceAfter_2, bob_balanceBefore_2.add(toBN(bob_surplus))); }); it("liquidate(), with 110% < ICR < TCR, can claim collateral, after another claim from a redemption", async () => { @@ -1671,6 +1691,9 @@ contract('TroveManager - in Recovery Mode', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // NEW: force redemption to come from troves (not the buffer) + await emptyRedemptionBufferAsOwner(); + // Dennis redeems 40, so Bob has a surplus of (200 * 1 - 40) / 200 = 0.8 ETH await th.redeemCollateral(dennis, contracts, B_netDebt); let price = await priceFeed.getPrice(); diff --git a/utils/js/proxyHelpers.js b/utils/js/proxyHelpers.js index fb6a06b..8b0198e 100644 --- a/utils/js/proxyHelpers.js +++ b/utils/js/proxyHelpers.js @@ -127,6 +127,10 @@ class BorrowerOperationsProxy extends Proxy { return this.forwardFunction(params, 'getRedemptionBufferFeeRBTC(uint256)') } + async getRedemptionBufferFeeRBTCWithPrice(...params) { + return this.forwardFunction(params, 'getRedemptionBufferFeeRBTCWithPrice(uint256,uint256)') + } + async getNewTCRFromTroveChange(...params) { return this.proxyFunction('getNewTCRFromTroveChange', params) } diff --git a/utils/js/testHelpers.js b/utils/js/testHelpers.js index b27d589..08bebca 100644 --- a/utils/js/testHelpers.js +++ b/utils/js/testHelpers.js @@ -2064,12 +2064,14 @@ class TestHelper { // ------------------------------------------------------------------ if (zusdFromBuffer.gt(toBN("0"))) { // Optional safety: reset allowance first for non-standard ERC20s - await contracts.zusdToken.approve(contracts.troveManager.address, 0, { from: redeemer }); + await contracts.zusdToken.approve(contracts.troveManager.address, 0, { from: redeemer, gasPrice: 0 }); await contracts.zusdToken.approve( contracts.troveManager.address, zusdFromBuffer, - { from: redeemer } + { from: redeemer, + gasPrice: 0 + } ); } From d0f9ac8dc1a44fdbbfd0440a57053b960d8051eb Mon Sep 17 00:00:00 2001 From: bribri-wci Date: Thu, 25 Dec 2025 18:35:16 -0500 Subject: [PATCH 13/13] Tests fix --- tests/js/TroveManagerTest.js | 86 +++++++++--------------------------- 1 file changed, 20 insertions(+), 66 deletions(-) diff --git a/tests/js/TroveManagerTest.js b/tests/js/TroveManagerTest.js index 493ae8b..1f9c8e8 100644 --- a/tests/js/TroveManagerTest.js +++ b/tests/js/TroveManagerTest.js @@ -64,52 +64,6 @@ contract('TroveManager', async accounts => { const openNueTrove = async (params) => th.openNueTrove(contracts, params); const withdrawZUSD = async (params) => th.withdrawZUSD(contracts, params); - const drainRedemptionBufferWithWhale = async () => { - await emptyRedemptionBufferAsOwner(); - /*// Use the same price source your tests are using - const price = toBN(await priceFeed.getPrice()); - - const bufferBalRBTC = toBN(await contracts.redemptionBuffer.getBalance()); // RBTC wei - const drainZUSD = bufferBalRBTC.mul(price).div(mv._1e18BN); // max ZUSD buffer can cover (floor) - - if (drainZUSD.eq(toBN(0))) return; - - // --- Fund whale with enough ZUSD (scan all accounts, not just alice/bob/carol) --- - let whaleBal = toBN(await zusdToken.balanceOf(whale)); - let needed = drainZUSD.sub(whaleBal); - - if (needed.gt(toBN(0))) { - const all = await web3.eth.getAccounts(); - - for (const src of all) { - if (src === whale) continue; - - const srcBal = toBN(await zusdToken.balanceOf(src)); - if (srcBal.eq(toBN(0))) continue; - - const send = srcBal.gte(needed) ? needed : srcBal; - await zusdToken.transfer(whale, send, { from: src }); - - needed = needed.sub(send); - if (needed.eq(toBN(0))) break; - } - } - - whaleBal = toBN(await zusdToken.balanceOf(whale)); - assert.isTrue( - whaleBal.gte(drainZUSD), - `whale doesn't have enough ZUSD to drain buffer: have=${whaleBal.toString()} need=${drainZUSD.toString()}` - ); - - // --- Drain: use the updated redemption helper so allowance/hints match buffer-first redeem --- - await th.redeemCollateralAndGetTxObject(whale, contracts, drainZUSD, th._100pct); - - // Optional sanity: after drain, buffer should not be able to cover even 1 wei of ZUSD - const bufferAfter = toBN(await contracts.redemptionBuffer.getBalance()); - const maxZusdAfter = bufferAfter.mul(price).div(mv._1e18BN); - assert.equal(maxZusdAfter.toString(), "0");*/ - }; - const emptyRedemptionBufferAsOwner = async () => { const bufferBal = toBN(await contracts.redemptionBuffer.getBalance()); if (bufferBal.eq(toBN(0))) return; @@ -2473,7 +2427,7 @@ contract('TroveManager', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); const nonce = th.generateNonce(); const deadline = th.toDeadline(1000 * 60 * 60 * 60 * 24 * 28 ); @@ -2595,7 +2549,7 @@ contract('TroveManager', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // get ERC2612 permission from alice for stability pool to spend DLLR amount const permission = await signERC2612Permit(dennis_signer, nueMockToken.address, dennis_signer.address, troveManager.address, redemptionAmount.toString()); @@ -2694,7 +2648,7 @@ contract('TroveManager', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // Dennis redeems 20 ZUSD // Don't pay for gas, as it makes it easier to calculate the received Ether @@ -2785,7 +2739,7 @@ contract('TroveManager', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // Dennis redeems 20 ZUSD // Don't pay for gas, as it makes it easier to calculate the received Ether @@ -2876,7 +2830,7 @@ contract('TroveManager', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // Dennis redeems 20 ZUSD // Don't pay for gas, as it makes it easier to calculate the received Ether @@ -2973,7 +2927,7 @@ contract('TroveManager', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // Dennis redeems 20 ZUSD // Don't pay for gas, as it makes it easier to calculate the received Ether @@ -3048,7 +3002,7 @@ contract('TroveManager', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // Flyn redeems collateral await contracts.zusdToken.approve( @@ -3116,7 +3070,7 @@ contract('TroveManager', async accounts => { await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // Flyn redeems collateral with only two iterations await contracts.zusdToken.approve( @@ -3165,7 +3119,7 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // ZUSD redemption is 55000 US const ZUSDRedemption = dec(55000, 18); @@ -3195,7 +3149,7 @@ contract('TroveManager', async accounts => { // Skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // ZUSD redemption is 49900 ZUSD const ZUSDRedemption = dec(49900, 18); // await zusdToken.balanceOf(B) //dec(59800, 18) @@ -3238,7 +3192,7 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); const { firstRedemptionHint, @@ -3357,7 +3311,7 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); await contracts.zusdToken.approve( troveManager.address, @@ -3412,7 +3366,7 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); await contracts.zusdToken.approve( troveManager.address, @@ -3470,7 +3424,7 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); await contracts.zusdToken.approve( troveManager.address, @@ -3781,7 +3735,7 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // Erin attempts to redeem 400 ZUSD const { @@ -3863,7 +3817,7 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // Erin tries to redeem 1000 ZUSD try { @@ -4019,7 +3973,7 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); await contracts.zusdToken.approve( troveManager.address, @@ -4187,7 +4141,7 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // Bob attempts to redeem his ill-gotten 101 ZUSD, from a system that has 100 ZUSD outstanding debt try { @@ -4555,7 +4509,7 @@ contract('TroveManager', async accounts => { const C_balanceBefore = toBN(await web3.eth.getBalance(C)); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // whale redeems 360 ZUSD. Expect this to fully redeem A, B, C, and partially redeem D. await th.redeemCollateral(whale, contracts, redemptionAmount); @@ -4665,7 +4619,7 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); // Drain the redemption buffer so this test will complete successufully - await drainRedemptionBufferWithWhale(); + await emptyRedemptionBufferAsOwner(); // whale redeems ZUSD. Expect this to fully redeem A, B, C, and partially redeem 15 ZUSD from D. const redemptionTx = await th.redeemCollateralAndGetTxObject(whale, contracts, redemptionAmount, th._100pct, { gasPrice: 0 });