Concur Finance contest - ShadowyNoobDev's results

Incentives vote-and-rewards sharing protocol

General Information

Platform: Code4rena

Start Date: 03/02/2022

Pot Size: $75,000 USDC

Total HM: 42

Participants: 52

Period: 7 days

Judge: GalloDaSballo

Total Solo HM: 21

Id: 83

League: ETH

Concur Finance

Findings Distribution

Researcher Performance

Rank: 31/52

Findings: 4

Award: $231.19

๐ŸŒŸ Selected for report: 1

๐Ÿš€ Solo Findings: 0

Findings Information

Awards

31.0722 USDC - $31.07

Labels

bug
duplicate
3 (High Risk)

External Links

Lines of code

https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol#L52

Vulnerability details

[Shelter] Withdraw doesnโ€™t check that claimant has withdrawn.

Impact

It allows a claimant to call Shelter.withdraw() multiple times to drain the pool.

Proof of Concept

Call Shelter.withdraw() multiple times

Add a require check require(!claimed[_token][_to], "unable to claim again"); at the beginning of the withdraw() function to prevent a claimant from making multiple claims.

#0 - GalloDaSballo

2022-04-12T23:34:27Z

Duplicate ofย #246

Awards

7.97 USDC - $7.97

Labels

bug
duplicate
2 (Med Risk)

External Links

Lines of code

https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConcurRewardPool.sol#L34

Vulnerability details

[ConcurRewardPool] Possible reentrancy when claiming rewards

Impact

Since the reward tokens are transferred before the balances are set to 0, it is possible to perform a reentrancy attack if the reward token has some kind of call back functionality e.g. ERC777. pBTC is an ERC777 token that is currently available on Convex. A similar attack occurred with imBTC on uniswap v1.

Proof of Concept

  • Preparation
    1. Assume that pBTC is used as extra rewards for this victim convex pool.
    2. A malicious user interacts with Concur through a smart contract. He follows the standard flow and has some rewards to be claimed.
    3. The malicious user interacts with this smart contract to register a bad tokensToSend() callback function through the ERC-1820 contract.
    4. In this tokensToSend() function, he calls ConcurRewardPool.claimRewards() n-1 more times to drain contract.
  • Attack
    1. When he calls ConcurRewardPool.claimRewards() for the first time, the pBTC reward tokens are transferred.
    2. You can see from the pBTC contract on line 871 that _callTokensToSend(from, from, recipient, amount, "", ""); is called inside the transfer() function.
    3. If you trace to the _callTokensToSend function definition to line 1147, you will notice that it calls IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData); on line 1159.
    4. Since the malicious user already registered a bad tokensToSend() function, this function will be called thus draining majority of the pBTC rewards available on the ConcurRewardPool contract.

You can also find a walkthrough replicating a similar attack here.

  • Use a nonReentrant modifier
  • set balances to 0 first before disbursing the rewards

#0 - GalloDaSballo

2022-04-17T16:14:16Z

The warden has shown how using a specific reward token can lead to reentrnacy for the function claimRewads

Because the finding is contingent on a specific token that enables the exploit, I believe Medium Severity to be appropriate

Awards

127.2691 USDC - $127.27

Labels

bug
QA (Quality Assurance)

External Links

QA report

  • ConvexStakingWrapper
    1. non critical, _getTotalSupply() function name is quite misleading because it doesnโ€™t actually get you the total supply but rather, it gets you the total number of tokens staked in the ConvexStakingWrapper contract.
    2. low, it is possible to avoid rounding errors when calculating d_reward in the _calcRewardIntegral() function by calculating the 20%, storing it in a local variable and then update d_rewards by subtracting this 20% away instead of the current d_reward = (d_reward * 4) / 5;
    3. low, deposit() function has a bad comment in the natspec. Fix should be @param _amount amount to deposit.
    4. low, as confirmed with leekt on discord, this comment //no-op for cvx, crv rewards in the addRewards() function is deprecated and should be removed.

#0 - GalloDaSballo

2022-04-27T13:51:42Z

_getTotalSupply -> Non-critical

Rounding -> Agree

Comment -> Non-critical

Comment iv -> non-critical

Valuable report that ultimately helps the dev, just has very few findings, but I do appreciate the conciseness

#1 - GalloDaSballo

2022-04-27T15:06:45Z

1+++

Awards

64.8796 USDC - $64.88

Labels

bug
G (Gas Optimization)

External Links

Gas optimisation

  • ConvexStakingWrapper
    1. In the addRewards function, you should store IRewardStaking(convexBooster).poolInfo(_pid) into a locally scoped variable so it is cheaper to retrieve mainPool (line 94) and lptoken (line 98).
    2. In the addRewards function, you should use extraToken on line 131 instead of calling IRewardStaking(extraPool).rewardToken() to retrieve it again.
  • MasterChef
    1. This require statement require(_token != address(0), "zero address"); in the add() function is IMO not required because only the owner can call this function.
    2. This require statement require(user.amount > 0, "MasterChef: nothing to withdraw"); in the withdraw() function is not needed because if user.amount is actually 0, the function will still work correctly as intended since the next require statement will ensure that the user cannot withdraw more than his balance.
    3. This if branch if (_amount > 0) in the withdraw() function is also not needed because user.amount is not modified when _amount is 0.

#0 - GalloDaSballo

2022-04-02T13:42:30Z

In the addRewards function, you should store IRewardStaking(convexBooster).poolInfo(_pid) into a locally scoped variable so it is cheaper to retrieve mainPool (line 94) and lptoken (line 98).

Wouldn't actually save gas as both slots are read separately

In the addRewards function, you should use extraToken on line 131 instead of calling IRewardStaking(extraPool).rewardToken() to retrieve it again.

Would save 97 gas

This require statement require(_token != address(0), "zero address"); in the add() function is IMO not required because only the owner can call this function.

Refreshing take but ultimately will give 0 as someone else would argue the opposite and have a point

This require statement require(user.amount > 0, "MasterChef: nothing to withdraw"); in the withdraw() function is not needed because if user.amount is actually 0, the function will still work correctly as intended since the next require statement will ensure that the user cannot withdraw more than his balance.

Same as above, valid argument, no points

This if branch if (_amount > 0) in the withdraw() function is also not needed because user.amount is not modified when _amount is 0.

This saves 3 gas (pays 3 to save 6) 3

#1 - GalloDaSballo

2022-04-02T13:42:38Z

100 gas saved

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