Redemptions
It is critical for dUSD to maintain a stable peg to the USD. One issue to protect against is when dUSD's price trading at less than 100 cents on the dollar specifically on the orderbook. This can happen if the demand for ETH is so much higher than dUSD that dUSD holders are willing to sell at a discount. The primary way Ditto handles this is through redemptions.
Ditto redemptions follow the idea of Liquity's redemptions in spirit. Redemptions are the ability to redeem LUSD for ETH at face value (i.e. 1 LUSD for $1 of ETH), which happens immediately in Liquity because the Troves are always sorted from least to greatest collateral ratio. But keeping all Troves (CDPs) sorted can be costly since each transaction that affects any trove will need to modify the sorted list, which is why Liquity has a hint system to make it more gas efficient to know where to place changed Troves in the sorted list.
Ditto's shortRecords
(similar to a Trove/CDP) are not sorted, rather they are organized by each address. This is because, unlike in other protocols, each user can have multiple shortRecord
positions at once. So, a different approach needs to be created to implement a redemption-like system where users are able to redeem the stable asset for ETH while only doing it against the lowest CR shortRecords
.
Because it's expensive to sort all shortRecords
or to prove that a given shortRecord
is the lowest CR, Ditto introduces a two-step process for redemption that includes a dispute period. Instead of redeeming in one transaction, the redemption has to be claimed in a separate transaction if the dispute time period passes without issue. To dissuade users from redeeming against really high CR shortRecords
, or just shortRecords
out of order, a penalty gets applied to incorrect proposers.
In a sense, proposed redemptions are optimistic. They apply changes to the existing shortRecords
but can be reverted/restored if a dispute happens in between the time that a proposer might claim the redemption.
Redemptions in Ditto has four main functions:
proposeRedemption
: a user can propose an amount of dUSD (the stable asset) to redeem ETH against (with a fee)disputeRedemption
: a user can get a fee from the proposer for disputing an incorrect proposalclaimRedemption
: a user can get the ETH that they proposed correctly after the claim time period has passedclaimRemainingCollateral
: an extra function, where the shorter that was correctly redeemed on can get back any remaining collateral ifclaimRedemption
has not been called
Proposing a Redemption
The first action in a redemption is called a proposal since redemptions don't happen immediately as one transaction. The system doesn't know if the redemption is valid until a specific dispute period passes. A redeemer (referred to as proposer in this phase) provides a list of shortRecords
to redeem against.
A key concept is that the collateral
and ercDebt
of the proposed shortRecord
is decreased at the moment of proposal, even if the proposal is incorrect. The decreased amount is stored in the proposer's slate, which is saved in the proposer's AssetUSer.SSTORE2Pointer
(More on that below in the Technical section). If properly disputed, the collateral
and ercDebt
of the proposed shortRecord's
will be updated accordingly. In most cases, shortRecords
that are proposed are "fully proposed", meaning that the ercDebt
becomes zero. However, the shortRecord
is not closed because deleteShortRecord()
is not called on proposal. A shortRecord
is closed only after the dispute period ends and the redemption has been claimed.
Partial redemptions can occur too, but are limited to the first and last proposal in the slate. The last proposal can be partial because redemptionAmount
can be less than the total ercDebt
of proposed shorts. The first proposal can also be partial, which is best illustrated by this example:
- There is a
shortRecord
withercDebt
of $5000 - Proposer A partially proposes this
shortRecord
. The remainingercDebt
is $2500. - Proposer B creates their own slate, with this
shortRecord
as their first proposal. ThisshortRecord
is now "fully proposed withercDebt
of 0 - Proposer A is correctly disputed and this
shortRecord
is removed from their slate. TheercDebt
is now $2500 - Proposer B's first proposer is now technically "partially proposed" with
ercDebt
of $2500
Constraints
- There can only be one proposal at a time per user
- Proposers need to escrow their dUSD: enough
ercEscrowed
to cover theredemptionAmount
- Proposers need a minimum amount:
redemptionAmount
needs to be at leastminShortErc
- The list of
shortRecords
needs to have an debt amount of at leastminShortErc
- Any partially redeemed
shortRecords
must have a debt amount of at leastminShortErc
after redemption - Proposers needs to have enough
ethEscrowed
to cover theredemptionFee
shortRecords
that are invalid to be proposed:- if it's not sorted
- if it's greater than the max allowable CR
- if it's already closed
- if it's already redeemed on (
ercDebt
is 0) - if it's the user's ShortRecord
Fees
The only fee for proposals are redemptionFees
. This is another concept borrowed from Liquity where fees are implemented to throttle the rate of redemptions. redemptionFees
increases based on the amount of debt being proposed and decreases based on the time elapsed between proposals.
A proposer can input their preferred maxRedemptionFee
. The proposal reverts if redemptionFee
exceeds the user's maxRedemptionFee
, protecting them from unexpected costs.
Time to Dispute
timeToDispute
allows users to dispute any invalid proposals for a reward. It is based on the shortRecord
with the highest CR in the proposal slates. The higher the CR, the longer the timeToDispute
. The longer wait increases the chance for disputers to find any invalid proposals. Disputes are not permitted once timeToDispute
ends.
Disputing a Redemption
A disputer can provide a single shortRecord
with a CR lower than a shortRecord
in the proposal. If valid, it will remove all shortRecord
s that were invalid (not correctly sorted), and applies a penalty based on difference in incorrect debt. The disputer is rewarded by receiving this penalty, which effectively is paid by the proposer.
Constraints
- Cannot dispute yourself
- Cannot dispute if the redeemer has no proposal
- Cannot dispute with any ShortRecord already in the proposal itself
- Can only dispute with another ShortRecord that
- has a CR lower than the target ShortRecord
- has a
updatedAt
time earlier than the time that the proposal was created (there is also a buffer in case the proposal takes time to transaction and to prevent front-running a proposal with a newly created ShortRecord that has a CR lower than a proposal that the proposer didn't know about)
Claiming a Redemption
After a redeemer's timeToDispute
has elapsed, they can claim the ETH earned from the redemption. After claiming the ETH, the redeemer's AssetUSer.SSTORE2Pointer
is deleted, which allows the redeemer to propose new shortRecords
again.
When a redeemer calls claimRedemption
, all of the valid shortRecords
in the slate are closed out and their remaining collateral is given back to the shorter. Closing out the shortRecord
allows the shorter to re-use that shortRecord
in the future.
NOTE:
shortRecords
that have been already claimed by shorters viacliamRemaingCollateral
will skipped in this function call.
Constraints
- Cannot claim if redeemer has no active proposal
- Cannot claim if
timeToDispute
has not elapsed
Claiming Remaining Collateral
claimRemainingCollateral
is an extra function that allows shorters who have been redeemed against to claim their remaining collateral and close out the shortRecord
that was redeemed in the case where a redeemer fails to call claimRedemption
. This function requires the shorter to pass in the redeemer's address and the correct index of the shortRecord
in the redeemer's slate. If verified, the shorter's shortRecord
is closed and the remaining collateral is given back to the shorter.
NOTE: If a redeemer has already called
claimRedemption
, theirAssetUSer.SSTORE2Pointer
is deleted. This prevents verification and thus prevents a shorter from claiming their remaining collateral multiple times.
Technical
Proposal Storage
SSTORE2
is used (specifically solmate
) to save proposals when proposeRedemption
is run in a gas-efficient way rather than the usual way of saving data. Unlike the rest of the protocol, this implementation is uniquely suited to take advantage of it because a proposal is immutable (and thus doesn't need to be changed). Given each user can only propose once, Ditto can save the resulting address
of saving all the proposal data in SSTORE2
, making it cheaper to both read/write rather than saving each ShortRecord in storage.
See 0xsequence/sstore2 for more details on gas savings, taking advantage of different scalings of gas cost for contract creation (using contracts themselves as storage). In particular, once the system needs to read/write 32 bytes, it's more efficient to use SSTORE2
than SSTORE.
Since the interface to write data is SSTORE.write(bytes memory data)
, proposeRedemption creates the bytes version of each valid ShortRecord by concating all the data into bytes
. To get back the ShortRecord[]
, Ditto can read each bytes in a loop since each property is a fixed bytes. Some light use of assembly is used to make writing/reading more gas efficient.
Rather than updating the SSTORE value during proposal and dispute, the system saves and update the last index of the array that SSTORE2 points to. This index is saved in AssetUser.slateLength
. Upon proposal, the AssetUser.slateLength
is set to the length of the proposal slate, which has datatype of MTypes.ProposalInput[]
. If successfully disputed, then the AssetUser.slateLength
is decreased (Or in the case where the entire thing is incorrect, the entire AssetUSer.SSTORE2Pointer
is deleted). This is an additional gas saving technique.