2.0 RC docsView 1.x docs
v2.0.0-rc.1 release candidate
The API is frozen and later candidates carry fixes only, except the receipt-free and Safe create surface of /fhe-airdrop, which is @beta. Published on the next dist-tag; latest stays on 1.6.0 until 2.0.0.
Splitting roles from a Safe
grantInstanceRoles waits for each grant to mine before simulating the next, which hangs on a threshold Safe whose transactions only execute once owners co-sign. encodeInstanceRoleSplit (beta, like the rest of the receipt-free surface) turns the same plan into ordered grantRole / revokeRole calls the Safe executes atomically. Steps and outcomes name the grantee holder; planInstanceRoleSplit refuses an assignment.admin equal to the caller with InvalidArgumentError, because granting and then revoking the caller's own admin would strip it. The role getters on every client are SCREAMING_SNAKE methods (PAUSER_ROLE(), SIGNER_ROLE(), DEFAULT_ADMIN_ROLE() and so on).
safe-role-split.ts
ts
import { encodeInstanceRoleSplit, planInstanceRoleSplit } from "@tokenops/sdk/fhe-airdrop";

// assignment.admin must not be the Safe itself: omit admin to keep it.
const steps = planInstanceRoleSplit({ assignment, caller: safeAddress, airdropType: "merkle" });
// steps: RoleGrantStep[] - { kind, role: RoleName, holder }

// roleConstants: the bytes32 values, e.g. read once from the instance with
// airdrop.PAUSER_ROLE(), airdrop.MERKLE_ADMIN_ROLE(), airdrop.DEFAULT_ADMIN_ROLE()
const calls = encodeInstanceRoleSplit({ airdrop, steps, roleConstants });
// calls: InstanceRoleCall[] ({ to, data, value: 0n }) in plan order -
// propose them to the Safe as one MultiSend batch.
Airdrop v2 · Roles · 10@tokenops/sdk/fhe-airdrop

Airdrop v2 role constants.

Every instance role goes to the factory-injected admin at init, except the self-administered fee collector role and SIGNER_ROLE, which goes to params.signer.

RolePermits
PAUSER_ROLE
0x65d7a28e3265b37a…0d862a
pause() / unpause() on the instance. Halts every claim path immediately; does not affect admin/treasury methods.
WINDOW_ADMIN_ROLE
0xdbb0f7f8b01ce70e…e63294
extendClaimWindow(newEndTime). Forward-only, and only if canExtendClaimWindow was set true at create - otherwise reverts ExtensionNotAllowed regardless of who calls it.
TREASURY_ROLE
0xe1dcbdb91df27212…5dfca9
withdrawConfidential(recipient) - sweeps the instance's entire encrypted token pool, in the clear-never amount. Refused with ClawbackRequiresPauseError while the campaign is unpaused inside its claim window: pause first, or run it outside the window.
RESCUER_ROLE
0xcf6f9f892731e14b…31a050
rescueERC20, rescueOtherConfidentialToken and rescueNativeToken (ETH, zero-fee campaigns only - NativeRescueRequiresZeroFeeError otherwise). The two token rescues BOTH reject the campaign's own token via CannotRescueAirdropToken - ERC-7984 and ERC-20 are not disjoint in practice, since a wrapper token answers both interfaces, so the pool was never out of rescueERC20's reach by type alone. Use withdrawConfidential for the pool.
FEE_COLLECTOR_ROLE
0x2dca0f5ce7e75a4b…038821
withdrawGasFee - sweeps the ETH claim fees recipients attached. Self-administered (re-parented to itself at init), so only a current holder can grant or revoke it, not even DEFAULT_ADMIN_ROLE. planInstanceRoleSplit refuses to plan a grant for this role for exactly that reason.
UPGRADER_ROLE
0x189ab7a9244df084…c9d2e3
upgradeToAndCall - authorizes a UUPS upgrade. Inert on clone-mode instances (the proxy layer itself refuses the call), and unreachable on any Merkle instance since the SDK refuses mode: "uups" for Merkle outright.
DISCLOSURE_ADMIN_ROLE
0xcf8a7913f3d76add…feb0c8
adminGetCurrentBalance, adminDiscloseBalanceToParty, adminBatchDiscloseBalanceToParties, and bypasses gate #1 (the anti-theft sender-ACL check) on discloseHandleToParty / batchDiscloseHandlesToParty.
SIGNER_ROLE
0xe2f4eaae4a9751e8…285f70
ECDSA-only. Authorizes EIP-712 claim digests - checked before signature verification, so an ERC-1271 staticcall is only reached for an already-authorized signer. Granted to the signer passed to createEcdsaAirdrop at init. Hold it in a dedicated key that is never EIP-7702-delegated (or an ERC-1271 contract signing the same digest): a delegation gives the key code, verification switches to ERC-1271 against the delegate, and every outstanding voucher fails InvalidSignature. Pre-check vouchers with isSignatureValid.
MERKLE_ADMIN_ROLE
0x434a11e5e6141714…938def
Merkle-only. setMerkleRoot(newRoot) - reverts FeatureDisabledError if the campaign was created with isMerkleRootMutable: false.
DELEGATION_ADMIN_ROLE
0xa434a7eae4dc33d3…52e91e
Lives on the per-instance ComplianceRoleManager clone, not on the airdrop instance itself. Gates addDelegate / revokeDelegate - the client-side compliance delegate roster (separate from the irrevocable platform delegate).

Factory roles

These five roles live on the AirdropFactory singleton, not on a campaign instance, and are not part of the instance role constants. They are seeded at factory deploy time and are administrable afterwards through the factory client's own grantRole / revokeRole / renounceRole - what there is no equivalent of here is planInstanceRoleSplit, so a factory handover is assembled call by call rather than planned as a set. DEFAULT_ADMIN_ROLE is floored at one live member, so grant the successor before renouncing. On Sepolia all five sit on the deployer account; on mainnet DEFAULT_ADMIN_ROLE is held by one account and the four operational roles by another ( FACTORY_ROLE_CONCENTRATION_NOTE). Check the current holders with getRoleMembers rather than assuming a split.

RolePermits
FEE_MANAGER_ROLE
0x6c0757dc3e6b28b2…04ff1c
setFeeCollector, setDefaultGasFee, setCustomFee, disableCustomFee on the factory. NOT setMaxGasFee - the ceiling those fees must sit under is DEFAULT_ADMIN_ROLE's, so the fee manager moves fees only inside a range it cannot raise.
IMPL_MANAGER_ROLE
0xa9921a7b6cf4e3ab…1b231f
setEcdsaImplementation, setMerkleImplementation. Rotating a pointer affects only new creates - existing clones hardcode their impl in bytecode.
COMPLIANCE_WIRING_ROLE
0x7a00f6efd9a6d752…ad0d44
setComplianceManagerImpl, setComplianceDelegate, setDefaultDelegateToCompliance, setCompliancePolicy, clearCompliancePolicy. The constructor seeds a non-zero platform delegate with compliance ON, so there is no wiring step after deploy; setComplianceDelegate rotates the delegate but can never clear it.
UPGRADE_MANAGER_ROLE
0xa76ace73a908083d…726db5
setDefaultUpgradeable, setUpgradeabilityPolicy, clearUpgradeabilityPolicy - the gate a mode: "uups" create is checked against via effectiveUpgradeable(creator).
DEFAULT_ADMIN_ROLE
0x0000000000000000…000000
Administers the other four factory roles, gates setMaxGasFee, and can grant/revoke/renounce. Floored at one live member, matching the instances: revoking or renouncing the sole holder reverts LastAdmin, and granting the role to the zero address reverts ZeroAdminGrant, so the set can neither be emptied nor satisfied by a member nobody controls. A handover is the supported route - grant the successor first, then renounce. Per FACTORY_ROLE_CONCENTRATION_NOTE, one deployer key holds all five factory roles on Sepolia, while mainnet holds DEFAULT_ADMIN_ROLE separately from the four operational roles - check useFactoryRoleMembers rather than assume a split.

Reading + managing