Skip to main content

Contract reference

This page is the engineering reference for the on-chain pieces of the non-custodial stack. Addresses and audit reports are published below per chain as phases ship.

Address conventions (EVM)​

All four EVM contracts share the same deployer (a deterministic CREATE2 deployer, e.g. 0x4e59b44847b379578588920cA78FbF26c0B4956C) and the same init-code hash per version, so the factory and hub addresses are the same on every EVM chain. That simplifies client code and docs.

ChainchainIdPaymentLinkFactorySubscriptionHubFeeSplitter
Base8453published on deploypublished on deploypublished on deploy
Ethereum1samesamesame
Arbitrum One42161samesamesame
Optimism10samesamesame
Polygon PoS137samesamesame

PaymentLinkReceiver addresses are per-link: factory.computeAddress(keccak256(linkId)).

EVM: PaymentLinkFactory​

Functions​

  • computeAddress(bytes32 linkId) external view returns (address) — deterministic CREATE2 address. Can be called before any deploy.
  • deployAndPay(bytes32 linkId, Split calldata split, address token, uint256 amount) external payable — deploys the clone on first pay and forwards funds.
  • isDeployed(bytes32 linkId) external view returns (bool).
  • setSplit(bytes32 linkId, Split calldata split) external — callable only before first pay; sets the immutable split that the clone will be initialized with.
  • pause() / unpause() — multisig-only.

Events​

  • ReceiverDeployed(bytes32 indexed linkId, address receiver)
  • SplitRegistered(bytes32 indexed linkId, Split split)

Split struct​

struct Split {
address merchant;
address platform;
address referral;
address bridgeFee;
uint16 platformBps;
uint16 referralBps;
uint16 bridgeFeeBps;
}

Sum of bps fields must be < 10000. Remainder goes to the merchant.

EVM: PaymentLinkReceiver (minimal clone)​

Functions​

  • pay(address token, uint256 amount) external payable — main entrypoint. token == address(0) means native.
  • refund(bytes32 txId, address to, uint256 amount) external — restricted to the keeper EOA or deployer multisig.
  • split() external view returns (Split memory).

Events​

  • Paid(bytes32 indexed linkId, address indexed payer, address indexed token, uint256 amount, bytes32 txId)
  • Refunded(bytes32 indexed linkId, bytes32 indexed txId, address to, uint256 amount)

Reverts​

  • InvalidToken, InvalidAmount, SplitFailed, AlreadyRefunded.

EVM: FeeSplitter​

Functions​

  • split(Split calldata s, address token, uint256 amount) external payable — internal call from PaymentLinkReceiver and SubscriptionHub. Not typically called directly.

Events​

  • SplitExecuted(bytes32 indexed txId, address merchant, uint256 merchantAmount, address platform, uint256 platformAmount, address referral, uint256 referralAmount, address bridgeFee, uint256 bridgeFeeAmount)

EVM: SubscriptionHub​

Functions​

  • createSubscription(bytes32 id, address payer, IERC20 token, uint256 amount, uint64 interval, uint256 cap, Split calldata split) external — registers the subscription on-chain.
  • charge(bytes32 id) external — public and permissionless; enforces timing, cap, allowance, and balance.
  • charge(bytes32[] ids) external — batch version; each id independently validated.
  • cancel(bytes32 id) external — callable by the payer.
  • subscription(bytes32 id) external view returns (Subscription memory).
  • isDue(bytes32 id) external view returns (bool) — helper for keepers.

Events​

  • SubscriptionCreated(bytes32 indexed id, address indexed payer, IERC20 token, uint256 amount, uint64 interval, uint256 cap)
  • Charged(bytes32 indexed id, uint256 amount, uint64 nextChargeAt)
  • ChargeSkipped(bytes32 indexed id, bytes32 reason) — reasons: NotDue, InsufficientAllowance, InsufficientBalance, AlreadyChargedThisPeriod, CapExceeded, Paused.
  • Canceled(bytes32 indexed id)

Invariants enforced in charge​

  • block.timestamp >= subscription.nextChargeAt
  • block.timestamp > subscription.lastChargedAt (monotonic)
  • subscription.amountCharged + subscription.amount <= subscription.cap
  • token.allowance(subscription.payer, address(this)) >= subscription.amount
  • token.balanceOf(subscription.payer) >= subscription.amount

Example payer flow​

// 1. Payer signs an allowance to the hub
await usdc.approve(subscriptionHub.address, cap)

// 2. Merchant (via OrcaRail) creates the subscription on-chain
await subscriptionHub.createSubscription(
id,
payer,
usdc,
amount,
interval,
cap,
split
)

// 3. Anyone calls charge when due
await subscriptionHub.charge(id)

Accounts​

#[account]
pub struct PaymentLink {
pub link_id: [u8; 32],
pub expected_mint: Pubkey,
pub expected_amount: u64,
pub split: Split,
pub created_at: i64,
pub status: PaymentStatus,
}

PDA seeds: [b"pl", link_id].

Instructions​

  • initialize(link_id, expected_mint, expected_amount, split) — lazy init on first use.
  • pay(link_id, amount) — CPI-transfers from payer's ATA into the PDA's ATA, then CPI-splits.
  • refund(link_id) — multisig-gated.

Events​

  • PaymentReceived { link_id, payer, amount }
  • PaymentRefunded { link_id, to, amount }

Solana: subscription program​

Accounts​

#[account]
pub struct Subscription {
pub id: [u8; 32],
pub payer: Pubkey,
pub mint: Pubkey,
pub amount: u64,
pub interval: i64,
pub cap: u64,
pub amount_charged: u64,
pub next_charge_at: i64,
pub last_charged_at: i64,
pub split: Split,
}

Instructions​

  • create(id, payer, mint, amount, interval, cap, split) — registers subscription.
  • charge(id) — public; validates clock, cap, delegate authority, then CPI transfer_checked as delegate.
  • cancel(id) — payer only.

Events (via program logs)​

  • SubscriptionCreated, Charged { amount, next_charge_at }, ChargeSkipped { reason }, Canceled.

Example payer flow​

// 1. Payer approves program PDA as delegate on their USDC ATA
await token.approve(payerUsdcAta, programPda, capAmount)

// 2. create() registers the subscription PDA
// 3. charge() can be called by anyone once due

Descriptor template​

wsh(multi(2, <merchant_pubkey>, <platform_pubkey>))

Taproot equivalent (v2, once MuSig2 tooling matures):

tr(<NUMS>, multi_a(2, <merchant_pubkey>, <platform_pubkey>))

Derivation​

  • Merchant key: derived from the merchant's onboarded xpub, one index per payment link.
  • Platform key: derived from the platform HSM xpub, one index per payment link.

Refund PSBT​

At deposit time, an unsigned PSBT is assembled that returns deposit minus fee to the payer. Both signers sign it immediately; the result is stored encrypted and can be broadcast at any time without further signing.

Audit reports​

Published here as they complete. Entries include: vendor, scope, commit, date, report link, fix commit.

(Reports land as phases ship.)

See also​