Skip to content

Chapter 2: On-Chain Infrastructure

This chapter defines the org-side on-chain architecture: the Safe treasury, the modules attached to it, and the events they emit. Everything in this chapter lives on Base at launch.

Every other layer in the system — the financial document layer, the event indexer, the fiat ramp facade, the bridge facade — depends on the contracts and events defined here. This is the foundation.

A Safe on Base holds the organization’s USDC. Organizations either create a new Safe through Capxul (with their signers as owners and Capxul modules pre-installed) or connect an existing Safe and install Capxul modules via a co-signed transaction.

The Safe is a standard Safe{Core} multisig. Capxul does not modify or fork the Safe contracts. All customization happens through modules and the Zodiac Roles Modifier.

One module. Extends Zodiac’s Module.sol. Handles all discrete (non-streaming) payments from the Safe.

executePayment(
address token,
address recipient,
uint256 amount,
bytes32 documentHash,
uint8 paymentType
)

When called, the module tells the Safe to execute an ERC20 transfer via exec(). It emits:

PaymentExecuted(
address indexed safe,
address indexed recipient,
address indexed token,
uint256 amount,
bytes32 documentHash,
uint8 paymentType
)

The documentHash links to the corresponding financialDocument in Convex (see Chapter 7). It is computed as keccak256 of a canonical subset of the document’s fields (amount, recipient, invoice number, timestamp). This creates the bidirectional link between the on-chain payment and the off-chain financial record.

The paymentType enum distinguishes:

  • Invoice payment
  • One-off transfer
  • Off-ramp send (org side, to a provider deposit address)
  • Bridge send (org side, to a bridge contract)

The module itself contains zero permission logic. It executes. Zodiac Roles handles who can call it and under what constraints.

Vanilla LlamaPay deployed on Base as a standalone contract. Not a module. The Safe interacts with LlamaPay through standard calls that Zodiac Roles authorizes.

In V1 (Paynest), the fork added on-chain username strings and recovery mechanisms for streams sent to wrong addresses. In V2, neither is needed. Streams always target the employee’s smart account (pull-based principle). If an employee leaves, the org stops the stream. Accrued but unclaimed funds remain in LlamaPay for the employee to withdraw at any time. On cancellation, unstreamed funds return to the Safe automatically.

The Safe creates and manages streams via Zodiac Roles:

  • The “payroll_admin” role can call createStream and depositToStream on the LlamaPay contract address
  • The “treasury_ops” role can additionally call cancelStream

This scoping is configured in the Roles Modifier, not in LlamaPay.

The indexer listens to LlamaPay’s native events:

  • Stream creation — Create mutation: records the stream in Convex, links to the org and employee
  • Stream withdrawal — Create mutation: generates a claim receipt in financialDocuments
  • Stream cancellation — Update mutation: marks the stream as cancelled in Convex

Zodiac Roles Modifier (Reuse, No Custom Code)

Section titled “Zodiac Roles Modifier (Reuse, No Custom Code)”

The Roles Modifier sits between callers and the Safe. Instead of enabling the Payment Module directly on the Safe, the Roles Modifier is enabled on the Safe. The Payment Module and LlamaPay interactions operate through the Roles Modifier.

“payroll_admin” role. Assigned to org admin wallet addresses. Can call executePayment on the Payment Module where amount is under a configured USDC threshold (the spending limit). Can call createStream and depositToStream on LlamaPay.

“treasury_ops” role. Assigned to senior admin or org owner. Can call executePayment with any amount. Can call cancelStream on LlamaPay. Can modify Roles configuration.

“offramp_backend” role. Assigned to Capxul’s backend service address. Can call executePayment only where paymentType is off-ramp and recipient matches a Roles-configured allowlist of verified provider deposit addresses. This is the org-side off-ramp flow (see Chapter 5).

“bridge_backend” role. Same pattern as offramp_backend, scoped to bridge contract addresses and bridge payment type.

Spending Limits and Multi-Approver Thresholds

Section titled “Spending Limits and Multi-Approver Thresholds”

The financial document layer defines spending limits as a business rule (a junior admin can approve invoices up to 5,000 USDC; above that requires a senior admin). The enforcement is layered:

  • Primary enforcement (Convex). The approval flow in Convex checks the approver’s spending limit before submitting the on-chain transaction. If the invoice exceeds their limit, it escalates in Convex. The transaction is never submitted on-chain.
  • Safety net (Zodiac Roles). The Roles Modifier enforces the spending limit on-chain. If the Convex enforcement is bypassed somehow (bug, direct contract call), the Roles Modifier rejects the transaction.

This means the Convex approval workflow handles the UX (escalation, multi-approver queuing, notifications), and the Roles Modifier handles the cryptographic guarantee.

Roles and permissions are configured via transactions from Safe owners. Capxul’s frontend provides a UI for org admins to manage roles (add a payroll admin, set spending limits, assign the offramp_backend role). Each change submits a transaction to the Roles Modifier contract on-chain. The Convex backend mirrors this configuration for the approval workflow UI, but the on-chain state in the Roles Modifier is the source of truth for enforcement.

The following events are what the indexer expects. This is the contract between the on-chain layer and the off-chain layer.

PaymentExecuted(address indexed safe, address indexed recipient, address indexed token, uint256 amount, bytes32 documentHash, uint8 paymentType) — Match-and-update mutation: finds the financialDocument with matching documentHash, writes back the txHash, transitions status to paid.

  • Stream creation event — Create mutation: records the stream in Convex, links to the org and employee.
  • Stream withdrawal event — Create mutation: generates a claim receipt in financialDocuments.
  • Stream cancellation event — Update mutation: marks the stream as cancelled in Convex.

Transfer(address indexed from, address indexed to, uint256 value) on USDC — Used by the indexer for treasury balance tracking (transfers in/out of Safe addresses) and smart account balance tracking.

Module enabled/disabled, ownership changes. Lower priority but useful for audit and operational awareness.

Contracts and the indexer are co-designed. These conventions ensure clean processing:

  1. Include the document hash in payment events. The bytes32 document hash is the primary key for matching on-chain events to off-chain documents.
  2. Use indexed parameters for filterable fields. For PaymentExecuted: Safe address, recipient address, and document hash are indexed (the three topics).
  3. Emit events for all state changes. Solidity storage writes are not visible to external observers — only events are.
  4. Consistent naming convention. {Action}{Subject}: PaymentExecuted, StreamCreated, StreamModified, StreamCancelled, FundsClaimed. Each event name maps to exactly one indexer handler. No overloading.

Multi-Chain Treasury (When It Becomes Necessary)

Section titled “Multi-Chain Treasury (When It Becomes Necessary)”

At V2 launch, the Safe treasury lives on Base. Payments to other EVM chains are bridged outbound (see Chapter 6). No child Safes needed.

  1. Gas economics shift. If more than 20-30 payments per month go to the same destination chain, a child Safe starts saving money versus individual bridge transactions.
  2. Regulatory or custody requirements. If a jurisdiction requires that treasury be segregated by entity or geography.
  3. Latency requirements. If an org needs instant payments on a destination chain, pre-funding a child Safe eliminates bridge finality latency.
  4. Receiving funds on other chains. If an org receives revenue on Arbitrum or Polygon, a child Safe on that chain can receive funds natively.

When child Safes arrive, Zodiac handles governance:

  • The master Safe on Base uses the Zodiac Connext Module to send governance instructions to child Safes on other EVM chains via Connext’s cross-domain messaging (minutes of latency).
  • The Zodiac AMB Bridge Module is an alternative using native rollup bridges (more trust-minimized, slower).
  • Both can coexist: Connext for operational speed, native bridge for high-value governance changes.

Key distinction: Zodiac cross-chain modules move governance instructions, not funds. The bridge providers (deBridge, LI.FI, per Chapter 6) move funds. These are separate concerns.

The Safe registry in Convex supports multiple Safes per org with a role field from day one. V2 launches with one Safe per org (role: “primary”, chain: Base). The schema accommodates child Safes without migration.