Scruffy Chartreuse Wolverine
Medium
The StreamEscrow contract allows users to specify the streamLengthInTicks
during stream creation without enforcing a minimum bound.
This flexibility can be exploited by malicious actors to set streamLengthInTicks
to extremely low values (e.g., streamLengthInTicks
= 1), effectively converting the streaming mechanism into an immediate ETH transfer. This bypasses the intended time-distribution of funds.
function createStream(uint256 nounId, uint16 streamLengthInTicks) 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 });
emit StreamCreated(nounId, msg.value, streamLengthInTicks, ethPerTick, newEthStreamedPerTick, streamLastTick);
}
- The attacker must be an address permitted to create streams (
allowedToCreateStream[msg.sender] == true
). - The attacker must send ETH when creating the stream.
- The attacker sets
streamLengthInTicks
to a very low value during stream creation. - The contract computes
ethPerTick = msg.value / 1 = msg.value
, effectively streaming the entire ETH amount in a single tick. - Upon the next
forwardAll
call, the entire ETH amount is sent to the treasury immediately. - The streaming mechanism intended to distribute ETH over time is effectively bypassed, allowing immediate transfer of funds.
Implement a minimum allowable value for streamLengthInTicks to prevent setting it to excessively low values.
uint16 public constant MIN_STREAM_LENGTH_IN_TICKS = 10; // Example minimum
function createStream(uint256 nounId, uint16 streamLengthInTicks) public payable {
require(streamLengthInTicks >= MIN_STREAM_LENGTH_IN_TICKS, "streamLengthInTicks below minimum allowed");
// Existing logic...
}