Lybra Finance - No12Samurai's results

A protocol building the first interest-bearing omnichain stablecoin backed by LSD.

General Information

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

Lybra Finance

Findings Distribution

Researcher Performance

Rank: 39/132

Findings: 2

Award: $232.59

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: DelerRH

Also found by: DelerRH, HE1M, LaScaloneta, No12Samurai, RedTiger, adeolu, ayden, bart1e, f00l, pep7siup

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-828

Awards

43.047 USDC - $43.05

External Links

Lines of code

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

Vulnerability details

ProtocolRewardsPool Calculates the Wrong Reward Concerning Decimals

This report identifies vulnerabilities in the ProtocolRewardsPool contract related to the getReward() and notifyRewardAmount() functions. These vulnerabilities lead to incorrect calculations when distributing rewards.

Impact

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.

Proof of Concept

The ProtocolRewardsPool contract contains critical flaws in the getReward() and notifyRewardAmount() functions, leading to incorrect calculations .

Flaw in 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.

Flaw in 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.

Tools Used

Manual Review

To address these critical vulnerabilities, the following modifications are recommended:

Flaw in getReward function:

Modify line ProtocolRewardsPool.sol#209 to the following:

uint256 tokenAmount = (reward - eUSDShare - peUSDBalance) * (10 ** token.decimals()) / 1e18;

Flaw in notifyRewardAmount function:

Modify line ProtocolRewardsPool.sol#236 to the following:

rewardPerTokenStored = rewardPerTokenStored + (amount * 1e36 / ( 10 ** token.decimals())) / totalStaked();

Assessed type

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)

Findings Information

🌟 Selected for report: No12Samurai

Also found by: 0xRobocop, Brenzee, Toshii, kutugu

Labels

bug
2 (Med Risk)
downgraded by judge
primary issue
satisfactory
selected for report
sponsor confirmed
M-10

Awards

189.5413 USDC - $189.54

External Links

Lines of code

https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L190-L218

Vulnerability details

Incorrect Reward Distribution Calculation in 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.

Impact

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.

Proof of Concept

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.

Tools Used

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.

Assessed type

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

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