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

ZeroTrust - In the _splitWithdrawRequest() function, there exists an issue that causes both the from and to requestId to be 0 #41

Closed
sherlock-admin4 opened this issue Jul 3, 2024 · 8 comments
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Escalation Resolved This issue's escalations have been approved/rejected High A valid High severity issue Reward A payout will be made for this issue

Comments

@sherlock-admin4
Copy link
Contributor

sherlock-admin4 commented Jul 3, 2024

ZeroTrust

High

In the _splitWithdrawRequest() function, there exists an issue that causes both the from and to requestId to be 0

Summary

In the _splitWithdrawRequest() function, there exists an issue that causes both the from and to requestId to be 0

Vulnerability Detail

 function _splitWithdrawRequest(address _from, address _to, uint256 vaultShares) internal {
 @>       WithdrawRequest storage w = VaultStorage.getAccountWithdrawRequest()[_from];
        if (w.requestId == 0) return;

        // 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;
        }

        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");

        // Either the request gets set or it gets incremented here.
@>      toWithdraw.requestId = w.requestId;
        toWithdraw.vaultShares = toWithdraw.vaultShares + vaultShares;
        toWithdraw.hasSplit = true;
    }

The ‘w’ is the storage WithdrawRequest of _from. In the case of w.vaultShares equaling vaultShares, the storage WithdrawRequest of _from is deleted (meaning the storage is reset to 0). Then w.requestId becomes 0, so toWithdraw.requestId is 0.
Funds cannot be retrieved from third-party protocols, leading to a loss of funds.

Impact

Funds cannot be retrieved from third-party protocols, leading to a loss of funds.

Code Snippet

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

Tool used

Manual Review

Recommendation

Please use memory cached variables.

Duplicate of #6

@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-admin4
Copy link
Contributor Author

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

0xmystery commented:

w has been assigned and will not be reset

@sherlock-admin4 sherlock-admin4 changed the title Hidden Laurel Cod - In the _splitWithdrawRequest() function, there exists an issue that causes both the from and to requestId to be 0 ZeroTrust - In the _splitWithdrawRequest() function, there exists an issue that causes both the from and to requestId to be 0 Jul 11, 2024
@sherlock-admin4 sherlock-admin4 added the Non-Reward This issue will not receive a payout label Jul 11, 2024
@0502lian
Copy link

Escalate

Let’s take a look at some of the key lines of code.

 function _splitWithdrawRequest(address _from, address _to, uint256 vaultShares) internal {
 @>       WithdrawRequest storage w = VaultStorage.getAccountWithdrawRequest()[_from];
        
        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];
        } 
        // 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");

        // Either the request gets set or it gets incremented here.
@>      toWithdraw.requestId = w.requestId;
        toWithdraw.vaultShares = toWithdraw.vaultShares + vaultShares;
        toWithdraw.hasSplit = true;
    }

Because w is in storage, after deletion, w.requestId = 0, and toWithdraw.requestId will also be 0.
Although VaultStorage.getSplitWithdrawRequest()[w.requestId].totalVaultShares = w.vaultShares; retains the correct w.vaultShares,
VaultStorage.getAccountWithdrawRequest() and VaultStorage.getSplitWithdrawRequest() are completely different data structures.

function getAccountWithdrawRequest() internal pure returns (mapping(address => WithdrawRequest) storage store) {
        assembly { store.slot := ACCOUNT_WITHDRAW_SLOT }
    }

function getSplitWithdrawRequest() internal pure returns (mapping(uint256 => SplitWithdrawRequest) storage store) {
        assembly { store.slot := SPLIT_WITHDRAW_SLOT }
    }

WithdrawRequest and SplitWithdrawRequest are different data structures.

struct WithdrawRequest {
    uint256 requestId;
    uint256 vaultShares;
    bool hasSplit;
}

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

Because both VaultStorage.getAccountWithdrawRequest()[_from].requestId and VaultStorage.getAccountWithdrawRequest()[_to].requestId are set to 0, VaultStorage.getSplitWithdrawRequest()[right requestId] does not have an index that can be accessed.

So funds cannot be withdraw from third-party protocols, leading to a loss of funds.

@sherlock-admin3
Copy link

Escalate

Let’s take a look at some of the key lines of code.

 function _splitWithdrawRequest(address _from, address _to, uint256 vaultShares) internal {
 @>       WithdrawRequest storage w = VaultStorage.getAccountWithdrawRequest()[_from];
        
        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];
        } 
        // 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");

        // Either the request gets set or it gets incremented here.
@>      toWithdraw.requestId = w.requestId;
        toWithdraw.vaultShares = toWithdraw.vaultShares + vaultShares;
        toWithdraw.hasSplit = true;
    }

Because w is in storage, after deletion, w.requestId = 0, and toWithdraw.requestId will also be 0.
Although VaultStorage.getSplitWithdrawRequest()[w.requestId].totalVaultShares = w.vaultShares; retains the correct w.vaultShares,
VaultStorage.getAccountWithdrawRequest() and VaultStorage.getSplitWithdrawRequest() are completely different data structures.

function getAccountWithdrawRequest() internal pure returns (mapping(address => WithdrawRequest) storage store) {
        assembly { store.slot := ACCOUNT_WITHDRAW_SLOT }
    }

function getSplitWithdrawRequest() internal pure returns (mapping(uint256 => SplitWithdrawRequest) storage store) {
        assembly { store.slot := SPLIT_WITHDRAW_SLOT }
    }

WithdrawRequest and SplitWithdrawRequest are different data structures.

struct WithdrawRequest {
    uint256 requestId;
    uint256 vaultShares;
    bool hasSplit;
}

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

Because both VaultStorage.getAccountWithdrawRequest()[_from].requestId and VaultStorage.getAccountWithdrawRequest()[_to].requestId are set to 0, VaultStorage.getSplitWithdrawRequest()[right requestId] does not have an index that can be accessed.

So funds cannot be withdraw from third-party protocols, leading to a loss of funds.

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 13, 2024
@amankakar
Copy link

Same Issue as #6.

@mystery0x
Copy link
Collaborator

Escalate

Let’s take a look at some of the key lines of code.

 function _splitWithdrawRequest(address _from, address _to, uint256 vaultShares) internal {
 @>       WithdrawRequest storage w = VaultStorage.getAccountWithdrawRequest()[_from];
        
        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];
        } 
        // 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");

        // Either the request gets set or it gets incremented here.
@>      toWithdraw.requestId = w.requestId;
        toWithdraw.vaultShares = toWithdraw.vaultShares + vaultShares;
        toWithdraw.hasSplit = true;
    }

Because w is in storage, after deletion, w.requestId = 0, and toWithdraw.requestId will also be 0. Although VaultStorage.getSplitWithdrawRequest()[w.requestId].totalVaultShares = w.vaultShares; retains the correct w.vaultShares, VaultStorage.getAccountWithdrawRequest() and VaultStorage.getSplitWithdrawRequest() are completely different data structures.

function getAccountWithdrawRequest() internal pure returns (mapping(address => WithdrawRequest) storage store) {
        assembly { store.slot := ACCOUNT_WITHDRAW_SLOT }
    }

function getSplitWithdrawRequest() internal pure returns (mapping(uint256 => SplitWithdrawRequest) storage store) {
        assembly { store.slot := SPLIT_WITHDRAW_SLOT }
    }

WithdrawRequest and SplitWithdrawRequest are different data structures.

struct WithdrawRequest {
    uint256 requestId;
    uint256 vaultShares;
    bool hasSplit;
}

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

Because both VaultStorage.getAccountWithdrawRequest()[_from].requestId and VaultStorage.getAccountWithdrawRequest()[_to].requestId are set to 0, VaultStorage.getSplitWithdrawRequest()[right requestId] does not have an index that can be accessed.

So funds cannot be withdraw from third-party protocols, leading to a loss of funds.

Please see my comment on #6.

@WangSecurity
Copy link

Agree it's a valid issue, planning to accept the escalation and duplicate with #6.

@WangSecurity WangSecurity added High A valid High severity issue Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label 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

Result:
High
Duplicate of #6

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

Escalations have been resolved successfully!

Escalation status:

@sherlock-admin3 sherlock-admin3 removed the Excluded Excluded by the judge without consulting the protocol or the senior label Jul 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Escalation Resolved This issue's escalations have been approved/rejected High A valid High severity issue Reward A payout will be made for this issue
Projects
None yet
Development

No branches or pull requests

7 participants