Sablier

Sablier
DeFiFoundry
53,440 USDC
View results
Submission Details
Severity: medium
Invalid

Necessity of Reentrancy Guard on withdraw, withdrawMax, and withdrawMaxAndTransfer Functions

Description

The withdraw, withdrawMax, and withdrawMaxAndTransfer functions in the SablierV2Lockup contract are designed to handle withdrawals and transfers of funds and NFTs. It might be assumed that there is no need for a reentrancy guard on these functions because, once the funds are withdrawn, there are no funds left to re-enter and withdraw. However, this assumption is not necessarily safe. Reentrancy attacks can exploit the gap between state changes, and without a guard, an attacker could potentially reenter the function before the state is fully updated.

1: Withdraw Function:

function withdraw(
uint256 streamId,
address to,
uint128 amount
) public override noDelegateCall notNull(streamId) updateMetadata(streamId) {
// Function logic...
}

2: withdrawMax Function

function withdrawMax(uint256 streamId, address to) external override {
withdraw({ streamId: streamId, to: to, amount: _withdrawableAmountOf(streamId)
// function logic
});
}

3: withdrawMaxAndTransfer

function withdrawMaxAndTransfer(
uint256 streamId,
address newRecipient
) external override noDelegateCall notNull(streamId)
// function logic

Proof of concept

An attacker initiates a call to withdraw, withdrawMax, or withdrawMaxAndTransfer, which triggers the withdrawal of funds.

1: The attacker calls one of the functions.

2: Before the state variables (withdrawn amount, isDepleted flag) are fully updated, the attacker reenters the function.

3: The reentrant call exploits the gap between state changes, potentially leading to multiple withdrawals or incorrect state updates.

Impact

1: Without a reentrancy guard, an attacker could reenter the withdraw, withdrawMax, and withdrawMaxAndTransfer functions before the state variables are fully updated. This could lead to inconsistencies in the contract's state, such as incorrect withdrawn amounts or isDepleted flags.

2: An attacker could exploit the timing of state updates to manipulate the contract's state, potentially leading to unauthorized withdrawals or other malicious activities.

Tools Used

Manual review

Recommendations

Implementing a reentrancy guard, such as the nonReentrant modifier from OpenZeppelin's ReentrancyGuard, can prevent reentrant calls and ensure that state updates are atomic and consistent.

Here is the updated code with Reentrancy Guard:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SablierV2Lockup is ReentrancyGuard {
// Other contract code...
function withdraw(
uint256 streamId,
address to,
uint128 amount
) public override noDelegateCall notNull(streamId) updateMetadata(streamId) nonReentrant {
// function logic
function withdrawMax(uint256 streamId, address to) external override nonReentrant {
withdraw({ streamId: streamId, to: to, amount: _withdrawableAmountOf(streamId) })
// function logic ;
}
function withdrawMaxAndTransfer(
uint256 streamId,
address newRecipient
) external override noDelegateCall notNull(streamId) nonReentrant });
// function logic
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.