Function withdraw()
is used to withdraw token from NFT:
function withdraw(
uint256 streamId,
address to,
uint128 amount
)
public
override
noDelegateCall
notNull(streamId)
updateMetadata(streamId)
{
// Check: the stream is not depleted.
if (_streams[streamId].isDepleted) {
revert Errors.SablierV2Lockup_StreamDepleted(streamId);
}
// Check: the withdrawal address is not zero.
if (to == address(0)) {
revert Errors.SablierV2Lockup_WithdrawToZeroAddress(streamId);
}
// Check: the withdraw amount is not zero.
if (amount == 0) {
revert Errors.SablierV2Lockup_WithdrawAmountZero(streamId);
}
// Retrieve the recipient from storage.
address recipient = _ownerOf(streamId);
// Check: if `msg.sender` is neither the stream's recipient nor an approved third party, the withdrawal address
// must be the recipient.
if (to != recipient && !_isCallerStreamRecipientOrApproved(streamId)) { // <---- (1)
revert Errors.SablierV2Lockup_WithdrawalAddressNotRecipient(streamId, msg.sender, to);
}
// Check: the withdraw amount is not greater than the withdrawable amount.
uint128 withdrawableAmount = _withdrawableAmountOf(streamId);
if (amount > withdrawableAmount) {
revert Errors.SablierV2Lockup_Overdraw(streamId, amount, withdrawableAmount);
}
// Retrieve the sender from storage.
address sender = _streams[streamId].sender;
// Effects and Interactions: make the withdrawal.
_withdraw(streamId, to, amount);
// Interaction: if `msg.sender` is not the recipient and the recipient is a contract, try to invoke the
// withdraw hook on it without reverting if the hook is not implemented, and also without bubbling up
// any potential revert.
if (msg.sender != recipient && recipient.code.length > 0) {
try ISablierV2Recipient(recipient).onLockupStreamWithdrawn({ // <---- (2)
streamId: streamId,
caller: msg.sender,
to: to,
amount: amount
}) { } catch { }
}
// Interaction: if `msg.sender` is not the sender, the sender is a contract and is different from the
// recipient, try to invoke the withdraw hook on it without reverting if the hook is not implemented, and also
// without bubbling up any potential revert.
if (msg.sender != sender && sender.code.length > 0 && sender != recipient) { // <---- (3)
try ISablierV2Sender(sender).onLockupStreamWithdrawn({
streamId: streamId,
caller: msg.sender,
to: to,
amount: amount
}) { } catch { }
}
}
From (1), it can be seen that if msg.sender
is neither the stream's recipient nor an approved third party, but withdrawal address is the recipient, transaction wont be revert. Moreover, after this condition, transaction cant be revert in (2) and (3) due to try - catch request. Which mean, it is able for other user to withdraw token for anyone to NFT owner's address.
From documentation, it is said that blur marketplace is one of marketplace that can be used to trade NFT. But blur marketplace have buy now pay later option, which require user to deposit token to blur's contract to lock token link, after transfer, NFT owner will be blur's contract:
function borrow(
LoanOffer calldata offer,
bytes calldata signature,
uint256 loanAmount,
uint256 collateralTokenId
) external returns (uint256 lienId) {
lienId = _borrow(offer, signature, loanAmount, collateralTokenId);
/* Lock collateral token. */
offer.collection.safeTransferFrom(msg.sender, address(this), collateralTokenId); // <---
/* Transfer loan to borrower. */
_POOL.transferFrom(offer.lender, msg.sender, loanAmount);
}
When user use this option, attacker can call withdraw()
function to transfer token to blur's address, reduce total token amount can be withdrawn from NFT. Attacker can not get profit from this attack, but victim will lose tokens forever.
Token are lost and stuck in the blur's contract
Manual review
Do not allow other user except NFT's owner or approved user call withdraw()
function
https://docs.codehawks.com/hawks-auditors/how-to-determine-a-finding-validity
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.