Sablier

Sablier
DeFiFoundry
53,440 USDC
View results
Submission Details
Severity: medium
Invalid

Incorrect Stream Status from the Manipulation of the Block Timestamp

Summary

The vulnerability is arise on the block timestamp to determine the status of a stream, it's allow Miners that can manipulate the block timestamp within a certain range to benefit from specific transactions.

Vulnerability Details

The _statusOf function is determines the status of a stream based on the current block timestamp and the stream's start time, here the function first checks if the stream is depleted or was canceled and returns the respective status

if (_streams[streamId].isDepleted) {
return Lockup.Status.DEPLETED;
} else if (_streams[streamId].wasCanceled) {
return Lockup.Status.CANCELED;
}

and then checks if the current block timestamp is less than the stream's start time. If true, it returns PENDING

if (block.timestamp < _streams[streamId].startTime) {
return Lockup.Status.PENDING;
}

and then move to calculates the streamed amount and compares it to the deposited amount. and If the streamed amount is less, it returns STREAMING; otherwise, it returns SETTLED here

if (_calculateStreamedAmount(streamId) < _streams[streamId].amounts.deposited) {
return Lockup.Status.STREAMING;
} else {
return Lockup.Status.SETTLED;
}

The issue is arises from that the function is relie on the block timestamp to determine the PENDING status and allow Miners to manipulate the block timestamp within a certain range to delay or accelerate the transition between PENDING and STREAMING statuses and also Miners can adjust the block timestamp backward to keep a stream in PENDING status longer than intended, and delaying the recipient's ability to withdraw funds also there is a possibility that miners can adjust the block timestamp forward to transition a stream to STREAMING status prematurely, allowing withdrawals to start earlier than intended.

  • i test with a scenario that confirm this :
    i simulate a stream and set it to start 1 hour from the current time with a deposited amount of 1000 tokens.
    and adjusted the block timestamp within a ±15-minute range and checked the stream's status here is the details :

    • Stream ID: 1

    • Start Time: datetime.now() + timedelta(hours=1) (1 hour from current time)

    • Deposited Amount: 1000 tokens

    • in Manipulation the Miner adjusts the block timestamp backward by 15 minutes.

    • The stream remains in the PENDING status because the manipulated timestamp is still less than the start time.

here is the test :

import random
from datetime import datetime, timedelta
# Mock of the SablierV2Lockup contract behavior
class LockupStatus:
PENDING = 'PENDING'
STREAMING = 'STREAMING'
SETTLED = 'SETTLED'
CANCELED = 'CANCELED'
DEPLETED = 'DEPLETED'
class LockupStream:
def __init__(self, start_time, deposited_amount, is_depleted=False, was_canceled=False):
self.start_time = start_time
self.deposited_amount = deposited_amount
self.is_depleted = is_depleted
self.was_canceled = was_canceled
self.withdrawn_amount = 0
def calculate_streamed_amount(self, current_time):
# Simple linear streaming logic
duration = (current_time - self.start_time).total_seconds()
if duration < 0:
return 0
# Assuming a simple model where deposited amount is streamed linearly over 1 hour
streamed_amount = min(self.deposited_amount, (duration / 3600) * self.deposited_amount)
return streamed_amount
class SablierV2Lockup:
SAFETY_BUFFER = timedelta(minutes=5)
def __init__(self):
self._streams = {}
def _status_of(self, stream_id, current_time, current_block):
stream = self._streams[stream_id]
if stream.is_depleted:
return LockupStatus.DEPLETED
elif stream.was_canceled:
return LockupStatus.CANCELED
if current_time < stream.start_time + self.SAFETY_BUFFER:
return LockupStatus.PENDING
streamed_amount = stream.calculate_streamed_amount(current_time)
if streamed_amount < stream.deposited_amount:
return LockupStatus.STREAMING
else:
return LockupStatus.SETTLED
def add_stream(self, stream_id, start_time, deposited_amount):
self._streams[stream_id] = LockupStream(start_time, deposited_amount)
# Mock setup
contract = SablierV2Lockup()
stream_id = 1
start_time = datetime.now() + timedelta(hours=1) # Stream starts in 1 hour
deposited_amount = 1000
contract.add_stream(stream_id, start_time, deposited_amount)
# Simulate with random block timestamp manipulation
def simulate_block_timestamp_manipulation():
current_time = datetime.now()
manipulated_time = current_time + timedelta(minutes=random.randint(-15, 15))
current_block = random.randint(1000, 2000) # Mocking block number
status = contract._status_of(stream_id, manipulated_time, current_block)
return current_time, manipulated_time, status
# Running simulations to check for potential vulnerabilities
simulation_results = []
for _ in range(100):
result = simulate_block_timestamp_manipulation()
simulation_results.append(result)
simulation_results

Impact

Recipients cannot start withdrawing funds as the stream is incorrectly kept in the PENDING status.

Tools Used

manual review

Recommendations

need a to add a Safety Buffer and Reference Multiple Timestamps

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.