Skip to content

Route rebate to extraData receiver for sponsored depositors - #970

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

Route rebate to extraData receiver for sponsored depositors#970
mswilkison wants to merge 2 commits into
devfrom
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

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

@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.

@piotr-roslaniec
piotr-roslaniec enabled auto-merge (squash) August 5, 2026 12:12
mswilkison and others added 2 commits August 5, 2026 14:12
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>
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 force-pushed the codex/sponsored-depositor-rebate-routing branch from 3ba6515 to d81ae59 Compare August 5, 2026 12:12
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@mswilkison, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 89398701-0bbd-494b-8f6d-ffef0ce87768

📥 Commits

Reviewing files that changed from the base of the PR and between 443d6df and d81ae59.

📒 Files selected for processing (6)
  • solidity/contracts/bridge/Bridge.sol
  • solidity/contracts/bridge/BridgeGovernance.sol
  • solidity/contracts/bridge/BridgeState.sol
  • solidity/contracts/bridge/Deposit.sol
  • solidity/test/bridge/Bridge.Deposit.test.ts
  • solidity/test/bridge/Bridge.Parameters.test.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@piotr-roslaniec
piotr-roslaniec changed the base branch from main to dev August 26, 2026 15:07
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