DEV Community

Daniel
Daniel

Posted on

How a Positive offsetSeconds Bug Let a CROSS Forge Mint Twice the Intended ERC20 Period Limit

ERC20MintLimited is not an unlimited forge minter.

It authorizes a forge to mint only while that forge remains inside the configured capacity for the current period.

This finding breaks that second authorization boundary.

With a positive offsetSeconds, CROSS calculates the current period start incorrectly. At the native UTC boundary, PeriodManager.getPeriodStartForTime() can return the next offset boundary even though that boundary is still in the future.

ERC20PeriodMintLimit interprets the changed timestamp as a new period and restores a full LIMIT of capacity before the real configured period has ended.

The PoC demonstrates:

Configured limit
100,000 tokens per period

First mint
100,000 tokens

Remaining capacity
0

Configured +9h period ended?
NO

Capacity at UTC boundary
100,000 tokens

Second mint
100,000 tokens

Total inside the same intended period
200,000 tokens
Enter fullscreen mode Exit fullscreen mode

I reported the finding as Critical through the CertiK SkyShield CROSS bug bounty program. CROSS finalized it as Low, and the report earned $109.40.

Public report and PoC

The severity dispute turns on whether forge membership alone authorizes the amount being minted. The contract does not support that interpretation.

The mint policy has two gates

The base mint path requires onlyForge:

modifier onlyForge() {
    if (!isForge(_msgSender())) {
        revert TokenBase__OnlyForge(_msgSender());
    }
    _;
}
Enter fullscreen mode Exit fullscreen mode

But ERC20MintLimited routes minting through ERC20PeriodMintLimit, which also checks the available period capacity:

if (periodCapacity < amount) {
    revert ERC20PeriodMintLimit__ExceedsPeriodLimit(
        amount,
        periodCapacity
    );
}

_periodCapacity = periodCapacity - amount;

super.mint(to, amount);
Enter fullscreen mode Exit fullscreen mode

The effective policy is therefore:

caller is an authorized forge

AND

amount <= remaining capacity
for the current configured period
Enter fullscreen mode Exit fullscreen mode

The PoC does not claim that a random EOA bypasses onlyForge.

It proves that a forge with zero remaining capacity can receive a new full capacity before the period actually ends and mint supply that the period limit should reject.

The positive offset formula is wrong

For positive offsets, the vulnerable implementation effectively computes:

floor(timestamp / duration)
× duration
+ offsetSeconds
Enter fullscreen mode Exit fullscreen mode

For an offset period, the timestamp must be shifted before flooring:

floor(
    (timestamp - offsetSeconds)
    / duration
)
× duration
+ offsetSeconds
Enter fullscreen mode Exit fullscreen mode

The PoC uses:

duration = 86,400 seconds
offsetSeconds = 32,400 seconds
LIMIT = 100,000 tokens
Enter fullscreen mode Exit fullscreen mode

The intended period is:

[1000 days + 9 hours,
 1001 days + 9 hours)
Enter fullscreen mode Exit fullscreen mode

One second before the UTC boundary, the forge mints the full LIMIT. Capacity becomes zero, and an additional one wei mint is rejected.

Then the timestamp reaches:

1001 days
Enter fullscreen mode Exit fullscreen mode

The configured +9h period still has nine hours remaining.

The correct current period start is still:

1000 days + 9 hours
Enter fullscreen mode Exit fullscreen mode

The PoC records:

block.timestamp
86486400

expected period start
86432400

actual period start
86518800
Enter fullscreen mode Exit fullscreen mode

So CROSS returns a current period start that is greater than block.timestamp.

That impossible timestamp is the root cause.

The future timestamp restores capacity

ERC20PeriodMintLimit resets capacity whenever the calculated period start changes:

uint256 currentPeriodStart =
    _period.getCurrentPeriodStart();

if (currentPeriodStart != _periodStartTime) {
    _periodStartTime = currentPeriodStart;
    periodCapacity = _limit;
}
Enter fullscreen mode Exit fullscreen mode

At the UTC boundary, the bad positive offset calculation changes currentPeriodStart even though the real configured period is still active.

Capacity is restored to LIMIT.

The same forge then mints another full LIMIT, and the call reaches the real ERC20 mint path.

This is not merely an incorrect view value. totalSupply increases.

The PoC isolates the bug

The Foundry proof uses the real ERC20MintLimited preset, ERC20PeriodMintLimit, PeriodManager, onlyForge, and ERC20 mint path.

It contains three tests:

Test What it proves
testBug The full limit is consumed, capacity resets too early, and the same forge mints a second full limit before the actual period ends.
testControl An extra mint is correctly rejected while capacity is legitimately zero before the false reset.
testAccess A non forge caller cannot mint.

The run completed with:

3 passed
0 failed
0 skipped
Enter fullscreen mode Exit fullscreen mode

The relevant result is:

Non forge mint
REJECTED

First forge mint
LIMIT

Capacity afterward
0

Extra forge mint before UTC
REJECTED

Configured +9h period still active at UTC
YES

Returned period start in future
YES

Capacity at UTC
LIMIT

Second forge mint
LIMIT

Total minted in intended period
2 × LIMIT

Excess over configured limit
1 × LIMIT
Enter fullscreen mode Exit fullscreen mode

This is why the issue cannot be reduced to a generic access control discussion. Access control is working. The quantitative mint authorization is not.

Why I classified it as Critical

At the time of submission, the CROSS SkyShield scope listed Unauthorized mint, burn, or transfer of crypto assets as a Critical smart contract impact.

My report maps the second full LIMIT mint to that category because the supply exceeds the amount authorized by the preset for the active period.

After the first mint:

remaining period capacity = 0
Enter fullscreen mode Exit fullscreen mode

Before the false boundary, another mint correctly reverts.

The real configured period has not changed one second later. Only the incorrect PeriodManager result changes.

That result creates capacity that should not exist and allows another full supply increase.

If forge membership alone authorized every amount, ERC20MintLimited would add no security boundary beyond onlyForge.

But it does add one: a per period quantitative limit.

The PoC demonstrates one full LIMIT of supply above that limit.

That is the technical basis for my Critical assessment.

Why the Low rationale is not supported by the PoC

The downgrade explanation raised four points:

Downgrade point What the code and PoC show
Minting remains restricted to forges Correct, but the finding is an amount authorization bypass, not a forge identity bypass.
The effect is bounded and timing dependent The bound is still one full extra LIMIT in the demonstrated period. Timing is deterministic at the native boundary.
Native aligned windows remain near LIMIT The affected positive offset configuration is supported. A different configuration working correctly does not repair this one.
DEFAULT_ADMIN_ROLE can adjust LIMIT A manual parameter response does not fix the incorrect period calculation or restore automatic enforcement.

None of those points changes the core state transition:

period not ended
capacity already 0
extra mint should revert
future period start returned
capacity reset to LIMIT
second full LIMIT mint succeeds
totalSupply increases
Enter fullscreen mode Exit fullscreen mode

I therefore consider the Low rationale technically inconsistent with the behavior demonstrated by the contract and PoC.

That conclusion does not require speculating about motive. The public evidence supports a strong technical disagreement. It does not establish bad faith.

Bounded does not mean harmless

The PoC uses:

LIMIT = 100,000 tokens
Enter fullscreen mode Exit fullscreen mode

and produces:

intended maximum
100,000 tokens

actual minted
200,000 tokens

excess
100,000 tokens
Enter fullscreen mode Exit fullscreen mode

The issue does not need infinite minting to violate the security policy.

The entire purpose of the rate limit is to prevent the forge from creating that additional supply before the configured period ends.

The relevant question is whether the contract creates supply that its own limit logic should reject.

The PoC shows that it does.

Fix

The offset must be applied before the timestamp is floored.

Conceptually:

shifted =
timestamp - offset

periodStart =
shifted
- (shifted % duration)
+ offset
Enter fullscreen mode Exit fullscreen mode

The period calculation should always preserve:

periodStart <= timestamp
Enter fullscreen mode Exit fullscreen mode

and:

timestamp < periodStart + duration
Enter fullscreen mode Exit fullscreen mode

For the PoC configuration, the regression should be:

mint LIMIT before UTC boundary
SUCCEEDS

mint 1 extra wei
REVERTS

reach UTC boundary
capacity remains 0

mint LIMIT
REVERTS

reach actual +9h boundary
capacity resets to LIMIT
Enter fullscreen mode Exit fullscreen mode

The report also recommends normalizing offsets by the period duration so equivalent offsets map to the same canonical boundary.

Broader audit lesson

Role authorization and quantitative authorization are separate controls.

A protocol can correctly enforce:

who may call
Enter fullscreen mode Exit fullscreen mode

while incorrectly enforcing:

how much that caller may do
Enter fullscreen mode Exit fullscreen mode

The same reasoning applies to mint quotas, withdrawal ceilings, bridge caps, spending limits, epoch budgets, and rate limits.

When a quantity is part of the policy, bypassing that quantity is an authorization failure even if the caller identity is valid.

Time based controls add another useful invariant:

A function returning the start of the current period must never return a timestamp in the future.

That invariant would have exposed this bug directly.

Conclusion

The CROSS bug is a positive offset period accounting failure in the mint limit path.

After an authorized forge consumes its full LIMIT, capacity should remain zero until the actual offset boundary.

Instead, at the earlier UTC boundary, PeriodManager can report the next +9h boundary as the current period start.

ERC20PeriodMintLimit treats that future timestamp as a new period, restores a full LIMIT, and allows another full mint while the same configured period is still active.

The PoC proves that non forge callers remain blocked, exhausted capacity normally rejects extra minting, the period has not ended, capacity resets early, and total minting reaches 2 × LIMIT.

The relevant authorization question is not only:

is the caller a forge?
Enter fullscreen mode Exit fullscreen mode

It is:

is the caller a forge
and does it still have capacity
inside the current configured period?
Enter fullscreen mode Exit fullscreen mode

The second condition fails because of CROSS's period calculation.

That is why I reported the finding as Critical, and why I do not consider the Low downgrade technically supported by the demonstrated behavior.

Top comments (0)