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: 20/75
Findings: 2
Award: $281.07
🌟 Selected for report: 1
🚀 Solo Findings: 0
1.2646 USDC - $1.26
https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L485-L521
When calling the swapAVAXForExactTokens
function, if (msg.value > amountsIn[0]) _safeTransferAVAX(_to, amountsIn[0] - msg.value)
is executed, which is for refunding any excess amount sent in; this is confirmed by this function's comment as well. However, executing amountsIn[0] - msg.value
will always revert when msg.value > amountsIn[0]
is true. Developers who has the design of the swapAVAXForExactTokens
function in mind could develop front-ends and contracts that will send excess amount when calling the swapAVAXForExactTokens
function. Hence, the users, who rely on these front-ends and contracts for interacting with the swapAVAXForExactTokens
function will always find such interactions being failed since calling this function with the excess amount will always revert. As a result, the user experience becomes degraded, and the usability of the protocol becomes limited.
https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L485-L521
/// @notice Swaps AVAX for exact tokens while performing safety checks /// @dev will refund any excess sent ... function swapAVAXForExactTokens( uint256 _amountOut, uint256[] memory _pairBinSteps, IERC20[] memory _tokenPath, address _to, uint256 _deadline ) external payable override ensure(_deadline) verifyInputs(_pairBinSteps, _tokenPath) returns (uint256[] memory amountsIn) { ... if (msg.value > amountsIn[0]) _safeTransferAVAX(_to, amountsIn[0] - msg.value); }
Please add the following test in test\LBRouter.Swaps.t.sol
. This test will pass to demonstrate the described scenario.
function testSwapAVAXForExactTokensIsUnableToRefund() public { uint256 amountOut = 1e18; (uint256 amountIn, ) = router.getSwapIn(pairWavax, amountOut, false); IERC20[] memory tokenList = new IERC20[](2); tokenList[0] = wavax; tokenList[1] = token6D; uint256[] memory pairVersions = new uint256[](1); pairVersions[0] = DEFAULT_BIN_STEP; vm.deal(DEV, amountIn + 500); // Although the swapAVAXForExactTokens function supposes to refund any excess sent, // calling it reverts when sending more than amountIn // because executing _safeTransferAVAX(_to, amountsIn[0] - msg.value) results in arithmetic underflow vm.expectRevert(stdError.arithmeticError); router.swapAVAXForExactTokens{value: amountIn + 1}(amountOut, pairVersions, tokenList, DEV, block.timestamp); }
VSCode
https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L520 can be updated to the following code.
if (msg.value > amountsIn[0]) _safeTransferAVAX(_to, msg.value - amountsIn[0]);
#0 - Shungy
2022-10-24T06:00:56Z
I believe this finding to be valid.
The issue exists in this line: https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBRouter.sol#L520
Also “Sending excess amount” would be happening regardless of the interface, due to changing price. So this issue effects anyone using this router.
Since this would only cause revert issues in a contract that can be easily replaced, I believe a severity low or medium to be appropriate. For example this was submitted as Low by @trust1995 : https://github.com/code-423n4/2022-10-traderjoe-findings/issues/418
#1 - GalloDaSballo
2022-10-26T18:26:21Z
Best submission because has coded POC, and is brief and clear
#2 - c4-judge
2022-11-10T15:41:10Z
GalloDaSballo marked the issue as primary issue
#3 - GalloDaSballo
2022-11-13T19:53:29Z
The finding is valid, sending more than necessary will revert. No loss to end-users was shown, and the revert will prevent the tx from finishing.
Because of that, considering the fact that a accurate measure could be calculated, meaning that functionality can be side-stepped, despite recommending fixing, I think the finding is QA - Low Severity as the revert is self-inflicted
#4 - GalloDaSballo
2022-11-13T19:53:31Z
L
#5 - c4-judge
2022-11-13T19:53:38Z
#6 - GalloDaSballo
2022-11-20T22:28:56Z
Per discussion from backstage I've re-reviewed the finding.
I have poked around with the test provided and tweaked it a little, this is the last one I have
function testSwapAVAXForExactTokensIsUnableToRefund() public { uint256 amountOut = 1e18; (uint256 amountIn, ) = router.getSwapIn(pairWavax, amountOut, false); IERC20[] memory tokenList = new IERC20[](2); tokenList[0] = wavax; tokenList[1] = token6D; uint256[] memory pairVersions = new uint256[](1); pairVersions[0] = DEFAULT_BIN_STEP; vm.deal(DEV, 50 * amountIn + 500); // Although the swapAVAXForExactTokens function supposes to refund any excess sent, // calling it reverts when sending more than amountIn // because executing _safeTransferAVAX(_to, amountsIn[0] - msg.value) results in arithmetic underflow vm.expectRevert(stdError.arithmeticError); router.swapAVAXForExactTokens{value: amountIn + 1}(amountOut, pairVersions, tokenList, DEV, block.timestamp); router.swapAVAXForExactTokens{value: amountIn}(amountOut, pairVersions, tokenList, DEV, block.timestamp); vm.expectRevert(stdError.arithmeticError); router.swapAVAXForExactTokens{value: amountIn + 100000}(amountOut, pairVersions, tokenList, DEV, block.timestamp); }
Ultimately we can conclude that any amount below amountIn
will result in a revert due to insufficient amountOut, and any amount above amountIn
will revert due to the highlighted overflow.
The function is a routing function, which is meant to allow some degree of slippage.
My original downgrade was based on the idea that the function can run, however, after it being flagged, I must agree that the availability of the function is impaired in the majority of ordinary cases (a .10% / .5% slippage is expected under most operations / FEs)
For this reason, I agree with upgrading back to Medium Severity.
I'd like to thank @shung and hyh for their feedback
#7 - Simon-Busch
2022-11-21T06:10:12Z
Reverted to M as requested by @GalloDaSballo
#8 - Simon-Busch
2022-11-21T06:37:43Z
Duplicates:
#9 - c4-judge
2022-11-23T21:05:02Z
GalloDaSballo marked the issue as selected for report
#10 - Simon-Busch
2022-12-05T06:27:27Z
Marked this issue as Satisfactory as requested by @GalloDaSballo
#11 - Simon-Busch
2022-12-05T06:41:32Z
revert, wrong action
279.8109 USDC - $279.81
https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBPair.sol#L304-L413 https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/libraries/TokenHelper.sol#L40-L52 https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/libraries/TokenHelper.sol#L74-L81
When calling the swap
function, tokenY.safeTransfer(_to, _amountOut)
or tokenX.safeTransfer(_to, _amountOut)
is executed, where the TokenHelper
library's safeTransfer
function is used. According to the implementation of the TokenHelper
library's safeTransfer
function, tokenY
and tokenX
's contract existences are not checked. It is possible that a token becomes non-existent in the future; for instance, if some bugs are found for the output token, it is possible that the output token will be destroyed through calling its selfdestruct
function after migrating its data to another contract for fixing these bugs. When this happens, such output token becomes non-existent but calling the swap
function to swap for it does not revert. As a result, the user who swaps for a non-existent output token will lose the input token amount while receiving no output token amount.
https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBPair.sol#L304-L413
function swap(bool _swapForY, address _to) external override nonReentrant returns (uint256 amountXOut, uint256 amountYOut) { ... if (_swapForY) { amountYOut = _amountOut; tokenY.safeTransfer(_to, _amountOut); } else { amountXOut = _amountOut; tokenX.safeTransfer(_to, _amountOut); } }
https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/libraries/TokenHelper.sol#L40-L52
function safeTransfer( IERC20 token, address recipient, uint256 amount ) internal { if (amount != 0) { (bool success, bytes memory result) = address(token).call( abi.encodeWithSelector(token.transfer.selector, recipient, amount) ); _catchTransferError(success, result); } }
https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/libraries/TokenHelper.sol#L74-L81
function _catchTransferError(bool success, bytes memory result) private pure { // Look for revert reason and bubble it up if present if (!(success && (result.length == 0 || abi.decode(result, (bool))))) { assembly { revert(add(32, result), mload(result)) } } }
First, in test\mocks\
, please add the following mock token contract.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './ERC20.sol'; contract ERC20MockWithSelfdestruct is ERC20Mock { constructor() ERC20Mock("Test", "T", 18) {} function kill() external { selfdestruct(payable(msg.sender)); } }
Then, please add the following code in test\LBPair.Swaps.t.sol
.
ERC20MockWithSelfdestruct
as follows.import "./mocks/ERC20MockWithSelfdestruct.sol";
LiquidityBinPairSwapsTest
contract.LBPair pair_; ERC20MockWithSelfdestruct erc20MockWithSelfdestruct; uint256 amountXIn_;
setUp
function. Note this will break other tests so please only use this setUp
code for this test.function setUp() public { token6D = new ERC20MockDecimals(6); token18D = new ERC20MockDecimals(18); factory = new LBFactory(DEV, 8e14); ILBPair _LBPairImplementation = new LBPair(factory); factory.setLBPairImplementation(address(_LBPairImplementation)); setDefaultFactoryPresets(DEFAULT_BIN_STEP); addAllAssetsToQuoteWhitelist(factory); router = new LBRouter(ILBFactory(DEV), IJoeFactory(DEV), IWAVAX(DEV)); pair = createLBPairDefaultFees(token6D, token18D); /** add the following code for POC purpose */ // deploy the ERC20 mock token, which has the selfdestruct functionality erc20MockWithSelfdestruct = new ERC20MockWithSelfdestruct(); factory.addQuoteAsset(erc20MockWithSelfdestruct); // create a pair with token6D and erc20MockWithSelfdestruct pair_ = createLBPairDefaultFees(token6D, erc20MockWithSelfdestruct); uint256 tokenAmount = 100e18; erc20MockWithSelfdestruct.mint(address(pair_), tokenAmount); uint256[] memory _ids = new uint256[](1); _ids[0] = ID_ONE; uint256[] memory _liquidities = new uint256[](1); _liquidities[0] = Constants.PRECISION; pair_.mint(_ids, new uint256[](1), _liquidities, DEV); amountXIn_ = 10e18; (uint256 amountYOut_, ) = router.getSwapOut(pair_, amountXIn_, true); // ALICE has amountXIn_*2 of token6D at this moment token6D.mint(ALICE, amountXIn_ * 2); // ALICE swaps amountXIn_ of token6D for erc20MockWithSelfdestruct vm.startPrank(ALICE); token6D.transfer(address(pair_), amountXIn_); pair_.swap(true, ALICE); vm.stopPrank(); // after this swap, ALICE has amountXIn_ of token6D left and has received amountYOut_ of erc20MockWithSelfdestruct assertEq(token6D.balanceOf(ALICE), amountXIn_); assertEq(erc20MockWithSelfdestruct.balanceOf(ALICE), amountYOut_); // simulate a situation where mockERC20WithSelfdestruct is destroyed after its data is migrated to another contract for fixing some bugs erc20MockWithSelfdestruct.kill(); /** */ }
function testLoseInputTokenAndNotReceiveOutputTokenWhenOutputTokenBecomesNonExistent() public { // ALICE swaps amountXIn_ of token6D for erc20MockWithSelfdestruct again vm.startPrank(ALICE); token6D.transfer(address(pair_), amountXIn_); pair_.swap(true, ALICE); vm.stopPrank(); // after ALICE's second swap, ALICE has no token6D left assertEq(token6D.balanceOf(ALICE), 0); // however, ALICE has not received any erc20MockWithSelfdestruct in return this time since erc20MockWithSelfdestruct does not exist anymore vm.expectRevert(bytes("")); erc20MockWithSelfdestruct.balanceOf(ALICE); }
VSCode
In the TokenHelper
library's safeTransfer
function, the existence of the contract for token
should be checked before performing the low-level call. If such contract does not exist, calling this function should revert.
#0 - Minh-Trng
2022-10-23T20:35:48Z
the swap function is supposed to be called from periphery contracts that performs safety checks (such as slippage). in this case it would revert
#1 - Shungy
2022-10-24T05:43:11Z
I also find this finding to be invalid.
Agreed with @Minh-Trng .
Additionally having a malicious token in the pair is not something that can be solved by LB. You cannot stop honeypots and such either.
#2 - GalloDaSballo
2022-10-27T21:33:20Z
Neat quality but making it dup of https://github.com/code-423n4/2022-10-traderjoe-findings/issues/376 as it's briefer
#3 - GalloDaSballo
2022-11-13T19:34:55Z
L
#4 - c4-judge
2022-11-13T19:35:05Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#5 - c4-judge
2022-11-16T21:07:19Z
GalloDaSballo marked the issue as grade-c
#6 - c4-judge
2022-11-16T21:08:12Z
GalloDaSballo marked the issue as grade-b