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

88618 sc high assessment approval can execute a replacement loss schedule the risk consortium never reviewed

Submitted on Aug 14th 2026 at 07:17:12 UTC by @Tradi3 for Audit Comp | Firelight

  • Report ID: #88618

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/immunefi-team/audit-comp-firelight/blob/v1_audit_ready/contracts/core/IncidentManager.sol

  • Impacts:

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Brief/Intro

approveCurrentAssessment(uint256 incidentId) identifies the incident but not the assessment round or loss schedule reviewed by the Risk Consortium.

After the consortium signs an approval for round N, the curator can cancel that round, create and submit round N+1 with a different loss schedule, and then execute the unchanged approval. The contract resolves currentAssessmentRoundId at execution, so the signature approves and pays the replacement schedule.

The curator does not need the consortium to collude, approve without review, or sign new calldata. The PoC has separate curator and approver accounts, signs the approval before replacement, and broadcasts the same signed transaction afterward.

Root cause

The approver's calldata contains only incidentId:

_setCurrentAssessmentStatus() reads the mutable pointer at execution:

Meanwhile, CURATOR_ROLE may cancel a submitted round. The next call to addAssessmentLosses() increments currentAssessmentRoundId, builds a new draft, and submitCurrentAssessment() makes it approvable.

Nothing in the signed call commits to the round the consortium reviewed.

Attack sequence

1

Create and submit round 1

The curator creates incident 1, adds a 100-unit schedule to round 1, and submits it.

2

Review and sign approval

The Risk Consortium reviews round 1 and signs:

3

Cancel the reviewed assessment

Before execution, the curator calls cancelCurrentAssessment(1).

4

Create a replacement schedule

The curator adds a new 900-unit schedule. This opens round 2.

5

Submit round 2

The curator calls submitCurrentAssessment(1).

6

Execute the original approval

The original signed transaction is broadcast unchanged.

7

Pay the replacement schedule

approveCurrentAssessment(1) reads currentAssessmentRoundId == 2 and pays 900 units.

The cancellation and replacement can be completed after the Safe transaction reaches its signature threshold but before execution. Safe signatures bind the target and transaction data, not the target contract's storage. Because the data contains only incidentId, the Safe cannot commit to the reviewed round. This follows the Safe v1.4.1 transaction hash construction, which commits to the call data and Safe execution fields but cannot bind a round absent from that data.

Impact

The production-vault PoC starts with one staker holding 1,000 units of redeemable principal. The consortium reviews a 100-unit assessment. The replacement schedule pays 900 units from the real FirelightVault:

This destroys 90% of the staker's redeemable principal even though the independent payout authority approved only the 100-unit schedule. A replacement schedule can consume the vault collateral backing outstanding cover and leave the protocol unable to honor those obligations.

Why this is a protocol-constraint bypass

The disclosed role model gives the curator authority to propose incidents and loss schedules. It gives the Risk Consortium separate authority to validate that schedule and approve the payout. The published audit calls the consortium the primary on-chain check and its independence from the curator the most important assumption in the claim flow.

This report does not rely on the consortium approving a fabricated or excessive schedule. It reviews and signs the 100-unit schedule correctly. The defect lets the curator change the meaning of that existing approval and execute a different 900-unit schedule without further consortium authorization.

That exceeds the curator's proposal authority by bypassing the independent approval constraint.

Known-issue and grouping check

The disclosed aggregate-claims issue concerns the absence of a cross-incident remainingCoverage cap. This issue is different:

  • the root cause is missing round identity in approval calldata;

  • the replacement round uses a separate, valid 900-unit allocation;

  • the consortium never reviewed the executed schedule;

  • a remainingCoverage decrement does not bind an approval to a round;

  • the minimal fix is approval-state binding, not coverage accounting.

The published audit, contest known issues, repository history, issues, and pull-request discussions do not disclose this round-substitution path.

Bind approval and rejection to the reviewed round:

Apply the same binding to rejectCurrentAssessment().

Round ID is sufficient because assessment losses cannot be changed while a round is UNDER_EVALUATION; changing the schedule requires canceling/rejecting it and opening a new round. A schedule hash would also work but is not necessary.

https://gist.github.com/krutftw/cea459673576c9cc4f99d996057e3031

Proof of Concept

Target commit: 42f9ea5e43d88b35197b901acc2a8c24b314fa62 (v1_audit_ready)

Steps to reproduce

1

Check out the target commit

2

Save the test file

Save the complete test below as test/PoC_AssessmentRoundSwap.js.

3

Run the test

From the repository root, run:

Expected terminal result

The first case is a control showing that round 1 pays the reviewed 100-unit schedule. The second signs approveCurrentAssessment(1), replaces round 1 with a 900-unit round, and broadcasts the unchanged signed transaction. The third repeats the path against the production FirelightVault fixture and proves the staker's redeemable principal falls from 1,000 to 100.

PoC SHA-256: EB31EFB7D93A8FBBC09584CA2954BDB7CBD0E828B32FFD87E6CB3FCD381D1510

Complete test

Was this helpful?