Platform: Code4rena
Start Date: 14/10/2022
Pot Size: $100,000 USDC
Total HM: 12
Participants: 75
Period: 9 days
Judge: GalloDaSballo
Total Solo HM: 1
Id: 171
League: ETH
Rank: 2/75
Findings: 3
Award: $13,556.88
🌟 Selected for report: 2
🚀 Solo Findings: 1
0.3268 USDC - $0.33
LBToken._transfer
caches the _fromBalance
and _toBalance
and then updates the _balances
mapping based on these cached values. This does not work correctly when _from == _to
. In this case, we have (for _from == _to == address(A)
:
_balances[_id][address(A)] = _fromBalance - _amount; _balances[_id][address(A)] = _toBalance + _amount;
The first decrement of the balance is therefore simply discarded and the balance is only incremented. An attacker can double his own balance like this.
The following diff shows a scenario where an attacker doubles his own balance by a self-transfer:
--- a/test/LBToken.t.sol +++ b/test/LBToken.t.sol @@ -91,6 +91,9 @@ contract LiquidityBinTokenTest is TestHelper { assertEq(pair.balanceOf(DEV, _ids[0]), 0); assertEq(pair.balanceOf(ALICE, _ids[0]), 0); assertEq(pair.balanceOf(BOB, _ids[0]), amountIn / 3); + vm.prank(BOB); + pair.safeTransferFrom(BOB, BOB, _ids[0], amountIn / 3); + assertEq(pair.balanceOf(BOB, _ids[0]), amountIn / 3 * 2); } function testSafeBatchTransferNotApprovedReverts() public {
Do not allow self transfers.
#0 - trust1995
2022-10-23T21:56:53Z
Dup of #422
#1 - GalloDaSballo
2022-10-26T16:35:49Z
#2 - c4-judge
2022-11-23T18:28:33Z
GalloDaSballo marked the issue as not a duplicate
#3 - c4-judge
2022-11-23T18:29:08Z
GalloDaSballo marked the issue as duplicate of #299
#4 - Simon-Busch
2022-12-05T06:39:53Z
Marked this issue as Satisfactory as requested by @GalloDaSballo
🌟 Selected for report: Lambda
13555.2857 USDC - $13,555.29
LBRouter.removeLiquidity
reorders tokens when the user did not pass them in the pair order (ascending order):
if (_tokenX != _LBPair.tokenX()) { (_tokenX, _tokenY) = (_tokenY, _tokenX); (_amountXMin, _amountYMin) = (_amountYMin, _amountXMin); }
However, when returning amountX
and amountY
, it is ignored if the order was changed:
(amountX, amountY) = _removeLiquidity(_LBPair, _amountXMin, _amountYMin, _ids, _amounts, _to);
Therefore, when the order of the tokens is swapped by the function, the return value amountX
("Amount of token X returned") in reality is the amount of the user-provided token Y that is returned and vice versa.
Because this is an exposed function that third-party protocols / contracts will use, this can cause them to malfunction. For instance, when integrating with Trader Joe, something natural to do is:
(uint256 amountAReceived, uint256 amountBReceived) = LBRouter.removeLiquidity(address(tokenA), address(tokenB), ...); contractBalanceA += amountAReceived; contractBalanceB += amountBReceived;
This snippet will only be correct when the token addresses are passed in the right order, which should not be the case. When they are not passed in the right order, the accounting of third-party contracts will be messed up, leading to vulnerabilities / lost funds there.
First consider the following diff, which shows a scenario when LBRouter
does not switch tokenX
and tokenY
, resulting in correct return values:
--- a/test/LBRouter.Liquidity.t.sol +++ b/test/LBRouter.Liquidity.t.sol @@ -57,7 +57,9 @@ contract LiquidityBinRouterTest is TestHelper { pair.setApprovalForAll(address(router), true); - router.removeLiquidity( + uint256 token6BalBef = token6D.balanceOf(DEV); + uint256 token18BalBef = token18D.balanceOf(DEV); + (uint256 amountFirstRet, uint256 amountSecondRet) = router.removeLiquidity( token6D, token18D, DEFAULT_BIN_STEP, @@ -70,7 +72,9 @@ contract LiquidityBinRouterTest is TestHelper { ); assertEq(token6D.balanceOf(DEV), amountXIn); + assertEq(amountXIn, token6BalBef + amountFirstRet); assertEq(token18D.balanceOf(DEV), _amountYIn); + assertEq(_amountYIn, token18BalBef + amountSecondRet); } function testRemoveLiquidityReverseOrder() public {
This test passes (as it should). Now, consider the following diff, where LBRouter
switches tokenX
and tokenY
:
--- a/test/LBRouter.Liquidity.t.sol +++ b/test/LBRouter.Liquidity.t.sol @@ -57,12 +57,14 @@ contract LiquidityBinRouterTest is TestHelper { pair.setApprovalForAll(address(router), true); - router.removeLiquidity( - token6D, + uint256 token6BalBef = token6D.balanceOf(DEV); + uint256 token18BalBef = token18D.balanceOf(DEV); + (uint256 amountFirstRet, uint256 amountSecondRet) = router.removeLiquidity( token18D, + token6D, DEFAULT_BIN_STEP, - totalXbalance, totalYBalance, + totalXbalance, ids, amounts, DEV, @@ -70,7 +72,9 @@ contract LiquidityBinRouterTest is TestHelper { ); assertEq(token6D.balanceOf(DEV), amountXIn); + assertEq(amountXIn, token6BalBef + amountSecondRet); assertEq(token18D.balanceOf(DEV), _amountYIn); + assertEq(_amountYIn, token18BalBef + amountFirstRet); } function testRemoveLiquidityReverseOrder() public {
This test should also pass (the order of the tokens was only switched), but it does not because the return values are mixed up.
Add the following statement in the end:
if (_tokenX != _LBPair.tokenX()) { return (amountY, amountX); }
#0 - GalloDaSballo
2022-11-14T14:18:33Z
The Warden has shown how, due to an inconsistent re-ordering, the removeLiqudity function can return incorrect (swapped) amounts.
While invariants are not broken, this is an example of an incorrect function behaviour.
For this reason, despite no loss of value, I believe Medium Severity to be appropriate as the potential impact warrants an increased severity.
#1 - c4-judge
2022-11-14T14:18:40Z
GalloDaSballo marked the issue as selected for report
1.2646 USDC - $1.26
In LBToken._burn
, the _beforeTokenTransfer
hook is called with from = address(0)
and to = _account
:
_beforeTokenTransfer(address(0), _account, _id, _amount);
Through a lucky coincidence, it turns out that this in the current setup does not cause a high severity issue. _burn
is always called with _account = address(this)
, which means that LBPair._beforeTokenTransfer
is a NOP. However, this wrong call is very dangerous for future extensions or protocol that built on top of the protocol / fork it.
Let's say the protocol is extended with some logic that needs to track mints / burns. The canonical way to do this would be:
function _beforeTokenTransfer( address _from, address _to, uint256 _id, uint256 _amount ) internal override(LBToken) { if (_from == address(0)) { // Mint Logic } else if (_to == address(0)) { // Burn Logic } }
Such an extension would break, which could lead to loss of funds or a bricked system.
Call the hook correctly:
_beforeTokenTransfer(_account, address(0), _id, _amount);
#0 - GalloDaSballo
2022-11-11T21:21:28Z
The warden has shown how, due to a typo / programming mistake, the hook for burning tokens is called with incorrect parameters.
Because of the caller being the pair, this ends up having reduced impact.
#1 - c4-judge
2022-11-11T21:21:38Z
GalloDaSballo marked the issue as primary issue
#2 - c4-judge
2022-11-11T21:21:45Z
GalloDaSballo marked the issue as selected for report
#3 - Simon-Busch
2022-12-05T06:26:54Z
Marked this issue as Satisfactory as requested by @GalloDaSballo
#4 - Simon-Busch
2022-12-05T06:34:56Z
Revert, wrong action