Basin - LokiThe5th's results

A composable EVM-native decentralized exchange protocol.

General Information

Platform: Code4rena

Start Date: 03/07/2023

Pot Size: $40,000 USDC

Total HM: 14

Participants: 74

Period: 7 days

Judge: alcueca

Total Solo HM: 9

Id: 259

League: ETH

Basin

Findings Distribution

Researcher Performance

Rank: 11/74

Findings: 1

Award: $543.14

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Eeyore

Also found by: Brenzee, LokiThe5th, Trust, oakcobalt, pontifex

Labels

bug
3 (High Risk)
satisfactory
duplicate-136

Awards

543.1424 USDC - $543.14

External Links

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/9403cf973e95ef7219622dbbe2a08396af90b64c/src/Well.sol#L590

Vulnerability details

Impact

Wells have the ability to shift funds to other Wells as part of gas-efficient multi-pool swaps. [This natspec explanation of this can be find here](insert link). The sync function is intended to synchronize the underlying token amounts with the token reserves of the Well. But there is a bug which allows the next user calling swapFrom, with the target token set as the same as the token that was received, to receive all the funds added to the reserves from calling sync.

When tokens are transferred into the Well, through shift's from another Well, a call to sync should update the reserves for that specific token.

This code updates the reserves in sync:

reserves[i] = _tokens[i].balanceOf(address(this));

But a user may inadvertently transfer the received tokens out if they are swapping into the same token that was received by the Well.

The issue arises because sync does not update the totalSupply (i.e. it does not mint new LP tokens), so if the token the user is swapping to is the same token that was updated with sync (let's say tokenA was transferred and reserve[tokenA] was updated with sync), the Well can't account for the updated reserves of tokenA because the calculation for reserves[tokenA] in from calcReserve in ConstantProduct2.sol uses the lpTokenSupply ** 2 (which can't be updated via sync) and the resesrves[tokenB] (which wasn't affected by sync as no tokenB was received).

This means that the incorrect calculation result from calcReserve (which derives the expected reserves from the totalSupply of LP Tokens and the reserves[tokenB]) gets subtracted from reserveJBefore (which holds the updated reserves). This effectively sweeps the tokenA received from direct transfers (or shifts) and accounted for with sync into the next swapFrom.

As shift and sync are likely to be called as part of regular token swaps, this bug is very likely to occur in production and may cause the protocol in it's aggregate (due to the nature of Well's shifting funds for gas-efficiency) to lose funds. For this reason it was submitted as high impact.

Proof of Concept

The below code can be placed inside the Well.AddLiquidity.t.sol file, but it requires a slight modification to the TestHelper.sol file to mint 186000 token0 and 100 token1 to the Well when setting up liquidity - this is to simulate a DAI-WETH pair for this PoC. The PoC below shows a user exploiting the issue to receive 10 WETH in exchange for 10 DAI.

function test_PoC_Sync_Interceptor() public prank(user) { IERC20[] memory _tokens = well.tokens(); /// Let's send tokens to this address to keep the outputs clean address manipulationRecipient = address(0x30); /// Tokens get sent to the protocol directly, either via `shift` from another `Well` or through other means _tokens[1].transfer(address(well), 10 ether); /// `sync` is called to add these funds to the token reserves well.sync(); /// A user calls a normal `swapFrom` for a small amount and recieves the amount that was transferred to the well directly. console.log("Attacker Captures Funds: ", well.swapFrom(_tokens[0], _tokens[1], 10 * 10**18, 0, manipulationRecipient, type(uint256).max)); /// The user who had 0 WETH now has more than 10 WETH assertGt(_tokens[1].balanceOf(manipulationRecipient), 10 ether); }

Tools Used

Manual code review. Foundry.

It is recommended that sync be reworked to take into account the fact that lpTokenSupply should update in line with the token reserves.

Assessed type

Math

#0 - c4-pre-sort

2023-07-13T10:56:39Z

141345 marked the issue as duplicate of #136

#1 - alcueca

2023-08-03T08:04:43Z

Correct, but misses the point that this can be used by an attacker for a greater loss of funds.

#2 - c4-judge

2023-08-03T08:04:48Z

alcueca marked the issue as partial-50

#3 - lokithe5th

2023-08-10T11:53:38Z

Thank you for the comment Judge. I would respectfully raise the following point with regards to the partial credit:

The intention was to show that this could happen during normal operation too, when wells shift() funds to other wells, not only during attacks. This makes the issue even more insidious than a potential attack vector: the issue would occur regardless if an attacker became aware of it. The Well is "primed" for the issue to occur after shift has been called. The next user (friend or foe) which calls swapFrom would profit from it.

#4 - alcueca

2023-08-15T20:52:08Z

The vulnerability described is a very slow drain on the protocol funds. While there are losses, they would be detected early on and a complete drain would be avoided.

If this vulnerability would have been reported on its own, it would still have been a High. In addition, the report includes a working PoC which is appreciated.

I am now of the mind that penalizing this report just because another report managed to find a higher impact, both being High, is unfair. Therefore I'm restoring the full credit.

#5 - c4-judge

2023-08-15T20:52:15Z

alcueca marked the issue as full credit

#6 - c4-judge

2023-08-15T21:06:51Z

alcueca marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter