Hi everyone that’s still around! This is my first post on the forum, but felt it better than the discord to actually log ideas and keep the conversation going.
Here’s my ideas that have been thought out on discord and hope to hear others, that we can build into a SIP and take a vote.
Here’s the issue:
Ultimately I believe we need to come up with an idea to protect borrowers so that people feel safe pledging their Bitcoin as backing for issuing ZUSD/DLLR. We want the supply of DLLR to grow, not contract. Currently people are scared to deposit their bitcoin for fear it will be redeemed the moment there’s a correction in price even at 600% collateral ratios. This creates a significantly inefficient use of capital where you end up with $33M worth of Bitcoin in the system, yet only a 10th of it is used to issue ZUSD. It also prevents people from adding Bitcoin to the system because there is significant risk even if you’re over collateralized by 600%.
My possible solutions:
- Build a redemption buffer owned by SOV stakers. This idea offloads the risk to stakers rather than the collateral providers. Stakers currently make a fee percentage of every ZUSD issued against BTC. I’m thinking split the fee so stakers receive half of the fee in the newly issued ZUSD and the remaining half in BTC from the borrower that is deposited into the redemption buffer that is used as the first line of redemption. If this redemption buffer ever grows large enough that it no longer makes sense, stakers can vote to distribute some of it back to stakers. Or we could have it automatically happen if it ever reaches some arbitrary number like 30% of all ZUSD. When someone redeems ZUSD against the RBTC redemption buffer that ZUSD will be distributed to stakers.
- I believe we want to disincentivize redemptions especially from arbitragers using Zero against the RBTC/DLLR AMM. The reason being as Zero currently stands you cannot possibly get a better price for purchasing RBTC on rootstock than through Zero. This is not what you want for a product like this. It essentially means collateral providers are agreeing to sell their BTC at literally mid market prices (no market maker in the world would do this). If we had the staker owned redemption buffer then stakers would be agreeing to that. It’s just a horrible idea all around. You absolutely want collateral providers to have the ability to repay their loans at mid market price, but not other parties. So my best idea to solve this is to model like Tether and have a minimum redemption size of $100K ZUSD and a minimum set fee of $1K per redemption.
I think it’s important that earlier discussions on this be looked at from a larger time frame. US dollar only has high demand in major market corrections - after a over valuation of assets. When the decisions were being made on Zero redemption and origination it was at the end of a major Bitcoin/equity market correction and demand for US Dollars were falling. There were concerns about a depeg of ZUSD if redemption fees were too high. During that same period USDC and USDT also lost their pegs. That is not just a coincidence with the bottom of the market in early 2023 - that’s a very expected move as US dollars are sold to repurchase assets at suppressed prices.
If we can limit redemptions through Zero it also opens an opportunity for actual markets to emerge trading DLLR for other stable coins at low spreads. I’m currently working on a rootstock contract to make markets for DOC/DLLR and RBTC/DLLR so that DLLR holders have more utility at lower spreads and no fees. I’m positive if we can increase the friction for Zero redemption, then we will see a wider spread use of DLLR as the supply grows.
Please share your ideas and any comments on my own (which were helped by others in discord discussions). I really hope we can make some traction on improving Zero. Thank you all!
—– Update: ——
I think idea #2 or anything surrounding modifying the redemption rates seems to have a lot of contention. I’m all for focusing on adding a Redemption Buffer and then see how it goes and come back to redemption fee modifications at a later time if we still feel it’s necessary.
Here’s the PR code modifications in progress for discussion:
— System-Level Changes —
A) New “RedemptionBuffer” subsystem (major new feature)
New in the updated code:
-
A new RBTC buffer pool (
RedemptionBuffer) is introduced. -
Borrowing/minting ZUSD now funds this buffer via a new RBTC fee charged in
BorrowerOperations. -
Redemptions now run buffer-first:
-
Redeemer can receive RBTC directly from
RedemptionBufferat oracle price (swap-style), -
and only then redeem remaining ZUSD from troves (classic redemption).
-
This feature does not exist at all in your old versions.
B) Redemption semantics changed: buffer portion is a swap, not a debt burn
Old redemption:
-
Entire redeemed ZUSD is burned
-
System debt (
ActivePool) is decreased by the same amount
New redemption:
-
Trove-sourced portion behaves like old (burn + decrease system debt)
-
Buffer-sourced portion:
-
ZUSD is transferred to
FeeDistributor(not burned) -
ActivePool debt is NOT decreased for that portion
-
RBTC is paid out from the buffer
-
This is a meaningful change to the economics/settlement behavior.
C) FeeDistributor now accepts RBTC from RedemptionBuffer + can be triggered by it
Old:
-
FeeDistributor.distributeFees()callable only by BorrowerOps or TroveManager -
FeeDistributor.receive()accepts RBTC only fromActivePool
New:
-
FeeDistributor:-
accepts RBTC from ActivePool OR RedemptionBuffer
-
allows RedemptionBuffer to call
distributeFees() -
adds buffer-address config
-
D) Reentrancy hardening added (TroveManagerBase + shared storage + BorrowerOperations)
Old:
-
No shared
nonReentrant/requireNotEnteredat TroveManagerBase layer -
No BorrowerOperations reentrancy guard state
New:
-
Adds reentrancy guard framework in
TroveManagerBase -
Adds shared
_reentrancyStatusin TroveManager storage (so TroveManager + delegatecall module share the same lock) -
Adds BorrowerOperations reentrancy state as well
E) Explicit liquidation accounting fix (batch recovery mode)
Old bug (in batch recovery mode liquidation):
-
vars.entireSystemCollupdated by subtracting onlycollToSendToSP -
did not subtract
collSurplus, which can skew TCR/recovery-mode transitions
New fix:
-
subtracts both:
-
collToSendToSP -
collSurplus
-
Contract-by-contract diff
1) RedemptionBuffer (NEW CONTRACT)
Added entirely in the updated code.
Key capabilities (new):
-
Receives RBTC (tracked via
totalBufferedColl) -
deposit()(only BorrowerOperations) -
withdrawForRedemption(to, amount)(only TroveManager) -
distributeToStakers(amount)(owner-only; forwards to FeeDistributor + triggers distribution) -
syncBalance()(owner-only accounting reconciliation) -
getBalance()view -
receive()updates internal accounting
Nothing in the old code corresponds to this contract.
2) BorrowerOperations (old → new)
Added: RedemptionBuffer configuration & fee quoting
New functions/events you did not have in old:
-
setRedemptionBufferAddress(address) -
setRedemptionBufferRate(uint256) -
getRedemptionBufferAddress() -
getRedemptionBufferRate() -
getRedemptionBufferFeeRBTC(uint256 _ZUSDAmount)(usesfetchPrice()) -
getRedemptionBufferFeeRBTCWithPrice(uint256 _ZUSDAmount, uint256 _price)(view) -
Events:
-
RedemptionBufferAddressChanged -
RedemptionBufferRateChanged
-
Changed behavior: openTrove() now skims RBTC into the buffer
Old _openTrove:
-
Trove collateral =
msg.value -
No extra fee to any buffer
New _openTrove:
-
Computes a buffer fee in RBTC based on
_ZUSDAmountandprice -
Requires
msg.value > bufferFee -
Trove collateral used for ICR/NICR + stored on trove =
msg.value - bufferFee -
bufferFeeis deposited intoRedemptionBuffer
Impact: opening a trove now costs extra RBTC beyond the collateral you want in the trove.
Changed behavior: debt increases also pay the buffer fee
Old _adjustSenderTrove:
-
Coll change computed from
msg.valuevs_collWithdrawal -
If debt increase, msg.value could be
0(unless also topping up collateral)
New _adjustSenderTrove (when _isDebtIncrease == true):
-
Computes
bufferFeefrom minted_ZUSDChange(and price) -
Requires
msg.value >= bufferFee -
Treats
collTopUp = msg.value - bufferFeeas the actual collateral increase -
Deposits
bufferFeetoRedemptionBuffer
Very important compatibility change:
-
In old code, you could do “withdraw collateral + increase debt” with
msg.value == 0. -
With a mandatory fee in
msg.value, the new code must separate fee vs actual coll top-up so that “withdraw coll + increase debt” remains possible. (Your new code does this by using “collTopUp” rather than raw msg.value as the collateral-add component.)
Added: rounding correctness for RBTC fee
New:
_ceilDiv(a,b)and fee conversion uses ceil-division so the buffer fee is not rounded down when converting ZUSD-value fee → RBTC.
Old:
- No such fee conversion existed.
Added: BorrowerOperations reentrancy state
New:
- BorrowerOperations now carries reentrancy state (not present in the old storage) and uses it to guard externally callable flows.
3) BorrowerOperationsStorage (old → new)
Old storage fields end at:
-
IMassetManager public massetManager; -
IFeeDistributor public feeDistributor;
New storage adds:
-
IRedemptionBuffer internal redemptionBuffer; -
uint256 internal redemptionBufferRate; -
uint256 internal _reentrancyStatus;
Migration note: if this is a proxy-based upgrade, this is a storage layout change (safe only if appended and ordering is preserved). If you redeploy, it’s irrelevant.
4) TroveManagerRedeemOps (old → new)
Changed behavior: adds a buffer-first redemption stage
Old redemption flow:
-
Find redeemable troves (lowest ICR above MCR)
-
Redeem against troves only
-
Compute fee on
totalETHDrawn -
Pay fee from ActivePool → FeeDistributor
-
Burn ZUSD from redeemer
-
Decrease ActivePool ZUSD debt
-
Send net ETH to redeemer
New redemption flow (high-level):
-
Swap from RedemptionBuffer first (at oracle price):
-
RBTC out of buffer
-
ZUSD goes to FeeDistributor (transferFrom)
-
-
Redeem remaining from troves (same concept as old)
-
Compute base rate and redemption fees using (troves + buffer) drawn ETH
-
Pay:
-
trove-sourced fee from ActivePool
-
buffer-sourced fee from RedemptionBuffer
-
-
Send:
-
trove-sourced net RBTC from ActivePool
-
buffer-sourced net RBTC from RedemptionBuffer
-
Major semantic change: allowance requirement appears (buffer portion)
Old:
- No ZUSD allowance needed for
redeemCollateralsince TroveManager burns directly from the user via privilegedburn(...).
New:
-
For the buffer swap portion, the module uses
ZUSD.transferFrom(redeemer, FeeDistributor, amount) -
That means the redeemer must have approved the TroveManager for ZUSD (at least for the swapped portion).
-
Your new code also adds an explicit revert message when allowance is insufficient.
Refactor: parameter packing and helper extraction
Old:
_redeemCollateral(...)took many parameters directly and contained the full trove walk inline.
New:
-
Redemption parameters are packed into a struct (e.g.
RedeemParams) -
Trove-walk extracted into helper (e.g.
_redeemFromTroves) -
Buffer swap extracted into helper (e.g.
_swapFromBuffer)
This is mostly a maintainability/stack-depth refactor, but it also supports the buffer insertion cleanly.
5) TroveManager (old → new)
Added: RedemptionBuffer wiring
Old:
-
No buffer address stored
-
No setter
New:
-
Adds
setRedemptionBufferAddress(address)(owner-only; checkContract) -
Stores
IRedemptionBufferin TroveManager storage for use by delegatecall module
Added/changed: reentrancy protection at TroveManager layer
Old:
- No
nonReentrant/requireNotEnteredguard pattern shared with the module.
New:
-
TroveManagerBase introduces a shared reentrancy guard
-
TroveManager + TroveManagerRedeemOps share the same
_reentrancyStatusstorage slot -
Certain external entrypoints (esp. redemption) are protected from reentrancy.
Bug fix: Batch liquidation in Recovery Mode (collSurplus accounting)
Old _getTotalFromBatchLiquidate_RecoveryMode:
vars.entireSystemColl = vars.entireSystemColl.sub(singleLiquidation.collToSendToSP);
New:
vars.entireSystemColl = vars.entireSystemColl
.sub(singleLiquidation.collToSendToSP)
.sub(singleLiquidation.collSurplus);
Effect: system collateral tracking is correct when capped liquidations create surplus.
6) TroveManagerStorage (old → new)
Old has:
-
address public troveManagerRedeemOps; -
core addresses/pools
-
trove data and reward sums
-
no reentrancy state
-
no buffer pointer
New adds:
-
IRedemptionBuffer internal redemptionBuffer; -
uint256 internal _reentrancyStatus;(shared lock used across TroveManager + delegatecall module)
7) TroveManagerBase (old → new)
Added: shared reentrancy guard framework
Old:
- No reentrancy guard
New adds:
-
_NOT_ENTERED,_ENTERED -
nonReentrantmodifier -
requireNotEnteredmodifier -
initialization of
_reentrancyStatus
8) FeeDistributor (old → new)
Added: RedemptionBuffer address + authorization
Old:
-
distributeFees()allowed callers:-
BorrowerOperations
-
TroveManager
-
-
receive()allowed sender:- ActivePool only
New:
-
Adds
setRedemptionBufferAddress(address)(owner-only) -
Adds getter for buffer address
-
distributeFees()now also allowsmsg.sender == redemptionBuffer -
receive()now also allows RBTC fromredemptionBuffer
Why it matters: buffer-funded redemption fees and distributions would revert against the old FeeDistributor.
9) FeeDistributorStorage (old → new)
Old:
- no buffer address
New adds:
_redemptionBufferAddress(stored to authorize caller + RBTC sender)
Practical integration impacts (things frontends/bots must change)
- Opening a trove / increasing debt now requires extra RBTC
-
The transaction must include enough RBTC to cover:
-
desired trove collateral
-
+ redemption buffer fee
-
-
You can quote this fee via the new
BorrowerOperations.getRedemptionBufferFeeRBTC(...).
- Redemptions may require ZUSD approval
- If the redemption uses the buffer portion, the module uses
transferFrom, so the redeemer needs to approve TroveManager for ZUSD (at least for the buffer-swapped amount).
- Deployment/config order matters
You must set:
-
BorrowerOperations.setRedemptionBufferAddress(...) -
BorrowerOperations.setRedemptionBufferRate(...) -
TroveManager.setRedemptionBufferAddress(...) -
FeeDistributor.setRedemptionBufferAddress(...)
…and wireRedemptionBuffer.setAddresses(...)appropriately.