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
Rank: 1/13
Findings: 3
Award: $12,011.00
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: cmichel
2.9984 ETH - $10,723.39
cmichel
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); // ... }
More shares for the amount are burned than required and users will lose rewards in the end.
Demonstrating that users will lose rewards:
1000 amount
and received 1000 shares
, and v.exchangeRate = 1.0
. (This user is the single staker)1000 tokens
accrue for the validator, tokensGivenToValidator = 1000
. User should be entitled to 1000 in principal + 1000 in rewards = 2000 tokens.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.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
0.0055 ETH - $19.84
cmichel
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.
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
🌟 Selected for report: cmichel
0.2998 ETH - $1,072.34
cmichel
The getValidatorsDetails
function iterates over all validatorsN
elements.
The same happens for other view functions like
getDelegatorDetails
.
The transactions can fail if used by another smart contract on-chain. This happens in case the number of validators gets too large and the transaction would consume more gas than the block limit. It will also cost a lot of gas as the entire validator set is always fetched.
Consider pagination for the view functions by adding startIndex, count
parameters that read the array from index startIndex
and read only count
many elements.
#0 - kitti-katy
2021-10-26T05:31:54Z
The getters weren't designed to be called in any transactions, but agreed, if an external contract will use it, it might fail. Right now we have only 10 validators who are whitelisted. In the upgraded version, there will be no whitelist and anyone would be able to become a validator. The current list is small enough, and we will handle the getters in the next contract version.
#1 - GalloDaSballo
2021-11-02T15:35:50Z
Agree with the finding, the function can fail given certain conditions
Am ok with a nofix
cmichel
The addValidator
function does not restrict the commissionRate
parameter and can therefore be set to more than 100%
.
Note that the setValidatorCommissionRate
correctly performs this restriction.
Setting the commission rate to more than 100% would lead to an inflated amount given to the validator and empty the contract faster than the epochs computed via endBlock
.
Consider adding a require(commissionRate < divider, "Rate must be less than 100%");
check.
#0 - kitti-katy
2021-10-21T19:42:57Z
duplicate of #20
#1 - GalloDaSballo
2021-11-02T15:36:25Z
Duplicate of #20