diff --git a/contracts/BorrowerOperations.sol b/contracts/BorrowerOperations.sol index 8043271..b583427 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, @@ -26,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: @@ -84,6 +98,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( @@ -98,6 +114,7 @@ contract BorrowerOperations is /** Constructor */ constructor(address _permit2) public { permit2 = IPermit2(_permit2); + _reentrancyStatus = _NOT_ENTERED; } // --- Dependency setters --- @@ -159,17 +176,75 @@ 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 setRedemptionBufferAddress(address _buffer) external override onlyOwner { + require(_buffer != address(0), "BorrowerOps: zero buffer address"); + checkContract(_buffer); + 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 getRedemptionBufferAddress() 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 + override + returns (uint256) + { + if (redemptionBufferRate == 0) { + return 0; + } + require(_price > 0, "BorrowerOps: invalid price"); + return _calcRedemptionBufferFeeRBTC(_ZUSDAmount, _price); + } + function openTrove( uint256 _maxFeePercentage, uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external payable override { + ) external payable override nonReentrant { _openTrove(_maxFeePercentage, _ZUSDAmount, _upperHint, _lowerHint, msg.sender); } @@ -178,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)); @@ -190,6 +265,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, @@ -224,15 +322,22 @@ 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); + // --- 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"); + + 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( - msg.value, + coll, true, vars.compositeDebt, true, @@ -243,7 +348,7 @@ contract BorrowerOperations is // Set the trove struct's properties contractsCache.troveManager.setTroveStatus(msg.sender, 1); - contractsCache.troveManager.increaseTroveColl(msg.sender, msg.value); + contractsCache.troveManager.increaseTroveColl(msg.sender, coll); contractsCache.troveManager.increaseTroveDebt(msg.sender, vars.compositeDebt); contractsCache.troveManager.updateTroveRewardSnapshots(msg.sender); @@ -253,8 +358,15 @@ 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 collateral to Active Pool + _activePoolAddColl(contractsCache.activePool, coll); + + // NEW: send the fee to the RedemptionBuffer + if (bufferFee > 0) { + redemptionBuffer.deposit{ value: bufferFee }(); + } + + // Mint the ZUSDAmount to the borrower _mintZusdAndIncreaseActivePoolDebt( contractsCache.activePool, contractsCache.zusdToken, @@ -262,6 +374,7 @@ contract BorrowerOperations is _ZUSDAmount, vars.netDebt ); + // Move the ZUSD gas compensation to the Gas Pool _mintZusdAndIncreaseActivePoolDebt( contractsCache.activePool, @@ -274,7 +387,7 @@ contract BorrowerOperations is emit TroveUpdated( msg.sender, vars.compositeDebt, - msg.value, + coll, vars.stake, BorrowerOperation.openTrove ); @@ -282,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); } @@ -291,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); } @@ -301,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); } @@ -311,7 +424,7 @@ contract BorrowerOperations is uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external override { + ) external payable override nonReentrant { _adjustTrove(msg.sender, 0, _ZUSDAmount, true, _upperHint, _lowerHint, _maxFeePercentage); } @@ -323,7 +436,7 @@ contract BorrowerOperations is uint256 _ZUSDAmount, address _upperHint, address _lowerHint - ) external override returns (uint256) { + ) external payable override nonReentrant returns (uint256) { address thisAddress = address(this); uint256 balanceBefore = zusdToken.balanceOf(thisAddress); @@ -352,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); } @@ -362,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); } @@ -373,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); } @@ -384,7 +497,7 @@ contract BorrowerOperations is bool _isDebtIncrease, address _upperHint, address _lowerHint - ) external payable override { + ) external payable override nonReentrant { _adjustTrove( msg.sender, _collWithdrawal, @@ -405,7 +518,7 @@ contract BorrowerOperations is address _upperHint, address _lowerHint, IMassetManager.PermitParams calldata _permitParams - ) external payable override { + ) external payable override nonReentrant { _adjustNueTrove( _maxFeePercentage, _collWithdrawal, @@ -427,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, @@ -585,15 +698,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) @@ -601,8 +728,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; @@ -690,13 +818,18 @@ 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 { + 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); @@ -710,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); @@ -775,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); } @@ -859,6 +992,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/BorrowerOperationsStorage.sol b/contracts/BorrowerOperationsStorage.sol index 9e7d769..abadea5 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,19 @@ 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; + + // --------------------------------------------------------------------- + // 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..46f239f 100644 --- a/contracts/Dependencies/TroveManagerBase.sol +++ b/contracts/Dependencies/TroveManagerBase.sol @@ -25,6 +25,24 @@ 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; + } + + modifier requireNotEntered() { + require(_reentrancyStatus != _ENTERED, "TroveManager: locked"); + _; + } + /** --- Variable container structs for liquidations --- @@ -152,6 +170,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. @@ -351,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/Dependencies/TroveManagerRedeemOps.sol b/contracts/Dependencies/TroveManagerRedeemOps.sol index 2686be6..eb1b80c 100644 --- a/contracts/Dependencies/TroveManagerRedeemOps.sol +++ b/contracts/Dependencies/TroveManagerRedeemOps.sol @@ -8,39 +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 -/// TroveManagerBase constructor param is bootsrap period when redemptions are not allowed +/// @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; - /** 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. - */ + // --------------------------------------------------------------------- + // 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, @@ -50,26 +99,31 @@ 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 { + /** + * @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, defaultPool, @@ -81,108 +135,241 @@ contract TroveManagerRedeemOps is TroveManagerBase { ); RedemptionTotals memory totals; - _requireValidMaxFeePercentage(_maxFeePercentage); + // --- 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 less than total ZUSD supply + + // Confirm redeemer's balance is <= total ZUSD supply (sanity check; mirrors original intent). assert(contractsCache.zusdToken.balanceOf(msg.sender) <= totals.totalZUSDSupplyAtStart); - totals.remainingZUSD = _ZUSDamount; - address currentBorrower; + // ------------------------------------------------------------ + // 1) Swap against RedemptionBuffer FIRST (ZUSD -> RBTC) + // - 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; + (totals.remainingZUSD, ethFromBuffer, zusdSwappedToFeeDistributor) = _swapFromBuffer( + contractsCache, + _ZUSDamount, + totals.price, + msg.sender + ); + + // ------------------------------------------------------------ + // 2) Redeem remaining from troves (normal redemption path) + // + // 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); + } - if ( - _isValidFirstRedemptionHint( - contractsCache.sortedTroves, - _firstRedemptionHint, - totals.price - ) - ) { - currentBorrower = _firstRedemptionHint; + // 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 + ); + + // ------------------------------------------------------------ + // 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); + totals.ETHFee = ethFeeFromTroves.add(ethFeeFromBuffer); + + _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); + contractsCache.activePool.decreaseZUSDDebt(totals.totalZUSDToRedeem); + } + + // ------------------------------------------------------------ + // 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() (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 + 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) { + contractsCache.activePool.sendETH(msg.sender, ethToSendFromTroves); + } + + uint256 ethToSendFromBuffer = ethFromBuffer.sub(ethFeeFromBuffer); + if (ethToSendFromBuffer > 0) { + redemptionBuffer.withdrawForRedemption(payable(msg.sender), ethToSendFromBuffer); + } + } + + // --------------------------------------------------------------------- + // 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, + RedeemParams memory _params + ) 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 { - currentBorrower = contractsCache.sortedTroves.getLast(); - // Find the first trove with ICR >= MCR + // Otherwise start from the last trove and find first with ICR >= MCR. + currentBorrower = _contractsCache.sortedTroves.getLast(); while ( currentBorrower != address(0) && - _getCurrentICR(currentBorrower, totals.price) < liquityBaseParams.MCR() + _getCurrentICR(currentBorrower, _totals.price) < liquityBaseParams.MCR() ) { - currentBorrower = contractsCache.sortedTroves.getPrev(currentBorrower); + currentBorrower = _contractsCache.sortedTroves.getPrev(currentBorrower); } } - // 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); + // 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); - - _applyPendingRewards( - contractsCache.activePool, - contractsCache.defaultPool, - currentBorrower - ); + 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, + _contractsCache, currentBorrower, - totals.remainingZUSD, - totals.price, - _upperPartialRedemptionHint, - _lowerPartialRedemptionHint, - _partialRedemptionHintNICR + _totals.remainingZUSD, + _totals.price, + _params ); - 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 + // 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); - totals.totalETHDrawn = totals.totalETHDrawn.add(singleRedemption.ETHLot); + _totals.totalZUSDToRedeem = _totals.totalZUSDToRedeem.add(singleRedemption.ZUSDLot); + _totals.totalETHDrawn = _totals.totalETHDrawn.add(singleRedemption.ETHLot); - totals.remainingZUSD = totals.remainingZUSD.sub(singleRedemption.ZUSDLot); + _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. - // Use the saved total ZUSD supply value, from before it was reduced by the redemption. - _updateBaseRateFromRedemption( - totals.totalETHDrawn, - totals.price, - totals.totalZUSDSupplyAtStart - ); - - // Calculate the ETH fee - totals.ETHFee = _getRedemptionFee(totals.totalETHDrawn); - - _requireUserAcceptsFee(totals.ETHFee, totals.totalETHDrawn, _maxFeePercentage); - - // Send the ETH fee to the feeDistributorContract address - contractsCache.activePool.sendETH(address(feeDistributor), totals.ETHFee); - feeDistributor.distributeFees(); - - totals.ETHToSendToRedeemer = totals.totalETHDrawn.sub(totals.ETHFee); - - emit Redemption( - _ZUSDamount, - totals.totalZUSDToRedeem, - totals.totalETHDrawn, - 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); } - ///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 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, @@ -193,24 +380,29 @@ 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, 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 + /// @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, @@ -222,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), @@ -230,17 +423,32 @@ 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); } + // --------------------------------------------------------------------- + // 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, @@ -255,35 +463,113 @@ 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(); + } + + // --------------------------------------------------------------------- + // RedemptionBuffer swap stage + // --------------------------------------------------------------------- + + /** + * @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, + uint256 _ZUSDAmount, + uint256 _price, + address _redeemer + ) 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); } - /// Redeem as much collateral as possible from _borrower's Trove in exchange for ZUSD up to _maxZUSDamount + // --------------------------------------------------------------------- + // 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, 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 + // 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 + // 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 + // 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 + // 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); @@ -295,20 +581,25 @@ contract TroveManagerRedeemOps is TroveManagerBase { * 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 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 != _partialRedemptionHintNICR || _getNetDebt(newDebt) < MIN_NET_DEBT) { + 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, - _upperPartialRedemptionHint, - _lowerPartialRedemptionHint + _params.upperPartialRedemptionHint, + _params.lowerPartialRedemptionHint ); + // Persist updated trove state. Troves[_borrower].debt = newDebt; Troves[_borrower].coll = newColl; _updateStakeAndTotalStakes(_borrower); @@ -326,10 +617,15 @@ contract TroveManagerRedeemOps is TroveManagerBase { } /** + @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 + 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, @@ -344,8 +640,9 @@ contract TroveManagerRedeemOps is TroveManagerBase { 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 + + // Base rate is always non-zero after redemption + assert(newBaseRate > 0); // Update the baseRate state variable baseRate = newBaseRate; @@ -357,11 +654,17 @@ contract TroveManagerRedeemOps is TroveManagerBase { } /** + @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, 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. + + 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, @@ -373,7 +676,7 @@ contract TroveManagerRedeemOps is TroveManagerBase { // Update Active Pool ZUSD, and send ETH to account _contractsCache.activePool.decreaseZUSDDebt(_ZUSD); - // send ETH from Active Pool to CollSurplus Pool + // 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); } diff --git a/contracts/FeeDistributor.sol b/contracts/FeeDistributor.sol index 086b848..34a913e 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"; @@ -23,6 +22,8 @@ contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor event ZUSDDistributed(uint256 _zusdDistributedAmount); event RBTCistributed(uint256 _rbtcDistributedAmount); + event RedemptionBufferAddressChanged(address _redemptionBufferAddress); + // --- Dependency setters --- function setAddresses( @@ -62,17 +63,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); } @@ -89,6 +121,7 @@ contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor 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) { @@ -119,11 +152,14 @@ contract FeeDistributor is CheckContract, FeeDistributorStorage, IFeeDistributor 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 d6ed054..8507cf8 100644 --- a/contracts/Interfaces/IBorrowerOperations.sol +++ b/contracts/Interfaces/IBorrowerOperations.sol @@ -4,9 +4,16 @@ 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. +/// @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 --- @@ -21,6 +28,12 @@ interface IBorrowerOperations { event SortedTrovesAddressChanged(address _sortedTrovesAddress); event ZUSDTokenAddressChanged(address _zusdTokenAddress); event ZEROStakingAddressChanged(address _zeroStakingAddress); + /// @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. + event RedemptionBufferRateChanged(uint256 _redemptionBufferRate); event TroveCreated(address indexed _borrower, uint256 arrayIndex); event TroveUpdated( @@ -65,6 +78,45 @@ interface IBorrowerOperations { address _zeroStakingAddress ) external; + /// @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 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 getRedemptionBufferAddress() external view returns (address); + + /// @notice Returns the configured redemption buffer rate (1e18 precision). + function getRedemptionBufferRate() external view returns (uint256); + + // --- 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). @@ -129,6 +181,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 +192,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 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 new file mode 100644 index 0000000..a9054da --- /dev/null +++ b/contracts/Interfaces/IRedemptionBuffer.sol @@ -0,0 +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 into the buffer. + /// @dev Expected caller: BorrowerOperations (onlyBorrowerOps in implementation). + function deposit() external payable; + + /// @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 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; + + /// @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); +} diff --git a/contracts/Interfaces/ITroveManager.sol b/contracts/Interfaces/ITroveManager.sol index bb0c442..2ac602f 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 setRedemptionBufferAddress(address _buffer) external; + /// @return Trove owners count function getTroveOwnersCount() external view returns (uint256); 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(); + } } 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/contracts/RedemptionBuffer.sol b/contracts/RedemptionBuffer.sol new file mode 100644 index 0000000..dd912ad --- /dev/null +++ b/contracts/RedemptionBuffer.sol @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; + +import "./Dependencies/Ownable.sol"; +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, + 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); + } + + // --------------------------------------------------------------------- + // 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"); + + // With the FeeDistributor patch above, this can now succeed. + 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); + } +} diff --git a/contracts/TroveManager.sol b/contracts/TroveManager.sol index cfcc2f0..118364c 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,13 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { emit TroveManagerRedeemOpsAddressChanged(_troveManagerRedeemOps); } + // --- Redemption buffer config --- + function setRedemptionBufferAddress(address _buffer) external override onlyOwner { + require(_buffer != address(0), "TroveManager: zero buffer address"); + checkContract(_buffer); + redemptionBuffer = IRedemptionBuffer(_buffer); + } + // --- Getters --- function getTroveOwnersCount() external view override returns (uint256) { @@ -132,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 --- @@ -391,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, @@ -594,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[] calldata _troveArray) external 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; @@ -722,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); @@ -845,13 +858,13 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { return NICR; } - function applyPendingRewards(address _borrower) external override { + 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 { + function updateTroveRewardSnapshots(address _borrower) external override requireNotEntered { _requireCallerIsBorrowerOperations(); return _updateTroveRewardSnapshots(_borrower); } @@ -880,12 +893,12 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { coll = coll.add(pendingETHReward); } - function removeStake(address _borrower) external override { + function removeStake(address _borrower) external override requireNotEntered { _requireCallerIsBorrowerOperations(); return _removeStake(_borrower); } - function updateStakeAndTotalStakes(address _borrower) external override returns (uint256) { + function updateStakeAndTotalStakes(address _borrower) external override requireNotEntered returns (uint256) { _requireCallerIsBorrowerOperations(); return _updateStakeAndTotalStakes(_borrower); } @@ -937,7 +950,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { _activePool.sendETH(address(_defaultPool), _coll); } - function closeTrove(address _borrower) external override { + function closeTrove(address _borrower) external override requireNotEntered { _requireCallerIsBorrowerOperations(); return _closeTrove(_borrower, Status.closedByOwner); } @@ -966,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 returns (uint256 index) { + function addTroveOwnerToArray(address _borrower) external override requireNotEntered returns (uint256 index) { _requireCallerIsBorrowerOperations(); return _addTroveOwnerToArray(_borrower); } @@ -1058,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 { + function decayBaseRateFromBorrowing() external override requireNotEntered { _requireCallerIsBorrowerOperations(); uint256 decayedBaseRate = _calcDecayedBaseRate(); @@ -1092,7 +1105,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 requireNotEntered { _requireCallerIsBorrowerOperations(); Troves[_borrower].status = Status(_num); } @@ -1100,7 +1113,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function increaseTroveColl( address _borrower, uint256 _collIncrease - ) external override returns (uint256) { + ) external override requireNotEntered returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newColl = Troves[_borrower].coll.add(_collIncrease); Troves[_borrower].coll = newColl; @@ -1110,7 +1123,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function decreaseTroveColl( address _borrower, uint256 _collDecrease - ) external override returns (uint256) { + ) external override requireNotEntered returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newColl = Troves[_borrower].coll.sub(_collDecrease); Troves[_borrower].coll = newColl; @@ -1120,7 +1133,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function increaseTroveDebt( address _borrower, uint256 _debtIncrease - ) external override returns (uint256) { + ) external override requireNotEntered returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newDebt = Troves[_borrower].debt.add(_debtIncrease); Troves[_borrower].debt = newDebt; @@ -1130,7 +1143,7 @@ contract TroveManager is TroveManagerBase, CheckContract, ITroveManager { function decreaseTroveDebt( address _borrower, uint256 _debtDecrease - ) external override returns (uint256) { + ) external override requireNotEntered returns (uint256) { _requireCallerIsBorrowerOperations(); uint256 newDebt = Troves[_borrower].debt.sub(_debtDecrease); Troves[_borrower].debt = newDebt; @@ -1170,7 +1183,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)); } @@ -1187,7 +1200,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)); } @@ -1205,7 +1218,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 7fd6458..49a0011 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,13 @@ 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; + + // --------------------------------------------------------------------- + // Reentrancy guard (shared across TroveManager + delegatecall modules) + // --------------------------------------------------------------------- + // 0 = uninitialized, 1 = not entered, 2 = entered + uint256 internal _reentrancyStatus; } 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..0e6da6a 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)); @@ -329,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); @@ -387,7 +406,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 +444,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 +507,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 +532,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 +629,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 +707,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 +725,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..1f9c8e8 100644 --- a/tests/js/TroveManagerTest.js +++ b/tests/js/TroveManagerTest.js @@ -64,6 +64,25 @@ contract('TroveManager', async accounts => { const openNueTrove = async (params) => th.openNueTrove(contracts, params); const withdrawZUSD = async (params) => th.withdrawZUSD(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(); permit2 = contracts.permit2; @@ -875,9 +894,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 +2426,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 emptyRedemptionBufferAsOwner(); + const nonce = th.generateNonce(); const deadline = th.toDeadline(1000 * 60 * 60 * 60 * 24 * 28 ); const permitTransferFrom = { @@ -2429,6 +2451,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 +2548,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 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()); // 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 +2647,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 emptyRedemptionBufferAsOwner(); + // 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 +2738,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 emptyRedemptionBufferAsOwner(); + // 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 +2829,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 emptyRedemptionBufferAsOwner(); + // 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 +2926,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 emptyRedemptionBufferAsOwner(); + // 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 +2996,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 emptyRedemptionBufferAsOwner(); + // 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 +3064,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 emptyRedemptionBufferAsOwner(); + // 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 +3118,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 emptyRedemptionBufferAsOwner(); // ZUSD redemption is 55000 US const ZUSDRedemption = dec(55000, 18); @@ -3040,6 +3148,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 emptyRedemptionBufferAsOwner(); // ZUSD redemption is 49900 ZUSD const ZUSDRedemption = dec(49900, 18); // await zusdToken.balanceOf(B) //dec(59800, 18) @@ -3079,6 +3189,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 emptyRedemptionBufferAsOwner(); + const { firstRedemptionHint, partialRedemptionHintNICR @@ -3108,6 +3223,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 +3243,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 +3310,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 emptyRedemptionBufferAsOwner(); + await contracts.zusdToken.approve( + troveManager.address, + amount.toString(), + { + from: carol, + gasPrice: 0 + } + ); const redemptionTx = await troveManager.redeemCollateral( amount, alice, @@ -3212,7 +3353,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 +3365,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 emptyRedemptionBufferAsOwner(); + await contracts.zusdToken.approve( + troveManager.address, + A_debt.toString(), + { + from: carol, + gasPrice: 0 + } + ); await troveManager.redeemCollateral( A_debt, alice, @@ -3272,7 +3423,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 emptyRedemptionBufferAsOwner(); + 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 +3495,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 +3734,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 emptyRedemptionBufferAsOwner(); // Erin attempts to redeem 400 ZUSD const { @@ -3581,6 +3749,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 +3764,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 +3816,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 emptyRedemptionBufferAsOwner(); // Erin tries to redeem 1000 ZUSD try { @@ -3793,7 +3972,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 emptyRedemptionBufferAsOwner(); + await contracts.zusdToken.approve( + troveManager.address, + _120_ZUSD.toString(), + { + from: erin, + gasPrice: 0 + } + ); const redemption_1 = await troveManager.redeemCollateral( _120_ZUSD, firstRedemptionHint, @@ -3824,6 +4013,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 +4050,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 +4140,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 emptyRedemptionBufferAsOwner(); // Bob attempts to redeem his ill-gotten 101 ZUSD, from a system that has 100 ZUSD outstanding debt try { @@ -4301,6 +4508,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 emptyRedemptionBufferAsOwner(); + // whale redeems 360 ZUSD. Expect this to fully redeem A, B, C, and partially redeem D. await th.redeemCollateral(whale, contracts, redemptionAmount); @@ -4341,6 +4551,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); @@ -4405,6 +4618,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 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 }); @@ -4481,9 +4696,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 +4727,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,9 +4737,13 @@ contract('TroveManager', async accounts => { // skip bootstrapping phase await th.fastForwardTime(timeValues.SECONDS_IN_ONE_WEEK * 2, web3.currentProvider); + // 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, @@ -4532,6 +4751,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, @@ -4546,14 +4773,26 @@ 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 } = 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_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 3572759..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); @@ -1570,9 +1588,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)); @@ -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(); @@ -2470,9 +2493,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 +2530,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..8b0198e 100644 --- a/utils/js/proxyHelpers.js +++ b/utils/js/proxyHelpers.js @@ -123,6 +123,14 @@ class BorrowerOperationsProxy extends Proxy { return this.forwardFunction(params, 'claimRedeemedCollateral(address)') } + async getRedemptionBufferFeeRBTC(...params) { + 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 a1dee4e..08bebca 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,99 @@ 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, gasPrice: 0 }); + + await contracts.zusdToken.approve( + contracts.troveManager.address, + zusdFromBuffer, + { from: redeemer, + gasPrice: 0 + } + ); + } + + // ------------------------------------------------------------------ + // 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) => {