5042002) and verifiable on
ArcScan. No admin, no pause, no upgrade path:
funds are only ever with the user or the contract.
Addresses
CtrlArcZ was deployed at block 51326557 (CTRL_ARCZ_DEPLOY_BLOCK) and
StealthAnnouncer at 53756547 (STEALTH_ANNOUNCER_DEPLOY_BLOCK); event scans
start there rather than at 0, because Arc caps an eth_getLogs range at 10,000
blocks.
CtrlArcZ: protected transfer
The escrow. Registers per-integrator configs and holds transfers until claimed, cancelled or reclaimed.SpendPolicyFactory + SpendPolicyAccount
The payer-side shield. The factory clones a policy-bound account per intent; the account enforces the policy and gates every spend on the co-signer.
The CREATE2 salt is
keccak256(abi.encode(ownerHash, userSalt, keccak256(abi.encode(params)))),
so the address commits to the entire policy. Change the target, the cap or the
expiry and it is a different address, which is what lets a funder check that the
box it is about to pay into is the box it specified.
A spend needs only the co-signer’s signature. The funds are hard-locked to the
pre-set target and capped by maxAmount, so the co-signer can only push the
payment the payer set up, never steal or redirect. The payer signs nothing blind;
their only on-chain action is a normal USDC transfer to fund the account.
init takes (token, cosigner, vaultHash, target, maxAmount, perPullMax, expiry, interval, mode). There is no owner and no vault stored: the owner is bound
only through the salt (ownerHash) and the return address only through the
commitment vaultHash = keccak256(abi.encode(vault)). The readable getters are
token, cosigner, target, vaultHash, maxAmount, perPullMax, expiry,
interval, mode, spent and nonce; there is no owner() or vault() getter
at all, so a merchant paid from the account cannot read who funded it.
Signatures are EIP-712 typed data over Spend(address target,uint256 amount,uint256 nonce,uint8 action), where action is 0 for pay and 1 for pull so a pay
authorization can never be replayed as a pull. The domain binds the account and
chain, so a signature cannot be replayed across accounts or chains. Use
spendTypedData / spendDigest in the SDK to build the exact bytes, or read the
account’s spendDigest(amount, action) getter.
Vault
Holds the owner’s USDC, funds accounts, receives sweeps.deposit(amount) pulls in
against an approval, fundAccount(account, amount) and withdraw(to, amount) are
owner-only, and balance() reads the holding. Every outflow goes through one
private _moveOut seam, which is where the APS confidential transfer slots in.
The
Vault contract is the vault-funded flow, not a requirement. A box binds its
return address through vaultHash, and since both sweeps now need msg.sender == vault, the bound address has to be one that can send a transaction. The reference
app binds the payer’s own wallet for a private payment and the stealth address for a
subscription. Use the Vault contract when you want one shared treasury that funds many
boxes, and bind sweeps to an address you control.StealthAnnouncer
An ERC-5564 announcement registry (schemeId 1), standalone from the factory and
the account. A payer publishes a box’s ephemeral public key here so only their own
viewing key can rediscover it later. Nothing in the announcement identifies the
payer to anyone else, and the SDK scans it from
STEALTH_ANNOUNCER_DEPLOY_BLOCK forward. See
Stealth boxes.
On Arc a USDC ERC-20 transfer moves the recipient’s native balance, so the account
and vault implement
receive() payable. Without it, funding a contract on Arc
would revert; a plain EVM never surfaces this.