A new user shows up with an address and no RBTC in it. Every transaction, including the very first one, needs gas, and gas on Rootstock is paid in RBTC. RIF Relay removes that requirement: a user pays with a token they already hold, or gets the transaction sponsored outright, while a separate account submits it and pays the actual gas.
At RootstockLabs, I led the team that extended and productionized RIF Relay, including the smart-contract changes required to support Boltz’s Rootstock integration, a non-custodial Bitcoin and Lightning swap service. This post covers how RIF Relay’s relay flow and smart-wallet architecture work, and what changed when a real integration stopped fitting the generic model.
The relay mechanism#
RIF Relay is a fork-turned-redesign of the Gas Station Network (GSN), adapted for Rootstock. Five roles matter:
- Requester: the end user’s EOA. They sign a request; they don’t submit a transaction.
- Smart Wallet: a per-user contract account, owned by the Requester, that executes the call and pays the relay fee.
- Relay Verifier: checks, off-chain and before anything happens on-chain, that the Requester’s fee token is accepted and their balance covers it.
- Relay Server: an off-chain daemon with a Relay Manager (a staked EOA) and one or more Relay Workers (EOAs that submit transactions and pay gas in RBTC).
- Relay Hub: the on-chain entry point. It verifies the submitting worker belongs to a staked manager, then hands the request to the Smart Wallet.
A signed, gas-less request becomes a normal, gas-paid transaction by passing through this pipeline. RIF Relay’s own docs call the wrapping transaction an “envelope.”
sequenceDiagram
participant U as Requester (EOA)
participant C as Relay Client
participant S as Relay Server
participant V as Relay Verifier
participant H as Relay Hub
participant W as Smart Wallet
participant D as Destination Contract
U->>C: sign relay request
C->>S: HTTP: relay request
S->>V: verify token accepted + balance (off-chain)
S->>S: wrap in envelope,
sign with Relay Worker key
S->>H: relayCall(envelope)
H->>H: verify Worker belongs to
staked Manager
H->>W: execute(request, sig)
W->>W: verify signature + nonce
W->>D: call(data)
W->>S: pay fee (token or RBTC)
The Verifier check happens off-chain, in the client and again in the relay server, before submission: it’s how the server avoids paying gas for a request that would just revert.
The Smart Wallet is what makes this safe without touching the destination contract: because the wallet, not the Relay Worker, is msg.sender, ordinary contracts work under RIF Relay unmodified. RIF Relay’s own v0.1, itself a GSN fork, instead relied on a _msgSender()/_msgData() pattern, which meant contracts had to unwrap the real sender from calldata themselves. Routing execution through a real per-user contract account sidesteps that; v0.2 replaced the _msgSender() dependency with the counterfactual smart-wallet design described next.
Smart wallets, deployed cheaply and on demand#
A Smart Wallet’s address can be deterministically derived via CREATE2 before the wallet itself is ever deployed. Both the client and the relay server compute it off-chain, and the wallet can receive funds at that address before it exists as a contract; it only needs to actually deploy once its owner wants to do something.
Deployment itself is a proxy, not a full contract: the factory’s getCreationBytecode() returns a short constructor plus a fixed runtime stub wrapping a shared implementation address:
602D3D8160093D39F3 363D3D373D3D3D3D363D73 <masterCopy address> 5AF43D923D90803E602B57FD5BF3That runtime is the same idea as EIP-1167’s minimal proxy (the 5af4 at the end is DELEGATECALL), close to but not byte-identical to the canonical EIP-1167 encoding. Every user’s Smart Wallet is a thin delegate-call shim onto one shared logic contract, not a copy of the whole implementation. That significantly reduces the cost of maintaining a per-user wallet model.
The project’s own gas measurements show what the v0.1-to-v0.2 redesign bought in practice:
| Version | Smart Wallet template | Deployment overhead (gas) |
|---|---|---|
| v0.1 | SmartWallet | 172,400 |
| v0.2 | SmartWallet | 97,695 |
| v0.2 | CustomSmartWallet | 98,070 |
Roughly a 43% cut, though not from the proxy pattern alone: v0.2 also removed hardcoded gas-overhead validations from the Relay Hub and moved the old Paymaster’s pre-transaction checks off-chain into the Verifier, so less of the relay’s own bookkeeping happens on-chain per call. (Figures from RIF Relay’s own gas-costs documentation; v1’s numbers weren’t published at the time.)
Each piece in this pipeline owns a distinct responsibility, with some checks deliberately repeated across trust boundaries. The Relay Server decides whether it’s willing to relay a request at all and avoids paying gas for one likely to fail. The Relay Hub verifies that the submitting Relay Worker belongs to a staked Relay Manager, then routes execution onward. The Smart Wallet is where a user’s actual authorization and replay protection live, and where the destination call and fee payment happen. The Verifier does integration-specific validation before any of this reaches the chain, so a request that would fail gets rejected for free instead of on-chain for a fee. That division of responsibility is what Boltz’s integration ended up testing.
Where the standard wallet’s assumptions break: Boltz#
The interesting part of the integration wasn’t adding another destination contract. Boltz violated two assumptions built into the standard relay flow: the user might have no fee token at all, and the destination protocol already had its own authorization mechanism.
Boltz runs non-custodial swaps across Bitcoin, Lightning, and Rootstock. Onboarding a new Rootstock user through it hit the same problem RIF Relay exists to solve in the first place, one level down: to claim RBTC from a swap, a user needed RBTC to pay for the claim transaction. Rootstock’s own writeup on the integration calls this exactly what it is, a chicken-and-egg problem. The Rootstock leg of a swap is a hashlock-and-timelock contract (RIF Relay’s own interface for it is called NativeSwap; Boltz’s underlying implementation, shared across the chains it supports, is EtherSwap): claiming requires supplying a preimage whose hash matches the one committed when the swap was opened, the same primitive as a Lightning HTLC. Two things follow. First, the funds being claimed are native RBTC, so there’s no ERC-20 sitting around to pay a relay fee from. Second, and more interesting: the swap contract has already, independently, verified that whoever calls claim() knows the right preimage. A second, general-purpose signature check on top of that would just be re-checking something the protocol already guarantees.
That’s the actual engineering question behind this integration: what is the minimum authorization the wallet needs to enforce, without duplicating guarantees the swap protocol already provides?
RIF Relay’s contracts answer it with two wallets, at two different points on that spectrum:
BoltzSmartWalletkeeps the full signed-forward-request model, still checking the owner’s signature and nonce exactly like the standard wallet, and only extends fee payment to accept native RBTC directly alongside the existing ERC-20 path. This is the conservative fix: it solves the “no fee token” half of the problem without touching authorization at all.MinimalBoltzSmartWalletis the more significant change, but not in the sense of dropping signature verification from the flow. The wallet contract itself exposes noexecute()path at all (it isn’t even aBaseSmartWallet), and its ownMinimalBoltzRelayVerifierunconditionally rejects any ordinary relay call. The entire lifecycle is deploy-and-claim in a single transaction:MinimalBoltzSmartWalletFactorystill verifies the requester’s EIP-712 signature on the deploy request exactly like the standard flow, and only then callsinitialize(), which runs the claim. What the wallet adds on top of that already-verified request is a narrower constraint:BoltzUtils.validateClaimSignaturechecks that the calldata being executed matches one of Boltz’s two knownclaim()selectors, so even a validly-signed deploy request can’t make the wallet call anything else. The pairedMinimalBoltzDeployVerifieradds a third check, off-chain and before submission, confirming against the realNativeSwapcontract that a swap with those exact parameters actually exists. Authorization isn’t removed here, it’s split across four boundaries: the factory verifies who’s asking, the wallet constrains what they can execute, the Verifier checks whether the request is worth submitting, andNativeSwapremains the actual source of truth for whether the claim itself, hashlock and timelock included, is valid.
graph TD
subgraph Standard["Standard flow"]
direction TB
A1["User signs
EIP-712 forward request"] --> A2["SmartWallet
verifies signature + nonce"]
A2 --> A3["Destination contract
executes"]
end
subgraph Boltz["Boltz claim flow"]
direction TB
V["Deploy Verifier: checks swap
exists on NativeSwap
(off-chain, before submission)"]
B1["User signs
deploy request"] --> B2["Factory verifies
signature"]
B2 --> B3["MinimalBoltzSmartWallet
restricts execution to
known claim() selectors"]
B3 --> B4["NativeSwap contract enforces
hashlock + timelock,
releases RBTC"]
V -.->|"must pass before
relay server submits"| B2
end
The minimal wallet wasn’t the only option the team weighed. Keeping Boltz on the standard signed-forward-request wallet, and sponsoring the claim transaction outright, were both considered and rejected. The minimal design won on several grounds: it reduces execution overhead for the relayed claim, running through a smaller contract with no execute() path, no domain separator, and no signature-recovery logic to pay for, rather than the overhead of a general-purpose implementation built to support arbitrary future calls; it presents a narrower surface for auditors to review than the standard wallet’s full signature-and-nonce logic; it avoided modifying a contract Boltz already had deployed and in live use; and it avoids requiring anyone to sponsor claims outright, which wasn’t going to scale economically as swap volume grew.
This is a live deployment, not a hypothetical. Public on-chain records place the four Boltz-specific Verifier contracts in the same block on Rootstock mainnet, on September 16, 2024 (verifiable directly on the Rootstock explorer); Boltz’s and Rootstock’s own posts about the integration describe its public launch about two months later, in November 2024. It runs as a paid service, a small percentage fee on each relayed claim, not fully sponsored. A user finishing a Bitcoin-to-Rootstock swap through Boltz claims their RBTC and pays the relay fee out of the exact funds being claimed, without ever having held RBTC beforehand, which is the whole point of gas-relaying, applied to a case the original design didn’t anticipate.
Takeaway#
Gas-relaying removes a real onboarding constraint: users can interact with an application before they own the chain’s native gas token. Doing that safely takes more than moving gas payment to another account, though. It’s a system spanning off-chain infrastructure, relayer economics, smart-wallet execution, replay protection, fee collection, and application-level authorization, and the boundaries between those pieces are where the design decisions actually live.
The Boltz integration is a case where the generic model’s assumptions stopped fitting. The standard wallet assumed authorization came from a signed forward request and that fees were paid in an ERC-20 token, and neither held for a swap where the asset being claimed was native RBTC and the destination protocol already enforced its own claim conditions. Rather than forcing the generic model onto that case, the wallet was specialized around the guarantees the swap protocol already provided: constrain what the wallet can execute, validate the request before paying to relay it, and leave the swap contract responsible for its own claim conditions.
The broader lesson generalizes past this one integration: when two protocols meet, security mechanisms shouldn’t be duplicated across both by default. Identify which component already owns a given guarantee, preserve that boundary, and add only the checks the integration actually needs on top of it.
Resources#
- RIF Relay: umbrella repo and docs
- RIF Relay Contracts: Smart Wallet, factory, and Verifier implementations
- RIF Relay Server
- RIF Relay Client
- Boltz
- Boltz’s
EtherSwapcontract (boltz-core): the underlying swap implementation RIF Relay’sNativeSwapinterface targets - “Boltz Integrates RIF Relay to Simplify Bitcoin Layer Swaps” (Rootstock blog)
- “Hello, Rootstock Swaps!” (Boltz blog)
- EIP-1167: Minimal Proxy Contract
