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
Rank: 11/52
Findings: 7
Award: $1,702.44
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: mtz
Also found by: 0x1f8b, Czar102, GalloDaSballo, GeekyLumberjack, Randyyy, Rhynorater, Ruhum, ShadowyNoobDev, bitbopper, cccz, cmichel, csanuragjain, danb, gzeon, hickuphh3, hyh, leastwood
31.0722 USDC - $31.07
csanuragjain
Funds can be stolen by calling withdraw again and again
Navigate to contract at https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol
Observe the withdraw function
function withdraw(IERC20 _token, address _to) external override { require(activated[_token] != 0 && activated[_token] + GRACE_PERIOD < block.timestamp, "shelter not activated"); uint256 amount = savedTokens[_token] * client.shareOf(_token, msg.sender) / client.totalShare(_token); claimed[_token][_to] = true; emit ExitShelter(_token, msg.sender, _to, amount); _token.safeTransfer(_to, amount); }
Change withdraw function like below:
function withdraw(IERC20 _token, address _to) external override { require(!claimed[_token][_to] , "Already claimed"); ... claimed[_token][_to] = true; ... }
#0 - GalloDaSballo
2022-04-12T22:51:17Z
Duplicate of #246
🌟 Selected for report: WatchPug
Also found by: CertoraInc, bobi, csanuragjain, danb, hickuphh3, leastwood
298.6186 USDC - $298.62
csanuragjain
Deposit and withdraw are done over msg.sender instead of _recipient
Navigate to contract https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol
Observe the deposit and withdraw function
UserInfo storage user = userInfo[_pid][_msgSender()];
Change UserInfo storage user to
UserInfo storage user = userInfo[_pid][_recipient];
#0 - GalloDaSballo
2022-04-12T00:15:18Z
The warden identified a logical fallacy in withdraw and deposit, the sponsor confirmed
Am conflicted on raising to high as this kind of breaks the code
#1 - GalloDaSballo
2022-04-12T22:18:36Z
Duplicate of #229
🌟 Selected for report: csanuragjain
Also found by: gzeon
530.9989 USDC - $531.00
csanuragjain
onlyClient can deactivate a token even after deadline is passed and transfer all token balance to itself
Navigate to contract at https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol
Observe that token can only be deactivated if activated[_token] + GRACE_PERIOD > block.timestamp. We will bypass this
onlyClient activates a token X using the activate function
Assume Grace period is crossed such that activated[_token] + GRACE_PERIOD < block.timestamp
Now if onlyClient calls deactivate function, it fails with "too late"
But onlyClient can bypass this by calling activate function again on token X which will reset the timestamp to latest in activated[_token] and hence onlyClient can now call deactivate function to disable the token and retrieve all funds present in the contract to his own address
Add below condition to activate function
function activate(IERC20 _token) external override onlyClient { require(activated[_token]==0, "Already activated"); activated[_token] = block.timestamp; savedTokens[_token] = _token.balanceOf(address(this)); emit ShelterActivated(_token); }
#0 - GalloDaSballo
2022-04-12T22:53:35Z
The warden has identified a way for the client to trick the shelter into sending all tokens to the client, effectively rugging all other users.
Because this is contingent on a malicious client I believe Medium Severity to be more appropriate
🌟 Selected for report: leastwood
Also found by: CertoraInc, Czar102, WatchPug, csanuragjain, hickuphh3, kirk-baird
csanuragjain
User rewards are lost
Navigate to contract at https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol
Observe the updatePool function
Lets say User deposited some amount when block.number = endBlock-x, this will set pool.lastRewardBlock=endBlock -x
Now assume next activity on this contract happens when block.number = endBlock+y, say User makes a deposit which triggers updatePool
updatePool will simply return without updating accConcurPerShare since
if(block.number >= endBlock) { pool.lastRewardBlock = block.number; return; }
Do not return if block.number >= endBlock instead make pool.lastRewardBlock = endBlock. Also change getMultiplier such that
function getMultiplier(uint256 _from, uint256 _to) public view returns (uint256) { if (_to <= endBlock) { return _to.sub(_from); } else if (_from >= endBlock) { return 0; } else { return endBlock.sub(_from); } }
#0 - GalloDaSballo
2022-04-17T14:34:58Z
The warden has identified a way in which the contract will not release rewards that are due for a depositor.
Because the check doesn't accrue until the last eligible block, the reward loss can be quantified as: LastTimeAccrueBeforeEndBlock - endBlock
The finding is valid, but because it pertains to loss of yield, and because the loss can be quantified and reduced by simply calling at the last available block, I believe Medium Severity to be more appropriate
#1 - GalloDaSballo
2022-04-20T00:40:24Z
Duplicate of #107
🌟 Selected for report: hickuphh3
Also found by: 0x1f8b, 0xw4rd3n, BouSalman, CertoraInc, Czar102, Dravee, IllIllI, Randyyy, Rhynorater, Ruhum, ShadowyNoobDev, Sleepy, SolidityScan, WatchPug, bitbopper, cccz, cryptphi, csanuragjain, defsec, gzeon, harleythedog, hubble, hyh, kenta, kirk-baird, leastwood, mtz, pauliax, peritoflores, rfa, robee, samruna, throttle, wuwe1, ye0lde
175.7213 USDC - $175.72
csanuragjain
Contract - https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol
In deactivate function, deactivation is rejected even if activated[_token] + GRACE_PERIOD = block.timestamp even when it should be allowed till Grace period.
Remediation: This should be corrected by changing require condition to below:
require(activated[_token] != 0 && activated[_token] + GRACE_PERIOD >= block.timestamp, "too late");
Contract - https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol
In withdraw function, Zero address checks can be added for to address which can prevent losses
Remediation:
require(to!=address(0), "Incorrect address");
Contract: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol
In withdraw function, Funds will stuck if user deposited a amount and then isDepositor[_depositor] is set to false by Admin. Now user cannot withdraw the amount since onlyDepositor will fail
Remediation: Withdraw should be independent of onlyDepositor
Contract : https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol
In massUpdatePools function, pid 0 should not be updated as poolInfo[_pid] is filled by dummy zero address token in constructor
constructor(IERC20 _concur, uint _startBlock, uint _endBlock) Ownable() { startBlock = _startBlock; endBlock = _endBlock; concur = _concur; poolInfo.push( PoolInfo({ depositToken: IERC20(address(0)), allocPoint : 0, lastRewardBlock : _startBlock, accConcurPerShare : 0, depositFeeBP : 0 })); }
Remediation: The loop in massUpdatePools function should start with value 1 instead of 0
Contract: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol
In safeConcurTransfer function, transfer function is used
Remediation: use safeTransfer instead of transfer which is more secure
Contract : https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol
a. In deposit & withdraw function, both accepts 0 pid which is zero address pool added by constructor, which means user funds would get added to incorrect pool.
b. Similarly non existent pid will also be accepted by both of these functions.
c. Offcourse these incorrect pid will not incur any reward since pool.accConcurPerShare will always be 0 which means user amount is added to a pool without any reward
Remediation: Add a check to see if pid>0 and pid<poolInfo.length
Contract: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/StakingRewards.sol
In notifyRewardAmount function, Since block.timestamp value will change slightly while calculating periodFinish (when compared to lastUpdateTime) so periodFinish will actually become lastUpdateTime+rewardsDuration+x which is incorrect and would impact rewardPerToken by making it slightly higher
lastUpdateTime = block.timestamp; periodFinish = block.timestamp + rewardsDuration;
Remediation: Store block.timestamp locally and then use local variable to update periodFinish and lastUpdateTime
Contract: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol
In addRewards function, it is not checked if extraToken is crv. Only cvx check is present
Remediation: Add below check
if (extraToken == crv) {rewards[_pid][CRV_INDEX].pool = extraPool;}
Contract: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol
Non existent pid will create crv and cvx entries for rewards[_pid] since rewards[_pid].length == 0
Remediation: Revert if mainPool is zero address, require (mainPool!=address(0));
Contract: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConcurRewardPool.sol
In claimRewards function observe that No emit event is fired after successful reward claim by user. Ideally a new event should be triggered showing that reward was claimed successfully by user
Contract: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/StakingRewards.sol
In notifyRewardAmount function, if Admin added a reward 100 once block.timestamp >= periodFinish. Now if Admin decides to add 200 rewards calling this function at block.timestamp >= periodFinish then contract considers total reward as 200 and discards the reward 100 added initially
if (block.timestamp >= periodFinish) { rewardRate = reward / rewardsDuration; // old reward is not considered }
#0 - GalloDaSballo
2022-04-24T15:23:19Z
Incorrect Condition Finding: Imprecise by 1 second valid but non-critical
Zero Address check missing Fair
User fund stuck Dup of #238 (med)
Incorrect PID updation I don't believe this has any impact beside wasting gas
Insecure transfer method used Agree
User funds can be added to 0 or non-existent pid with 0 rewards Technically this can happen only in niche cases, in lack of impact I agree with non-critical
Extra reward will be given to users TX happens in the same block at the same time, there is no passing of time during a transaction, have to disagree with this one
Missing condition: Not sure if done on purpose by the sponsor but interesting finding
rewards entries are made even for incorrect/non-existent pid I think this is mostly an oddity of the system but has no actual impact
Reward lost Dup of #107 (med)
#1 - GalloDaSballo
2022-04-27T14:57:23Z
2+++++
#2 - JeeberC4
2022-04-28T03:31:56Z
@CloudEllie please create 2 new issue for the Med findings above.
#3 - JeeberC4
2022-04-28T04:04:19Z
Generating QA Report for warden, preserving original title: Non critical & Low findings
#4 - CloudEllie
2022-04-28T20:52:04Z
Created separate issues for upgraded findings: #267 and #268
🌟 Selected for report: WatchPug
Also found by: 0x0x0x, 0x1f8b, 0x510c, 0xNot0rious, 0xngndev, BouSalman, CertoraInc, Dravee, Heartless, IllIllI, Jujic, Randyyy, Ruhum, ShadowyNoobDev, Sleepy, SolidityScan, Tomio, bitbopper, csanuragjain, defsec, gzeon, hickuphh3, kenta, mtz, pauliax, peritoflores, rfa, robee, sabtikw, throttle, wuwe1, ye0lde
257.8374 USDC - $257.84
csanuragjain
Gas savings
https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConcurRewardPool.sol claimRewards function : use ++i instead of i++ in loop claimRewards function : Save _tokens[i] in local variable and then use the local variable within loop claimRewards function : Add check require(getting>0, "No reward left for token "+_tokens[i]) after line 36
https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol withdraw function : claimed[_token][_to] is never used
https://github.com/code-423n4/2022-02-concur/blob/main/contracts/USDMPegRecovery.sol withdraw function : Add check to see user balance is enough - require(user.usdm>=_withdrawal.usdm && user.pool3>=_withdrawal.pool3,"Insuffiencient balance")
https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol pendingConcur function : Condition is missing for allocPoint being 0 - Change if (block.number > pool.lastRewardBlock && lpSupply != 0) to if (block.number > pool.lastRewardBlock && lpSupply != 0 && pool.allocPoint != 0) pendingConcur/deposit/withdraw function : Add a check require(pid<=poolInfo.length - 1) preventing invalid pid deposit/withdraw function : Add check require(_amount>0) preventing further actions
https://github.com/code-423n4/2022-02-concur/blob/main/contracts/StakingRewards.sol rewardPerToken function : Add below condition so that function reverts - if(lastTimeRewardApplicable() <= lastUpdateTime){return rewardPerTokenStored;}
https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol deposit/withdraw function : add a condition to revert if amount=0
#0 - GalloDaSballo
2022-03-31T00:59:20Z
claimRewards function : use ++i instead of i++ in loop 2 per loop
claimRewards function : Save _tokens[i] in local variable and then use the local variable within loop Agree as this will avoid the length check, 3 per time, so 9gas
claimRewards function : Add check require(getting>0, "No reward left for token "+_tokens[i]) after line 36 100 for the CALL and 2200 for the values being unchanged (Hot read + 100 for set value to same value)
withdraw function : claimed[_token][_to] is never used This will avoid a needles SSTORE, so 20k gas
withdraw function : Add check to see user balance is enough - require(user.usdm>=_withdrawal.usdm && user.pool3>=_withdrawal.pool3,"Insuffiencient balance")
Ultimately I disagree with this one as the tx will revert due to underflow if trying to withdraw more than available. This increases gas cost when the tx doesn't revert.
I don't think the rest of the findings will save any gas.
The formatting on this report is bad, would recommend the warden to use a markdown editor
#1 - GalloDaSballo
2022-03-31T00:59:51Z
Total Gas saved: 22311