Clean Indigo Stallion
High
ETH Refund Mechanism in streamEscrow::cancelStream
Allows Noun Owners to Reclaim Funds Based on Market conditions, Exposing DAO to Potential Losses
Currently, Noun owners can call the streamEscrow::cancelStream
function to withdraw any remaining ETH before the stream reaches maturity. Given that each stream duration is up to 4 years, this provides Noun owners with a prolonged window to give back their NFT if the market value of the NFT depreciates below the amount of ETH
locked, thereby exposing NounsDAO to significant financial risk over the long term.
The streamEscrow::cancelStream
function allows a Noun owner to reclaim any remaining ETH from the stream and return the Noun to the nounsRecipient
.
https://github.com/sherlock-audit/2024-11-nounsdao/blob/main/nouns-monorepo/packages/nouns-contracts/contracts/StreamEscrow.sol#L167-L186
//@audit give back the nounId to nounsRecipient
nounsToken.transferFrom(msg.sender, nounsRecipient, nounId);
//@audit cancel the stream here
streams[nounId].canceled = true;
Stream memory stream = streams[nounId];
ethStreamedPerTick -= stream.ethPerTick;
ethStreamEndingAtTick[stream.lastTick] -= stream.ethPerTick;
// calculate how much needs to be refunded
uint256 ticksLeft = stream.lastTick - currentTick;
uint256 amountToRefund = stream.ethPerTick * ticksLeft;
//@audit refund ETH to noun owner
(bool sent, ) = msg.sender.call{ value: amountToRefund }('');
require(sent, 'failed to send eth');
As shown in the code above, the stream is first canceled, and then the remaining ETH is calculated by subtracting stream.lastTick
from currentTick
, and multiplying the result by stream.ethPerTick
. This means that if 2 years remain in the stream, and the Noun owner initially locked 80 ETH, they would receive 40 ETH
back after those 2 years.
minTickDuration
is 1 day and cannot be updated after deployment ofstreamEscrow
: https://github.com/sherlock-audit/2024-11-nounsdao/blob/main/nouns-monorepo/packages/nouns-contracts/script/StreamEscrow/DeployAuctionHouseV3StreamEscrowMainnet.s.sol#L19- in
createStream
function,streamLengthInTicks
argument is1460
(4 years) according to google docs https://docs.google.com/document/d/1gxLSkRQooJtcqMm3I86cOJpsnaObwEE5ergqkSl87kM/edit?tab=t.0 - Once a stream is created, its
stream.lastTick
cannot be changed unless the NFT owner modifies it usingfastForward
function
In this scenario, the ETH price increases while the NFT price crashes. This creates an opportunity for the user to reclaim more ETH than the NFT is worth:
1- The user places the highest bid in the current auction and wins Noun #88 for 100 ETH.
2- 80 ETH is locked in streamEscrow
and is released gradually over 4 years.
3- After 1 year, the user's Noun price drops significantly, but ETH price has increased 5X.
4- The user sees that 3/4 of their ETH is still locked in the stream, now worth much more than the Noun.
5- The user cancels the stream, withdraws the ETH, and returns the now underpriced Noun.
NounsDAO faces significant losses by giving Noun owners a 4 years window to cancel the stream and reclaim their remaining ETH whenever the value of the NFT drops well below the locked ETH. This design allows Noun owners to withdraw remaining ETH whenever the market value of their NFT drops below the amount they’ve locked in. For example, if a user wins an auction with a 100 ETH bid and locks 80 ETH (after the 20% treasury fee), they are committing the ETH to the stream for 4 years. In this volatile crypto market, ETH can fluctuate significantly (e.g. ETH increased from 2200 to 3500 in just one month). After 2 years, if the NFT price drops below 10 ETH but the user still has 40 ETH remaining in the stream, they can easily withdraw that ETH and return the underpriced NFT.
There are several ways to mitigate this issue, i will list some of them here: 1- Limit the amount of locked ETH that can be withdrawn. 2- Allow ETH to be withdrawn only within a specific window after the auction. 3- Implement a cooldown period between withdrawals, and allow only partial withdrawals at a time. 4- Prevent the return of ETH if the NFT’s market value is significantly below its locked ETH.