SponsorRegistry
0x0000000000000000000000000000000000002619VerifiedNative balance0USDX
Account typeSmart contract
NetworkTuven 99359
Overview
- Contract name
- SponsorRegistry
- Verification
- Verified source
- Balance updated at block
- 12000
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// src/userland/SponsorRegistry.sol
/// @title SponsorRegistry
/// @notice Gas-policy registry: decides, per sender, whether gas is paid in a
/// sponsored ERC-20 instead of USDX, and which token / fee / recipient
/// applies.
///
/// The Arc EL handler reads this contract's storage slots DIRECTLY (not
/// via a CALL to `resolveGas`). This means the storage layout is FIXED:
/// adding or reordering state variables will silently break gas accounting.
/// `resolveGas` exists only as a convenience view for off-chain callers.
///
/// Model:
/// - A "gas plan" = {token, feePerTx, feeBeneficiary}. Multiple plans
/// coexist, so different SBTs can charge different tokens.
/// - Each authorized source (an SBT) is bound to one plan via
/// `sourcePlan`. Issuing the SBT sets the holder's `userPlan`; revoke
/// clears it. One plan per user (a later issue overwrites).
/// - Gas tokens must be ERC-20 whose `balanceOf` mapping is at storage
/// slot 0 (MintableERC20 from TokenFactory) so the handler can debit
/// any of them uniformly.
/// - A sponsored sender whose plan is unconfigured (token = 0) or who
/// lacks the fee ⇒ the handler REJECTS the tx (no USDX fallback).
///
/// @dev STORAGE LAYOUT IS FROZEN. The handler hardcodes slot offsets for
/// `multisig` (slot 0), `plans` (slot 1), `sourcePlan` (slot 2), and
/// `userPlan` (slot 3). Any upgrade via setCode MUST preserve this layout
/// exactly — inserting or reordering variables will corrupt gas accounting
/// without any on-chain error. `multisig` MUST stay at slot 0 additionally
/// because the genesis predeploy bootstraps it via `alloc.storage`.
contract SponsorRegistry {
// --- slot 0 (genesis-bootstrapped) ---
address public multisig;
/// @notice A gas plan: charge `feePerTx` of `token` per tx, credited to `feeBeneficiary`.
struct GasPlan {
address token;
uint256 feePerTx;
address feeBeneficiary;
}
/// @notice planId → plan. planId 0 is reserved as "none".
mapping(uint256 => GasPlan) public plans;
/// @notice source (e.g. an SBT) → the planId it grants. 0 = not authorized.
mapping(address => uint256) public sourcePlan;
/// @notice holder → planId. 0 = not sponsored (pays USDX).
mapping(address => uint256) public userPlan;
event MultisigChanged(address indexed oldMs, address indexed newMs);
event PlanSet(uint256 indexed planId, address token, uint256 feePerTx, address feeBeneficiary);
event SourcePlanSet(address indexed source, uint256 indexed planId);
event SponsoredSet(address indexed who, uint256 indexed planId);
error NotMultisig();
error NotAuthorizedSource();
error ZeroAddress();
error ZeroPlan();
modifier onlyMultisig() {
if (msg.sender != multisig) revert NotMultisig();
_;
}
/// @dev When predeployed at genesis (no constructor runs), the `multisig` slot
/// must be set via the genesis `alloc.storage` for the registry address.
constructor(address _multisig) {
if (_multisig == address(0)) revert ZeroAddress();
multisig = _multisig;
}
// ====================================================================
// Source-facing (the only mutation a source/SBT performs)
// ====================================================================
/// @notice Place/clear a holder on the calling source's plan. Callable only by
/// an authorized source (one bound to a non-zero plan via setSourcePlan).
function setSponsored(address who, bool on) external {
uint256 plan = sourcePlan[msg.sender];
if (plan == 0) revert NotAuthorizedSource();
uint256 newPlan = on ? plan : 0;
userPlan[who] = newPlan;
emit SponsoredSet(who, newPlan);
}
// ====================================================================
// Multisig admin
// ====================================================================
function setMultisig(address newMs) external onlyMultisig {
if (newMs == address(0)) revert ZeroAddress();
emit MultisigChanged(multisig, newMs);
multisig = newMs;
}
/// @notice Configure a gas plan. `planId` must be non-zero; `token` and
/// `feeBeneficiary` non-zero (`feePerTx` may be 0 = free for that plan).
function setPlan(uint256 planId, address token, uint256 feePerTx, address feeBeneficiary)
external
onlyMultisig
{
if (planId == 0) revert ZeroPlan();
if (token == address(0) || feeBeneficiary == address(0)) revert ZeroAddress();
plans[planId] = GasPlan(token, feePerTx, feeBeneficiary);
emit PlanSet(planId, token, feePerTx, feeBeneficiary);
}
/// @notice Bind a source (e.g. an SBT) to a plan, authorizing it to sponsor
/// holders onto that plan. `planId = 0` de-authorizes the source. The
/// plan need not be configured yet — until it is, sponsored holders on
/// it are rejected by the handler.
function setSourcePlan(address source, uint256 planId) external onlyMultisig {
if (source == address(0)) revert ZeroAddress();
sourcePlan[source] = planId;
emit SourcePlanSet(source, planId);
}
// ====================================================================
// Views
// ====================================================================
/// @notice True if the holder pays gas in a sponsored token (not USDX).
function isSponsored(address who) external view returns (bool) {
return userPlan[who] != 0;
}
/// @notice Gas-policy hook the Arc EL handler calls per transaction.
/// @return sponsored true → pay gas in `feeToken`; false → pay USDX normally.
/// @return feeToken ERC-20 to charge (0 if plan unconfigured → handler rejects).
/// @return feeAmount flat fee per tx, in feeToken's smallest units.
/// @return feeRecipient where the fee is credited.
function resolveGas(address sender)
external
view
returns (bool sponsored, address feeToken, uint256 feeAmount, address feeRecipient)
{
uint256 planId = userPlan[sender];
if (planId == 0) return (false, address(0), 0, address(0));
GasPlan storage p = plans[planId];
return (true, p.token, p.feePerTx, p.feeBeneficiary);
}
}
Contract ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_multisig",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "NotAuthorizedSource",
"type": "error"
},
{
"inputs": [],
"name": "NotMultisig",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroPlan",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "oldMs",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newMs",
"type": "address"
}
],
"name": "MultisigChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "planId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "feePerTx",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "feeBeneficiary",
"type": "address"
}
],
"name": "PlanSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "source",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "planId",
"type": "uint256"
}
],
"name": "SourcePlanSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "who",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "planId",
"type": "uint256"
}
],
"name": "SponsoredSet",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "who",
"type": "address"
}
],
"name": "isSponsored",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "multisig",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "plans",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "feePerTx",
"type": "uint256"
},
{
"internalType": "address",
"name": "feeBeneficiary",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "resolveGas",
"outputs": [
{
"internalType": "bool",
"name": "sponsored",
"type": "bool"
},
{
"internalType": "address",
"name": "feeToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "feeAmount",
"type": "uint256"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newMs",
"type": "address"
}
],
"name": "setMultisig",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "planId",
"type": "uint256"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "feePerTx",
"type": "uint256"
},
{
"internalType": "address",
"name": "feeBeneficiary",
"type": "address"
}
],
"name": "setPlan",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "source",
"type": "address"
},
{
"internalType": "uint256",
"name": "planId",
"type": "uint256"
}
],
"name": "setSourcePlan",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "who",
"type": "address"
},
{
"internalType": "bool",
"name": "on",
"type": "bool"
}
],
"name": "setSponsored",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "sourcePlan",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "userPlan",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]