fix(espresso): validate CRE workflow metadata in EspressoRewardsConsumer - #184
Merged
BkChoy merged 2 commits intoJul 27, 2026
Merged
Conversation
onReport only checked msg.sender == forwarder and ignored the Keystone report metadata. The KeystoneForwarder verifies DON signatures over the report bytes only and routes any DON-signed report to any caller-chosen receiver, so the sender check alone lets any party able to run a workflow on the same shared DON forge a report and inflate lifetimeRewards, minting unbacked stESP (capped per rebase by maxRewardChangeBPS, compounding toward insolvency). Validate the workflow owner and workflow name from the report metadata against the authorized CRE workflow (set immutably at construction) before acting on a report, per Chainlink's required Keystone receiver pattern. Update the prod deploy script and tests accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1marcghannam
force-pushed
the
fix/espresso-rewards-consumer-validate-workflow-metadata
branch
from
June 23, 2026 03:45
61e42dc to
e527e05
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
EspressoRewardsConsumer.onReportauthenticated reports with a single check,msg.sender == forwarder, and ignored the Keystone report metadata (workflow_owner/workflow_name). That is not sufficient to authenticate a CRE report.The Chainlink
KeystoneForwarder(0x0b93082D9b3C7C97fAcd250082899BAcf3af3885, livetypeAndVersion=KeystoneForwarder 1.0.0):report()(no caller gating; security is signature based),keccak256(keccak256(rawReport) || reportContext)only, so the receiver is not part of the signed payload and any DON-signed report can be routed to any receiver by anyone,transmissionId = keccak(receiver, workflowExecutionId, reportId)), so a report already delivered to the attacker's own contract can be re-delivered to this consumer with a fresh id.The same shared DON (
configId 0x100000001) signs reports for every workflow running on it. The only field identifying which workflow produced a given report is the metadata thatonReportdiscarded. As a result, any party able to run a workflow on that DON could:abi.encode([vaultId], [inflatedLifetimeRewards]),(rawReport, reportContext, signatures)from their own (normal) delivery,forwarder.report(EspressoRewardsConsumer, rawReport, reportContext, signatures),msg.sendercheck, and push arbitrarylifetimeRewardsinto the strategy (sized to stay undermaxRewardChangeBPS, 3%, to clear theRewardsTooHighguard).Each accepted report inflates
totalDepositswith no ESP entering the protocol, minting unbacked stESP. Repeated across rebase cycles this compounds at roughly 3%/cycle and drives the pool toward insolvency. The phantom rewards can never be realized (claiming requires valid EspressoauthData), and because_updateLifetimeRewardsis monotonic, the inflation also bricks the legitimate oracle's lower, true updates.Fix
Validate the report source in
onReport, the required Keystone receiver pattern (cf. Chainlink's ownKeystoneFeedsConsumer):workflow_name(10 bytes) andworkflow_owner(20 bytes) from the metadata the forwarder passes (workflow_cid(32) || workflow_name(10) || workflow_owner(20) || report_id(2)).UnauthorizedWorkflowunless both match the authorized CRE workflow, which is set immutably at construction.Immutable (rather than owner settable) values keep the contract's existing minimal, all-immutable design and minimize the authorization attack surface. If the workflow is ever redeployed, deploy a new consumer and point the strategy at it via
setRewardsOracle.Tests
test/espressoStaking/espresso-rewards-consumer.test.tsextended:workflow_ownerrevertsUnauthorizedWorkflowand the strategy is left untouched (updateCount == 0),workflow_namereverts,supportsInterfacetests preserved.npx hardhat test --network hardhat test/espressoStaking/*.test.ts-> 67 passing, no regressions.Deployment / migration notes
The constructor signature changed to
(_forwarder, _strategy, _workflowOwner, _workflowName). The authorized workflow is already wired in4-deploy-rewards-consumer.tswith values verified against a live report delivered to the current consumer (0xe69D92f6910b45dA1D6Ddfb380efaa6AF56e33F9):workflowOwner = 0x90510C6bAB8cc0f6C964c46970264Dbb8B7B2857workflowName = 0x39363364363236363730(UTF-8 "963d626670")Rolling this out to the live system:
ESP_EspressoRewardsConsumer(new address; the constructor changed).0xe69D92...to the new consumer address. The workflow logic, report body, owner, and name stay the same.EspressoStrategy.setRewardsOracle(newConsumer).Pair steps 2 and 3 so the active oracle and the workflow's delivery target match. A missed update in the brief window is harmless (
lifetimeRewardsis monotonic; the next report catches up). Do not re-register the workflow under a new owner/name, or the metadata would no longer match the consumer's configured values.Context
Addresses the Immunefi report "Forged Keystone reports mint unbacked stESP: EspressoRewardsConsumer ignores report metadata." Verified against the live deployment: the consumer is the active
rewardsOracleon the strategy,maxRewardChangeBPS == 300, the forwarder is the canonicalKeystoneForwarder 1.0.0, and the configured workflow owner/name match the metadata of a real report delivered to the live consumer.🤖 Generated with Claude Code