Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

novaman33 - _splitWithdrawRequest will make invalid withdraw requests in an edge case #6

Open
sherlock-admin4 opened this issue Jul 3, 2024 · 13 comments
Labels
Escalation Resolved This issue's escalations have been approved/rejected Has Duplicates A valid issue with 1+ other issues describing the same vulnerability High A valid High severity issue Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed

Comments

@sherlock-admin4
Copy link
Contributor

sherlock-admin4 commented Jul 3, 2024

novaman33

High

_splitWithdrawRequest will make invalid withdraw requests in an edge case

Summary

When an account is deleveraged, _splitWithdrawRequest is called so that pending withdraw requests of the account that is being liquidated are split between them and the liquidator. However when the account is being fully liquidated, the old withdraw request is deleted which creates an invalid Id for the liquidator's withdrawRequest.

Vulnerability Detail

In _splitWithdrawRequest the request of the from address is being read by storage:

 WithdrawRequest storage w = VaultStorage.getAccountWithdrawRequest()[_from];

Then the following check is made to delete the withdraw request of the from account if all the vault tokens are being taken from him:

 if (w.vaultShares == vaultShares) {
            // If the resulting vault shares is zero, then delete the request. The _from account's
            // withdraw request is fully transferred to _to
            delete VaultStorage.getAccountWithdrawRequest()[_from];
        } 

Here the delete keyword is used to reset the withdraw request of the from account. However the w variable is still a pointer to this place in storage meaning that resetting VaultStorage.getAccountWithdrawRequest()[_from] will also be resetting w. As a result w.requestId=0 and

toWithdraw.requestId = w.requestId;

Here the new requestId is equal to 0 which is the default value meaning that this withdraw request will not be recognized by _finalizeWithdrawsManual and the other finalize withdraw functions and all these vault shares will be lost. Also if initiateWithdraw is called the old vaultShares will be wiped out for the new shares to be withdrawn.

Impact

Loss of vaut tokens for the liquidator.

Code Snippet

https://github.com/sherlock-audit/2024-06-leveraged-vaults/blob/14d3eaf0445c251c52c86ce88a84a3f5b9dfad94/leveraged-vaults-private/contracts/vaults/common/WithdrawRequestBase.sol#L221

Tool used

Manual Review

Recommendation

Store the requestId of the from address in another memory variable.

@github-actions github-actions bot closed this as completed Jul 5, 2024
@github-actions github-actions bot added the Excluded Excluded by the judge without consulting the protocol or the senior label Jul 5, 2024
@sherlock-admin2
Copy link
Contributor

1 comment(s) were left on this issue during the judging contest.

0xmystery commented:

w has been assigned and will not be reset

@sherlock-admin3 sherlock-admin3 changed the title Sunny Lava Mallard - _splitWithdrawRequest will make invalid withdraw requests in an edge case novaman33 - _splitWithdrawRequest will make invalid withdraw requests in an edge case Jul 11, 2024
@sherlock-admin3 sherlock-admin3 added the Non-Reward This issue will not receive a payout label Jul 11, 2024
@novaman33
Copy link

Escalate,
This is a valid issue. w is a pointer and is being reset when mapping is deleted.

@sherlock-admin3
Copy link

Escalate,
This is a valid issue. w is a pointer and is being reset when mapping is deleted.

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

@sherlock-admin4 sherlock-admin4 added the Escalated This issue contains a pending escalation label Jul 11, 2024
@amankakar
Copy link

This Issue is dup of #89

You can observe that there is no return or halt in the flow when a request is deleted. The same requestID, which has been deleted, retains its storage scope. Consequently, the requestID assigned to the new withdrawal request for the liquidator will always be zero in this scenario. This is a significant issue that could result in asset loss and needs to be reconsidered.
For proof, I have also added simple code demonstrating that the requestID will be 0 after deletion.

@mystery0x
Copy link
Collaborator

This Issue is dup of #89

You can observe that there is no return or halt in the flow when a request is deleted. The same requestID, which has been deleted, retains its storage scope. Consequently, the requestID assigned to the new withdrawal request for the liquidator will always be zero in this scenario. This is a significant issue that could result in asset loss and needs to be reconsidered. For proof, I have also added simple code demonstrating that the requestID will be 0 after deletion.

Please provide a coded POC alleging your claim. I will also request the sponsors looking into your report.

@novaman33
Copy link

novaman33 commented Jul 15, 2024

@mystery0x

// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.22;

import "forge-std/Test.sol";

struct WithdrawRequest {
    uint256 requestId;
    uint256 vaultShares;
    bool hasSplit;
}
contract TestRequestID is Test {
    address alice = makeAddr("aLice");
    address bob = makeAddr("bob");

    struct SplitWithdrawRequest {
        uint256 totalVaultShares; // uint64
        uint256 totalWithdraw; // uint184?
        bool finalized;
    }

    function setUp() external {}

    uint256 public constant DISTRIBUTION_DIVISOR = 100 ether;

    function testRequestId() external {
        _createRequest();
        _splitWithdrawRequest(address(alice), address(bob), 100);
        WithdrawRequest storage w = VaultStorage.getAccountWithdrawRequest()[
            bob
        ];
        assert(w.requestId == 0);
    }

    function _splitWithdrawRequest(
        address _from,
        address _to,
        uint256 vaultShares
    ) internal {
        WithdrawRequest storage w = VaultStorage.getAccountWithdrawRequest()[
            _from
        ];
        if (w.requestId == 0) return;
        // @audit : Ingore this code as it does not have any impact in our case 
        // Create a new split withdraw request
        // if (!w.hasSplit) {
        //     SplitWithdrawRequest memory s = VaultStorage
        //         .getSplitWithdrawRequest()[w.requestId];
        //     // Safety check to ensure that the split withdraw request is not active, split withdraw
        //     // requests are never deleted. This presumes that all withdraw request ids are unique.
        //     require(s.finalized == false && s.totalVaultShares == 0);
        //     VaultStorage
        //     .getSplitWithdrawRequest()[w.requestId].totalVaultShares = w
        //         .vaultShares;
        // }

        console.log("Request ID is Before Delete :", w.requestId);

        if (w.vaultShares == vaultShares) {
            // If the resulting vault shares is zero, then delete the request. The _from account's
            // withdraw request is fully transferred to _to
            delete VaultStorage.getAccountWithdrawRequest()[_from];
        } else {
            // Otherwise deduct the vault shares
            w.vaultShares = w.vaultShares - vaultShares;
            w.hasSplit = true;
        }

        // Ensure that no withdraw request gets overridden, the _to account always receives their withdraw
        // request in the account withdraw slot.
        WithdrawRequest storage toWithdraw = VaultStorage
            .getAccountWithdrawRequest()[_to];
        require(
            toWithdraw.requestId == 0 || toWithdraw.requestId == w.requestId,
            "Existing Request"
        );

        console.log("Request ID is After Delete :", w.requestId);
        // Either the request gets set or it gets incremented here.
        toWithdraw.requestId = w.requestId; // @audit : the request id will be zero here
        toWithdraw.vaultShares = toWithdraw.vaultShares + vaultShares;
        toWithdraw.hasSplit = true;
    }

    function _createRequest() internal {
        WithdrawRequest storage accountWithdraw = VaultStorage
            .getAccountWithdrawRequest()[alice];

        accountWithdraw.requestId = uint256(uint160(address(alice)));
        accountWithdraw.vaultShares = 100;
        accountWithdraw.hasSplit = false;
    }
}

library VaultStorage {
    /// @notice Emitted when vault settings are updated
    // event StrategyVaultSettingsUpdated(StrategyVaultSettings settings);
    // Wrap timestamp in a struct so that it can be passed around as a storage pointer
    struct LastClaimTimestamp {
        uint256 value;
    }

    /// @notice account initiated WithdrawRequest
    uint256 private constant ACCOUNT_WITHDRAW_SLOT = 1000008;

    function getAccountWithdrawRequest()
        internal
        pure
        returns (mapping(address => WithdrawRequest) storage store)
    {
        assembly {
            store.slot := ACCOUNT_WITHDRAW_SLOT
        }
    }
}
  Request ID is Before Delete : 964133639515395071211053928642046522704087555788
  Request ID is After Delete : 0

@WangSecurity
Copy link

I agree with the escalation, it is a nice find. Planning to accept it and validate it with High severity since it will happen with every full liquidation. The duplicate is #89, @mystery0x are there other duplicates as well?

@wtp0x
Copy link

wtp0x commented Jul 15, 2024

#41

@WangSecurity WangSecurity added High A valid High severity issue Has Duplicates A valid issue with 1+ other issues describing the same vulnerability labels Jul 16, 2024
@sherlock-admin2 sherlock-admin2 added Reward A payout will be made for this issue and removed Non-Reward This issue will not receive a payout labels Jul 16, 2024
@WangSecurity
Copy link

WangSecurity commented Jul 16, 2024

Result:
High
Has duplicates

@WangSecurity WangSecurity reopened this Jul 16, 2024
@sherlock-admin2
Copy link
Contributor

sherlock-admin2 commented Jul 16, 2024

Escalations have been resolved successfully!

Escalation status:

@sherlock-admin3 sherlock-admin3 removed the Escalated This issue contains a pending escalation label Jul 16, 2024
@sherlock-admin4 sherlock-admin4 added the Escalation Resolved This issue's escalations have been approved/rejected label Jul 16, 2024
@mystery0x
Copy link
Collaborator

I agree with the escalation, it is a nice find. Planning to accept it and validate it with High severity since it will happen with every full liquidation. The duplicate is #89, @mystery0x are there other duplicates as well?

#6, #41, and #89 are the only three reports submitting this finding.

@sherlock-admin3 sherlock-admin3 added Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed and removed Excluded Excluded by the judge without consulting the protocol or the senior labels Jul 22, 2024
jeffywu added a commit to notional-finance/leveraged-vaults that referenced this issue Aug 7, 2024
@sherlock-admin2
Copy link
Contributor

sherlock-admin2 commented Aug 9, 2024

The protocol team fixed this issue in the following PRs/commits:
notional-finance/leveraged-vaults#106

jeffywu added a commit to notional-finance/leveraged-vaults that referenced this issue Aug 9, 2024
jeffywu added a commit to notional-finance/leveraged-vaults that referenced this issue Aug 9, 2024
jeffywu added a commit to notional-finance/leveraged-vaults that referenced this issue Aug 12, 2024
jeffywu added a commit to notional-finance/leveraged-vaults that referenced this issue Aug 13, 2024
jeffywu added a commit to notional-finance/leveraged-vaults that referenced this issue Aug 13, 2024
@sherlock-admin2
Copy link
Contributor

The Lead Senior Watson signed off on the fix.

jeffywu added a commit to notional-finance/leveraged-vaults that referenced this issue Sep 9, 2024
* fix: adding post mint and redeem hooks

* test: changes to base tests

* config: changes to config

* feat: changes to global

* feat: changes to trading

* feat: changes to utils

* feat: changes to single sided lp

* feat: vault storage

* fix: misc fixes

* fix: staking vaults

* fix: solidity versions

* fix: test build

* fix: adding staking harness

* fix: adding initialization

* fix: initial test bugs

* fix: weETH valuation

* fix: deleverage collateral check

* fix: initial harness compiling

* fix: initial test running

* fix: acceptance tests passing

* test: migrated some tests

* fix: withdraw tests

* test: adding deleverage test

* fix: adding liquidation tests

* test: withdraw request

* test: finalize withdraws manual

* test: tests passing

* fix: single sided lp tests with vault rewarder

* fix: putting rewarder tests in

* fix: reward tests running

* fix: vault rewarder address

* fix: initial staking harness

* fix: adding staking harness

* fix: initial PT vault build

* fix: moving ethena vault code

* fix: moving etherfi code

* feat: adding pendle implementations

* fix: staking harness to use USDC

* fix: curve v2 adapter for trading

* test: basic tests passing

* fix: adding secondary trading on withdraw

* fix tests

* fix: trading on redemption

* fix: ethena vault config

* fix: switch ethena vault to sell sDAI

* fix warnings

* fix: more liquidation tests passing

* fix: ethan liquidation tests

* pendle harness build

* fix: initial tests passing

* fix: adding pendle oracle

* fix: test deal token error

* fix: changing pendle liquidation discount

* fix: all tests passing

* fix: etherfi borrow currency

* fix: adding more documentation

* change mainnet fork block

* properly update data seed files

* fix arbitrum tests

* fix test SingleSidedLP:Convex:crvUSD/[USDT]

* fix: can finalize withdraws

* fix: refactor withdraw valuation

* fix: pendle expiration tests

* fix: pendle pt valuation

* remove flag

* fix: remove redundant code path

* fix: initial commit

* fix: vault changes

* fix: vault changes

* fix: some tests passing

* fix: fixing more tests

* fix: updated remaining tests

* fix: split withdraw bug

* fix: new test

* fix: remaining tests

* fix: split withdraw reqest bug

* feat: add PendlePTKelp vault

* update oracle address, fix tests

* Address CR comments

* add test_canTriggerExtraStep

* fix tests

* fix: run tests

* feat: adding generic vault

* feat: update generate tests

* fix: changes from merge

* fix: adding has withdraw requests

* fix: update oracle address for network

* fix: merge kelp harness

* fix: base tests passing

* fix: move generation config

* fix: initial pendle test generation

* fix: mainnet tests passing

* fix: vault rewarder

* fix: more pendle tests

* fix: pendle dex test

* fix: adding camelot dex

* fix: update usde pt

* fix: adding camelot adapter

* fix: support configurable dex

* fix: adding more PT vaults

* fix: approval bug

* fix: update dex information

* fix: mainnet tests passing

* fix: update arbitrum pendle tests

* fix: update deployment addresses

* test: add balancer v2 batch trade

* fix: add given out batch trade

* fix: remove trade amount filling

* fix: add some comments

* fix: audit issue #60

* fix: switch to using getDecimals

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#73

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#72

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#70

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#66

* test: adding pendle oracle test

* fix:  sherlock-audit/2024-06-leveraged-vaults-judging#69

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#64

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#43

* fix: audit issue #18

* fix: move slippage check

* fix: add comment back

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#56

* test: adding test that catches math underflow

* fix: adding test for vault shares

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#44

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#6

* test: adds test to check split withdraw request value

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#78

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#80

* fix: updating valuations for tests

* fix: update run tests

* fix: remove stETH withdraws from Kelp in favor of ETH withdraws

* fix: update tests for pendle rs eth

* fix: resolve compile issues

* fix: rsETH oracle price

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#87

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#67

* fix: sherlock-audit/2024-06-leveraged-vaults-judging#6

* test: update tests for invalid splits

* fix: sherlock fix review comments

* merge: merged master into branch

* fix: empty reward tokens

* fix: claim rewards tests

* fix: liquidation tests

* fixing more tests

* fix: allowing unused reward pools

* test: migrating reward pools

* fix: rewarder test

* fix: claim rewards before withdrawing

* fix: deployed vault rewarder lib on arbitrum

* fix: deployed new tbtc vault

* docs: adding deployment documentation

* fix: update config

---------

Co-authored-by: sbuljac <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Escalation Resolved This issue's escalations have been approved/rejected Has Duplicates A valid issue with 1+ other issues describing the same vulnerability High A valid High severity issue Reward A payout will be made for this issue Sponsor Confirmed The sponsor acknowledged this issue is valid Will Fix The sponsor confirmed this issue will be fixed
Projects
None yet
Development

No branches or pull requests

8 participants