Proposal/Ideas to Increase Zero Redemption Friction

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:

  1. 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.
  2. 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 RedemptionBuffer at 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 from ActivePool

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/requireNotEntered at TroveManagerBase layer

  • No BorrowerOperations reentrancy guard state

New:

  • Adds reentrancy guard framework in TroveManagerBase

  • Adds shared _reentrancyStatus in 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.entireSystemColl updated by subtracting only collToSendToSP

  • 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) (uses fetchPrice())

  • 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 _ZUSDAmount and price

  • Requires msg.value > bufferFee

  • Trove collateral used for ICR/NICR + stored on trove = msg.value - bufferFee

  • bufferFee is deposited into RedemptionBuffer

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.value vs _collWithdrawal

  • If debt increase, msg.value could be 0 (unless also topping up collateral)

New _adjustSenderTrove (when _isDebtIncrease == true):

  • Computes bufferFee from minted _ZUSDChange (and price)

  • Requires msg.value >= bufferFee

  • Treats collTopUp = msg.value - bufferFee as the actual collateral increase

  • Deposits bufferFee to RedemptionBuffer

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:

  1. Find redeemable troves (lowest ICR above MCR)

  2. Redeem against troves only

  3. Compute fee on totalETHDrawn

  4. Pay fee from ActivePool → FeeDistributor

  5. Burn ZUSD from redeemer

  6. Decrease ActivePool ZUSD debt

  7. Send net ETH to redeemer

New redemption flow (high-level):

  1. Swap from RedemptionBuffer first (at oracle price):

    • RBTC out of buffer

    • ZUSD goes to FeeDistributor (transferFrom)

  2. Redeem remaining from troves (same concept as old)

  3. Compute base rate and redemption fees using (troves + buffer) drawn ETH

  4. Pay:

    • trove-sourced fee from ActivePool

    • buffer-sourced fee from RedemptionBuffer

  5. 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 redeemCollateral since TroveManager burns directly from the user via privileged burn(...).

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 IRedemptionBuffer in TroveManager storage for use by delegatecall module

Added/changed: reentrancy protection at TroveManager layer

Old:

  • No nonReentrant / requireNotEntered guard pattern shared with the module.

New:

  • TroveManagerBase introduces a shared reentrancy guard

  • TroveManager + TroveManagerRedeemOps share the same _reentrancyStatus storage 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

  • nonReentrant modifier

  • requireNotEntered modifier

  • 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 allows msg.sender == redemptionBuffer

  • receive() now also allows RBTC from redemptionBuffer

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)

  1. 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(...).

  1. 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).
  1. Deployment/config order matters
    You must set:
  • BorrowerOperations.setRedemptionBufferAddress(...)

  • BorrowerOperations.setRedemptionBufferRate(...)

  • TroveManager.setRedemptionBufferAddress(...)

  • FeeDistributor.setRedemptionBufferAddress(...)
    …and wire RedemptionBuffer.setAddresses(...) appropriately.

2 Likes

Just wanted to say a huge thank You for the detailed post - the rBTC redemption buffer owned by stakers is genuinely one of the most elegant ideas I’ve seen for Zero in a long time (at least since @one_digit post about Resurecting Zero). Tether-style minimums are also interesting option to consider. Really appreciate You writing it up properly here.

Quick update with today’s numbers so we’re all on the same page:

  • ZUSD supply: ~3.22 M

  • Collateral: ~348 rBTC → TCR ≈ 1,000 %

  • Current redemption fee: already 2.72 % (and it climbs quickly with every new redemption because of the dynamic baseRate)

Those numbers come from this analytics page.

As You see redemption fees are already very high. These redemptions are no longer small arbitrage plays, but deliberate moves that are fine paying 2–3 % to force-out rBTC at oracle price.

That makes me think Your two proposals are still very intersting, but we might get the fastest relief by pairing them with a tiny extra layer that removes the fear for borrowers right now - for example a simple 24-hour redemption delay (still lets people exit, but stops instant attacks and panic cascades).

Once borrowing psychology flips and supply starts growing again, Your buffer + minimum-size ideas could become the perfect second line of defence.

Thanks again for sparking this - this is exactly the discussion we needed.

1 Like

You’re welcome!!! Thank YOU for the well thought out response!! :pink_heart: Let’s drop idea #2 or anything related to redemption fees and just focus on building a Redemption Buffer owned and controlled by stakers. I’ve updated the main post to share some possible code changes to the Zero contracts. Please keep the ideas flowing! I don’t want this to fall off like all of @one_digit ‘s efforts!

On Discord You have written

Some would be against. Now the whole 5% goes to stakers, if Your proposal would be accepted, it would be reduced to half. And rest would be distributed only when there is a redemption happening. On the other hand - more LoC’s - more fees for stakers. So if ZERO becomes attractive again, they will earn more anyway.

Maybe, but as I have the code now, stakers can vote to distribute the RBTC in the RedemptionBuffer contract. So in effect that RBTC in there is owned by stakers until redeemed, but we’re just going to store it for the long term to protect borrowers and hopefully incentivize more utilization of the collateral. And like you say, if ZUSD grows, that will lead to likely more fees overall even if the rate is cut in half!

Since You’ve mentioned that You now have the code ready, please keep in mind that, to ensure full transparency and safety, any modification to the smart-contract logic should:

  • be very clearly and extensively commented in the code,

  • include comprehensive unit and integration tests covering the new behavior,

  • go through a formal SIP so the community can discuss the trade-offs,

  • and undergo at least a thorough second review (ideally an external audit) before deployment.

This way, reviewers, testers, and users can quickly understand what changed and why, and we minimize the risk of unintended side effects.

Yeah it’s not that ready! lol I’ve been looking at the unit tests. Likely going to need some help with those, they are extensive! And so many of them are built assuming there’s no change in the collateral upon opening a new trove. I uploaded a draft PR so that people could see the proposed changes in actual code and help design it from there.

But still coments would be very helpful, to understand what was changed and why.

1 Like