-
Notifications
You must be signed in to change notification settings - Fork 135
[Contracts] Redemption helper #1164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
c7cc3d8
836893c
b6db2cd
806dc90
fa8246d
f9d1d38
e05e768
32dc8cf
f5af88b
ec87dc2
5148f24
34dda5d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| interface IRedemptionHelper { | ||
| struct SimulationContext { | ||
| address troveManager; | ||
| address sortedTroves; | ||
| bool redeemable; | ||
| uint256 price; | ||
| uint256 proportion; | ||
| uint256 attemptedBold; | ||
| uint256 redeemedBold; | ||
| uint256 iterations; | ||
| } | ||
|
|
||
| struct Redeemed { | ||
| uint256 bold; | ||
| uint256 coll; | ||
| } | ||
|
|
||
| function simulateRedemption(uint256 _bold, uint256 _maxIterationsPerCollateral) | ||
| external | ||
| returns (SimulationContext[] memory branch, uint256 totalProportions); | ||
|
|
||
| // Find the maximal amount of BOLD that can be redeemed proportionally within | ||
| // a given iteration limit. This helps prevent the redeemer from overpaying on | ||
| // the redemption fee. | ||
| // | ||
| // Also returns the expected fee that will be paid (as a percentage), and the | ||
| // expected collateral amounts that will be paid out in exchange for the | ||
| // redeemed BOLD. The latter may be used to calculate the _minCollRedeemed | ||
| // parameter passed to redeemCollateral(). | ||
| function truncateRedemption(uint256 _bold, uint256 _maxIterationsPerCollateral) | ||
| external | ||
| returns (uint256 truncatedBold, uint256 feePct, Redeemed[] memory redeemed); | ||
|
|
||
| // Wrapper around CollateralRegistry's redeemCollateral() that adds slippage | ||
| // protection in the form of a minimum acceptable collateral amounts parameter. | ||
| function redeemCollateral( | ||
| uint256 _bold, | ||
| uint256 _maxIterationsPerCollateral, | ||
| uint256 _maxFeePct, | ||
| uint256[] memory _minCollRedeemed | ||
| ) external; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| pragma solidity 0.8.24; | ||
|
|
||
| import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; | ||
| import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
| import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol"; | ||
|
|
||
| import {_100pct, DECIMAL_PRECISION} from "./Dependencies/Constants.sol"; | ||
| import {IAddressesRegistry} from "./Interfaces/IAddressesRegistry.sol"; | ||
| import {IBoldToken} from "./Interfaces/IBoldToken.sol"; | ||
| import {ICollateralRegistry} from "./Interfaces/ICollateralRegistry.sol"; | ||
| import {IRedemptionHelper} from "./Interfaces/IRedemptionHelper.sol"; | ||
| import {ISortedTroves} from "./Interfaces/ISortedTroves.sol"; | ||
| import {ITroveManager} from "./Interfaces/ITroveManager.sol"; | ||
| import {LatestTroveData} from "./Types/LatestTroveData.sol"; | ||
|
|
||
| contract RedemptionHelper is IRedemptionHelper { | ||
| using SafeERC20 for IERC20; | ||
|
|
||
| struct RedemptionContext { | ||
| IERC20 collToken; | ||
| uint256 collBalanceBefore; | ||
| uint256 collRedeemed; | ||
| } | ||
|
|
||
| uint256 public immutable numBranches; | ||
| ICollateralRegistry public immutable collateralRegistry; | ||
| IBoldToken public immutable boldToken; | ||
| IAddressesRegistry[] public addresses; // only used off-chain, so we don't care about storage cost | ||
|
|
||
| constructor(ICollateralRegistry _collateralRegistry, IAddressesRegistry[] memory _addresses) { | ||
| require(_addresses.length == _collateralRegistry.totalCollaterals(), "Wrong number of registries"); | ||
| numBranches = _addresses.length; | ||
| collateralRegistry = _collateralRegistry; | ||
| boldToken = _collateralRegistry.boldToken(); | ||
|
|
||
| for (uint256 i = 0; i < _addresses.length; ++i) { | ||
| require(_collateralRegistry.getTroveManager(i) == _addresses[i].troveManager(), "TroveManager mismatch"); | ||
| addresses.push(_addresses[i]); | ||
| } | ||
|
|
||
| boldToken.approve(address(_collateralRegistry), type(uint256).max); | ||
| } | ||
|
|
||
| // Meant to be called off-chain | ||
| // Not a view because price fetching has side-effects | ||
| function simulateRedemption(uint256 _bold, uint256 _maxIterationsPerCollateral) | ||
| public | ||
| returns (SimulationContext[] memory branch, uint256 totalProportions) | ||
| { | ||
| branch = new SimulationContext[](numBranches); | ||
|
|
||
| // First priority: proportional to unbacked debt | ||
| for (uint256 i = 0; i < numBranches; ++i) { | ||
| branch[i].troveManager = address(addresses[i].troveManager()); | ||
| branch[i].sortedTroves = address(addresses[i].sortedTroves()); | ||
| (branch[i].proportion, branch[i].price, branch[i].redeemable) = | ||
| ITroveManager(branch[i].troveManager).getUnbackedPortionPriceAndRedeemability(); | ||
| if (branch[i].redeemable) totalProportions += branch[i].proportion; | ||
| } | ||
|
|
||
| // CS-BOLD-013: truncate redemption if it would exceed total unbacked debt | ||
| if (0 < totalProportions && totalProportions < _bold) _bold = totalProportions; | ||
|
|
||
| // Fallback: proportional to total debt | ||
| if (totalProportions == 0) { | ||
| for (uint256 i = 0; i < numBranches; ++i) { | ||
| branch[i].proportion = ITroveManager(branch[i].troveManager).getEntireBranchDebt(); | ||
| if (branch[i].redeemable) totalProportions += branch[i].proportion; | ||
| } | ||
| } | ||
|
|
||
| if (totalProportions == 0) return (branch, totalProportions); | ||
|
|
||
| for (uint256 i = 0; i < numBranches; ++i) { | ||
| if (!branch[i].redeemable) continue; | ||
|
|
||
| branch[i].attemptedBold = _bold * branch[i].proportion / totalProportions; | ||
| if (branch[i].attemptedBold == 0) continue; | ||
|
|
||
| uint256 lastZombieTroveId = ITroveManager(branch[i].troveManager).lastZombieTroveId(); | ||
| uint256 lastTroveId = ISortedTroves(branch[i].sortedTroves).getLast(); | ||
|
|
||
| (uint256 troveId, uint256 nextTroveId) = lastZombieTroveId != 0 | ||
| ? (lastZombieTroveId, lastTroveId) | ||
| : (lastTroveId, ISortedTroves(branch[i].sortedTroves).getPrev(lastTroveId)); | ||
|
|
||
| for ( | ||
| branch[i].iterations = 0; | ||
| branch[i].iterations < _maxIterationsPerCollateral || _maxIterationsPerCollateral == 0; | ||
| ++branch[i].iterations | ||
| ) { | ||
| if (branch[i].redeemedBold == branch[i].attemptedBold || troveId == 0) break; | ||
|
|
||
| LatestTroveData memory trove = ITroveManager(branch[i].troveManager).getLatestTroveData(troveId); | ||
| if (trove.entireColl * branch[i].price / trove.entireDebt >= _100pct) { | ||
| branch[i].redeemedBold += | ||
| Math.min(branch[i].attemptedBold - branch[i].redeemedBold, trove.entireDebt); | ||
| } | ||
|
|
||
| troveId = nextTroveId; | ||
| nextTroveId = ISortedTroves(branch[i].sortedTroves).getPrev(nextTroveId); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Meant to be called off-chain | ||
| // Not a view because price fetching has side-effects | ||
| function truncateRedemption(uint256 _bold, uint256 _maxIterationsPerCollateral) | ||
| external | ||
| returns (uint256 truncatedBold, uint256 feePct, Redeemed[] memory redeemed) | ||
| { | ||
| (SimulationContext[] memory branch, uint256 totalProportions) = | ||
| simulateRedemption(_bold, _maxIterationsPerCollateral); | ||
|
|
||
| if (totalProportions == 0) return (0, 0, redeemed); | ||
|
|
||
| truncatedBold = _bold; | ||
| for (uint256 i = 0; i < numBranches; ++i) { | ||
| if (branch[i].redeemable && branch[i].proportion > 0) { | ||
| uint256 extrapolatedBold = branch[i].redeemedBold * totalProportions / branch[i].proportion; | ||
| if (extrapolatedBold < truncatedBold) truncatedBold = extrapolatedBold; | ||
| } | ||
| } | ||
|
|
||
| feePct = collateralRegistry.getRedemptionRateForRedeemedAmount(truncatedBold); | ||
| redeemed = new Redeemed[](numBranches); | ||
|
|
||
| for (uint256 i = 0; i < numBranches; ++i) { | ||
| (uint256 redemptionPrice,) = addresses[i].priceFeed().fetchRedemptionPrice(); | ||
| redeemed[i].bold = truncatedBold * branch[i].proportion / totalProportions; | ||
| redeemed[i].coll = redeemed[i].bold * (DECIMAL_PRECISION - feePct) / redemptionPrice; | ||
| } | ||
| } | ||
|
|
||
| function redeemCollateral( | ||
| uint256 _bold, | ||
| uint256 _maxIterationsPerCollateral, | ||
| uint256 _maxFeePct, | ||
| uint256[] memory _minCollRedeemed | ||
| ) external { | ||
| require(_minCollRedeemed.length == numBranches, "Wrong _minCollRedeemed length"); | ||
|
|
||
| RedemptionContext[] memory branch = new RedemptionContext[](numBranches); | ||
|
|
||
| for (uint256 i = 0; i < numBranches; ++i) { | ||
| branch[i].collToken = collateralRegistry.getToken(i); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if it’s worth, but we may store the tokens, even immutably like we do it collateral registry, to save some gas.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered this too. The 3 calls to Or we could use storage, but unless we pass an access list with the TX, it would actually end up costing more, since cold slots take 2.1K to SLOAD. In the end, I didn't think the 1.35K potential savings warranted adding the extra immutable optimization. |
||
| branch[i].collBalanceBefore = branch[i].collToken.balanceOf(address(this)); | ||
| } | ||
|
|
||
| uint256 boldBalanceBefore = boldToken.balanceOf(address(this)); | ||
|
|
||
| boldToken.transferFrom(msg.sender, address(this), _bold); | ||
| collateralRegistry.redeemCollateral(_bold, _maxIterationsPerCollateral, _maxFeePct); | ||
|
|
||
| for (uint256 i = 0; i < numBranches; ++i) { | ||
| branch[i].collRedeemed = branch[i].collToken.balanceOf(address(this)) - branch[i].collBalanceBefore; | ||
|
danielattilasimon marked this conversation as resolved.
Outdated
|
||
| require(branch[i].collRedeemed >= _minCollRedeemed[i], "Insufficient collateral redeemed"); | ||
| if (branch[i].collRedeemed > 0) branch[i].collToken.safeTransfer(msg.sender, branch[i].collRedeemed); | ||
| } | ||
|
|
||
| uint256 boldRemaining = boldToken.balanceOf(address(this)) - boldBalanceBefore; | ||
| if (boldRemaining > 0) boldToken.transfer(msg.sender, boldRemaining); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m having hard time understanding this part. It seems that
truncatedBoldcan be overridden at any branch iteration. Why do we know the last one is the good one? I’ll get back to it later, but maybe a comment may help.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For each branch, we determine how much the total BOLD redeemed across all branches would have been if the redemption was proportional (we call this
extrapolatedBold). Then we take the smallest of those amounts.Normally, this is no different from the input
_boldamount. However, if any of the branches terminated because of the iteration limit, the redeemed amount on that branch will be less than proportional. Since such redemptions end up redeeming less BOLD than intended, they pay a higher fee than necessary (since the fee is fixed at the beginning of the TX, before the actually redeemed amounts are known).The point of
truncateRedemption()is to find the maximal amount of BOLD that can be redeemed within a given iteration limit without overpaying on the redemption fee.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment that hopefully explains this.