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: 25/52
Findings: 4
Award: $467.39
🌟 Selected for report: 0
🚀 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
https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol#L52-L58
The Shelter.withdraw()
function doesn't check whether the caller has already claimed their funds. Thus a user can withdraw as many times as they want.
The withdraw()
function only checks whether the requested token is activated and whether the request is made in a specific period: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol#L53
It doesn't verify whether the user has already claimed. Thus, the user can call it multiple times to withdraw more funds than they are eligible to: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol#L54-L57
none
The function modifies the claimed
map which seems to be used to represent who has already claimed their funds. But, the map isn't used anywhere else. Also, it sets the _to
address as the one that claimed. But, that should be the msg.sender
since the msg.sender
is the one who has access to funds. Otherwise, the user could call withdraw()
multiple times and use a different _to
parameter.
So add the following:
function withdraw(IERC20 _token, address _to) external override { require(activated[_token] != 0 && activated[_token] + GRACE_PERIOD < block.timestamp, "shelter not activated"); require(!claimed[_token][msg.sender], "already claimed"); uint256 amount = savedTokens[_token] * client.shareOf(_token, msg.sender) / client.totalShare(_token); claimed[_token][msg.sender] = true; emit ExitShelter(_token, msg.sender, _to, amount); _token.safeTransfer(_to, amount); }
#0 - GalloDaSballo
2022-04-12T23:05:13Z
Duplicate of #246
🌟 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
135.9604 USDC - $135.96
Stuff like a guardian being added to the contract or the treasury address being changed. All of that should be accompanied by an event.
When converting to a smaller bit uint it might result in a smaller number if the value is higher than the maximum value of that uint type. Before converting it check whether the passed value is small enough.
require(x <= type(uint192).max); uint192 y = uint192(x);
It shouldn't really be an issue here tho and since I don't expect anybody to have more than 2 ** 196 - 1
tokens to abuse this.
Some tokens don't revert if a transfer fails. Instead they just return false
. Since
the ConvexStakingWrapper
contract potentially works with arbitrary ERC20 tokens it's recommended to use SafeERC20 when working with it. Or at least check the return values:
In case of a fee-on-transfer ERC20 token the savedTokens
balance will not represent the actual balance of the contract:
You can either disable the use of fee-on-transfer tokens for donations or use the actual balance:
// disable uint oldBalance = _token.balanceOf(address(this)); _token.safeTransferFrom(msg.sender, address(this), _amount); uint newBalance = _token.balanceOf(address(this)); require(_amount == (newBalance - oldBalance)); savedTokens[_token] += _amount;
// use actual balance _token.safeTransferFrom(msg.sender, address(this), _amount); savedTokens[_token] = _token.balanceOf(address(this));
By repedeatly calling the activate()
function for a specific token they can extend the timestamp
at which withdrawal by users is enabled.
With each call activated[_token]
is reset to block.timestamp
. Thus the require statement in withdraw()
will fail for another week. There's nothing stopping the client from calling the function multiple times to keep delaying the withdrawal.
Fix it by not allowing already activated tokens to be reactivated. But with the current implementation of the contract, the client could deactivate and reactivate it.
function activate(IERC20 _token) external override onlyClient { require(!activated[_token]); activated[_token] = block.timestamp; savedTokens[_token] = _token.balanceOf(address(this)); emit ShelterActivated(_token); }
#0 - GalloDaSballo
2022-04-27T13:46:47Z
emit events for address or important state changes Informational
Conversion -> Low severity
use SafeERC20 or check return value of ERC20 functions Agree
Shelter donations result in wrong internal balance if fee-on-transfer token is used Dup of #180 (med)
Shelter can delay the user's ability to withdraw their funds indefinitely Dup of #28
All in all a neat small report, I do appreciate the directness, because 2 findings are going to be bumped to med, this report won't get much points, but personally I recommend all wardens that are shaky in their reports quality to just write something short and sweet like this one, with titles, links and concise comments
#1 - JeeberC4
2022-04-28T03:28:11Z
@CloudEllie please create new issue for the 2 Med findings above.
#2 - CloudEllie
2022-04-28T21:02:34Z
Created a separate issue for "Shelter donations result in wrong internal balance if fee-on-transfer token is used" - see #270
🌟 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
85.3051 USDC - $85.31
safeApprove()
Instead of calling the same function twice, cache the return value and use that.
Example:
Replace the call with extraToken
which was cached here: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol#L123
safeApprove()
https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol#L242-L244
The second safeApprove()
call can be removed. After the deposit()
the convexBooster
's allowance should be 0
already.
#0 - GalloDaSballo
2022-04-02T13:30:01Z
Will save 100 gas for STATICCALL as well as another 100 gas to read the storage value 200
Should save 100 gas as SLOT value is same (using 100 consistently in this contest, wardens DM if you have different values, used to be 800, now 100 ArrowGlacier)
The warden could have made a killing here in explaining exactly how much gas would have been saved. Will give each variable one COLD read value 2100 * 3 = 6300
Same deal -> 4 * 2100 = 8400
Total Gas Saved 15000