Skip to content

fix: validate all Chainlink feed timestamps against expiry in ChainlinkRelayerV1#715

Closed
mento-val wants to merge 3 commits intodevelopfrom
qa/fix-chainlink-relayer-staleness-check
Closed

fix: validate all Chainlink feed timestamps against expiry in ChainlinkRelayerV1#715
mento-val wants to merge 3 commits intodevelopfrom
qa/fix-chainlink-relayer-staleness-check

Conversation

@mento-val
Copy link
Copy Markdown

@mento-val mento-val commented Mar 12, 2026

Summary

Fixes a security bug (mento-core S-2 from QA audit MEN-5) where only the newest Chainlink aggregator timestamp was checked against the staleness expiry window.

Problem

In ChainlinkRelayerV1.relay(), when multiple aggregators are used:

// Before: only checks the newest timestamp
if (isTimestampExpired(newestChainlinkTs)) {
    revert ExpiredTimestamp();
}

With two aggregators where feed 0 is stale but feed 1 is fresh, the relay succeeds with stale data influencing the aggregated rate. Example:

Feed Age Expired?
Feed 0 (oldest) 700s Yes (expirySeconds=600)
Feed 1 (newest) 400s No
Spread 300s Within maxTimestampSpread

Current code: relay succeeds, stale price from Feed 0 contaminates the result.

Fix

// After: checks the oldest timestamp — if oldest is valid, all are valid
if (isTimestampExpired(oldestChainlinkTs)) {
    revert ExpiredTimestamp();
}

If the oldest timestamp is within the expiry window, all feeds are fresh. If the oldest is expired, at least one feed carries stale data and the relay reverts.

Tests

Adds test_revertsWhenOldestFeedIsExpiredButNewestIsNot to ChainlinkRelayerV1Test_relay_double demonstrating the exact vulnerability scenario.

This PR was initially closed due to CI failures in relay_triple and relay_full test suites. The root cause was that the test function was defined for relay_double (2 aggregators) and inherited by relay_triple (3) and relay_full (4). The inherited setUp timestamps caused a timestamp spread > maxTimestampSpread, firing TimestampSpreadTooHigh before ExpiredTimestamp.

CI fixes (commits 4634545, 8d814f0):

  • Warped block.timestamp to prevent underflow in the staleness test
  • Marked the base test function virtual and overrode it in relay_triple and relay_full to set all aggregator timestamps, keeping spread ≤300 and ensuring ExpiredTimestamp fires correctly

🤖 Generated with Claude Code

…nkRelayerV1

Previously, only the newest aggregator timestamp was checked against the
expiry window. A stale older feed could pass through undetected, allowing
stale price data to influence the aggregated rate.

Fix: validate the oldest aggregator timestamp. If the oldest is within
expiry, all feeds are within expiry. If the oldest is expired, at least
one feed carries stale data and the relay should fail.

Adds a regression test (test_revertsWhenOldestFeedIsExpiredButNewestIsNot)
demonstrating the vulnerability: oldest feed 700s old (expired), newest 400s
old (within expiry=600s), spread 300s (within maxTimestampSpread=300s).

Fixes: MEN-36 (mento-core S-2 from QA audit MEN-5)
@mento-val mento-val requested review from a team, Mouradif and chapati23 March 12, 2026 04:17
@mento-val
Copy link
Copy Markdown
Author

CI fix pushedtest_revertsWhenOldestFeedIsExpiredButNewestIsNot was panicking with arithmetic underflow (0x11) because Foundry starts block.timestamp at 1, and the test subtracts 700 from it. Added vm.warp(block.timestamp + 1000) at the top of the test to advance time enough for the subtraction to succeed. Also added the foundry-chainlink-toolkit/ remapping to remappings.txt (same as PR #713) since this branch needs it for the ChainlinkRelayerV1 tests to compile.

…set all aggregator timestamps

In relay_triple and relay_full, the inherited
test_revertsWhenOldestFeedIsExpiredButNewestIsNot() only set
timestamps for aggregators 0 and 1, leaving aggregators 2 (and 3)
at their setUp default (block.timestamp=1). After vm.warp(+1000)
this caused spread = newTs - 1 = 600 > maxTimestampSpread=300,
triggering TimestampSpreadTooHigh instead of the expected
ExpiredTimestamp.

Fix: override the test in relay_triple and relay_full to explicitly
set all aggregator timestamps so the spread stays within the 300s
limit and the expired-feed check fires correctly.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@mento-val mento-val reopened this Mar 19, 2026
Copy link
Copy Markdown
Author

@mento-val mento-val left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA Review — LGTM / Approved (reviewer cannot self-approve)

Fix logic: Correct.

The one-line change from newestChainlinkTsoldestChainlinkTs in the expiry check closes the security gap cleanly.

Why the logic holds:

  • maxTimestampSpread already constrains all feed timestamps within a window.
  • If oldestChainlinkTs is within the expiry window, every feed is guaranteed fresh (all others are newer).
  • If oldestChainlinkTs is expired, at least one feed carries stale data → relay correctly reverts.
  • The TimestampNotNew check (still using newestChainlinkTs) is unaffected and correct.

Test coverage: Appropriate.

  • test_revertsWhenOldestFeedIsExpiredButNewestIsNot in relay_double exercises the exact vulnerability (Feed 0: 700s, Feed 1: 400s, spread: 300s ≤ maxSpread, oldest expired → must revert).
  • Overrides in relay_triple and relay_full correctly set ALL aggregator timestamps so inherited setUp timestamps don't trigger a spurious TimestampSpreadTooHigh. These are not masking real failures — they're correctly isolating the expiry assertion from the spread assertion.
  • vm.warp(block.timestamp + 1000) before subtracting timestamps prevents underflow — correct defensive test practice.

CI: All 4 checks passing (Lint & Test, Slither, Echidna).

Remapping addition: foundry-chainlink-toolkit entry is benign test tooling infrastructure.

No issues found. LGTM — ready to merge.

@mento-val mento-val closed this Mar 25, 2026
@bayological bayological deleted the qa/fix-chainlink-relayer-staleness-check branch March 27, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants