Frontrunning in Reorgs
A low severity bug report, and the importance of bug bounty programs for continuous security.
By Jelle Gerbrandy
On April 19 we at Hats.Finance received a bug report through our Bounty Vault from Nik (@eth_nik_dev on twitter and telegram). Nik has been an appreciated contributor in the audit competitions we run, and we thank him for the report.
In this article, we explore the details of the vulnerability, its practical implications, the proposed fix, and the subsequent decision on the severity and payout
The vulnerability regards our TokenLockFactory
contract.
We use this contract to create TokenLock instances — these are contracts that are used for vested payouts and hold tokens that become available over time to a beneficiary. We use the token lock contract for bounty payouts (projects can choose to pay part or all of the bounty in a vested form) and we also use it to stream salaries to some of our collaborators.
The attack
The report points out a very interesting attack on our implementation of this pattern. The TokenlockFactory.createTokenLock()
function uses OpenZeppelin’s Clones.clone()
function to create new TokenLock
proxy instances. The starting observation is that the clone()
function uses the create
opcode to deploy a new proxy contract. This new proxy contract will then be available at an address that is calculated from the deployer’s address (i.e. the address of the factory contract) and a nonce (that represents how many previous contracts the factory has created).
So here is a possible attack:
- Ann calls
createTokenLock
to create aTokenLock
at address0x1234..
with Bill as a beneficiary - Ann funds the contract by sending tokens to
0x1234..
- A block reorg happens and the chain gets reset to a block before the contract was created in step 1.
- An attacker front-runs Ann’s transaction from step 1 to create a
TokenLock
at address0x1234..
and sets themselves as the beneficiary - After Ann’s transaction from step 1 is mined, the transaction from step 2. is mined as well and her tokens are sent to
0x1234..
- Profit! for the attacker who now controls the funds
This attack scenario describes a case in which an attacker takes all the funds send to a TokenLock contract. However, it is also an attack that is extremely difficult to execute in practice. First of all, it assumes that the funding of the token lock happens in a transaction that is separate from the creation of the token lock happens, and that the funder does not check if the destination contract is configured correctly — i.e. it is only when a user uses our contracts in a very specific way that there are actual funds at risk. Another observation is that the attack is only possible if a reorg happens at exactly the right moment — and block reorganisations are extremely rare in Proof-of-Stake.
That said, the vulnerability, although difficult to exploit, exists, and the fix is straightforward.
The fix
The limits of using create
are known, and the EVM offers an alternative way of calculating contract addresses that is more suitable for our use case and makes the attack not feasible.
Instead of using create
to create a new contract, we can use Clones.cloneDeterministic
from the OpenZeppelin contract to create the proxy contract using the create2
opcode. These functions take a salt
parameter that determines the address where the contract will be deployed. We can include some crucial configuration parameters — specifically, the beneficiary
— into thesalt
parameter. In practice, this means that the address of the tokenlock contract depends on the address of the beneficiary, and so cannot be hijacked by an attacker that front-runs the transaction.
Severity and payout
The size of the bounty paid out by our vault depends on two factors: the severity of the vulnerability, and the amount that is actually at risk in production.
The primary use of the TokenLockFactory
is to create contracts for payouts of bounties from our Vaults. When a bounty is paid out, the TokenLock
contract is created and funded in a single transaction — and so this procedure is not vulnerable to the attack described. We also use our contracts for paying some of our collaborators, but in these cases our procedures are such that some time (at least some days) pass between contract creation and their funding, during which the beneficiary is asked to confirm their address. So no funds are currently at risk
After some discussion, we decided to classify the bug as “low severity” (“contract does not work as expected”). According to our rules, payouts are capped by the amount “at risk in production at the time of disclosure”, which is 0. However, given that there is a small but real risk of falling victim to the exploit in the future, we decided to fix the issue, and to award the reporter with a 5,000 USDC and an NFT as a recognition for their efforts and to show our appreciation bringing our attention to such an interesting attack.
We believe that collaboration and continuous improvement are key to strengthening our platform’s security. We encourage more individuals to participate in our bug bounty and share their expertise. By doing so, you contribute to enhancing the overall security and stand a chance to earn rewards for your valuable insights.