Strimz
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.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.

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 a keccak256(subscriptionId, periodEndAt). 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.None) 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. We prove this with an invariant test that drives both submission paths through 128,000 fuzz calls and checks that the FeeCollector balance never exceeds the fees it has accrued. 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 by ADMIN_ROLE 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            # 55 tests including 128k-call invariant fuzz

Deployment addresses for testnet and mainnet live in @strimz/shared-config. Every Strimz process reads the same constants at boot. 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 signer recovered from the EIP-3009 or EIP-2612 signature is the payer your dashboard claims it was.
  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