Blog | Home | GitHub | LinkedIn | Twitter | Medium

Elliot Friedman, Builder, Smart Contract Engineer

TVL: $2,178,650,365  |  Total Transactions: 2,254,490 |  Deployed Smart Contracts: 67 |  Hack Losses: $0.00

You're Doing xERC20 Wrong

Please excuse the click-baity title, but I think we should have a conversation about xERC20. The standard, as implemented in the specification has a pretty serious issue that can stop user transfers across bridges with low capital requirements.

When I see an audit report, it usually focuses most of its time on critical and high issues, the type of stuff where funds go missing, or other really nasty things can happen. This is good that these types of issues are caught before having code go live. However, auditors tend to not focus on more architectural or mechanism-design flaws, though I have seen these findings before in reports.

The most pernicious bug is the one you didn't realize existed, the trade-off you didn't realize you made, the variable that was assumed to be correct.

xERC20 enshrines rate limits inside of the token itself to stop a rogue bridge provider from infinite-minting the token. Most implementations include pause functionalities so a guardian can stop a bridge from minting if it becomes compromised.

Here is the formula for determining the buffer in xERC20

So far, no issues here; we're just finding out what the current buffer is for a given bridge. Each bridge has two rate limits, as defined in EIP-7281. One for burning, and one for minting. Any time a mint happens, the minting buffer is depleted, and any time a burn happens, a burn buffer is depleted. This all seems to be in order, so let's examine the state of an xERC20 token deployed on two separate blockchains in its initial state.

State 0

Both mint and burn buffers are full; no one has minted or burned on either chain, so no problems.

Enter Bob. Bob is an instigator who spends his days talking about the ills of having too many opcodes in a blockchain. We'll leave the rest to your imagination.

Bob sees this design and figures out that for $200 a day in gas, he can completely stop the cross-chain bridging of this xERC20 token. What did Bob see that we missed?

Bob realized there is nothing in this implementation at an architecture level that stops a user from performing a DoS attack. All this attack requires is a small amount of tokens, and some scripts to move xERC20 tokens back and forth between chains.

Let's visualize this DoS attack. In the first state, Bob acquires some tokens—say 5 % of the buffer cap. Assuming a project isn't the most trusting of their bridging provider, this could be a relatively small upfront cost to acquire these tokens.

State 1

Now that Bob has 5 % of the buffer in his wallet, he will burn the asset on chain B and receive the asset on chain A. This will cause the burn buffer on chain B to deplete by 5 % and the mint buffer on chain A to deplete by 5 %.

State 2

Now that Bob received the asset on chain A, he'll just repeat this process by burning on chain A and receiving on chain B. This has the effect of depleting the burn buffer on chain A by 5 % and depleting the mint buffer on chain B by 5 %.

State 3

Now if Bob repeats this process 20 times in a short period of time, the buffers on both chains will be completely empty, and any user trying to bridge in either direction will be blocked as the rate limits are empty.

State 20

Now that the attack is complete, Bob can sell the tokens, and only have paid some slippage and gas fees to execute this attack. The faster the messaging providers for the xERC20 token are between chains, the less risk Bob has to take as he can execute his attack faster and sell his tokens sooner.

This attack could be generalized to more chains and more bridge providers. The more providers and chains in a configuration, the more transactions are necessary to empty the buffers.

We've described the problem in depth, but what is the solution? The solution involves merging both the mint and burn buffer into a single buffer that replenishes on burns and depletes on mints. When the buffer is not at the midpoint, it heals back to the midpoint, regardless if it's under or over the midpoint. This way, if Bob tries to execute this same attack, the only thing that would happen is he would burn gas.

Mint and Burn Buffers Merged

This new buffer has an upper bound, a midpoint, and a lower bound. In the Solidity Labs implementation, the lower bound was set to 0, and users define their upper bound. The midpoint is half of the upper bound. This rate-limited-midpoint implementation was audited by Halborn, where only informational issues were surfaced. Additionally, the library code was formally verified with rules and invariants using the Certora Prover to ensure correctness.

Let's step through Bob attempting the same attack on a hardened implementation using a rate limit with a midpoint instead of separate buffers for mints and burns.