Concur Finance contest - Ruhum'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: 25/52

Findings: 4

Award: $467.39

🌟 Selected for report: 0

🚀 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-L58

Vulnerability details

Impact

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.

Proof of Concept

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

Tools Used

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

Awards

135.9604 USDC - $135.96

Labels

bug
QA (Quality Assurance)
sponsor confirmed

External Links

Report

Non-Critical

emit events for address or important state changes

Stuff like a guardian being added to the contract or the treasury address being changed. All of that should be accompanied by an event.

don't convert to a smaller bit uint without verifying its value

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.

Low

use SafeERC20 or check return value of ERC20 functions

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:

Shelter donations result in wrong internal balance if fee-on-transfer token is used

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));

Shelter can delay the user's ability to withdraw their funds indefinitely

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

Awards

85.3051 USDC - $85.31

Labels

bug
G (Gas Optimization)
sponsor confirmed

External Links

#0 - GalloDaSballo

2022-04-02T13:30:01Z

cache values from external function calls

Will save 100 gas for STATICCALL as well as another 100 gas to read the storage value 200

remove unnecessary safeApprove()

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)

use constant state variables wherever you can

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

use immutable state variables wherever you can

Same deal -> 4 * 2100 = 8400

Total Gas Saved 15000

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