BOOK THIS SPACE FOR AD
ARTICLE ADBugs often reside where complexity exists.
In late November 2023, I made the decision to shift my focus after experiencing multiple unsuccessful attempts in bug hunting on ETH mainnet. The exploit on Avalanche’s Multichain Router V6 served as a reminder that while Ethereum undergoes thorough scrutiny and attracts numerous MEV bots, other L1 chains receive comparatively less attention. This is evident from the simplicity of the exploit and how it went unnoticed for almost two years. It is clear that complexity increases the likelihood of errors and oversights.
I filtered through the Avalanche projects listed on ImmuneFi, and Geode Finance caught my attention. It was a Solidity ported version of Curve, similar to Solidly / Nerve Finance.
Curve is one of frontiers in DeFi and there were many Solidity versions of forked Curve, e.g. Ellipsis, Swerve, and LeetSwap. Through these field tests, Curve forks got quite solid and this was the case for Geode Finance.
During my assessment, I could not discover any vulnerabilities in Geode Finance. However, Geode Finance features a unique token called gAVAX, which is based on the ERC1155 standard, and the pool code had the onERC1155Received function.
Hmm, we were here before…
That led to me thinking about reentrancy attacks because reentrancy was possible during AVAX or ERC1155 token transfers.
As I was already familiar with the Curve code, I quickly opened the popular Curve pool code.
Curve pools don’t have ERC1155 tokens but they process raw ETH so reentrancy is possible.
Common scenario was to directly transfer tokens to the pool to break token accounting. If any calculation used token.balanceOf, it could be manipulated using increased balance.
Fortunately (for the protocol), the code all operated on self.balances state variable so reentrancy couldn’t affect those. It also had reentrancy locks in important methods.
There were a few instances of using token.balanceOf. Either it was to check if the debtor had enough balance or to get the real amount of tokens transferred using a pair of balanceOf. Except one, in withdrawAdminFees().
withdrawAdminFees() was perfect point for possible attackers. It didn’t have a reentrancy lock, it used token.balanceOf. Bingo!
Soon, I became aware that the function in question held privileged access. Had it been a regular function accessible to anyone, I could have potentially triggered a reentrancy attack, leading to inconsistencies between the pool state and actual token balances. This reminded me of Curve’s extensive range of pool types, I wondered if a public withdrawAdminFees() function could be lurking somewhere.
I searched contracts using CodeIsLaw.
https://etherscan.io/address/0x94b17476a93b3262d87b9a326965d1e91f9c13e7#code — this pool met the conditions.
Next, I had to find reentrancy point, where external call happens before balance update and remove_liquidity_imbalance() was the one!
OK, I am almost there…
Taking a deep breath with a cup of coffee, I started writing the PoC.
The vulnerability was that some pools could be forced to be broken temporarily by calling the withdraw_admin_fees inside the fallback of remove_liquidity_imbalance.
Detailed steps:
Prepare tokensAdd one-sided liquidity using the tokens via add_liquidityRemove liquidity via remove_liquidity_imbalance, specify 1 wei for ETH withdrawal amount so that fallback is called while reducing token amount by a small number, like 10⁹ wei so that the function doesn’t revertInside fallback function (receive), call withdraw_admin_feesAs a result, token balance of the pool is reduced by some degree while incurring little loss (swap fee + add/remove liquidity rounding) on the caller’s side.
You can see the balance became lower than balance state variable.
Following the initial report submission, I proceeded to provide additional details with further implications in subsequent follow-up communications.
After several hours, I got a reply.
We went back and forth. The impact could be magnified using flashloans. Since there is OETH vault, we could flashloan ETH to mint OETH and use that to force the pool to send most of its tokens to the fee collector.
I always liked the Klein bottle team because they were one of the first innovators and their code was simple and mathematically neat. As I expected, the discussion was very smooth and the team decided to award max bounty — $250,000. I was surprised that the founder Michael Egorov handled the submission directly, it was a great experience talking to him.
Emergency fix was implemented soon after and then permanent fixes were proposed for governance voting. Now fee receivers check for reentrancy upon ETH receive, thus withdraw_admin_fees() will revert if we reenter.
Upon checking Curve forks in the wild, I observed that the majority of Solidity-based forks utilize privileged functions like withdrawAdminFees or use WETH instead of raw ETH in the removeLiquidity process. Consequently, these implementations are considered safe from the vulnerability.
The last days of 2023 were relatively quiet with few security incidents. However, personally, it was a period filled with excitement and captivating experiences.
I still feel that there is a relatively small number of serious security researchers compared to the vast amounts of DeFi protocols.
There are many bugs out there, there are opportunities there, but opportunity comes to the prepared mind.
Chance favors the prepared mind, and opportunity favors the bold.
Louis Pasteur, French microbiologist and chemist, during an 1854 lecture at the University of Lille
I am Marco Croc, a lead security researcher at KupiaSec.
KupiaSec is a blockchain security organization that aims to achieve the top quality in security review employing the most advanced security audit process.