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: 7/52
Findings: 7
Award: $2,886.19
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 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
Shelter withdraw
does not check if user already claimed. This allow any user with non-zero claim to drain the Shelter.
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); }
require(!claimed[_token][_to], "already claimed");
#0 - r2moon
2022-02-18T00:55:58Z
#1 - GalloDaSballo
2022-04-19T13:44:29Z
Dup of #246
🌟 Selected for report: GalloDaSballo
716.8485 USDC - $716.85
After depositing into Curve, underlying USDM and pool3 of each LP share will change due to
However, userLiquidity
does not update according to current underlying LP balance. When guardian remove liquidity from Curve, at least 1 of the token would have an increased balance. These balance cannot be claimed by anyone because sum of userLiquidity represent the old balance. If any of the token have a decreased balance (e.g. caused by pool imbalance), withdrawal would be served in first-come-first-served basis. Any gained tokens are stuck in the contract.
A share type accounting would be prefered, but can be complex considering there are 2 tokens.
#0 - GalloDaSballo
2022-04-19T01:17:30Z
Duplicate of #70
🌟 Selected for report: gzeon
1179.9976 USDC - $1,180.00
In README.me:
USDM deposits are locked based on the KPI’s from carrot.eth
However, USDM deposits are also locked until guardian remove liquidity because there are no mechanism to remove deposited USDM in withdraw
#0 - GalloDaSballo
2022-04-04T23:21:08Z
The warden has identified Admin Privilege in that the deposit contract is controlled by the admin and liquidity LPd into the Curve Pool cannot be withdrawn by user (although code for redemption is present).
Ultimately a refactoring that transforms this contract in something similar to a Yield Bearing Vault would solve for accounting while allowing an easier time adding and removing liquidity.
Personally I'd recommend the sponsor to denominate the deposit token in the CRV_LP token to avoid issues with Single Sided Exposure, which other findings in this contest already dicuss
🌟 Selected for report: csanuragjain
Also found by: gzeon
530.9989 USDC - $531.00
In the current design, client cannot deactivate a token after GRACE_PERIOD passed (L45). However, since activate
does not check if the token is activate, client can call activate
to reset the timestamp and call deactivate
immediately to steal all the donated token.
function activate(IERC20 _token) external override onlyClient { activated[_token] = block.timestamp; savedTokens[_token] = _token.balanceOf(address(this)); emit ShelterActivated(_token); }
require(activated[_token] == 0, "already activated");
#0 - GalloDaSballo
2022-04-19T13:48:01Z
Finding has some validity but it's missing the fact that deactivate
will send the funds back to the client which is immutable
There is definitely room for a denial of claims, however the warden hasn't proven that funds can be stolen.
#1 - GalloDaSballo
2022-04-19T13:48:05Z
Dup of #28
🌟 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
203.0461 USDC - $203.05
rouding issue ConvexStakingWrapper might lead to revert https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConvexStakingWrapper.sol#L178
if (reward.token == cvx || reward.token == crv) { IERC20(reward.token).transfer(treasury, d_reward / 5); d_reward = (d_reward * 4) / 5; } IERC20(reward.token).transfer(address(claimContract), d_reward);
fix by changing to
if (reward.token == cvx || reward.token == crv) { IERC20(reward.token).transfer(treasury, d_reward / 5); d_reward = d_reward - d_reward / 5; } IERC20(reward.token).transfer(address(claimContract), d_reward);
masterChef can be set to address(0) which will require redeployment https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConvexStakingWrapper.sol#L71
Cast to uint192 is unsafe, user trying to deposit more than 2^192 would recevie less deposits balance. https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConvexStakingWrapper.sol#L235
deposits[_pid][msg.sender].amount += uint192(_amount);
rewardNotifier can be set to address(0) which will require redeployment https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConcurRewardPool.sol#L15
USDMPegRecovery withdraw does not check if user have sufficient balance. While it would revert later in the procedure due to an overflow when deducting from user.usdm/user.pool3, it is better to have the check at the begining. https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/USDMPegRecovery.sol#L110
function withdraw(Liquidity calldata _withdrawal) external { Liquidity memory total = totalLiquidity; Liquidity memory user = userLiquidity[msg.sender]; if(_withdrawal.usdm > 0) { require(unlockable, "!unlock usdm"); usdm.safeTransfer(msg.sender, uint256(_withdrawal.usdm)); total.usdm -= _withdrawal.usdm; user.usdm -= _withdrawal.usdm; } if(_withdrawal.pool3 > 0) { pool3.safeTransfer(msg.sender, uint256(_withdrawal.pool3)); total.pool3 -= _withdrawal.pool3; user.pool3 -= _withdrawal.pool3; } totalLiquidity = total; userLiquidity[msg.sender] = user; emit Withdraw(msg.sender, _withdrawal); }
A lot of pattern in the code is like
RewardType memory reward = rewards[_pid][_index]; // do something to reward rewards[_pid][_index] = reward;
it might be better to use
RewardType storage reward = rewards[_pid][_index]; // do something to reward
gas-wise they should be the same, but using reference make it less likely to forget commit back to storage.
RADSs should be CONCURs https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/MasterChef.sol#L25
// We do some fancy math here. Basically, any point in time, the amount of RADSs
Math is safe by default in Solidity >=0.8.0 https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/MasterChef.sol#L14
using SafeMath for uint;
Also need to replace SafeMath function to normal arithmetic in the MasterChef contract.
#0 - GalloDaSballo
2022-04-24T15:36:27Z
Rounding issue in ConvexStakingWrapper Agree with rounding, don't believe any revert will happen
Lack input validation in constructor of ConvexStakingWrapper Agree
Cast to uint192 is unsafe Agree, dup of #194 (med)
Lack input validation in constructor of ConcurRewardPool Agree
USDMPegRecovery withdraw does not check if user have sufficient balance Disagree to fail early as it would cost more gas in the normal case
Use struct reference instead of memory copy Disagree
No need to use SafeMath in Solidity >=0.8.0 Gas at best
#1 - GalloDaSballo
2022-04-27T14:59:35Z
3
#2 - JeeberC4
2022-04-28T03:27:20Z
@CloudEllie please create new issue for the Med finding above.
#3 - CloudEllie
2022-04-28T21:07:12Z
Created a separate issue for "Cast to uint192 is unsafe" - see #271
🌟 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
69.3762 USDC - $69.38
> 0
is less efficient than != 0
for uint in require conditionRef: https://twitter.com/GalloDaSballo/status/1485430908165443590
$ grep -Rn "> 0" ./contracts/ | grep require ./contracts/MasterChef.sol:186: require(user.amount > 0, "MasterChef: nothing to withdraw"); ./contracts/StakingRewards.sol:94: require(amount > 0, "Cannot stake 0"); ./contracts/StakingRewards.sol:108: require(amount > 0, "Cannot withdraw 0");
Solidity ^0.8.4 allow the use of custom errors to optimize gas usage. https://blog.soliditylang.org/2021/04/21/custom-errors/
uint256 public startLiquidity; uint256 public step;
IERC20 public rewardsToken; IERC20 public stakingToken; address public rewardsDistribution;
Use uint(1) instead of bool(true) to save gas by avoiding masking ops https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/Shelter.sol#L17
mapping(IERC20 => mapping(address => bool)) public override claimed;
block.timestamp + GRACE_PERIOD
to activated[_token]
Since we are only interested in activated[_token] + GRACE_PERIOD
, might as well cache it when we call activate.
https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/Shelter.sol#L38
function activate(IERC20 _token) external override onlyClient { activated[_token] = block.timestamp + GRACE_PERIOD; savedTokens[_token] = _token.balanceOf(address(this)); emit ShelterActivated(_token); } function deactivate(IERC20 _token) external override onlyClient { require(activated[_token] != 0 && activated[_token] > block.timestamp, "too late"); activated[_token] = 0; savedTokens[_token] = 0; _token.safeTransfer(msg.sender, _token.balanceOf(address(this))); emit ShelterDeactivated(_token); } function withdraw(IERC20 _token, address _to) external override { require(activated[_token] != 0 && activated[_token] < 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); }
total.usdm += _deposits.usdm; user.usdm += _deposits.usdm;
can be changed to
total.usdm += _deposits.usdm; unchecked{user.usdm += _deposits.usdm;}
assuming user.usdm always <= total.usdm
similarly
total.pool3 += _deposits.pool3; user.pool3 += _deposits.pool3;
can be changed to
total.pool3 += _deposits.pool3; unchecked{user.pool3 += _deposits.pool3;}
// out = x * y unchecked{/} z function fmul(uint256 x, uint256 y, uint256 z) internal pure returns(uint256 out){ assembly{ if iszero(eq(div(mul(x,y),x),y)) {revert(0,0)} out := div(mul(x,y),z) } }
can be applied to floating multiplication such as in https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConvexStakingWrapper.sol#L180
d_reward = (d_reward * 4) / 5;
#0 - GalloDaSballo
2022-04-03T16:12:44Z
0 is less efficient than != 0 for uint in require condition 9 gas
Use custom errors Not useful without explanation
Variables that could be set immutable 5 * 2100 -> 10500
Use uint256 instead of bool 3 gas I believe
Cache block.timestamp + GRACE_PERIOD to activated[_token]
Would save 3 gas on withdraw
Unchecked safe math would save 40 gas
float multiplication optimization Not exactly sure if it will save more than just using unchecked, in lack of math / poc will give it 20 gas
Gas Saved 10575
Report is short and sweet, I appreciate it