keep-network / tbtc

Trustlessly tokenized Bitcoin on Ethereum ;)
https://tbtc.network
MIT License
213 stars 40 forks source link

Replacing the trusted price feed #254

Open mhluongo opened 5 years ago

mhluongo commented 5 years ago

This has been delayed to v2. MakerDAO's Medianizer will be used for v1.


The system needs price information at two critical times

1) When a new Deposit is created, the system needs to know how much collateral should be required of signers. If the collateral is all TBTC, no feed is necessary. 2) The system needs to know when a Deposit becomes undercollateralized, as well as "how much" to decide whether to move to the "courtesy call" or "liquidation" state.

Challenging deposit bonds

Let's ignore the first case for a moment, and focus on the second. Rather than treating price as a universal aspect of the system, we can focus on whether a particular deposit is undercollateralized. Anyone may challenge that a particular deposit is undercollateralized, and put some money down to prove it. If they're shown to be correct, they're paid a fee from liquidation.

To prove they're correct, the challenger puts down a bid for 1 BTC in ETH. We expect that the challenger will choose a favorable bid, rather than what they believe is the exact price, so if the bid is taken the challenger will make money. If the bid isn't taken within a timeout, we now have a ceiling on the BTC/ETH price.

Generalizing to an orderbook

Generalizing this idea further, we can build a 1-sided cross-chain orderbook of bids for BTC. We can consider the "price ceiling" of BTC as the deepest bid in the orderbook that has stood unfulfilled for some timeout. Of course, we don't want that price to move very often.

An attacker who wants to move the oracle price will have to eat through the orderbook, suffering price slippage- but a well-funded attacker can always defeat a thin orderbook. To prevent this, arbitrageurs must be allowed to insert bids into the orderbook before an attacker can clear out the book, effectively frontrunning an attacker.

New deposits

To decide the bond requirements for a new Deposit, all this scheme requires is a single bid that's been open long enough to be considered a legitimate price ceiling.

That time, however, might need to be as long as 24 hours- which leads to a poor deposit UX.

In the case that a price ceiling hasn't been established by the one-sided orderbook / challenge system, I suggest we fall back to Maker's existing BTC/USD and ETH/USD price feeds for new Deposit creation. If the feeds ever fail or aren't updated, we can fall back to the orderbook system.

The reliance on Maker feeds should be easy to turn off via user-activated governance. I'm working on better describing how that might look, but in the meantime a single privileged call to disable the Maker feed behavior by the contract owner should do it.

Thanks to @danrobinson @prestwich @nanexcool @afdudley and many others for the ideas behind this mechanism.

Tasks

Replaces #92

mhluongo commented 5 years ago

@gakonst this is a significant change to the state machine, and a bunch of stuff is floating still (eg the rewards for calling out undercollateralized deposits)

@prestwich could use your eyes here

cc @liamzebedee thought you'd like this

Shadowfiend commented 5 years ago

Build a maintainer to challenge deposits based on prices from popular exchanges

Probably want to integrate this into an existing executable (e.g. tbtc-maintainer), otherwise we're upping the operational overhead of running tBTC in rather annoying ways.

prestwich commented 5 years ago

Build a maintainer to challenge deposits based on prices from popular exchanges

Probably want to integrate this into an existing executable (e.g. tbtc-maintainer), otherwise we're upping the operational overhead of running tBTC in rather annoying ways.

We'd also want to put active arbitrage in as a feature of an existing executable

mhluongo commented 5 years ago

We'd also want to put active arbitrage in as a feature of an existing executable

This scares me a bit... interacting with eg exchange APIs as well as people's wallets.. I dunno. I certainly plan to do it with our own funds though

prestwich commented 5 years ago

Well yeah, but think of it from a reliability perspective. Giving people optional arb modules makes it more likely people will arb

gakonst commented 5 years ago

(long comment ahead)

Agreed that any feature which requires an actor to perform some activity based on off-chain data should be part of tbtc-maintainers.

Secret management is the risky thing here (API / private keys), so I'd recommend adding HSM support for anything requiring Ethereum Private Keys (https://github.com/certusone/yubihsm-go is a nice fit here, Loom had secp256k1 signer implementations for it). Not sure how you envision the deployment of tbtc-maintainers in the cloud, I'd expect that Keep would operate multiple of those in the beginning, so you want some infrastructure for distributing secrets in a secure manner, in which case Sops is a great choice: https://github.com/mozilla/sops, https://www.youtube.com/watch?v=V2PRhxphH2w


As for the 3 mechanisms described, I'm writing up an example for each (which will probably be transferred over to the design doc), to make sure we all are on the same page. I also describe how there is no need for a deposit oracle, since users can simply choose to abort (but possibly forfeit a deposit bond)

There is (probably) no oracle required for new deposits

  1. Depositor requests a deposit signing group creation (and OPTIONALLY deposits some constant amount of ETH as a bond, as a DoS protection mechanism)
  2. Assume price 50 ETH per BTC that N Signers agree upon via some method of communication (pre-agreed upon exchange API, BFT protocol, ...) - note, no oracle used
  3. Signers create a Deposit by sending 75 ETH (75/N per signer) to the deposit contract
  4. Depositor sees the 75 ETH deposit and they check the ETHBTC exchange rate via some API they use. Based on that exchange rate: 3a. If they find the collateral fair*, they send 1 BTC on Bitcoin + provide SPV proof on Ethereum. The deposit is created and is considered 150% collateralized (the bond from step 0 is also refunded if it was used in the protocol) 3b. If they find that the signers did not deposit enough, they can abort (and forfeit the bond from step 0)

*Note: fair, does not have to be 75 ETH. If the signers opted to created a Deposit that's only backed by 70 ETH, that's up to the Depositor to choose if it's sufficiently collateralized for their risk model.

The tradeoff of the aforementioned mechanism is the potential griefing of a Depositor or of a Signer:

Summary:

The above describes an idea for removing the oracle from the deposit procedure, by introducing a constant size griefing attack vector by allowing the user to choose whether to accept or reject the quote provided by the signers, allowing users to grief signers if there's no bond, or signers to grief users if there's a bond. This should require no code additions on the smart contracts.

Challenging Undercollateralized Deposits Example

  1. 1 BTC is worth 50 ETH, Signers put up 75 ETH to back it up for 150% collateralization (we allow usage of an oracle ONLY for this step)
  2. Coinbase APIs now say that 1 BTC is worth 62 ETH, making the deposit only 120% collateralized (hence it's in the courtesy call region, 75/62=1.20)
  3. Maintainer notices that, and triggers a state transition to COURTESY_CALL, by sending 61.99 ETH to the smart contract. Before T, the 61 ETH can only move if a swap is performed for 1 BTC.(implementation detail: HTLC-swap vs Summa SPV proof swap)
  4. Depending on how the market moves during this time there's a few possible outcomes:
    1. ETH went up, 1 BTC is worth 61 ETH (not enough to get it out of courtesy call, 75/61=1.22): Arbitrageur obtains 1 BTC from the markets for 61 ETH, swaps it for 61.99 ETH. Result: BTC:ETH Price in Contract State: 1:61.99. Arbitrageur: +0.99 ETH. Maintainer: -0.99 ETH + liquidation fee (must cover losses)
    2. ETH recovered, 1 BTC is worth 59 ETH (just enough to get it out of courtesy call, 75/59=1.27): Arbitrageur obtains 1 BTC from the markets for 59 ETH, swaps it for 61.99 ETH. Result: BTC:ETH Price in Contract State: 1:61.99. Arbitrageur: +2.99 ETH. Maintainer: -2.99 ETH (in this case the maintainer is disincentivized from actually posting an order, since they're losing money)
    3. ETH price holds or goes down, 1 BTC is worth >=62 ETH: The Arbitrageur put an order for lower than market, which can be expected to remain unfilled. Deposit gets liquidated. Result: BTC:ETH Price in contract State: 1:61.99. Arbitrageur: +liquidation fee

It does make sense as @prestwich said to add an arbitraging module in the maintainers, since if the arbitrageur is also the maintainer their returns are >=0 in all cases (sans gas fees). If a maintainer is not actively arbitraging, then in the case that ETH recovers enough to get the deposit out of the courtesy call, the maintainer loses the spread which can be bad.

This approach assumes that there's somebody interested to actually be the maintainer/arbitrageur, so the return must be attractive enough to justify these types of actions.

A vulnerability

The above does not prevent from the case that Mallory is a malicious maintainer/arbitrageur, who triggers the undercollateralization challenge and instantly fills it. We need the ability for multiple parties to be able to post orders, and we should pick a price among them.

The order book approach

This can be fixed by allowing multiple swaps of ETH for 1 BTC (facilitated by an atomic swap or SPV proof protocol as before) during the fixed period of time T. Users place their bids for 1 ETH and the contract functions as a ETHBTC exchange with an on-chain order book.

An attacker in this case could try to empty the order book right before the expiration period, and then set an arbitrary price of their choice. The price is determined by the HIGHEST OPEN order at T, The liquidation fee is split evenly among the parties that were part of the winning bid.

If at any point the orderbook is cleared, then extend the duration of the process by T. If >N orderbooks get cleared, fallback to an aggregate between the mean/median of the price given by each of the N orderbooks (plus Maker oracle potentially). If M<N orderbooks get cleared, the price is the mean/median of the price given by each of the M orderbooks.

mhluongo commented 5 years ago

Note: fair, does not have to be 75 ETH. If the signers opted to created a Deposit that's only backed by 70 ETH, that's up to the Depositor to choose if it's sufficiently collateralized for their risk model.

This reasoning breaks down without a holding period on deposits- otherwise a depositor and signers can collude to get cheaper liquid TBTC. Imagine the depositor was okay with a half-collateralized deposit, then walks with their TBTC while the signers can walk with their BTC- the loser is the total system collateralization, not either party.

That's why I've considered an optional fall-back to a centralized oracle- to avoid having to wait hours for a deposit to be "sufficiently unchallenged" to make minting TBTC safe if there's not a tight enough order putting a bound on the price. Challenges will still fix the issue, but it can be used to prevent crazy attacks without killing UX and requiring waiting periods when there's not already a less-bonded unchallenged deposit.

An alternative might be to require that bonds use a price greater than 85% of a centralized oracle, or suffer a waiting period... which gives us nice behavior if the oracle were to be removed via user action.

gakonst commented 5 years ago

@prestwich Most of my comment above is me unpacking the mechanism in longer form, what do you think about the extension of the challenge duration if the orderbook gets cleared w/ optional fallback to centralized oracle?

mhluongo commented 5 years ago

An attacker in this case could try to empty the order book right before the expiration period, and then set an arbitrary price of their choice.

I think the way to look at this might be "there is no price without a ceiling" rather than fill prices giving us data @gakonst - in fact, unfilled orders are what give us an assurance here

prestwich commented 5 years ago

The order book approach

This can be fixed by allowing multiple swaps of ETH for 1 BTC (facilitated by an atomic swap or SPV proof protocol as before) during the fixed period of time T. Users place their bids for 1 ETH and the contract functions as a ETHBTC exchange with an on-chain order book.

An attacker in this case could try to empty the order book right before the expiration period, and then set an arbitrary price of their choice. The price is determined by the HIGHEST OPEN order at T, The liquidation fee is split evenly among the parties that were part of the winning bid.

If at any point the orderbook is cleared, then extend the duration of the process by T. If >N orderbooks get cleared, fallback to an aggregate between the mean/median of the price given by each of the N orderbooks (plus Maker oracle potentially). If M<N orderbooks get cleared, the price is the mean/median of the price given by each of the M orderbooks.

this is not how I'd describe the mechanism. I think the introduction of a sampling interval T is unecessary. Instead, how about as follows:

Many ETH/BTC orders sit open on the books. Each order is marked with a timestamp of when it was placed. The price is the HIGHEST OPEN order whose timestamp is more than 90 minutes in the chain's subjective past.

Because cross-chain communication has ~1 hour of time lag (in the best case), our order book's on-Ethereum state is always ~1 hour behind its true state (which is to say, orders that have been filled will show as open during that time period). This means that an order sitting on the book 90 minutes has had an opportunity to be filled, but has not been filled. This means that the order does not represent a fair market price. The HIGHEST such order is therefore an objective bound on the fair market price.

However, suppose that an attacker wishes to push that bound. She could fill all open orders, wait the 60 minutes, and affect the price for 90 - 60 = 30 minutes. To prevent this, we have to allow observers to add liquidity to standing orders without waiting 90 minutes. This means that an attacker would be forced to fill an unbounded amount of off-market orders from any number of arbitrageurs. The more she wants to move the price, the higher the spread she pays to each arbitrageur to do so. Assuming the arbitrageurs collectively have more capital than the attacker, she can't effectively move the price.

liamzebedee commented 5 years ago

A naïve question - what drives liquidity for these auctions? With a trusted price feed, we are assuming some vested interest from those maintaining it. What makes this ETH<->BTC market better than using Binance for example?

prestwich commented 5 years ago

A naïve question - what drives liquidity for these auctions?

We'd have to bootstrap this marketplace. and possibly sell ETH periodically ourselves

With a trusted price feed, we are assuming some vested interest from those maintaining it. What makes this ETH<->BTC market better than using Binance for example?

They may not always have an interest in maintaining it "correctly." This removes our need to rely on the maintainers

mhluongo commented 5 years ago

I'm taking a crack at spec'ing this as it's closely related to #293, which is blocking a couple projects we're working with from integrating cc @gakonst