Lybra Finance - 3agle'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: 44/132

Findings: 2

Award: $212.43

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: georgypetrov

Also found by: 0xRobocop, 3agle, max10afternoon

Labels

bug
2 (Med Risk)
satisfactory
sponsor disputed
duplicate-931

Awards

202.5014 USDC - $202.50

External Links

Lines of code

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L72-L86 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L98-L116

Vulnerability details

Impact

  • The vulnerability in the withdrawal mechanism of the Lybra Finance protocol, which allows users to over-withdraw their stETH deposits without considering the negative rebase impact, poses a significant risk of insolvency.
  • This flaw can lead to a depletion of stETH reserves, making it impossible to fulfill withdrawal requests and resulting in potential losses for users who are unable to retrieve their full deposits.
  • Likelihood: Med
  • Impact: High-Loss of user funds

Proof of Concept

  • Alice, Bob, and Charlie are participants in the Lybra Finance protocol. They deposit stETH into the protocol using the depositAssetToMint function. The protocol records their deposits accordingly:
  • Balances:
    Alice's balance: 10 stETH Bob's balance: 8 stETH Charlie's balance: 5 stETH Total stETH vault balance: 23 stETH
  • A negative rebase event occurs, resulting in a 5% reduction in the value of stETH.
    New stETH vault balance: 0.95 * 23 = 21.85 stETH
  • Alice decides to take advantage of the flawed withdrawal mechanism. She calls the withdraw function and requests to withdraw her initial deposit of 10 stETH.
    Alice initiates withdrawal: 10 stETH Total stETH pool balance: `21.85-10 = 11.85 stETH`
  • Due to the flawed withdrawal mechanism, Alice is allowed to withdraw the full amount she initially deposited, disregarding the negative rebase impact. She successfully withdraws 10 stETH.
  • Alice's over-withdrawal, combined with the negative rebase, puts the protocol at risk of insolvency. The stETH pool's value is significantly reduced, and the remaining deposited assets may not be sufficient to cover the outstanding stETH liabilities.
  • Bob observes the insolvency risk and decides to withdraw his stETH to minimize potential losses. He calls the withdraw function and requests to withdraw his initial deposit of 8 stETH.
    Bob initiates withdrawal: 8 stETH Total stETH pool balance: `11.85 - 8 = 3.85 stETH`
  • Charlie, unaware of the insolvency risk, decides to withdraw his stETH. He calls the withdraw function and requests to withdraw his initial deposit of 5 stETH.
    Charlie initiates withdrawal: 5 stETH Total stETH pool balance: 3.85 - 5 = **FAIL**
  • As Charlie attempts to withdraw his stETH, the protocol realizes that it doesn't have enough stETH reserves to fulfill his withdrawal request.
  • The available stETH balance in the pool is insufficient due to Alice's and Bob's over-withdrawals and the negative rebase.
  • Therefore, Charlie loses 1.15 stETH

Insolvency and Losses: Due to the combination of over-withdrawals and the negative rebase, the protocol faces insolvency risk. Users like Charlie will be unable to fully withdraw their stETH deposits, leading to potential losses for them.

Tools Used

Manual Review

  • Recommend using a shares system instead of balances.
  • Following is the proposed mitigation
contract LybraFinance {
    mapping(address => uint256) public depositedShares; // Tracks the shares balance for each user
    uint256 public totalShares; // Total shares in the protocol
    
    function depositAssetToMint(
        uint256 assetAmount,
        uint256 mintAmount
    ) external virtual {
        require(
            assetAmount >= 1 ether,
            "Deposit should not be less than 1 stETH."
        );

        bool success = collateralAsset.transferFrom(
            msg.sender,
            address(this),
            assetAmount
        );
        require(success, "TF");

        uint256 sharesAmount = calculateShares(assetAmount); // Calculate shares based on the deposited asset amount

        totalShares += sharesAmount; // Update total shares
        depositedShares[msg.sender] += sharesAmount; // Update user's shares balance

        depositedTime[msg.sender] = block.timestamp;

        if (mintAmount > 0) {
            _mintEUSD(msg.sender, msg.sender, mintAmount, getAssetPrice());
        }
        emit DepositAsset(
            msg.sender,
            address(collateralAsset),
            assetAmount,
            block.timestamp
        );
    }
    
    function withdraw(address onBehalfOf, uint256 amount) external virtual {
        require(onBehalfOf != address(0), "TZA");
        require(amount > 0, "ZERO_WITHDRAW");
        
        uint256 sharesToWithdraw = calculateShares(amount); // Calculate the corresponding shares based on the requested amount
        require(
            depositedShares[msg.sender] >= sharesToWithdraw,
            "Withdraw amount exceeds deposited amount."
        );

        uint256 assetAmount = calculateAssetAmount(sharesToWithdraw); // Calculate the corresponding asset amount based on shares

        bool success = collateralAsset.transfer(onBehalfOf, assetAmount);
        require(success, "TF");

        totalShares -= sharesToWithdraw; // Update total shares
        depositedShares[msg.sender] -= sharesToWithdraw; // Update user's shares balance

        if (borrowed[msg.sender] > 0) {
            _checkHealth(msg.sender, getAssetPrice());
        }
        emit WithdrawAsset(
            msg.sender,
            address(collateralAsset),
            onBehalfOf,
            assetAmount,
            block.timestamp
        );
    }

    // Function to calculate shares based on the deposited asset amount
    function calculateShares(uint256 assetAmount) internal pure returns (uint256) {
        // Perform the necessary calculation based on the protocol's logic
        // ...
    }

    // Function to calculate the corresponding asset amount based on shares
    function calculateAssetAmount(uint256 shares) internal pure returns (uint256) {
        // Perform the necessary calculation based on the protocol's logic
        // ...
    }
}

Assessed type

Other

#0 - c4-pre-sort

2023-07-10T10:38:36Z

JeffCX marked the issue as primary issue

#1 - c4-sponsor

2023-07-18T07:14:27Z

LybraFinance marked the issue as sponsor disputed

#2 - LybraFinance

2023-07-18T07:14:41Z

That should be a concern for Lido. Our protocol does not consider the possibility of negative rebases.

#3 - c4-judge

2023-07-26T13:02:41Z

0xean marked the issue as satisfactory

#4 - 0xean

2023-07-27T18:09:53Z

Our protocol does not consider the possibility of negative rebases.

Considering the possibility is documented by Lido, I think integrators should also be considering the possibility.

#5 - c4-judge

2023-07-28T20:40:49Z

0xean marked the issue as selected for report

#6 - Rassska

2023-07-30T13:54:23Z

But 5% negative rebase is extremely rare! If it's so, the Lybra also should consider the Bunker Mode which could be enabled under a massive slashing! I don't think that the likelihood here is medium.

#7 - c4-judge

2023-07-31T23:21:12Z

0xean marked the issue as duplicate of #931

#8 - 0xean

2023-07-31T23:21:35Z

marked as dupe of #931 - both of these stem from the same underlying issue, the lack of accounting for negative rebases.

#9 - c4-judge

2023-08-02T14:40:25Z

0xean marked the issue as not selected for report

Awards

9.931 USDC - $9.93

Labels

bug
grade-b
high quality report
QA (Quality Assurance)
sponsor confirmed
Q-12

External Links

Issues Template

LetterNameDescription
NCNon-criticalNon risky findings
RRefactorChanging the code
Total Found Issues4

Non-Critical Template

CountExplanationInstances
[L-01]Missing checks2

Refactor Template

CountExplanationInstances
[R-01]Documentation Mismatch1
[R-02]Misleading comments2

[L-01] Missing checks

    /**
     * @notice Burn the amount of EUSD and payback the amount of minted EUSD
     * Emits a `Burn` event.
     * Requirements:
     * - `onBehalfOf` cannot be the zero address.
     * - `amount` Must be higher than 0.
     * @dev Calling the internal`_repay`function.
     */
    function burn(address onBehalfOf, uint256 amount) external {
        require(onBehalfOf != address(0), "BURN_TO_THE_ZERO_ADDRESS");
        require(amount>0,"ZERO_AMOUNT"); //Add this check
        _repay(msg.sender, onBehalfOf, amount);
    }

    /**
     * @notice The mint amount number of EUSD is minted to the address
     * Emits a `Mint` event.
     *
     * Requirements:
     * - `onBehalfOf` cannot be the zero address.
     * - `amount` Must be higher than 0. Individual mint amount shouldn't surpass 10% when the circulation reaches 10_000_000
     */
     
    function mint(address onBehalfOf, uint256 amount) external {
        require(onBehalfOf != address(0), "MINT_TO_THE_ZERO_ADDRESS");
        require(amount > 0, "ZERO_MINT");
        _mintEUSD(msg.sender, onBehalfOf, amount, getAssetPrice());
    }

[R-01] Documentation Mismatch

	/*
     * Requirements:
     * - `assetAmount` Must be higher than 0.
     * - `mintAmount` Send 0 if doesn't mint EUSD
     */
    function depositAssetToMint(uint256 assetAmount, uint256 mintAmount) external virtual {
        require(assetAmount >= 1 ether, "Deposit should not be less than 1 stETH.");

        bool success = collateralAsset.transferFrom(msg.sender, address(this), assetAmount);
        require(success, "TF");

[R-02] Misleading Comments

  • In EUSD.sol , the comment "the contract must not be paused", is used for following functions.
  • Recommend removing them as there is no pausing functionality
function transfer
function approve
function transferFrom
function increaseAllowance
function decreaseAllowance
function transferShares
function _approve
function _transferShares
function mint
function burn
function burnShares
  • In EUSD.sol, The example has mentioned wrong amount of tokens
/**
 * @title Interest-bearing ERC20-like token for Lybra protocol.
 *
 * EUSD balances are dynamic and represent the holder's share in the total amount
 * of Ether controlled by the protocol. Account shares aren't normalized, so the
 * contract also stores the sum of all shares to calculate each account's token balance
 * which equals to:
 *
 *   shares[account] * totalSupply / _totalShares
 *
 * For example, assume that we have:
 *
 *   _getTotalMintedEUSD() -> 1000 EUSD
 *   sharesOf(user1) -> 100
 *   sharesOf(user2) -> 400
 *
 * Therefore:
 *
 *   balanceOf(user1) -> 2 tokens which corresponds 200 EUSD  <- Should be 200 instead of 2
 *   balanceOf(user2) -> 8 tokens which corresponds 800 EUSD  <- Should be 800 instead of 8
 *
 * Since balances of all token holders change when the amount of total shares
 * changes, this token cannot fully implement ERC20 standard: it only emits `Transfer`
 * events upon explicit transfer between holders. In contrast, when total amount of
 * pooled Ether increases, no `Transfer` events are generated: doing so would require
 * emitting an event for each token holder and thus running an unbounded loop.
 */

#0 - JeffCX

2023-07-27T16:32:10Z

Well formatted

#1 - c4-pre-sort

2023-07-27T16:32:13Z

JeffCX marked the issue as high quality report

#2 - c4-judge

2023-07-28T00:03:50Z

0xean marked the issue as grade-b

#3 - c4-sponsor

2023-07-29T09:10:19Z

LybraFinance marked the issue as sponsor confirmed

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