Skip to content

Latest commit

 

History

History
147 lines (111 loc) · 6.22 KB

074.md

File metadata and controls

147 lines (111 loc) · 6.22 KB

Sweet Infrared Snake

High

TicksToForward can be arbitrarily large in fastForwardStream:StreamEscrow.sol (within limits) that allow attacker to submit arbitrarily large values

Summary

The fastForwardStream function in the smart contract allows the fast-forwarding of a stream of ETH. Although there is a limit to how many ticks the stream can be advanced (based on the difference between currentTick and stream.lastTick), the function does not impose a maximum limit on the ticksToForward value within the allowed range. This opens the door for users to exploit the function by submitting arbitrarily large values of ticksToForward (within the allowed range), manipulating the contract's internal state, draining ETH from the contract, or causing other unintended behavior.

Root Cause

The vulnerability arises in the following part of the fastForwardStream function: https://github.com/sherlock-audit/2024-11-nounsdao/blob/main/nouns-monorepo/packages/nouns-contracts/contracts/StreamEscrow.sol#L206

require(ticksToForward <= stream.lastTick - currentTick_, 'ticksToForward too large');

This line only ensures that ticksToForward does not exceed the difference between the current tick and the stream's last tick. However, there is no further restriction on how large ticksToForward can be within that range. An attacker could still submit a large, but valid, value of ticksToForward, potentially causing the contract to perform excessive state updates, drain ETH from the contract, or cause performance degradation due to the large number of state changes.

Internal pre-conditions

Stream associated with nounId = 1. currentTick = 100. lastTick = 1000. ethPerTick = 10.

External pre-conditions

No response

Attack Path

Attacker invokes fastForwardStream(1, 900). ticksToForward value is valid, so it gets accepted by the contract. The contract moves lastTick from 1000 to 100 (lastTick - ticksToForward = 1000 - 900 = 100). ETH (900 * 10 = 9000) is sent to the treasury.

Code example:

require(ticksToForward <= stream.lastTick - currentTick_, 'ticksToForward too large'); // Passes as 900 <= 900
uint32 newLastTick = stream.lastTick - ticksToForward; // newLastTick = 1000 - 900 = 100
ethStreamEndingAtTick[stream.lastTick] -= stream.ethPerTick; // Decreases ETH at tick 1000
streams[nounId].lastTick = newLastTick; // Updates lastTick to 100
ethStreamEndingAtTick[newLastTick] += stream.ethPerTick; // Increases ETH at tick 100
uint256 ethToStream = ticksToForward * stream.ethPerTick; // ethToStream = 900 * 10 = 9000
sendETHToTreasury(ethToStream); // Sends 9000 ETH to treasury

Impact

An attacker can repeatedly invoke the fastForwardStream function with large but valid values of ticksToForward, draining ETH from the contract.

The attacker successfully invokes the fastForwardStream function with ticksToForward = 900, which is within the valid range (since 1000 - 100 = 900). ETH amount of 9000 (calculated as 900 * 10) is transferred to the treasury. The contract’s internal state is updated with the new lastTick = 100, reflecting the fast-forwarded stream.

PoC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "ds-test/test.sol";
import "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";

contract FastForwardTest is DSTest {
    address attacker;
    uint256 nounId = 1;
    uint32 currentTick = 100;
    uint32 lastTick = 1000;
    
    // Mock Stream contract for testing purposes
    mapping(uint256 => Stream) public streams;
    mapping(uint32 => uint256) public ethStreamEndingAtTick;
    uint256 public ethStreamedPerTick;
    
    struct Stream {
        uint256 ethPerTick;
        uint32 lastTick;
    }

    function setUp() public {
        // Setup test data for the attacker and stream
        attacker = address(0x123);
        
        // Initial stream setup
        streams[nounId] = Stream({
            ethPerTick: 10,
            lastTick: lastTick
        });
    }

    // Test function to simulate fast-forward attack
    function testExploitableFastForward() public {
        uint32 ticksToForward = 900; // Arbitrary large number within the allowed range (1000 - 100)
        
        // Simulate the attacker calling the fastForwardStream function
        fastForwardStream(nounId, ticksToForward);
        
        // Check if the ETH has been manipulated (sent to treasury)
        assertEq(ethStreamEndingAtTick[lastTick - ticksToForward], 10); // Verify the ETH manipulation at the new tick
    }
    
    // Simulated fastForwardStream function
    function fastForwardStream(uint256 nounId, uint32 ticksToForward) public {
        require(ticksToForward > 0, 'ticksToForward must be positive');
        Stream memory stream = streams[nounId];
        uint32 currentTick_ = currentTick;
        require(isStreamActive(stream, currentTick_), 'stream not active');

        // move last tick
        require(ticksToForward <= stream.lastTick - currentTick_, 'ticksToForward 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); // Sends ETH to treasury
    }

    function isStreamActive(Stream memory stream, uint32 currentTick_) internal pure returns (bool) {
        return stream.lastTick >= currentTick_;
    }

    function sendETHToTreasury(uint256 amount) internal {
        // Simulated ETH sending logic to the treasury
        ethStreamedPerTick += amount;
    }
}

Mitigation

Add a cap for how many ticks a stream can be fast-forwarded at once to prevent an attacker from submitting excessively large values.

uint32 constant MAX_TICKS = 500; // Example cap to limit fast forwarding to a reasonable value
require(ticksToForward <= MAX_TICKS, "ticksToForward exceeds cap");