Lively Neon Scorpion
High
The escrowed funds could be maliciously fast-forwarded to the treasury if a Noun token is lent out on a NFT rental market.
When an owner lend their token out in a NFT rental market, the borrower might maliciously release the escrowed funds of the lent token to the treasury since the borrower will become the owner of the lent token temporally.
No response
No response
No response
NounsAuctionHouseV3
allows to create a stream for each new Noun sold on auction when the auction is settled.
The auctioned Noun token will be transferred to the winning bidder. A portion of the auction funds will be escrowed in StreamEscrow
and then gradually released to the treasury over a period of streamLengthInTicks
days.
The Noun owners may also fast-forward their stream as their wish by calling StreamEscrow#fastForwardStream()
.
NFT owners may choose to leverage their NFTs for financial gain, such as through rental or trading.
If the Noun token is sold, its new owner can decide whether to accelerate the release of escrowed funds.
However, if the Noun token is lent out, its escrowed funds could be maliciously fast-forwarded to the treasury. The lender has no way to return the token and cancel the stream to receive a refund by calling StreamEscrow#cancelStream()
.
E.g. a Noun token could be lent out in RENFT(a NFT rental market). RENFT market will create an escrowed wallet to hold the token. The escrowed wallet is not allowed to transfer the token to any other account except returning the token to its owner when the rental is terminated. The escrowed wallet can execute any call except those that could allow the token being transferred to another account, such as IERC721.approve()
, IERC721.safeTransferFrom()
, and IERC721.transferFrom()
. But it can call StreamEscrow#fastForwardStream()
successfully since the escrowed wallet is the owner of the token.
In the worst-case scenario, all escrowed funds for the lent token will be released to the treasury, leaving the owner unable to return the token and receive a refund.
The escrowed funds could be maliciously fast-forwarded to the treasury if a Noun token is lent out on a NFT rental
No response
The escrowed funds can be fast forwarded only when the caller is both the token owner and the bid winner: https://github.com/sherlock-audit/2024-11-nounsdao/blob/main/nouns-monorepo/packages/nouns-contracts/contracts/NounsAuctionHouseV3.sol:
function _settleAuction() internal {
INounsAuctionHouseV3.AuctionV2 memory _auction = auctionStorage;
require(_auction.startTime != 0, "Auction hasn't begun");
require(!_auction.settled, 'Auction has already been settled');
require(block.timestamp >= _auction.endTime, "Auction hasn't completed");
auctionStorage.settled = true;
uint256 amountToSendTreasury = (_auction.amount * immediateTreasuryBPs) / 10_000;
uint256 amountToStream = _auction.amount - amountToSendTreasury;
if (amountToSendTreasury > 0) {
_safeTransferETHWithFallback(owner(), amountToSendTreasury);
}
if (amountToStream > 0) {
- streamEscrow.forwardAllAndCreateStream{ value: amountToStream }(_auction.nounId, streamLengthInTicks);
+ streamEscrow.forwardAllAndCreateStream{ value: amountToStream }(_auction.nounId, streamLengthInTicks, _auction.bidder);
} else {
streamEscrow.forwardAll();
}
if (_auction.bidder == address(0)) {
nouns.burn(_auction.nounId);
} else {
nouns.transferFrom(address(this), _auction.bidder, _auction.nounId);
}
SettlementState storage settlementState = settlementHistory[_auction.nounId];
settlementState.blockTimestamp = uint32(block.timestamp);
settlementState.amount = ethPriceToUint64(_auction.amount);
settlementState.winner = _auction.bidder;
if (_auction.clientId > 0) settlementState.clientId = _auction.clientId;
emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount);
if (_auction.clientId > 0) emit AuctionSettledWithClientId(_auction.nounId, _auction.clientId);
}
struct Stream {
uint128 ethPerTick;
bool canceled;
uint32 lastTick;
+ address winner;
}
function createStream(uint256 nounId, uint16 streamLengthInTicks, address winner) public payable {
require(allowedToCreateStream[msg.sender], 'not allowed');
require(isApprovedOrOwner(msg.sender, nounId), 'only noun owner or approved');
require(!isStreamActive(nounId), 'stream active');
// register new stream
uint128 ethPerTick = toUint128(msg.value / streamLengthInTicks);
uint32 streamLastTick = currentTick + streamLengthInTicks;
ethStreamEndingAtTick[streamLastTick] += ethPerTick;
// the remainder is immediately streamed to the DAO
uint256 remainder = msg.value % streamLengthInTicks;
sendETHToTreasury(remainder);
uint128 newEthStreamedPerTick = ethStreamedPerTick + ethPerTick;
ethStreamedPerTick = newEthStreamedPerTick;
- streams[nounId] = Stream({ ethPerTick: ethPerTick, canceled: false, lastTick: streamLastTick });
+ streams[nounId] = Stream({ ethPerTick: ethPerTick, canceled: false, lastTick: streamLastTick, winner: winner });
emit StreamCreated(nounId, msg.value, streamLengthInTicks, ethPerTick, newEthStreamedPerTick, streamLastTick);
}
function fastForwardStream(uint256 nounId, uint32 ticksToForward) public {
require(ticksToForward > 0, 'ticksToForward must be positive');
require(nounsToken.ownerOf(nounId) == msg.sender, 'not noun owner');
Stream memory stream = streams[nounId];
+ require(stream.winner == msg.sender, 'not the winner');
uint32 currentTick_ = currentTick;
require(isStreamActive(stream, currentTick_), 'stream not active');
// move last tick
require(ticksToForward <= stream.lastTick - currentTick_, 'ticksToFoward too large');
uint32 newLastTick = stream.lastTick - ticksToForward;
ethStreamEndingAtTick[stream.lastTick] -= stream.ethPerTick;
streams[nounId].lastTick = newLastTick;
if (newLastTick > currentTick_) {
// stream is still active, so register the new end tick
ethStreamEndingAtTick[newLastTick] += stream.ethPerTick;
} else {
// no more ticks left, so finished the stream
ethStreamedPerTick -= stream.ethPerTick;
}
uint256 ethToStream = ticksToForward * stream.ethPerTick;
sendETHToTreasury(ethToStream);
emit StreamFastForwarded(nounId, ticksToForward, newLastTick, ethStreamedPerTick);
}