Skip to content

Latest commit

 

History

History
111 lines (73 loc) · 5.34 KB

034.md

File metadata and controls

111 lines (73 loc) · 5.34 KB

Feisty Pickle Starling

High

An attacker can completely DoS stream logic and severely harm DAO treasury by repeatedly canceling early

Summary

Providing full refund in case of stream canceling in StreamEscrow.sol will cause a significant loss of intended ETH streaming to the DAO treasury as an attacker will win Noun auctions and immediately cancel streams to reclaim 80% of their winning bid ETH and DoS the stream escrow logic by repeatedly doing this.

Root Cause

Allowing stream cancellation with full remaining ETH refund is a mistake as it allows attackers to manipulate the intended streaming mechanism while only losing the immediate DAO treasury payment portion (immediateTreasuryBPs, 20%, from README). Since there is no penalty mechanism, after winning an auction, an attacker can basically cancel a stream after the first tick, effectively blocks 99.99% of the stream amount. Attacker can repeat this attack everyday to completely DoS the streaming.

uint256 amountToSendTreasury = (_auction.amount * immediateTreasuryBPs) / 10_000;
uint256 amountToStream = _auction.amount - amountToSendTreasury;
function cancelStream(uint256 nounId) public {
        require(isStreamActive(nounId), "stream not active");

        // transfer noun to treasury
        nounsToken.transferFrom(msg.sender, nounsRecipient, nounId);

        // cancel stream
        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;
        (bool sent,) = msg.sender.call{value: amountToRefund}("");
        require(sent, "failed to send eth");

        emit StreamCanceled(nounId, amountToRefund, ethStreamedPerTick);
    }
  • Key points of this issue
  1. Providing full refund without penalty makes this attack vector sustainable. Attacker can cancel the stream after just streaming 1 tick, and got the full refund.
  2. It's easy. Attacker just needs to win auctions and cancel streams after first tick repeatedly.
  3. Cheap. (Cost = Noun NFT * 20% * days) is very low amount to completely DOS streams of Nouns. Attacker doesn't even need to be a whale.

https://github.com/sherlock-audit/2024-11-nounsdao/blob/main/nouns-monorepo/packages/nouns-contracts/contracts/StreamEscrow.sol#L167-L186 https://github.com/sherlock-audit/2024-11-nounsdao/blob/main/nouns-monorepo/packages/nouns-contracts/contracts/NounsAuctionHouseV3.sol#L333-L344

Internal pre-conditions

External pre-conditions

  1. Attacker to win auctions
  2. Attacker to cancel streams

Attack Path

  1. Attacker wins a Noun auction for 2.4 ETH (Nouns NFTs were auctioned for 2.4 ETH for the last 10 days averagely) Please check Nouns website https://nouns.wtf/

  2. Auction settles: 0.48 ETH (20%) goes directly to DAO treasury 1.92 ETH (80%) goes to stream escrow with streamLengthInTicks = 1460 (4 years, can't be modified, from README https://mirror.xyz/verbsteam.eth/jFi5T0aehwZws7Q6jdRn8xZmORu25TiyWz4G5LAHuaU)

  3. ethPerTick = 1.92 ETH / 1460 ≈ 0.0013 ETH

  4. Attacker immediately calls cancelStream() after first tick

  5. Attacker receives full refund of ~1.9186 ETH (original streamed amount minus one tick)

  6. Attacker repeats this attack everyday and does not let any of the streams to continue.

From the README mirror link (https://mirror.xyz/verbsteam.eth/jFi5T0aehwZws7Q6jdRn8xZmORu25TiyWz4G5LAHuaU) : "We are contemplating changing how auction winners pay for their Noun: instead of paying the full amount to the DAO right away, a small part would be paid to the DAO upon settlement".

So previously the full amount was being transferred to the DAO.

Let's do a quick calculation. Assume that attacker executes this attack for 30 days, 1 auction settles per day:

As stated at 1st point, we will use 2.4 ETH per NFT averagely.

  • Scenario 1 (previous system) 2.4 ETH * 30 days = 72 ETH/month --> 72 ETH transferred directly to DAO treasury right after the auctions.

  • Scenario 2 (stream escrow, attacked) 0.48 ETH * 30 days = 14.4 ETH/month --> 14.4 + 0.04 ETH (30x ethPerTick) transferred directly to DAO treasury right after the auctions and the first tick cancels. Also the remaining 57.5 ETH is NOT going to be streamed, because attacker has cancelled all streams and got the full refund.

  • Scenario 1 vs Scenario 2: Attacker effectively cut the streams which supposed to be streamed to DAO treasury, completely DoSed the streaming logic by not letting any of the streams to continue. 1 month of attack impact in long term: 57.5 ETH loss

This is a very easy attack for an attacker wants to manipulate Nouns DAO.

Attacker only spent 14.44 ETH/month and completely DoSed the streaming logic and dropped the Nouns DAO treasury reserves by 80%.

Impact

Complete DoS of stream escrow DAO treasury reserves down by 80% in the long term

PoC

Described in the Attack Path

Mitigation

  • The solution I recommend to this is to apply penalties as many other streaming/vesting platforms do, and do not allow users to cancel streams until some point.
  • Even if initial payment percentage can be modified by DAO proposal, attack vector will be still valid and executable.