Skip to content

Security model

Small, immutable contracts. Honest about what is unproven.

The design goal: a compromised gateway must not be able to take or redirect funds that are already at issued payment addresses, or touch your vault. It can still hand out new addresses, and the section below says exactly what that means.

Where the trust boundary sits

The gateway is in the middle of every payment and is deliberately given no way to keep or redirect money. The key that controls the vault stays with you.

Holds their own keys

Customer

  • Wallet

    Signs one EIP-3009 authorization. Never approves a spender and needs no gas.

  • Invoice address

    Computed before it exists. It commits to the token, destination, fee rate, merchant, invoice and chain.

USDC

Assumed compromisable

Plinth gateway

  • Watcher

    Re-verifies every transfer from its receipt against the official token and finality rule.

  • Relayer

    Pays gas and calls sweep(), which takes no arguments. Holds gas only, never merchant funds.

  • API and webhooks

    Derives status, signs and retries events. Cannot move money.

Sweep, minus fee

You hold the only key

Merchant vault

  • MerchantVault

    Non-upgradeable. The owner is the only authority.

  • Owner key

    Withdraws anywhere. Forwarding rules change only after a delay you set.

Funds only ever move customer, invoice address, then your vault or payout wallet (and the fee to the treasury).Contract tests also run against the real USDC bytecode on these two testnets (read-only forks).

Who can do what

Merchant (vault owner)

Can

  • Withdraw from the vault to any address
  • Set or change forwarding rules (delayed)
  • Transfer ownership (two-step)
  • Recover other tokens sent to their invoice addresses
  • Choose direct payouts to their own wallet, or go back to the vault

Cannot

  • Change an existing invoice address's destination

Gateway relayer

Can

  • Deploy invoice contracts and call sweep()
  • Submit customer-signed payment authorisations
  • Call the vault's permissionless forward() and policy-activation functions

Cannot

  • Withdraw from a vault
  • Choose a recipient for any transfer
  • Change a vault's owner or settlement rules
  • Redirect funds at a payment address that was already issued
  • Spend beyond per-network gas budgets

Anyone

Can

  • Trigger sweep() on any invoice. Funds can only reach its vault.
  • Submit a valid signed authorisation on the customer's behalf

Cannot

  • Replay a used authorisation
  • Alter invoice, merchant, vault or amount in a signed payment

The contracts

Five contracts, none upgradeable, none with an operator or admin role. Source and tests live in the repository's contracts directory.

Plinth contracts and their authorities
ContractAuthorityWhat it does
MerchantVaultowner onlywithdraw, rescueToken, scheduleSettlement, cancelPendingSettlement. forward() and applyPendingSettlement() are permissionless but cannot choose a recipient or amount.
VaultFactorynoneVault address commits to merchant, owner and delay, so nobody can squat a vault with a different owner.
InvoiceDepositnoneClone with immutable arguments (destination, fee rate, kind, merchant, invoice). sweep() splits the balance: the fee to the treasury, the rest to the baked-in destination (a vault or a payout wallet). The fee is capped at 1% in the implementation. A failing treasury transfer is skipped and the destination still receives everything.
InvoiceFactorynonepredict() and deployAndSweep(). For vault payments it requires a registered vault; the token, treasury and fee cap come from the implementation.
PaymentRouternonePulls the customer's EIP-3009 authorization and forwards it, split the same way, to the committed destination. Stateless, holds nothing between transactions.

Design

  • No proxies you do not control, no upgradeability, no selfdestruct.
  • Invoice contracts are minimal clones with immutable arguments. Nothing to initialise, so nothing to front-run.
  • Deposit addresses commit to the destination, fee rate, token, treasury, chain and contract version; changing any changes the address.
  • The platform fee is 0.20% by default, capped at 1% in the contract. The cap and the treasury address are fixed when the implementation is deployed; a new treasury means a new implementation version that affects only new addresses.
  • Payment authorisations use a nonce derived from every payment field, and the customer's signature covers it.
  • Unit, fuzz and invariant suites, plus fork tests against the real USDC bytecode on Arc Testnet and Base Sepolia. None of this is an independent audit.

Backend and relayer

  • The relayer key is separate from every merchant authority and sits behind a signer interface designed for KMS or HSM. A KMS adapter is not built yet.
  • Per-network daily gas budget, per-transaction fee cap, operation cap, minimum balance and an emergency stop for new actions.
  • A transaction is simulated before it is signed; reverting calls cost nothing.
  • Every observed transfer is re-verified from its receipt against the configured token and chain before it counts.
  • Before a payout wallet is used, the engine checks your signed choice, and a network stays closed if the deployed contracts do not read back the expected token, chain, version, treasury and fee cap.
  • Signatures, API secrets and passwords are redacted from logs and audit events.

Longer write-ups ship in the repository: docs/architecture.md and docs/verification.md (what was tested and what was not). There is no hosted copy of those documents yet.

If the backend is compromised

The honest version, because a payment address is just a number a customer copies.

What it cannot do

  • Move funds out of your vault, or change its owner or rules.
  • Redirect funds sent to a payment address that has already been issued: the destination is part of the address.
  • Reuse a customer's signed payment for a different amount, payer or recipient.

What it can do

  • Hand out new checkout addresses that pay an attacker instead of you. A customer cannot tell the difference by looking at an address.
  • Stop service, delay settlements, or spend the capped relayer gas budget.

Partial mitigation: the engine verifies your signed payout choice before it derives an address, which stops tampering with the database alone. It does not stop an attacker who controls the API process itself. Direct payouts add another trade-off: the payout wallet's key controls the money directly, and a change of wallet relies on email confirmation, a cooldown and a fresh sign-in.

Current limitations

  • The contracts have not been independently audited. Commission an audit before enabling any mainnet.
  • Mainnet deployment is deliberately not performed by this repository's tooling without explicit approval per network.
  • Wallet checkout supports ordinary accounts; smart-contract wallets need an EIP-1271 path that is not built yet.
  • Native-currency transfers to a deposit address on Arc are not event-detected; they surface through balance reconciliation only.
  • Stablecoin issuers can block addresses; a blocked vault, payout wallet or invoice address cannot receive or sweep funds.
  • A custodial payout mode, where Plinth holds the balance and you withdraw it, exists in the code but is switched off and is not offered. Where it is ever enabled, the gateway does hold a key that can move funds.
  • Payout wallets must be ordinary accounts (no contract code), and the Plinth treasury is assumed to be an ordinary account too.
  • No live public-testnet acceptance run has been recorded yet, and no SOC 2, ISO or similar certification exists.

Report a vulnerability

If you find a weakness in the contracts, the API or this site, tell us privately first so it can be fixed before it can be used.

The disclosure address is set per deployment (NEXT_PUBLIC_SECURITY_EMAIL) and has not been published yet. The operator must set it before launch.

Include what you found, how to reproduce it, the network and any transaction hashes. Do not post it publicly and do not test against funds that are not yours.

  • In scope: the contracts, the API, the checkout page and the dashboard.
  • Out of scope: social engineering, denial-of-service load tests, and issues in third-party services such as RPC providers or the token contracts themselves.
  • There is no bug bounty programme today. A formal safe-harbour statement will be published after legal review.

Read it, then break it

The sandbox runs on testnets only. Nothing there holds real value.