Platform: Code4rena
Start Date: 31/01/2023
Pot Size: $90,500 USDC
Total HM: 47
Participants: 169
Period: 7 days
Judge: LSDan
Total Solo HM: 9
Id: 211
League: ETH
Rank: 32/169
Findings: 6
Award: $716.50
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: waldenyan20
Also found by: 0xRobocop, KIntern_NA, Ruhum, cccz, hansfriese, mert_eren, minhtrng, peanuts
185.0021 USDC - $185.00
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L308
The changeRewardSpeed
function from the MultiRewardStaking.sol
contract lacks documentation on how exactly it should work. By its name and some comments above it, I infer that the function must change the rate of tokens rewards per unit of time.
For simplicity the context is the following: the function changeRewardSpeed
is called with the parameters: tokenA
, 2 tokens/second
. The state of the staking of tokenA at that point is:
(1) It still has 50 seconds more to go. (2) Its current speed is 1 token / s.
Assume that the balance of tokenAs of the contract at that point is enough to fullfill the obligation of paying all the users its rewards until the end of the staking (this was enforced during the creation of the staking). But that amount was calculated based on a speed, the 1 token / second one. Because we want to increase it to 2 tokens / second, the contract must take from the caller the amount of tokens that is equal to (speed2 - speed1) * time_to_go
. In other words it must multiply the difference of speeds by how much time is left, and take that tokens from the caller.
If it were to decrease the speed, then the tokenA's balance of the contract is enough.
So basically the function must change the state in three ways:
(1) Update the accrued rewards to take into account the time from the lastupdate
(2) Update the variable rewardsPerSecond
(3) If the change was an increment then take from the caller the tokens needed to complete the amount. If it is a decrease, then don't do this last step.
When reading the current implementation of the changeRewardSpeed
function it has a different behavior.
It not only changes the rewardPerSecond
variable but also the endRewardTimestamp
variable using the _calcRewardsEnd
function. Problems:
(1) The function name states that the only change must be the speed of rewards not the duration of the staking.
(2) The end state after executing the current implementation of changeRewardSpeed
function is invalid and can cause a temporally DoS on the staking of token A.
Explaining (2):
The problem is using the _calcRewardsEnd
function. This function is used properly at the creation of the staking to compute the end timestamp, it is also used correctly on the fundReward
function that has the intention to add more rewards with the same speed, so, the end timestamp must be increased.
In the case of the changeRewardSpeed
function, the _calcRewardsEnd
function is called with the parameters: current endTimestamp, the new speed and the contract's tokenA balance. The semantics of _calcRewardsEnd
is the following:
If it is called when the staking is created, simply compute the endTimestamp
to be able to distribute amount
of tokens given a rewardsPerSecond
speed.
If it is called in the middle of a staking (like when using the fundReward
function) is first add to amount
the amount of tokens that are left to be rewarded and then with the new amount
calculate the new endTimestamp.
So the effect of _calcRewardsEnd
on changeRewardSpeed
is that it will calculate a new endRewardTimestamp
like it will take the contract's tokenA balance as the funds to increase rewards and it will calculate the rewards that are left to be given using the new speed.
The Denial of Service will happen because of two things:
(1) The function does not take the surplus of tokens from the caller
(2) Even if it enforces the transfer of the surplus of tokens, that surplus is miscalculated because it does not take into account the tokens that are in debt from the last index of each user to the time the changeRewardSpeed
function was called.
Manual Review
Rewrite the function as:
function changeRewardSpeed(IERC20 rewardToken, uint160 rewardsPerSecond) external onlyOwner { RewardInfo memory rewards = rewardInfos[rewardToken]; if (rewardsPerSecond == 0) revert ZeroAmount(); if (rewards.lastUpdatedTimestamp == 0) revert RewardTokenDoesntExist(rewardToken); if (rewards.rewardsPerSecond == 0) revert RewardsAreDynamic(rewardToken); _accrueRewards(rewardToken, _accrueStatic(rewards)); // The speed is increasing if(rewards.rewardsPerSecond < rewardsPerSecond) { extraAmount = (rewardsPerSecond - rewards.rewardsPerSecond) * (rewards.rewardsEndTimestamp - block.timestamp); rewardToken.safeTransferFrom(msg.sender, address(this), extraAmount); rewardInfos[rewardToken].rewardsPerSecond = rewardsPerSecond; } // The speed is decreasing else { rewardInfos[rewardToken].rewardsPerSecond = rewardsPerSecond; // It might reimburse the caller some tokens. } }
#0 - captainmangoC4
2023-02-24T18:15:54Z
Created duplicate of #250 per judge's request
#1 - c4-judge
2023-02-25T14:48:43Z
dmvt changed the severity to 3 (High Risk)
#2 - c4-judge
2023-02-25T15:30:46Z
dmvt marked the issue as partial-50
#3 - c4-judge
2023-03-01T00:42:48Z
dmvt marked the issue as satisfactory
🌟 Selected for report: 0xdeadbeef0x
Also found by: 0Kage, 0xNazgul, 0xRobocop, Aymen0909, KIntern_NA, Kenshin, KingNFT, Krace, Kumpa, SadBase, aashar, apvlki, btk, cccz, critical-or-high, eccentricexit, fs0c, gjaldon, hansfriese, immeas, mert_eren, mgf15, mrpathfindr, orion, peanuts, rvi0x, rvierdiiev, supernova, ulqiorra, waldenyan20, y1cunhui
4.6115 USDC - $4.61
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L170
ERC777 has transfer hooks before and after the actual transfer of funds. These hooks can be used to take control of the flow of execution, and execute arbitrary logic, like reentering a contract.
The claimRewards
function on the MultiRewardStaking.sol
contract is implemented as follows:
function claimRewards(address user, IERC20[] memory _rewardTokens) external accrueRewards(msg.sender, user) { for (uint8 i; i < _rewardTokens.length; i++) { uint256 rewardAmount = accruedRewards[user][_rewardTokens[i]]; if (rewardAmount == 0) revert ZeroRewards(_rewardTokens[i]); EscrowInfo memory escrowInfo = escrowInfos[_rewardTokens[i]]; if (escrowInfo.escrowPercentage > 0) { _lockToken(user, _rewardTokens[i], rewardAmount, escrowInfo); emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, true); } else { _rewardTokens[i].transfer(user, rewardAmount); emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, false); } accruedRewards[user][_rewardTokens[i]] = 0; } }
As can be seen, the statement accruedRewards[user][_rewardTokens[i]] = 0;
that clears the rewardBalance
of the user is left at the end. So when the function is reentered the next check will pass:
uint256 rewardAmount = accruedRewards[user][_rewardTokens[i]]; if (rewardAmount == 0) revert ZeroRewards(_rewardTokens[i]);
Allowing the attacker to drain all the rewards balance (the one belonging to the ERC777 token) of the contract.
Manual review
Use a mutex on the function, so it cannot be reentered or move the statement accruedRewards[user][_rewardTokens[i]] = 0;
before the transfer, following The Checks-Effects-Interactions pattern.
#0 - c4-judge
2023-02-16T07:38:46Z
dmvt marked the issue as duplicate of #54
#1 - c4-sponsor
2023-02-18T12:10:52Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:51:44Z
dmvt marked the issue as partial-50
#3 - c4-judge
2023-02-23T01:10:16Z
dmvt changed the severity to 3 (High Risk)
#4 - c4-judge
2023-03-01T00:30:58Z
dmvt marked the issue as full credit
#5 - c4-judge
2023-03-01T00:45:07Z
dmvt marked the issue as satisfactory
122.6059 USDC - $122.61
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L667 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L667 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L608 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L618 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L631 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L644
The _verifyCreatorOrOwner(address vault)
function at the VaultController.sol contract is used to validate that the msg.sender
is the creator of the vault or is the owner
of the contract.
It looks like this:
function _verifyCreatorOrOwner(address vault) internal returns (VaultMetadata memory metadata) { metadata = vaultRegistry.getVault(vault); if (msg.sender != metadata.creator || msg.sender != owner) revert NotSubmitterNorOwner(msg.sender); }
The logic clause is wrong to achieve that purpose. The only way the function wont revert is if the msg.sender
is both the creator
of the vault and the owner
of the VaultController contract.
The affected functions are: addStakingRewardsTokens
, pauseAdapters
, pauseVaults
, unpauseAdapters
and unpauseVaults
.
There is no loss of funds for the users, but the functionality of the protocol as was designed would be affected
Manual Review
Change the logic-or for a logic-and:
function _verifyCreatorOrOwner(address vault) internal returns (VaultMetadata memory metadata) { metadata = vaultRegistry.getVault(vault); if (msg.sender != metadata.creator && msg.sender != owner) revert NotSubmitterNorOwner(msg.sender); }
#0 - c4-judge
2023-02-16T07:24:19Z
dmvt marked the issue as duplicate of #45
#1 - c4-sponsor
2023-02-18T12:08:22Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:19:06Z
dmvt marked the issue as satisfactory
#3 - c4-judge
2023-02-23T01:07:41Z
dmvt changed the severity to 3 (High Risk)
55.5006 USDC - $55.50
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L308
The changeRewardSpeed
function from the MultiRewardStaking.sol
contract lacks documentation on how exactly it should work. By its name and some comments above it, I infer that the function must change the rate of tokens rewards per unit of time.
For simplicity the context is the following: the function changeRewardSpeed
is called with the parameters: tokenA
, 2 tokens/second
. The state of the staking of tokenA at that point is:
(1) It still has 50 seconds more to go. (2) Its current speed is 1 token / s.
Assume that the balance of tokenAs of the contract at that point is enough to fullfill the obligation of paying all the users its rewards until the end of the staking (this was enforced during the creation of the staking). But that amount was calculated based on a speed, the 1 token / second one. Because we want to increase it to 2 tokens / second, the contract must take from the caller the amount of tokens that is equal to (speed2 - speed1) * time_to_go
. In other words it must multiply the difference of speeds by how much time is left, and take that tokens from the caller.
If it were to decrease the speed, then the tokenA's balance of the contract is enough.
So basically the function must change the state in three ways:
(1) Update the accrued rewards to take into account the time from the lastupdate
(2) Update the variable rewardsPerSecond
(3) If the change was an increment then take from the caller the tokens needed to complete the amount. If it is a decrease, then don't do this last step.
When reading the current implementation of the changeRewardSpeed
function it has a different behavior.
It not only changes the rewardPerSecond
variable but also the endRewardTimestamp
variable using the _calcRewardsEnd
function. Problems:
(1) The function name states that the only change must be the speed of rewards not the duration of the staking.
(2) The end state after executing the current implementation of changeRewardSpeed
function is invalid and can cause a temporally DoS on the staking of token A.
Explaining (2):
The problem is using the _calcRewardsEnd
function. This function is used properly at the creation of the staking to compute the end timestamp, it is also used correctly on the fundReward
function that has the intention to add more rewards with the same speed, so, the end timestamp must be increased.
In the case of the changeRewardSpeed
function, the _calcRewardsEnd
function is called with the parameters: current endTimestamp, the new speed and the contract's tokenA balance. The semantics of _calcRewardsEnd
is the following:
If it is called when the staking is created, simply compute the endTimestamp
to be able to distribute amount
of tokens given a rewardsPerSecond
speed.
If it is called in the middle of a staking (like when using the fundReward
function) is first add to amount
the amount of tokens that are left to be rewarded and then with the new amount
calculate the new endTimestamp.
So the effect of _calcRewardsEnd
on changeRewardSpeed
is that it will calculate a new endRewardTimestamp
like it will take the contract's tokenA balance as the funds to increase rewards and it will calculate the rewards that are left to be given using the new speed.
The Denial of Service will happen because of two things:
(1) The function does not take the surplus of tokens from the caller
(2) Even if it enforces the transfer of the surplus of tokens, that surplus is miscalculated because it does not take into account the tokens that are in debt from the last index of each user to the time the changeRewardSpeed
function was called.
Manual Review
Rewrite the function as:
function changeRewardSpeed(IERC20 rewardToken, uint160 rewardsPerSecond) external onlyOwner { RewardInfo memory rewards = rewardInfos[rewardToken]; if (rewardsPerSecond == 0) revert ZeroAmount(); if (rewards.lastUpdatedTimestamp == 0) revert RewardTokenDoesntExist(rewardToken); if (rewards.rewardsPerSecond == 0) revert RewardsAreDynamic(rewardToken); _accrueRewards(rewardToken, _accrueStatic(rewards)); // The speed is increasing if(rewards.rewardsPerSecond < rewardsPerSecond) { extraAmount = (rewardsPerSecond - rewards.rewardsPerSecond) * (rewards.rewardsEndTimestamp - block.timestamp); rewardToken.safeTransferFrom(msg.sender, address(this), extraAmount); rewardInfos[rewardToken].rewardsPerSecond = rewardsPerSecond; } // The speed is decreasing else { rewardInfos[rewardToken].rewardsPerSecond = rewardsPerSecond; // It might reimburse the caller some tokens. } }
#0 - c4-judge
2023-02-16T03:55:48Z
dmvt marked the issue as duplicate of #156
#1 - c4-sponsor
2023-02-18T11:58:35Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T16:05:16Z
dmvt changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-02-23T22:07:05Z
This previously downgraded issue has been upgraded by dmvt
#4 - c4-judge
2023-02-23T22:07:05Z
This previously downgraded issue has been upgraded by dmvt
#5 - c4-judge
2023-02-23T22:26:10Z
dmvt marked the issue as satisfactory
313.3026 USDC - $313.30
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardEscrow.sol#L191
The feeRecipient
of the MultiRewardEscrow.sol contract is used as the address to receive any fee from the vesting of staking rewards.
The vulnerability is that this address cannot be updated even by the owner
. If an incorrect address is set, all the fees generated will be lost. The other case is that the address was correctly set, but one of the following two things can happen:
If the account is an EOA it can be the case that it gets compromised and hence, all the fees earned and also the ones that will be generated in the future will also be compromised.
If the account is a contract, then it can have vulnerabilities in it that don't make it reliable.
Manual Review
Allow the owner to change the feeRecipient
variable
#0 - c4-judge
2023-02-16T04:14:19Z
dmvt marked the issue as duplicate of #186
#1 - c4-sponsor
2023-02-18T11:59:50Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T16:16:33Z
dmvt marked the issue as satisfactory
🌟 Selected for report: IllIllI
Also found by: 0x3b, 0xAgro, 0xBeirao, 0xMirce, 0xNineDec, 0xRobocop, 0xSmartContract, 0xTraub, 0xWeiss, 2997ms, 41i3xn, Awesome, Aymen0909, Bauer, Bnke0x0, Breeje, Cryptor, DadeKuma, Deathstore, Deekshith99, DevABDee, DevTimSch, Dewaxindo, Diana, Ermaniwe, Guild_3, H0, IceBear, Inspectah, JDeryl, Kaiziron, Kaysoft, Kenshin, Mukund, Praise, RaymondFam, Rickard, Rolezn, Ruhum, Sathish9098, SkyWalkerMan, SleepingBugs, UdarTeam, Udsen, Walter, aashar, adeolu, apvlki, arialblack14, ast3ros, btk, chaduke, chandkommanaboyina, chrisdior4, climber2002, codetilda, cryptonue, cryptostellar5, csanuragjain, ddimitrov22, descharre, dharma09, doublesharp, eccentricexit, ethernomad, fs0c, georgits, halden, hansfriese, hashminer0725, immeas, lukris02, luxartvinsec, matrix_0wl, merlin, mookimgo, mrpathfindr, nadin, olegthegoat, pavankv, rbserver, rebase, savi0ur, sayan, scokaf, seeu, shark, simon135, tnevler, tsvetanovv, ulqiorra, ustas, waldenyan20, y1cunhui, yongskiws, yosuke
35.4779 USDC - $35.48
The _transfer function of the MultiRewardStaking.sol
contract emits three events of Transfer
, one for the actual transfer and two more because it uses the internal functions _burn
and _mint
which also emits events of Transfer
.
Actual transfer emit Transfer(from, to, amount);
Mint emit Transfer(address(0), to, amount);
Burn emit Transfer(from, address(0), amount);
Off-chain monitors can get confused because of this. For example: Subtracting two times amount
of tokens from from
and adding two times amount
to to
.
MultiRewardStaking
contracts with the same _stakingToken
can lead to confusion.When a MultiRewardStaking
contract is initialized its body of code contains:
_name = string(abi.encodePacked("Staked ", IERC20Metadata(address(_stakingToken)).name())); _symbol = string(abi.encodePacked("pst-", IERC20Metadata(address(_stakingToken)).symbol()));
It writes to the _name
and _symbol
state variables based on the _stakingToken
variable. If more than one staking contract uses the same _stakingToken
they will have the same _name
and _symbol
, which can lead to confusion.
There is another instance of this on the initialize function of the Vault.sol
contract.
block.timestamp
to uint64
instead of uint32
Instances:
LoC 114 MultiRewardEscrow LoC 163 MultiRewardEscrow LoC 175 MultiRewardEscrow
Context: Here
Contracts that should disable initializers:
Vault.sol
, VaultController.sol
, YearnAdapter.sol
and BeefyAdapter
Vault.sol
contract does not inherit the IERC4626
interfaceIt is important to inherit the interface, so the compliance with the interface can be confirmed by the compiler, and also to add it to one of the supported interfaces (as EIP165).
Some instances found:
event TemplateUpdated(bytes32, bytes32)
at the TemplateRegistry.sol
contract.
error NotEndorsed(bytes32)
at the CloneFactory.sol
contract
event NoFee()
at the MultiRewardEscrow
contract
Vault.sol
initialize contract does not call the initialize functions of the Pausable
and Reentrancy
contracts
DeploymentController.sol: LoC 60 it copied the comment for addTemplateCategory. Here is not a Category that is being added, but a Template.
TemplateRegistry.sol: LoC 23 it should say 'DeploymentController' instead of AdminProxy. CloneRegistry.sol: LoC 21 it should say 'DeploymentController' instead of AdminProxy. CloneFactory.sol LoC 22 it should say 'DeploymentController' instead of AdminProxy.
#0 - c4-judge
2023-02-28T15:00:33Z
dmvt marked the issue as grade-b