EIP-1: Improvements to ESD Coupon Redemption

EIP-1: Improvements to ESD Coupon Redemption

Authors: Will Price + Robert Leshner

Background

The Empty Set Dollar protocol relies on Coupons to reduce the supply of ESD, in order to increase the price of ESD during a market contraction.

Debt is a tracking variable used to determine the incentive to swap ESD for Coupons; the larger the Debt relative to Supply, the larger the Coupon Premium. This incentive carries a cost; if Coupons are not exercised within 90 epochs, they expire worthless.

Coupons are the liability the protocol owes to those who take on the financial risk of expiry in order to contract the ESD supply to maintain the peg. The nomenclature of Debt and Coupons fail to fully capture the ESD primitives that they are meant to describe.

Collectively, this system is used to reward risk-takers for returning ESD to its $1.00 target price.

As currently implemented, after the target price is restored, existing Debt (the incentive to purchase Coupons) remains–leading to flawed incentives, unnecessary dilution to honest stakeholders, and market inefficiencies.

Situation:

As we saw during the 1st contraction cycle, it is possible for the following situation to occur:

  • TWAP above $1.00 and positive; the target price has been restored
  • Debt greater than zero (Coupons still available for purchase)

Although users are likely to purchase Coupons when TWAP is above $1, this is by no means guaranteed. As the level of Debt drops, the Coupon Premium may not be high enough to justify the perceived risk of coupon expiry.

Further, the current first-come-first-served Coupon redemption model means that there is no guarantee that users will be able to redeem immediately, even if the TWAP is above $1.

As we saw during the last contraction, this situation opens a window for a bot to profit from the couponing process without incurring any risk by doing the following:

  • Flash loan ESD from Uniswap
  • Buy Coupons
  • Advance the epoch
  • Redeem the Coupons
  • Pay back flash loan
  • Sell ESD profits for USDC on the open market

This method allows the bot to profit from the Coupon process without incurring risk and without a vested interest in the ESD system, and increases the expiration risk to honest Coupon holders. If governance does not act, this will happen again.

Proposed Solution:

Part I: Wipe debt before a positive rebase

Successfully redeemed Coupons dilute the other stakeholders in the system, so we should not be allowing more Coupons to be purchased than the minimum required to return to peg. As Debt is only used to measure the required Coupon Premium, once the price returns to $1, it should logically be reset to zero.

We propose that this step be during the advance() call to the Regulator. Pseudocode:

if (price.greaterThan(Decimal.one())) {
     setDebtToZero(); // ADDED THIS LINE
     growSupply(price);
     return;
     }

Part II: Require the advance() function to be called by EOA (non-smart contract)

If only Part I is implemented, the unproductive bot activity mentioned above is still possible.

We propose requiring that epoch advancement only be allowed by EOA to prevent this possibility. After much consideration, we believe this solution is better than an alternative - that is, requiring Coupons be more than 2 epochs old before they are eligible for redemption.

function advance() external incentivized {
        require(tx.sender==msg.origin, "advance must be called by EOA"); //ADDED THIS LINE
        Bonding.step();
        Regulator.step();
        Market.step();

        emit Advance(epoch(), block.number, block.timestamp);
    }

Other benefits:

  • A more robust CouponClipper market
  • More time to analyze and implement other changes to the Coupon & incentive mechanisms

Thanks to Jon Itzler and Will Sheehan for reviewing drafts of this proposal.

7 Likes

I think doing both “requiring coupons to be 2 epochs old for redemption” AND “EOA only” might be the best path.

if upcoming TWAP was >$1, one could wait for the first block where you could advance the epoch and then:

  1. buy 3M coupons (lets assume 15% premium)
  2. advance epoch
  3. redeem coupons.

the profit from this set of transactions would be worth $450k. There would be 2 feasible avenues to complete this set of transactions in a single block.

  1. 10002 gwei tx #1
  2. 10001 gwei tx #2
  3. 10000 gwei tx #3

OR

contract with a miner that is already offering private transaction ordering and attempt to mine the key block with only these 3 transactions in it. For a $450,000 payoff for each opportunity, i imagine this work may be worth a miner’s time.

With “requiring coupons to be 2 epochs old for redemption”, we could stop this attack.

Keeping the “EOAs” only rule is still great because it eliminates unknown unknown flash loan related attacks.

4 Likes

Great point! I would support the 2 epoch requirement in conjunction with the above. You’re right to point out the benefits of protecting against unknown unknowns via enforcing EOA.

1 Like

Great first proposal, thanks guys.

Part II makes sense and I agree with Scott’s suggestion to require both for the reasons already indicated.

Although I like the idea behind Part I, I think it could be exploited in a grief attack. We have seen significant contraction in liquidity during the debt cycles and most potential coupon buyers holding out for higher premiums. I can imagine a scenario where this results in the price staying below the peg for substantial time with only brief spikes slightly above the peg:

  1. The price falls below the peg, debt begins accruing for several epochs with no coupon bids because premiums are low.
  2. Liquidity dries up.
  3. Before premiums become attractive enough, an attacker pushes the price up just enough to clear the debt for one epoch.
  4. Price immediately falls below the peg again, but coupon premiums reset because debt was cleared.
  5. Cycle repeats, supply never shrinks, and price stagnates below the peg.

Hey Dave,

But wouldn’t players catch on to that cycle, eventually buying some coupons and that be enough to push it back up? Premiums might no longer be what they can be now, but it becomes a race to get the first batch of coupons and buying sub-dollar ESD to buy them with?

Maybe I’m out to lunch here, late, long week etc. :slight_smile:

Great proposal, @willprice and Robert. This makes a lot of sense. Reducing risk for honest Coupon holders and reducing unnecessary dilution for honest stakeholders are worthy goals and this proposal does a good job moving in the right direction.

I support @scott_lew_is’s suggestion of adding the rule around 2-epoch coupon age for redemption in addition to the EOA-only rule. Just to check though: are there any downsides to cutting off non-EOA accounts? I’m all for protecting against unknown unknowns, but am curious whether there are potential benefits we’d also be losing. I don’t think there are, but figured I’d surface the question for smarter people to consider.

To @dmaddock’s point, I agree we should keep an eye out for potential griefing attacks, but I’m not too worried about this one. If that should happen once or twice, there will be more selling pressure that materializes close to $1.00 and prevents the TWAP from climbing above the peg.

1 Like

Upfront full disclosure: I run an advance() bot, will run a CouponClipper bot, and would even compete against the “flash-debt-clearing bot” if that opportunity arises. (But I 100% agree that the flash-debt-clearing opportunity is not an efficient property of the system, and could even lead to its demise. I think it should be fixed.)

I agree with Part I.

I strongly disagree with Part II.

Restricting advance() to an EOA prevents flash loans, but does not prevent whales from doing exactly the same attack (with only marginally more risk) using 3 transactions.

It doesn’t solve the problem. It just locks in the strategy for whales-only. They’d just send three txs: one that buys coupons, another that advances the epoch, and another that redeems the coupons. Put very high gas prices (one wei apart, so they land in the right order) on all of them (which they can afford because they are a whale) and you have the same effect.

My recommendation: Don’t design against flash loans. Design against whales. If you do that, you get flash-loan resistance for free.

As a side note:
The require(msg.sender==tx.origin) will certainly make things harder and more costly for bots. It immediately rules out a lot of things we do for reducing costs – we can’t do “fail fast checks” (where we end execution early if advance has already been called by someone else, or if it is too early to call it), we can’t use gas tokens for efficiency, we can’t batch the advance() with other actions (like CouponClipper redeems or regular coupon redeems), etc. So it does make bot activity more expensive and more risky, but it’s not clear to me that bots are the enemy here.

The enemy is the fact that debt can be bought in the same block that it can be redeemed. I think requiring that coupons be at least 2 epochs old is a better solution. It eliminates the “no skin in the game” problem, and resolves the issue for both whales and flash loans.

Tl:Dr:

  • Delete debt first :white_check_mark:
  • Require coupons to be 2 epochs old :white_check_mark:
  • EOA only on advance() :x::x::x:

If you require that coupons be 2 epochs old, then there is no reason to do EOA-only (that just hurts bots and gets you nothing in return).

If you do EOA-only but don’t require coupons to be 2 epochs old, then whales will just do in 3 txs what the flash-bot did last time with one tx.

8 Likes

Thanks for bringing your unique perspective here @onewayfunction!

I think requiring that coupons be at least 2 epochs old is a better solution.

What is the complexity of the upgrade to require coupons to be 2 epochs old?

The require(msg.sender==tx.origin) will certainly make things harder and more costly for bots. It immediately rules out a lot of things we do for reducing costs – we can’t do “fail fast checks” (where we end execution early if advance has already been called by someone else, or if it is too early to call it), we can’t use gas tokens for efficiency, we can’t batch the advance() with other actions (like CouponClipper redeems or regular coupon redeems), etc. So it does make bot activity more expensive and more risky

I would not be upset if the proposal ends up passing without this element. That said, I want to play devil’s advocate here.

It seems to me that any benefit afforded by the use of gasTokens and fail-fast checks is likely to be competed away.

IMO the benefit of protecting against unknown unknowns slightly outweighs the friction of requiring an advance+redeem bot to issue multiple transactions.

1 Like

What is the complexity of the upgrade to require coupons to be 2 epochs old?

I would double-check this, but I think it would be a simple require statement in the redeemCoupons function.

The function is currently:

function redeemCoupons(uint256 epoch, uint256 couponAmount) external {
        decrementBalanceOfCoupons(msg.sender, epoch, couponAmount, "Market: Insufficient coupon balance");
        redeemToAccount(msg.sender, couponAmount);

        emit CouponRedemption(msg.sender, epoch, couponAmount);
    }

We’d change it to:

function redeemCoupons(uint256 epoch, uint256 couponAmount) external {
        require(epoch().sub(epoch) >= 2, "Market: Too soon.");
        decrementBalanceOfCoupons(msg.sender, epoch, couponAmount, "Market: Insufficient coupon balance");
        redeemToAccount(msg.sender, couponAmount);

        emit CouponRedemption(msg.sender, epoch, couponAmount);
    }

I would not be upset if the proposal ends up passing without this element.

:slight_smile:

That said, I want to play devil’s advocate here.

:smiling_imp:

It seems to me that any benefit afforded by the use of gasTokens and fail-fast checks is likely to be competed away.

Eventually it all does. Forcing EOA-only just makes playing the game more expensive (each advance() would just put more of the bot runner’s money in the hands of miners).

IMO the benefit of protecting against unknown unknowns slightly outweighs the friction of requiring an advance+redeem bot to issue multiple transactions.

I would say that going EOA-only doesn’t provide as much protection as it may seem. It doesn’t remove vulnerabilities, it just makes them exploitable only by more privileged people. That just delays the amount of time before they’re exploited.

Take the flash-debt-clearing bot for example. If purchaseCoupons and/or redeemCoupons were EOA-only, they couldn’t have done what they did (unless they had a lot of capital on hand) because they couldn’t have used flash-loans to do it. So we probably wouldn’t know about that vuln right now. We’d have to wait until a whale figured it out and started doing the same thing using their own capital and 3 txs.

So I think the sense of security that comes from going EOA-only is a false one. It just delays the discovery of already-existing vulnerabilities.

I don’t think we’ve ever seen any attack involving flash loans that wouldn’t have also been possible without the use of flash loans.

4 Likes

The bot dynamics around coupon redemption are a problem that being said I think we aren’t fully exploring the downsides of clearing debt immediately at >$1

By clearing debt “for free” here the symmetry of the inflation / contraction cycle is lost - particularly since it would not actually matter what the magnitude of the debt cleared is. My hunch is that this change will make the change from a contractionary period to inflationary period faster and steeper, which is pretty suboptimal for the theoretical use case here (stability). Despite the bot dynamics messing with the coupon’s fungibility/value, the period where the bot was executing the flash debt clearing was the most price stable period of ESD’s life

2 Likes

:wave: hey everyone!

Thanks for taking an interest in this EIP – and fantastic feedback. This was meant to jumpstart the discussion, and absolutely love the ideas / direction from @scott_lew_is and @onewayfunction towards replacing the EOA limitation with an epoch requirement–you’ve convinced me that it reaches the goal, in a more elegant fashion.

Excited to see this momentum!

2 Likes

@will_sheehan, your thinking matches my initial thinking on this. However, if no coupons expire, it effectively means that debt ends up being just as inflationary as expansion. If supply ends up expanding at the same rate regardless, it’s probably better to have debt get wiped and some positive action to happen, rather than watch coupon games continue over an extended period.

2 Likes

Agreed! Appreciate all the high quality feedback.

I’m happy with wiping the debt in conjunction with the 2 epoch requirement on coupon redemption.

2 Likes

First off just I wanted to say how cool it is to see everyone take this level of ownership over the advancement of the protocol :star_struck:

Most of my thoughts have already been expressed by others, but to add to the discussion:

Part I: We’ve thought about this a bit on our end as well. I like the idea in theory, but I agree with @dmaddock on the potential downsides. The risk is by removing the “extra bump” we get after re-stabilizing we may fall right back into a contraction. However, this extra bump is very expensive in regards to dilution, so it would be great if we in fact didn’t need it.

Part ||: I agree with @onewayfunction on this one. In general we shouldn’t be anti-bot (provided they’re adding value) and I’m not a fan of band-aid solutions that don’t actually solve the underlying problem. However, the unknown unknowns angle is intriguing. I would be in support of this purely on the defensive security angle (an unknown bug in the advance flow that could be exploited intra-transaction).

@scott_lew_is’s addendum: :100: agree. This is a more elegant and easily codeable way to solve at least the immediate redemption problem.

TL;DR

  • I: Not against, but would love for there to be more discussion on this.
  • ||: Agree, but disagree with the original motivations
  • @scott_lew_is ++: Strongly agree.
4 Likes

I want to reiterate this because I don’t think I emphasized the point enough (I’m trying to convince any hangers-on that EAO-only is not a good idea):

There are no attacks that can be done with a flash loan that cannot also be done by a whale without a flash loan.

If you analyze the call-trace of any atomic transaction – looking at all the internal calls that happen – you can reproduce them all via a sequence of EOA-based transactions.

It doesn’t even require colluding with miners. You can just use high gas prices and rely on standard tx sorting. You also don’t have to be the only 3 txs in the block. You just have to have your 3 txs mined before the first competing txs in that block.

So what I’m getting as is, there are known downsides to going EOA-only (e.g., helpful bots can’t use gas tokens, which can ~double their gas costs, making things like CouponClipper more expensive for bots+users, etc). There are no known downsides to allowing contracts to call all functions on the system. And any “unknown unknown” vulnerabilities related to allowing contracts to call functions are also vulnerabilities that would exist even if we went EOA-only.

So the EOA-only choice is a pareto-deficient outcome.

To make the situation more clear, consider this question: why not make all public and external functions EOA-only? Why not do that to protect against unknown unknowns related to flash loans in all other call paths?

And I think the answer there is the same: there are known downsides to doing it (lack of composability with other systems, users can’t use smart-contract wallets to interact with ESD, users can’t use advanced techniques to reduce their risks, etc). There are no known downsides to allowing contracts to call those functions. And any unknown unknown vuln could be exploit approximately as easily by a well-capitalized attacker using only EOAs.

There are no points of reentrancy in the advance() function and it takes no parameters – meaning that from the moment it is called until the moment it completes its execution, it cannot in any way be influenced by its caller. It will execute exactly the same whether called by an EOA or called from another contract.

:robot::heart:: The friendly bots want YOU to vote NO on the EOA-only proposal. :slight_smile:

8 Likes

I just want to clarify the proposed solution to wipe debt prior to positive rebase.

In the event of a TWAP above $1 in a positive rebase, the entire debt will be wiped immediately?

So the scenario is that everyone is happily playing a chicken game waiting for coupon premiums to increase before buying the debt, if someone decides to push price up for one epoch above $1(which would not be difficult given the thin liquidity during negative cycles), then the entire debt is reset to zero? So if after one positive epoch, the price goes back to negative, the entire debt would restart at zero.

Is that the correct understanding of this proposal?

1 Like

That’s correct @sabretooth.

In the proposed solution, if TWAP closes above $1.00 (the key being, time-weighted average; a short spike doesn’t meet the criteria), then Debt (pending Coupon issuance) is reset, and a small amount of Coupons will become redeemable in the following epoch.

In your “game of chicken” scenario, the proposed solution would increases the demand to purchase coupons now, instead of perpetually waiting for a better Coupon Premium (which we’ve seen in Contraction #1, and we’re seeing in Contraction #2 so far).

The proposed solution also decreases the risk of holding Coupons (the Debt overhang, after reaching the peg, pushes redemption dates into the future).

Are there potential griefs, including pushing the price up for an epoch? Yes. Is that costly to an attacker? Yes. Do worse griefs exist in the status quo? Absolutely, including the ability to use the Debt overhang to force-expire honest Coupon purchasers.

1 Like

Here is some code that implements the “debt deletion” and the “2-epoch min before coupon redemption”:

Diff check for your convenience (but you should do your own diff and not trust me :stuck_out_tongue:): https://www.diffchecker.com/McPTvCsj

The epoch parameter of the redeemCoupons function shadows the public epoch() function, so I renamed the parameter to couponEpoch to make the compiler happy (in case anyone was wondering why I renamed the input param).

Something I’d like to get other people’s eyes on:

We’re setting _state.balance.debt to 0 in the Regulator.step() function (as proposed). Please look at all the places in the code where _state.balance.debt is used and double-check that this doesn’t cause any unexpected behavior.

I don’t believe it does, but I wouldn’t feel comfortable unless that was checked by at least one other pair of eyes.

I have not tested this! It compiles and passes my sanity checks. But I have not written tests for it or run any tests on it.

Please do actually review it.

@eqparenthesis’s it would be great to get your eyes on it too, because I think you know the code best.

I haven’t deployed it to mainnet. (I’d actually prefer if someone else did that).

Economicially speaking, wouldn’t elimination of debt lead to accelerated but unsustianble growth?
This is a “business cycle” consideratrion, not a technological one. There is no consensus amongst econmists on this issue. There are case studies with regards to various bankruptcy laws. These debt eliminitaion methodologies are based on time and not more advanced econometrics, like GDP, or inflation.This proposal would be an innovative experiment. If the aim of this project is to create a currency, and not a computer game, then we should take into account the lessons that an institutiuon like Bank of England can teach us. They have 300 years of experience and data on this topic.

1 Like

The debt cannot be reset to zero immediately when TWAP is greater than 1, or there will be a problem of easy inflation. When TWAP is higher than 1, the coupon should be zeroed (cannot be bought) instead of the debt, and the debt should (gradually) offset the future expansion of inflation.

2 Likes