Different parsers, different results
TLDR; I found a critical vulnerability on GearBox protocol, result from different parsers of path parameters used between GearBox adapter and UniswapV3 Router.
Parsing confusion is one of techniques used to bypass various levels of protection. It occurs when two system use different parsers and get different results despite sharing the same input.
I found a critical vulnerability on a platform called GearBox, result from this parsing confusion of path paremater between GearBox adapter and UniswapV3 router.
I never saw this type of bug in DeFi space before, so after the fix is deployed and with team permission, I decided to publish this in the hope that it would benefit DeFi community in some ways.
What is Gearbox?
Gearbox is a generalized composable leverage protocol. To simply put it, it allows you to take leverage from your collateral assets and use borrowed funds (upto 3–4x) through CreditAccount across DeFi (Only allowed DeFi protocol, such as Uniswap V2/V3, Curve, Yearn).
Say, you want to go long on ETH. What you can do is:
(1) Provide a stablecoin (DAI, USDC) as a collateral and use leverage to borrow more stablecoin
(2) All your funds (collateral + borrowed) are now in your Credit Account (a smart contract, acting interface to outside DeFi protocol)
(3) Interact with CreditManager contract to use funds in your Credit Account to swap your stablecoin to ETH using Uniswap, Curve
Health Check is a very common (or must-have) functionality you can encounter on every lending & borrowing platform. It’s a critical protective mechanism to prevent platform from being Rekt.
For those who are not familiar with this, it’s a functionality to check that user’s account is solvent or not.
Needless to say, Gearbox also implements this functionality. If there is no this so-called Health Check, someone can just borrow the funds, swap to some useless ERC20 token and the protocol funds are lost forever in DeFi deep dark liquidity (this is obviously an exaggeration).
To prevent those actions to rekt the protocol, Gearbox only allows users to use their funds in CreditAccount through adapters. This way, they can be certain that user actions won’t lower their CreditAccount health to the point of no return.
Here is how they do it. (this is also a snippet of code where the vulnerability was found)
(1) exactOutput function is called by user
(2) Get user’s Credit Account address from Credit Manager
(3) Extract input token (tokenIn) and output token (tokenOut) from path (bytes32)
(4) Make sure that Uniswap router has an allowance to use token on CreditAccount
(5) Take a snapshot of token balance before the swap
(6) Perform swap operation (Basically, it tells CreditAccount to call exactOutput on Uniswap Router)
(7) Account’s health is checked to ensure that the value of output token bought is roughly the same or more than the value of input token sold after swap operation.
So, if health check is somehow not working, a malicious actor can steal funds from the pool by borrowing a large amount of fund and swap it for their controlled fake ERC-20 token.
and that’s the goal I set when I was looking for a vulnerability.
Never ever trust user input
What I love about smart contract vulnerability is that most of the time, I can control inputs of a function and it is almost always a malicious user’s input that could cause the problem.
In this function
exactOutput(ExactOutputParams calldata params) , I can control most of the parameters being sent to UniswapV3 Router except for
receipient that’s replaced in
paramsUpdate.recipient = creditAccount .
So, the goal is to bypass CreditAccount health check, and I can control these inputs.
I don’t know how I got an idea, maybe it came from my experiences with web vulnerability. My idea is that if somehow I could trick both contracts to use different tokenIn address, I then could bypass health check functionality.
To illustrate more, if I could trick Gearbox adapter to use
DAI as tokenIn and Uniswap Router to use
WETH as tokenIn.
WETH token in my CreditAccount will be used to swap for an output token but the value of
DAI sold for health check is
0 , so any value of an output token would pass the health check.
Different parsers, different results
With the idea, I went after both Gearbox and Uniswap code and noticed a different between their path parsers (how they extract tokens address from
With different method of parsing, it would definitely give different result for some payloads.
After spending some time thinking, I came up with a payload that could cause a discrepancy between the two parsers.
abi.encodePacked(WBTC, poolFee, WETH, DAI)
tokenIn = DAI , since it takes last 20 bytes as
tokenIn = WETH , since it takes from offset 23
and with different
tokenIn , as metioned above, a health check function could be bypassed.
In order to make a profitable exploit, attacker could
(1) Deploy a fake token on Uniswap
(2) Provide small liquidity to
fake/WETH with difference prices
1 fake = 1 WETH and
1 fake = 0.0000000001 WBTC
(3) Make a swap with this path payload
abi.encodePacked(WBTC, poolFee, fake, poolFee, WETH, DAI)
(4) The result would be like an attacker swap
1 WETH for
0.0000000001 WBTC and it would still pass health check.
(5) Attacker can then mint a lot of fake token and claim those lost
WETH from the pool.
Considering the impact of this bug <total funds in the pool was about 10m $>, I decided to submit a report to ImmuneFi right away on Friday. ImmuneFi quickly escalated the issue, the team responded, verify the bug within an hour and start the fixing.
The fix was deployed in about 6 hours, and I got paid bounty the next day. Kudos to both Gearbox team and ImmuneFi for quick response and resolve of the issue.
March 25 at 4:08 pm — Report sent
March 25 at 4:18 pm — ImmuneFi escalated the issue to the team
March 25 at 4:31 pm — Team responded
March 25 at 5:54 pm — Team confirmed that the bug is valid, contracts are paused
March 25 at 10:11 pm — Fix deployed
March 26 at 8:46 pm — Bounty paid in maximum
Link to bug bounty program: https://immunefi.com/bounty/gearbox/