StrimzStrimz
Concepts

On-chain architecture

The contract suite, the upgrade posture, and how you can verify it independently.

Strimz's smart contracts are deployed on Arc, Circle's stablecoin-native L1 where USDC is the gas token. The full Foundry workspace is open source at github.com/StrimzLab/strimz/tree/main/packages/contracts.

The contracts

ContractJobUpgradeable?
StrimzRegistryOff-chain merchants register here to get an onchainMerchantId. Holds payout addresses, fee bps, active flags, and parentMerchantId for sub-tenancy.UUPS
StrimzPaymentsOne-shot payments. Exposes pay() (classic approve-then-pay) and payWithAuthorization() (EIP-3009 token auth plus Strimz PayIntent, two signatures). Splits the fee to FeeCollector and the net to the payout.No, immutable.
StrimzSubscriptionsSubscription enrolment via createSubscription() or permitAndCreateSubscription() (EIP-2612). The scheduler calls batchCharge() for renewals. Owns the usedAttempts ledger.No, immutable.
StrimzAgentEscrowERC-8183 escrow used by the AutoPay Agent's commerce capability. Deployed paused: agent escrow is not available at launch.UUPS
FeeCollectorPer-token, per-merchant fee accounting. Withdrawals are gated by TREASURY_ROLE.UUPS
TokenWhitelistWhich ERC-20s are accepted (USDC, EURC, USYC) and which signing standards each implements (EIP-3009, EIP-2612).UUPS

Why some contracts are immutable

StrimzPayments and StrimzSubscriptions are the only two contracts in the suite that move customer funds. Once deployed, their logic cannot be changed. A stolen admin key cannot rewrite the code that pulls USDC from a payer's wallet, because there is no upgrade path to rewrite.

If we ever ship a fix to either of these, we deploy a fresh address and rotate the Registry's dependency pointers to it. Old transactions keep referring to the old address; new ones use the new one.

The Registry, FeeCollector, and TokenWhitelist stay UUPS because they hold policy: which merchants exist, what the fee schedule looks like, which tokens we accept. Policy changes often. Value-moving code should not.

Who controls the contracts

No single key controls the suite. Admin powers are split three ways.

PathHeld byWhat it can do
SlowAn OpenZeppelin TimelockController (48-hour delay on mainnet). Only the Strimz Safe proposes.Upgrade a contract, rotate a dependency pointer, change the token whitelist, grant any role.
FastThe Strimz Safe multisig. Two of three signers approve every transaction.Pause and unpause, set a merchant's fee within that merchant's own ceiling, switch a merchant off, withdraw fees, resolve escrow disputes, adjust agent reputation, revoke a service key.
Service keysThe API relayer and the scheduler, each with its own key.The relayer registers merchants. The scheduler charges subscriptions that are due.

A separate guardian key can pause the value-moving contracts and do nothing else. Every timelock operation emits CallScheduled when it is proposed, so anyone watching the timelock sees an upgrade or a pointer rotation before it can run. The key that deploys the contracts gives up every role in the same handoff that grants the table above. On Arc testnet the split takes effect with the next contract redeploy.

The two payment paths

Each value-moving contract exposes a classic entrypoint and a meta-tx entrypoint. The classic path is there for integrators who want to drive the wallet themselves. The meta-tx path is what the hosted checkout uses.

FlowClassicMeta-tx (default)
One-shot paymentpay(). Payer approves USDC, then calls pay() themselves.payWithAuthorization(). Payer signs an EIP-3009 authorization plus a Strimz PayIntent. The relayer broadcasts.
Subscription enrolmentcreateSubscription(). Payer approves, then calls it themselves.permitAndCreateSubscription(). Payer signs an EIP-2612 permit plus a Strimz SubscriptionIntent. The relayer creates the sub for them.
Wallet promptsTwo on-chain transactionsTwo off-chain typed-data signatures, one on-chain transaction
Who pays gasThe payerThe Strimz relayer

The second signature (PayIntent or SubscriptionIntent) binds the merchant, amount, and routing fields the token never sees. Without it, an intercepted EIP-3009 or permit signature could be replayed against a different merchant.

Both paths land in the same Transaction row, emit the same PaymentExecuted event, and pay the same fees. The full security model lives on the meta-tx flow page.

Deterministic charge idempotency

The single most security-relevant call in the suite is StrimzSubscriptions.batchCharge. The scheduler hands it an array of chargeAttemptId values, each one keccak256(abi.encodePacked(onchainSubId, currentPeriodEndAt, attempt)). The contract keeps a mapping(bytes32 => bool) usedAttempts and skips any entry it has already seen. The skipped subscription emits a SubscriptionChargeSkipped(subId, attemptId, ChargeOutcome.Duplicate) event; the rest of the batch keeps going.

In practical terms: the scheduler can resubmit the same batchCharge call as many times as it wants and never charge a customer twice. On the payments side, an invariant fuzz test drives both submission paths and checks that the FeeCollector balance never exceeds the fees it has accrued, so neither path can credit a fee twice.

ERC-7201 namespaced storage

Every upgradeable contract puts its state under a deterministic slot via ERC-7201. Adding fields in V2 cannot collide with V1's layout. The Upgradeability.t.sol suite asserts V1-to-V2 storage preservation through OpenZeppelin's Foundry Upgrades plugin.

StrimzPayments and StrimzSubscriptions use the same namespace pattern even though they aren't upgradeable. The reason is mundane: their dependency pointers (Registry address, FeeCollector address) can still be rotated through the timelock after deploy, so the storage discipline buys us a clean migration path on the fields that do change.

Reading the contracts yourself

git clone https://github.com/StrimzLab/strimz
cd strimz/packages/contracts
forge build
forge test

Contract addresses come from each deployment's output and are passed to every Strimz process through env vars such as STRIMZ_PAYMENTS_ADDRESS and STRIMZ_SUBSCRIPTIONS_ADDRESS. @strimz/shared-config holds token addresses, not contract addresses. There are no private deployments. What's in the repo is what runs in production.

Verifying a transaction

Every Strimz Transaction row carries an onchainTxHash. Drop it into Arc's block explorer and you can check, end to end:

  1. The EIP-3009 or EIP-2612 signature belongs to the payer your dashboard claims it was. For a smart-contract wallet, the contract checked it through the wallet's ERC-1271 isValidSignature.
  2. The fee split is consistent: amount == feeAmount + netAmount.
  3. The destination address matches the merchant's payoutAddress. (For the classic path that's the to field; for the single-signature path it's the contract's payout call.)
  4. The ref field (one-shot) or chargeAttemptId (subscription) is the value we recorded off-chain.

The chain is the source of truth. Strimz's database is a view the indexer maintains over the event log. If you ever want to rebuild it from scratch, you can.

On this page