For the complete documentation index, see llms.txt. This page is also available as Markdown.

89310 bc low author steered timestamp delta biases the difficulty retarget downward

Submitted on Aug 18th 2026 at 14:29:17 UTC by @Pyro for Audit Comp | Quantus

  • Report ID: #89310

  • Report Type: Blockchain/DLT

  • Report severity: Low

  • Target: https://github.com/immunefi-team/audit-comp-quantus-chain

  • 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

adjust_difficulty retargets from a single block-to-block timestamp delta, and writes the author's own timestamp back as the next baseline.

The adjustment is asymmetric:

let divisor_ms = (target_time_ms * 2 / 3).max(1);
let time_factor = (block_time_ms / divisor_ms) as i64;
let adjustment = core::cmp::max(1i64 - time_factor, -99i64);

At TARGET_BLOCK_TIME_MS = 12_000 the divisor is 8,000 ms, which gives these zones:

Delta

time_factor

adjustment

[500, 8000)

0

+1

[8000, 16000)

1

0

[16000, 24000)

2

-1

[24000, 32000)

3

-2

[32000, 40000)

4

-3

[40000, 48000)

5

-4

Down is unbounded to -99. Up stops at +1. A block in the honest zone repays nothing at all.

The delta is the author's own timestamp minus the previous author's, and the inherent accepts 30,000 ms of future drift:

The inflated timestamp is then written back as the next baseline, so the following blocks are forced into near-zero deltas while wall clock catches up:

One cycle, starting from difficulty 4,000,000:

Block

Delta

time_factor

adjustment

Difficulty

1

42,000 ms

5

-4

3,992,188

2

100 ms

0

+1

3,994,137

3

100 ms

0

+1

3,996,087

4

5,800 ms

0

+1

3,998,038

Four honest 12,000 ms blocks cover the same 48,000 ms of wall clock and leave difficulty at 4,000,000. The cycle books a 1,962 deficit. A 20,000 ms drift extracts the same -1 in three blocks instead of four, so the attacker runs it on every block it wins.

Nothing repays it. The pallet has no cumulative-elapsed-time or median-timestamp check, so the deficit compounds. One hundred cycles take 4,000,000 down to 3,808,556, and the decay continues for as long as the attacker keeps authoring. The PoC runs that loop.

Lower difficulty is a higher block rate, and emission is paid per block:

The whole issuance schedule is pulled forward by the same factor. Every miner has the same incentive to run it, so the bias is additive across the network.

Every governance and vesting deadline moves with it, because they are counted in blocks:

Referendum decision and enactment periods, the 45-day undeciding timeout, and vesting schedules all elapse early in wall-clock terms.

Recommendation

Bound the per-block move symmetrically, so one author-supplied timestamp cannot book a deficit that the capped recovery blocks are structurally unable to repay.

The stronger fix is a fixed multi-block retarget window, comparing the window's total elapsed time against its expected time. No single author controls that input.

Proof of Concept

calculate_difficulty is a public pure function, so the whole thing is a fold over timestamp deltas. Add to pallets/qpow/src/tests.rs.

Output:

Was this helpful?