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.
Recommended fix
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.
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.