Skip to content

Route rebate to extraData receiver for sponsored depositors#970

Open
mswilkison wants to merge 2 commits into
mainfrom
codex/sponsored-depositor-rebate-routing
Open

Route rebate to extraData receiver for sponsored depositors#970
mswilkison wants to merge 2 commits into
mainfrom
codex/sponsored-depositor-rebate-routing

Conversation

@mswilkison

@mswilkison mswilkison commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the gap where a T staker who mints through a relayer-style depositor (e.g. NativeBTCDepositor) loses their fee waiver because the Bridge sees the depositor contract as msg.sender and routes the RebateStaking lookup against that contract's (empty) stake instead of the L1 receiver's.

  • Adds a governance-managed sponsoredDepositors allowlist to BridgeState.
  • In Deposit._revealDeposit, when the caller is allowlisted and extraData != bytes32(0), the rebate is keyed off the address decoded from extraData rather than msg.sender.
  • deposit.depositor is unchanged — refund and finalize accounting are untouched; delegation via RebateStaking.getStaker keeps working.
  • New Bridge.setSponsoredDepositor(address, bool) (onlyGovernance) + isSponsoredDepositor(address) view; matching BridgeGovernance.setSponsoredDepositor(...) forwarder.
  • New event SponsoredDepositorSet(address indexed depositor, bool sponsored).
  • Storage: one slot consumed from the existing __gap (48 → 47), so layout remains upgrade-safe.

Why

depositTreasuryFeeDivisor = 500 on mainnet today (20 bps), so this is the fee a user with T staked would expect their rebate to reduce on every deposit. Without this routing, gasless minting through NativeBTCDepositor (live on mainnet at 0xad7c6d46f4a4bc2d3a227067d03218d6d7c9aaa5) always pays the full 20 bps because the depositor contract has no stake. After this change, the user's existing RebateStaking budget is honored exactly as it would be on a direct L1 reveal.

Allowlist scope

The allowlist is intentionally narrow. Membership means "extraData on reveals from this contract is the L1 address whose stake should be charged." Only NativeBTCDepositor satisfies that today. The allowlist ships empty; after merge, a single governance call adds the mainnet proxy:

bridgeGovernance.setSponsoredDepositor(
    0xad7c6d46f4a4bc2d3a227067d03218d6d7c9aaa5, // NativeBTCDepositor
    true
)

Explicitly out of scope: cross-chain depositors (Sui, StarkNet, Arbitrum/Base via Wormhole). Their extraData is an L2/non-EVM identifier, not an L1 staker. Cross-chain rebates are handled by #952 with its proper EIP-712/EIP-1271 authorization model.

Test plan

  • `npx hardhat test test/bridge/Bridge.Deposit.test.ts test/bridge/Bridge.Parameters.test.ts test/bridge/Bridge.Redemption.test.ts test/bridge/RebateStaking.test.ts test/bridge/Bridge.Governance.test.ts` — 824 passing
  • New tests verify:
    • `setSponsoredDepositor` access control + event + toggle on/off
    • Rebate consumes from receiver (not depositor) when depositor is allowlisted
    • Rebate falls back to depositor (unchanged) when depositor is not allowlisted
    • `deposit.depositor` and `deposit.extraData` are recorded unchanged
  • `npx hardhat compile` clean
  • `npx prettier --check` clean on all changed files
  • `slither` clean on changed files (no new findings)
  • Verified on-chain via `cast call`:
    • mainnet `depositTreasuryFeeDivisor = 500` (rebate is meaningful today)
    • `NativeBTCDepositor` at `0xad7c6d46...` returns the expected `bridge`, `tbtcVault`, and `quoteFinalizeDeposit = 0`

🤖 Generated with Claude Code

When a user mints through a relayer-style depositor contract such as
NativeBTCDepositor, the Bridge sees `msg.sender = depositor contract` and
applies the RebateStaking lookup against that contract — which has no T
stake of its own — instead of the actual L1 receiver encoded in
`extraData`. The user's existing rebate budget is bypassed and the full
deposit treasury fee (20 bps on mainnet today) is charged.

This change adds a governance-managed `sponsoredDepositors` allowlist to
`BridgeState`. When `_revealDeposit` is called by an allowlisted
depositor and `extraData != bytes32(0)`, the rebate is keyed off the L1
address decoded from `extraData` rather than `msg.sender`.
`deposit.depositor` is unchanged so refund and finalize accounting are
not affected; delegation through `RebateStaking.getStaker` continues to
work transparently.

Bridge exposes `setSponsoredDepositor(address, bool)` (onlyGovernance)
and `isSponsoredDepositor(address)`. BridgeGovernance gains a matching
forwarder following the existing `setRebateStaking` pattern. A new event
`SponsoredDepositorSet(address indexed depositor, bool sponsored)` is
emitted on allowlist changes. The allowlist consumes one slot from the
existing `__gap` (48 → 47) so storage layout remains upgrade-safe.

The allowlist is intentionally narrow: only depositors whose `extraData`
is provably the L1 address that should be charged belong on it. Today
that's `NativeBTCDepositor` only (not yet deployed to mainnet).
Cross-chain depositors whose `extraData` is an L2 user identifier (Sui,
StarkNet, Arbitrum/Base via Wormhole) must NOT be allowlisted and are
handled separately by the cross-chain rebate work in #952.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lrsaturnino
lrsaturnino previously approved these changes May 23, 2026

@lrsaturnino lrsaturnino left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Previously the sponsored-depositor branch silently fell back to keying
the rebate off the relay contract (which has no stake) if extraData was
absent or decoded to the zero address. That regressed the user to the
full 20 bps fee without surfacing the misconfiguration.

Require an allowlisted depositor to always provide a non-zero extraData
that decodes to a non-zero L1 address. NativeBTCDepositor already
satisfies this via AbstractBTCDepositor.revealDepositWithExtraData, so
production behavior is unchanged; the guard prevents a future allowlist
addition from silently regressing.

Add a test covering the no-extraData revert path.

@piotr-roslaniec piotr-roslaniec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Verified the rebate routing is safe (extraData is committed to in the P2(W)SH script hash, so a sponsored relayer cannot forge a different receiver), storage layout is upgrade-safe (gap 48 -> 47), and the require() guards added in 3ba6515 make the sponsored-depositor branch fail loud on missing/zero-address extraData.

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.

3 participants