Handling frontrunning in the permanent registrar


#1

One issue that is coming up in the implementation of a permanent registrar is the issue of frontrunning, and if and how to try and combat it.

Briefly frontrunning is an attack where someone watching the transaction pool notices that someone is trying to register a name, and sends their own transaction with a higher gas price to register the name first. They can do this without even knowing what the name is - knowing someone wanted it is enough - and they can then try and sell the name back to the person who originally tried to register it.

If we believe this will be a significant issue, we need to find some way to combat it. One way to do so is with precommitments: Instead of registering a name with a single contract call, you would need to precommit to the name by submitting a token combining the name and a secret. Then, once that is mined you can reveal the secret, and secure your claim to the name. A frontrunner does not know what name you are registering in step 1, and so can’t do anything, and by the time you reveal it in step 2, it’s too late for them to add their own precommitment.

The problem is, this is likely to significantly complicate the registration flow, as well as adding more complexity to the contract itself. Either you need a ‘reveal period’, and a third transaction for the revealer of the earliest commitment to confirm ownership, or you have a process whereby the first to reveal a precommitment gets the name, but during some period may have it taken back off them if someone reveals an earlier commitment. Neither of these seems ideal from a UX point of view!

I’d welcome other ideas on how we can prevent or discourage frontrunning without overcomplicating the registration process.


#2

this is for the FIFO instant buy registration right?
maybe we can make the process automatic through the client?

  • The client creates a random salt (without even showing it to the user)
  • sends the obfuscated TX to be mined
  • user signs the TX
  • The client watches the chain and once it detects that the TX has been mined it…
  • composes the TX with the previous random salt
  • warns the user that she needs to sign the second / reveal TX
  • the user signs the second (reveal) TX
  • the client then waits and watches for the TX to be mined
  • once it’s mined it shows “Domain correctly registered” or something like that

This can be done with a minimal explanation to the user (“the registration process can take a few seconds / minutes. Do not close this page as it will do some crypto mambojambo to be super secure and avoid lurking miners” … not actual text :wink: ) without having to explain secret salts, nor requiring to save it somewhere, nor explaining what a “reveal” transaction is.

In any case it takes only a few seconds-to 2-3 minutes for this to be processed, right? (I know it depends on the gasprice and current network congestion, but we can factor this variability in the warning that we show to the user)


#3

That’s a reasonable summary of how it could be structured in the UI. The issue is that you have to know somehow that there won’t be another, earlier claim to the domain revealed. I can think of two ways to do that:

  • Have a maximum period, after which the earliest revealed claim gets the name. This likely means a third transaction for claimants.
  • Give the name to the first claimant, but if other valid claims that were submitted earlier are revealed during the reveal period, reassign it to them. This only requires two interactions, but introduces some risk if using a name directly after purchasing it.

There’s also the issue of how well this will work during times of high congestion; with a low gas price it could take hours for the transaction to be mined, and the user would have to be ready to submit a new transaction once it is.


#4

Oh I see, this is for those cases in which maybe there is an interest from 2 or more users for the same domain, maybe even without a malicious frontrunner miner,
untill both TX are in the pool there is no way of knowing (for ENS) who is the rightfull owner, and the winner will be the one who will have put the highest gas fee… right?

So I see 2 possible solutions:
1 - in the TX 1 we send obfuscated with the salt, we also include a timestamp, captured by the client from a 3rd party / oracle ( witnet.io? )

  • in this case we inform the user that even if they put a higher gasprice, if there is another user who has launched the TX before them then the domain will be assigned to them, even if their TX is mined after XX hours/days (TBD)

(2- if there was a way to scan the tx pool before sending it this could help in avoiding useless transactions, but if there is a deterministic way for the client to identify a label even salted then this leads again to frontrunning and it doesn’t work)

This would be so easy if we had a centralized server where we could simply “lock” the domain :wink: untill the payment is confirmed.
Can we imagine a decentralized version of this Lock mechanism? something that does not require mining a TX?

  • maybe we broadcast something on whisper?
  • maybe we “lock it” in a testNet while we send the TX on mainnet? that is faster, and we cross reference it from there?

just ideas


#5

It’s not that we expect that scenario specifically - but we can’t prohibit it, and so have to write the code to deal with it.

We can use the time at which it was mined to measure this, we don’t need an external timestamp. But the problem remains that it complicates the code and UI and either requires provisional ownership, or a third ‘finalise’ TX.


#6

you mean we don’t want an off-chain time right?

the idea of the time being provided by the client was exactly to solve having to wait for the time at which it was mined.
this is a form of ensuring who came before. if it’s obfuscated the frontrunner will not know that time

maybe this is for an EIP
if the node itself allowed another field, the timestamp to be included in the TX when it’s sent to the mempool, a field that is filled by the node itself when it broadcasts the TX to the network
this will allow to have an attestation of precedence? (maybe a malicious node can try to create a tx in the past but I think it is possible to devise a mechanism whereas the number of TX in the pool is used as a possible nonce?! … this is OT :slight_smile: ) … let’s go back to the initial idea.

can we use a timestamp that is not the one provided by the miner? and that it’s inserted in the TX itself?


#7

We don’t need time from an offchain oracle. It doesn’t benefit us.

Sorry, I don’t follow.

How would that help?


#8

It just occurred to me that this can be simpler than I previously suggested. We don’t have to ensure the name goes to the earliest precommitment, especially since we’re only expecting one precommitment per name. Instead, we can hand it out to the first caller who reveals a valid precommitment (from a previous block), which simplifies operation significantly.


#9

Q. are the permanent registrar will be FIFO?


#10

You can front run this by buying a block for 1 wei higher gas price than the reveal.


#11

The plan is for it to be first-in-first-served.

Yeah, this occurred to Jeff and I after I posted too. I think Jeff is going to post a summary of our conversation here soon.


#12

As Micah mentioned, the scheme you describe can probably be frontrun, Lets say Alice is the victim, Bob is an evil miner, and C is the ENS contract.

A sends a commit transaction to C saying “I want the domain ‘q=obfuscated(Z.eth, salt)’”. Then A sends a reveal transaction “here is my money for q, and btw here is my secret salt that makes q open up to Z.eth”. Evil miner Bob sees this reveal tx hit mempool, does the computation to find Z, and then makes two transactions (instead of one), a valid commit of his own and a valid reveal of his own. Either Bob inserts these two txs into mempool with a higher gas price than alice, or colludes with miners to get his txs into the block first, and he wins the name instead of her.

So you have to order by commit time, which becomes harder, and additionally, commit-reveal is still problematic in some cases because just knowing that someone is committing to a certain contract may leak valuable information. In the context of ENS this may not matter as much, since you can obfuscate within the commit which specific domain is desired… but our research group at IC3 has done some thinking about this topic in the general case, and what we came up with was a technique we call submarine sends. We also have an open source project to develop a library called libsubmarine that we would like to be portable and reusable for the community to start solving this problem in smart contracts. It isn’t ready for prime-time yet, but may be ready by the time that ENS is ready for launch.

The way we do it requires 4 transactions right now (though I suspect you could do it in two if you optimized a bit), I’ll try to summarize briefly, though I might skip some minor details. There’s more detailed documentation on the ReadMe on Github, admittedly we haven’t done an awesome job of documentation but we’re trying to focus on polishing the library up to scratch.

  1. You generate a TXUnlock off chain. This is a transaction from a random address you do not own the private key for. You generate it by filling in the target ENS smart contract as your To: address and the value of your bid as the amount, maybe some arbitrary data as well, then instead of signing the signature with a private key you own, you
    generate r and s values which are random numbers derived from keccak256(witness, data, random nonce for randomness, etc). This gets you something that looks like a valid transaction, if the commit address which is ecrecover(r,s) actually had enough money to carry it out, so you don’t broadcast it.
  2. The first thing you do on chain is commit, you send the required amount of money + a little extra for gas costs to the commit address. This looks a send of money to a random new address, which happens a fair amount as it is, so it provides cover. Additionally this has the nice property that you’re not sending this transaction to the ENS contract so at this point the miners have no idea that you’re actually trying to make a bid.
  3. Now you reveal. You tell the library what block and tx index your commit happened in. You also tell during this part the witness/ENS name you want. The library stores this information.
  4. Now you have a challenge period. The purpose of this is to allow anyone else watching the network to call BS on your commitment. This is to prevent an attacker from claiming a commitment that doesn’t exist, or claiming that a previous non-related Tx is a commitment (when in fact it was not). Anyone who sees this can verify the validity of commitments and if a commitment is invalid submit a merkle proof to the library to prove that the referenced commitment is invalid; in this case the prover is rewarded and the commitment is slashed.
  5. Finally after the challenge period is over you make a finalize call, and the ENS contract can safely allocate the domain to the bidder.

(One important note: we rely on the blockhash() solidity call so our implementation has the explicit requirement that the distance between commit and reveal must be less than 256 blocks)

As you can see there’s a fair bit of overhead. You may decide preventing frontrunning on ENS isn’t worth the overhead. But if you decide to use our library or roll your own solution you may want to reference our code since we’ve already been trying to solve the problem in the general case.


#13

You can make this prohibitively difficult by requiring a delay of at least one block between commit and reveal. Now Bob needs to not only be able to frontrun, but also be able to censor transactions, which requires either a lot of money to waste on filling up blocks, or the collusion of the majority of miners.

This is clever - but I don’t think it will be useful for the new registrar. It would have been super handy for the auction registrar, though.


#14

If we’re doing a FIFO scheme, then we are assuming that two people wanting the same name would be rare. I would simplify this by making the reveal and setup transactions, the same. So you first register a obfuscated name, and then you send a second transaction that not only reveals it, but it also sets up owner, resolvers etc.

The problem is that its not enough to prove you have a precommitment, if you give the name to the first person to reveal a precommitment, then a miner can hold a transaction for a few blocks and do the two previous transaction. So I would make it that if you reveal a domain, it’s considered yours but there’s a window of time in which anyone can reveal an older commitment. I would put some reasonable limits, like 24 hours to reveal an older one and it can’t be older than X weeks, otherwise it opens a weird possibility where you register a bunch of names and let them wait dormant.


#15

If a user doesn’t submit their reveal transaction until the commit is included in a block, this requires a miner with the ability to cause reorgs - basically, a 51% attack.


#16

After I wrote this I realized it is a sort of stream of consciousness, but I have to step away and din’t want to delete it, so sorry in advance.

It doesn’t require a reorg, just buying blocks. The cost to buy a block at 5 nanoeth right now is 0.04 ETH, buying at 50 nanoeth is 0.4 ETH etc. If you only need to buy a handful of blocks, this could be a profitable attack. The best way to combat this particular vector is to make the time between commit and reveal high, like 100 blocks. This of course has negative impact on UX.

Ideally, you would need to figure out how much you want to make an attack cost the attacker, and compare that against a moving average of gas prices and then set the commit/reveal distance to be high enough to ensure protection against such an attack.

One option would be to ask the user, “How much protection against attacks do you want?” and if they put 100 ETH then you would separate the commit and reveal by 100 ETH worth of blocks at some reasonable gas price (e.g., 75th percentile).

The UX can let the user know the impact of their decision, that the more “insurance” they have the longer they have to leave the page open. This way users buying high-profile domains can opt to do a full day wait, while users buying low-profile domains can choose “reveal 2 blocks from now”.

The problem of course is if two people happen on the same domain at around the same time with different choices for reveal times. We don’t know that they are the same so we must delay all purchases by the longest chosen reveal. In order to curb DoS attacks, we would need to charge people for this insurance that they are buying, since every block delay they request hurts the UX for everyone else in the system.

:man_shrugging:


#17

I think the following is similar scenario to what Alex mentioned, although there was a higher bid at the end:

In this auction, there are two bids with the same amount:

Deed address : bid amount : commitment_block(newBid) : reveal_block
A) 0x8011c4e66b41df000c628b663c7752229071bce6 : 0.02 Ether : 6326032 : 6326049
B) 0xE8323A0Ff305F73B3cb4Cf75cbc96Fd2831728B7 : 0.02 Ether : 6310422 : 6326250

As you can see B was committed before A (15610 Blocks prior), however A’s owner revealed the bid 17 blocks after their commitment, which was in the first few minutes of reveal period.

The final result of the auction --in the absents of another higher bid-- would have been A.

FIFO clearly is not an optimal option.


#18

Just updating with our tentative approach:

  1. Anyone can commit to a hash, with no deposit required.
  2. Anyone can register a name by revealing a valid commitment to that name that is at least 10 minutes and no more than 1 day old.

The minimum delay puts a lower bound on how long an attacker would have to maintain a censorship attack for, while the maximum delay ensures that an attacker can’t simply submit a commitment in advance for every dictionary word.