Platform: Code4rena
Start Date: 23/06/2023
Pot Size: $60,500 USDC
Total HM: 31
Participants: 132
Period: 10 days
Judge: 0xean
Total Solo HM: 10
Id: 254
League: ETH
Rank: 39/132
Findings: 2
Award: $232.59
🌟 Selected for report: 1
🚀 Solo Findings: 0
43.047 USDC - $43.05
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L190-L218, https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L227-L240
ProtocolRewardsPool
Calculates the Wrong Reward Concerning DecimalsThis report identifies vulnerabilities in the ProtocolRewardsPool
contract related to the getReward()
and notifyRewardAmount()
functions. These vulnerabilities lead to incorrect calculations when distributing rewards.
In the ProtocolRewardsPool
contract, a user can call the getReward()
function to receive the rewards. The function first tries to pay the reward using eUSD
token, and if enough amount of tokens was not available, it will use peUSD
, and stableToken
in the next steps. However, when the protocol wants to send stableToken
, because of misuse of decimals()
function, the notifyRewardAmount()
reports a reward significantly more than actual amount, and getReward()
function pays an amount which is significantly less the actual amount.
The ProtocolRewardsPool
contract contains critical flaws in the getReward()
and notifyRewardAmount()
functions, leading to incorrect calculations .
getReward
function:The calculation of the token amount to be sent underestimates the reward. The flawed calculation is as follows:
(ProtocolRewardsPool.sol#L209)
uint256 tokenAmount = (reward - eUSDShare - peUSDBalance) * token.decimals() / 1e18;
For example, with reward
as 100 * 1e18
, eUSDShare
as 10 * 1e18
, peUSDBalance
as 10 * 1e18
, and 18 decimals for the token, the expected token amount to be sent is 80 * 1e18
. However, the flawed calculation results in 1440
, significantly less than the expected value.
notifyRewardAmount
function:The calculation of rewardPerTokenStored
is incorrect when updating the reward amount. The flawed calculation is as follows:
(ProtocolRewardsPool.sol#L236)
rewardPerTokenStored = rewardPerTokenStored + (amount * 1e36 / token.decimals()) / totalStaked();
For example, with rewardPerTokenStored
as 1e16
, amount
as 18 * 1e18
, token decimals as 18, and totalStaked()
as 100 * 1e18
, the expected rewardPerTokenStored
should be 19 * 1e16
. However, the flawed calculation results in 1e34
, significantly different from the expected value.
Manual Review
To address these critical vulnerabilities, the following modifications are recommended:
getReward
function:Modify line ProtocolRewardsPool.sol#209 to the following:
uint256 tokenAmount = (reward - eUSDShare - peUSDBalance) * (10 ** token.decimals()) / 1e18;
notifyRewardAmount
function:Modify line ProtocolRewardsPool.sol#236 to the following:
rewardPerTokenStored = rewardPerTokenStored + (amount * 1e36 / ( 10 ** token.decimals())) / totalStaked();
Decimal
#0 - c4-pre-sort
2023-07-09T14:28:39Z
JeffCX marked the issue as duplicate of #501
#1 - c4-judge
2023-07-28T15:40:23Z
0xean marked the issue as satisfactory
#2 - c4-judge
2023-07-28T19:46:50Z
0xean changed the severity to 2 (Med Risk)
🌟 Selected for report: No12Samurai
189.5413 USDC - $189.54
ProtocolRewardsPool
This report highlights a vulnerability in the ProtocolRewardsPool
contract. The getReward()
function, designed to distribute rewards to users, uses an incorrect calculation method that can result in incorrect reward distribution.
In the ProtocolRewardsPool
contract, a user can call the getReward()
function to receive the rewards. The function first tries to pay the reward using eUSD
token, and if enough amount of tokens was not available, it will use peUSD
, and stableToken
in the next steps. However, the protocol compares the number of shares with the amount of reward to send the reward. If one share corresponds to a value greater than 1 eUSD
, which is typically the case, users can be overpaid when claiming rewards. This can result in a significant discrepancy between the actual reward amount and the amount distributed.
When a user invokes the ProtocolRewardsPool.getReward()
function, the contract attempts to distribute the rewards using the EUSD
token:
(ProtocolRewardsPool.sol#L190-L218)
function getReward() external updateReward(msg.sender) { uint reward = rewards[msg.sender]; if (reward > 0) { rewards[msg.sender] = 0; IEUSD EUSD = IEUSD(configurator.getEUSDAddress()); uint256 balance = EUSD.sharesOf(address(this)); uint256 eUSDShare = balance >= reward ? reward : reward - balance; EUSD.transferShares(msg.sender, eUSDShare); if (reward > eUSDShare) { ERC20 peUSD = ERC20(configurator.peUSD()); uint256 peUSDBalance = peUSD.balanceOf(address(this)); if (peUSDBalance >= reward - eUSDShare) { peUSD.transfer(msg.sender, reward - eUSDShare); emit ClaimReward( msg.sender, EUSD.getMintedEUSDByShares(eUSDShare), address(peUSD), reward - eUSDShare, block.timestamp ); } else { if (peUSDBalance > 0) { peUSD.transfer(msg.sender, peUSDBalance); } ERC20 token = ERC20(configurator.stableToken()); uint256 tokenAmount = ((reward - eUSDShare - peUSDBalance) * token.decimals()) / 1e18; token.transfer(msg.sender, tokenAmount); emit ClaimReward( msg.sender, EUSD.getMintedEUSDByShares(eUSDShare), address(token), reward - eUSDShare, block.timestamp ); } } else { emit ClaimReward( msg.sender, EUSD.getMintedEUSDByShares(eUSDShare), address(0), 0, block.timestamp ); } } }
To determine the available shares for rewarding users, the function calculates the shares of the eUSD
token held by the contract and compares it with the total reward to be distributed.
Here is the code snippet illustrating this calculation:
uint256 balance = EUSD.sharesOf(address(this)); uint256 eUSDShare = balance >= reward ? reward : reward - balance;
However, the comparison of shares with the reward in this manner is incorrect.
Let's consider an example to understand the problem. Suppose rewards[msg.sender]
is equal to $10 worth of eUSD
, and the shares held by the contract are 9 shares. If each share corresponds to $10 worth eUSD
, the contract mistakenly assumes it does not have enough balance to cover the entire reward, because it has 9 shares; however, having 9 shares is equivalent to having $90 worth of eUSD
. Consequently, it first sends 9 shares, equivalent to $90 worth of eUSD
, and then sends $1 worth peUSD
. However, the sum of these sent values is $91 worth of eUSD
, while the user's actual reward is only $10 worth eUSD
.
This issue can lead to incorrect reward distribution, causing users to receive significantly more or less rewards than they should.
Manual Review
To address this issue, it is recommended to replace the usage of eUSDShare
with EUSD.getMintedEUSDByShares(eUSDShare)
in the following lines:
This ensures that the correct amount of eUSD
is transferred to the user while maintaining the accuracy of reward calculations.
Math
#0 - c4-pre-sort
2023-07-10T20:08:38Z
JeffCX marked the issue as primary issue
#1 - c4-sponsor
2023-07-14T09:19:44Z
LybraFinance marked the issue as sponsor confirmed
#2 - 0xean
2023-07-25T23:25:58Z
I will leave open for more comment, but this is probably more a "leak" of value type scenario than assets being lost or stolen directly. Therefore M is probably appropriate.
#3 - c4-judge
2023-07-25T23:26:05Z
0xean changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-07-28T18:35:50Z
0xean marked the issue as satisfactory
#5 - c4-judge
2023-07-28T20:45:42Z
0xean marked the issue as selected for report
#6 - KuTuGu
2023-07-29T03:55:52Z
This should be a repeat of #869