Covalent contest - cmichel's results

One unified API. One billion possibilities.

General Information

Platform: Code4rena

Start Date: 19/10/2021

Pot Size: $30,000 ETH

Total HM: 5

Participants: 13

Period: 3 days

Judge: GalloDaSballo

Total Solo HM: 4

Id: 43

League: ETH

Covalent

Findings Distribution

Researcher Performance

Rank: 1/13

Findings: 3

Award: $12,011.00

🌟 Selected for report: 2

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
resolved
sponsor confirmed

Awards

2.9984 ETH - $10,723.39

External Links

Handle

cmichel

Vulnerability details

The unstake function does not immediately update the exchange rates. It first computes the validatorSharesRemove = tokensToShares(amount, v.exchangeRate) with the old exchange rate.

Only afterwards, it updates the exchange rates (if the validator is not disabled):

// @audit shares are computed here with old rate
uint128 validatorSharesRemove = tokensToShares(amount, v.exchangeRate);
require(validatorSharesRemove > 0, "Unstake amount is too small");

if (v.disabledEpoch == 0) {
    // @audit rates are updated here
    updateGlobalExchangeRate();
    updateValidator(v);
    // ...
}

Impact

More shares for the amount are burned than required and users will lose rewards in the end.

POC

Demonstrating that users will lose rewards:

  1. Assume someone staked 1000 amount and received 1000 shares, and v.exchangeRate = 1.0. (This user is the single staker)
  2. Several epochs pass, interest accrues, and 1000 tokens accrue for the validator, tokensGivenToValidator = 1000. User should be entitled to 1000 in principal + 1000 in rewards = 2000 tokens.
  3. But user calls unstake(1000), which sets validatorSharesRemove = tokensToShares(amount, v.exchangeRate) = 1000 / 1.0 = 1000. Afterwards, the exchange rate is updated: v.exchangeRate += tokensGivenToValidator / totalShares = 1.0 + 1.0 = 2.0. The staker is updated with s.shares -= validatorSharesRemove = 0 and s.staked -= amount = 0. And the user receives their 1000 tokens but notice how the user's shares are now at zero as well.
  4. User tries to claim rewards calling redeemAllRewards which fails as the rewards are 0.

If the user had first called redeemAllRewards and unstake afterwards they'd have received their 2000 tokens.

The exchange rates always need to be updated first before doing anything. Move the updateGlobalExchangeRate() and updateValidator(v) calls to the beginning of the function.

#0 - GalloDaSballo

2021-10-30T00:22:13Z

Agree with the finding, using the old exchange rate ends up burning more shares than what would be correct The sponsor has mitigated the issue

Findings Information

🌟 Selected for report: xYrYuYx

Also found by: cmichel, defsec, pants

Labels

bug
duplicate
G (Gas Optimization)
sponsor disputed

Awards

0.0055 ETH - $19.84

External Links

Handle

cmichel

Vulnerability details

The ERC20.transfer() and ERC20.transferFrom() functions return a boolean value indicating success. This parameter should checked for success. Some tokens do not revert if the transfer failed but return false instead.

See transferToContract and transferFromContract which perform ERC20 transfers without checking for the return value.

Impact

As the CQT token is used which supposedly reverts on failed transfers, not checking the return value does not lead to any security issues. We still recommend checking it to abide by the EIP20 standard.

Consider using require(CQT.transferFrom(from, address(this), amount), "transfer failed") instead.

#0 - kitti-katy

2021-10-21T19:45:58Z

there is another solution #1

#1 - GalloDaSballo

2021-11-02T15:36:56Z

Duplicate of #1

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