Aurora Inflation Spend Bugfix Review: $6m Payout

Immunefi
Immunefi
Published in
7 min readJun 7, 2022

Summary

Whitehat pwning.eth submitted a critical vulnerability in Aurora on April 26 via Immunefi. The vulnerability consisted of an infinite spend bug that, if exploited by a malicious user, could have led to a direct loss of 70k ETH and $200m in other assets. Thankfully, owing to pwning.eth’s skills, that bug was responsibly disclosed, rather than being exploited. No user funds were lost, and Aurora quickly patched the bug, leading to a positive outcome for everyone.

Since the bug was assessed at a severity rating of critical, the whitehat has received a $6 million dollar payout from Aurora, the second largest bounty payout in history.

Aurora should be commended for having a well-run bounty program, a fast response time, and big bug bounty rewards that incentivize these kinds of project-saving reports. Aurora’s blogpost about the responsible disclosure can be read here.

Immunefi is pleased to have facilitated this responsible disclosure using our platform. Our goal is to make Web3 safer by incentivizing hackers to responsibly disclose bugs and receive clean money and reputation in exchange.

Since the affected piece of Aurora–the Rainbow Bridge–is a bridge, we’ll start off this bugfix review with a brief discussion of how bridges work before diving into the infinite spend vulnerability itself.

A Brief Introduction to Bridges

There are many blockchain networks in active use, like Ethereum, NEAR, Polygon, Fantom, Avalanche, BSC, or Solana, each with its own value proposition. As users (and money) pour into DeFi, there is an exploding demand for blockchains with low transaction fees and fast confirmations/finality. To meet this demand, there has been a proliferation of L2 scaling solutions and sidechains. However, these solutions aren’t (usually) able to interoperate and talk to each other. Many DeFi protocols are native to different chains. For example, Aave is native to Ethereum, but PancakeSwap is native to BSC.

The same goes for the exploding NFT space, where we see a substantial amount of collections not only launching on Ethereum, but also on Polygon, Solana and NEAR. But what if we want to move our precious NFTs or valuable tokens from one chain to another?

The answer to that question is blockchain bridge technology. Bridges are a way to connect two or more distinct blockchains and enable users to move assets between them. There are different bridge designs and approaches. There aren’t any standardized ways of designing your own bridge. We’re still in uncharted waters.

Bridges also have additional “attack surface” as compared to “regular” DeFi projects. While a yield farm or a decentralized exchange might have a collection of smart contracts and a dapp web page, a bridge usually needs monitoring nodes, validator keys, and secure communication channels on top of those contracts and dapp. This added complexity means more lines of code where bugs can hide and a greater variety of ways for hackers to break in.

The demand for moving tokens gained/earned on one network to another increases every day. The amount flowing through bridges is enormous.

Most of the bridges implement the so-called IOU (I owe you) approach: users are sending funds to the bridge protocol, where those funds are then locked by the bridge smart contract, while the bridge protocol issues the user an equivalent asset on the second network from the second bridge smart contract. Typically, the tokens on the destination chain are referred to as IOU tokens or wrapped tokens. For example if a user wants to send Ether from Ethereum to NEAR over the Rainbow Bridge, the Ether will be locked by the bridge contract on Ethereum side, and the user can redeem equivalent tokens from the bridge contract on NEAR. For every dollar moved from one chain to another, the bridge has to hold that dollar on its “native” chain in case the user wants to move the money back. As you can imagine, bridges accumulate a lot of funds.

What is Aurora?

Aurora is an implementation of an EVM built on the NEAR network that supports all tools available in the Ethereum ecosystem. Besides the EVM, Aurora developed the Rainbow Bridge which allows users to transfer assets between Ethereum, NEAR, and Aurora. In other words, it allows users to deposit ETH and ERC20 tokens from Ethereum mainnet to the nested layer of NEAR, which is Aurora.

We won’t explain how Aurora works in detail. You can read that in the official Aurora docs. What we will dive into is how Aurora handles withdrawals from Aurora to NEAR and between Aurora and Ethereum.

Two contracts in the Aurora Engine are particularly interesting to us: ExitToNear and ExitToEthereum. In short, they are special, built-in (precompiled) contracts that handle withdraw requests from the Aurora EVM.

In the template contract of mapped ERC20 tokens on Aurora, we can see the triggering of these two contracts in the following code:

In both cases, the logic looks fine for ERC20 tokens, as calling these functions would first burn the ERC20 tokens before proceeding with making a call to the special built-in contracts. That is the method of withdrawing ERC20 tokens from Aurora to NEAR or to Ethereum. What about ETH?

We still call these special contracts, but in a slightly different way. We simply send ETH to a special contract and the contract generates an event ExitToNear that records the sender, destination, and amount of this exit. At the end of the ExitToNear contract execution, exit_event_log containing the event info is returned. The code for the contract can be found here.

When the main execution is done, these logs along with all the other logs during the execution will be checked by filter_promises_from_logs in aurora-engine/engine/src/engine.rs.

As long as the Log is generated with hardcoded address ExitTo(Near|Ethereum)::ADDRESS, the log.data will be processed as new promises to be scheduled.

However, those transfer promises can be generated as long as the code of native contracts are invoked.

Vulnerability Analysis

In Ethereum, there are three major types of contract calls: regular CALL, STATICCALL, and DELEGATECALL. We won’t deal with STATICCALL as it is explicitly prohibited in the Aurora Engine. We will dive into the other two.

When contract A makes a CALL to contract B by calling foo(), the function execution relies on contract B’s storage, and the msg.sender is set to contract A.

This is because contract A called the function foo(), so that the msg.sender would be contract A’s address and msg.value would be the ETH sent along with that function call. Changes made to state during that function call can only affect contract B.

However, when the same call is made using DELEGATECALL, the function foo() would be called on contract B but in the context of contract A. This means that the logic of contract B would be used, but any state changes made by the function foo() would affect the storage of contract A msg.sender would point to the EOA who made the call in the first place. And what is important in the case of Aurora bug, msg.value would point to the first call context, not the second. In other words, Ether is not sent along delegatecall. (See example 2).

Knowing we can generate transfer promises from Aurora to NEAR, by calling ExitToNear built-in contract directly, we can use DELEGATECALL to trick the Aurora Engine into thinking we actually sent the Ether. Why would this work? Because the code only checks if the LOG was generated by the built-in address and it is assuming msg.value > 0 actually means that ETH was sent to that address.

If we use delegatecall() to call the native contract, the msg.value will be inherited from the original calling context, but the ETH is no longer passed to the native contract. The following contract does just that.

It takes the ETH value from the caller, triggers the exit event in the ExitToNear contract, then sends the nETH back to the caller. The nETH can be deposited into Aurora EVM again through Aurora Bridge, effectively doubling the attacker’s original balance.

Step by step:

  1. Bridge Ether from Ethereum to Aurora using Rainbow Bridge (Aurora Bridge)
  2. Deploy the malicious contract on Aurora that makes the delegatecall to the native contract ExitToNear i.e. 0xe9217bc70b7ed1f598ddd3199e80b093fa71124f
  3. Call the exploit function of the malicious contract. Aurora is tricked at this point to send nETH to the caller on NEAR from the Aurora bridge contract. The balance of attacker doesn’t change on Aurora
  4. Attacker then deposits back nETH to Aurora, doubling the attacker’s balance
  5. Repeat from step 3.

It’s important to note that this bug incurs the complex integration logic of NEAR and Aurora runtimes and such a discovery was impossible without deep analysis from pwning.eth.

Vulnerability Fix

An exit error is now returned if the address given does not match the inputs’ address, which disables the ability to call the contract with DELEGATECALL in a similar way to how Aurora disabled the STATICCALL. Aurora developed a test to ensure that the vulnerability is tracked, just in case a logic change might cause it to appear again.

Acknowledgements

We would like to thank pwning.eth for doing an amazing job and responsibly disclosing such an important bug. Big props also to the Aurora team who responded quickly to the report and patched it.

If you’d like to start bug hunting, we got you. Check out the Web3 Security Library, and start earning rewards on Immunefi — the leading bug bounty platform for web3 with the world’s biggest payouts.

And if you’re feeling good about your skillset and want to see if you will find bugs in the code, check out the bug bounty program from Aurora.

--

--

Immunefi
Immunefi

Immunefi is the premier bug bounty platform for smart contracts, where hackers review code, disclose vulnerabilities, get paid, and make crypto safer.