#41618 [BC-Insight] Timestamp unit doesn't match in GcCounter which causes premature transaction eviction
Submitted on Mar 17th 2025 at 03:48:13 UTC by @Rhaydden for Attackathon | Movement Labs
Report ID: #41618
Report Type: Blockchain/DLT
Report severity: Insight
Target: https://github.com/immunefi-team/attackathon-movement/tree/main/util/collections
Impacts:
A bug in the respective layer 0/1/2 network code that results in unintended smart contract behavior with no concrete funds at direct risk
Description
Brief/Intro
The problem is in the GcCounter impl where timestamps stored in slot units are directly compared against cutoff times in raw time units. This mismatch causes premature garbage collection of entries, resulting in the protocol losing track of in-flight transactions.
Vulnerability Details
In the increment method, timestamps are stored after division by the garbage collection slot duration:
let slot_timestamp = current_time / self.gc_slot_duration.get();
However, in gc, the comparison is done against raw time units:
pub fn gc(&self, current_time: u64) {
let cutoff_time = current_time - self.value_ttl.get(); // Raw time units
for (slot_timestamp, count) in self.value_lifetimes.iter() {
if slot_timestamp.load(Ordering::Relaxed) <= cutoff_time { // BUG: Comparing slot units to raw units
slot_timestamp.store(0, Ordering::SeqCst);
count.store(0, Ordering::SeqCst);
}
}
}
This causes entries to be evicted much earlier than intended because slot unit timestamps (which are divided by gc_slot_duration
) are being compared against much larger raw time values. Since slot timestamps are typically much smaller numbers (as they're divided by gc_slot_duration
), they will almost always be less than the raw cutoff time making entries to be evicted prematurely.
Impact Details
Transactions being tracked by the GcCounter will be prematurely evicted from the tracking system. This is particularly important as the GcCounter is used to track "transactions in flight" in the protocol's transaction pipeline. The protocol will lose track of transactions that are still being processed. This could result in duplicate transaction processing or transactions being dropped entirely.
References
https://github.com/immunefi-team/attackathon-movement//blob/a2790c6ac17b7cf02a69aea172c2b38d2be8ce00/util/collections/src/garbage/atomic/counted.rs#L57
https://github.com/immunefi-team/attackathon-movement//blob/a2790c6ac17b7cf02a69aea172c2b38d2be8ce00/util/collections/src/garbage/atomic/counted.rs#L96-L105
Proof of Concept
Proof of Concept
Heres an easy illustration of how this works:
```plaintext
Scenario: gc_slot_duration = 10, value_ttl = 100
Timeline (Real Time Units)
--------------------------
0 100 200 300 400 500 600 700 800 900 1000
|---------|---------|---------|---------|---------|---------|---------|---------|---------|---------|
^ ^ ^
| | |
Entry stored here Should expire Current time
(at t=900)
How it's STORED (Slot Units = Real Time / 10):
---------------------------------------------
0 10 20 30 40 50 60 70 80 90 100
|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
^ ^
| |
Entry stored at slot 10 Current slot 100
(real time 100 / slot_duration 10 = slot 10)
THE BUG:
--------
When GC runs at time 1000:
- Stored timestamp = 10 (because 100/10 = 10)
- Cutoff calculation = 1000 - 100 = 900 (in real time units)
- Comparison: 10 <= 900 // TRUE! But shouldn't be!
- Result: Entry gets incorrectly evicted
CORRECT BEHAVIOR:
----------------
When GC runs at time 1000:
- Stored timestamp = 10 (in slot units)
- Cutoff calculation = (1000 - 100)/10 = 90 (converted to slot units)
- Comparison: 10 <= 90 // TRUE, and correct!
- Result: Entry gets evicted at the right time
Impact Example:
--------------
Real time: 500
Entry timestamp in slots: 50 (500/10)
Cutoff time (wrong): 500 - 100 = 400
Compare: 50 <= 400 ✓ (INCORRECTLY EVICTS)
Real time: 500
Entry timestamp in slots: 50 (500/10)
Cutoff time (correct): (500 - 100)/10 = 40
Compare: 50 <= 40 ✗ (CORRECTLY KEEPS)
Fix
Convert cutoff_time
to slot units for proper comparison.
pub fn gc(&self, current_time: u64) {
- let cutoff_time = current_time - self.value_ttl.get();
+ let cutoff_time = (current_time - self.value_ttl.get()) / self.gc_slot_duration.get();
for (slot_timestamp, count) in self.value_lifetimes.iter() {
if slot_timestamp.load(Ordering::Relaxed) <= cutoff_time {
slot_timestamp.store(0, Ordering::SeqCst);
count.store(0, Ordering::SeqCst);
}
Was this helpful?