The companion account
The StarkWare pool requires itsuser_addr to implement SNIP-6
is_valid_signature, which advanced account classes (like Chipi’s SHHH
wallets) deliberately do not expose. Instead of changing an audited account
class, each user gets a companion: a lightweight standard account (any
SNIP-6 class, e.g. OpenZeppelin
0x061dac032f228abef9c6626f995015233097ae253a7f72d68552db02f2971b8f) that
acts as the pool identity.
- Derived, not created:
deriveCompanionKeypair(anchor)+deriveCompanionAddress({publicKey, classHash})are deterministic from the user’s owner-independent anchor. Cache the address; never the keys. - Counterfactual: it exists on-chain only after first use.
execShielddeploys it automatically (self-paid from gas-tank-fronted STRK, salt = pubkey so the deployed address equals the derived one). - Never holds user funds at rest: deposits pass through it into the pool; withdrawals pass back out to the user’s real wallet; leftover fronted STRK sweeps back to the gas tank.
Headless example
Multi-token operations
Shield several tokens at once
execShieldMany batches N deposits into ONE apply_actions — one proof, one
transaction, one 4-STRK pool fee for the whole batch (mainnet-proven by
the S5 smoke, scripts/multi-token-smoke.mjs). It takes the same
CompanionCtx as execShield:
execShieldMany handles for you:
- Approvals are aggregated. When STRK itself is in the batch, its deposit approval and the pool’s 4-STRK fee approval share one allowance slot — separate approves would overwrite each other, so they are summed into a single approval.
- Single-item batches delegate to the proven single-token path — calling
it with one deposit is exactly
execShield.
requestGasFunding + waitForFunding), with one addition: the companion
must hold every deposit token before the call, and when STRK is one of the
deposits it needs GAS_FUND_STRK plus the STRK being shielded — the
fronted envelope is gas/fee money, never the deposit.
Unshielding several tokens
There is nounshieldMany yet: each withdrawal is its own proof, its own
transaction, and its own 4-STRK pool fee. Two operational constraints,
both found on mainnet by the S5 smoke:
- Refuel before every withdrawal. The gateway validates ~7.8 STRK of max resource bounds per transaction, so gas funded once runs dry after the first withdrawal.
- Wait for the prover’s view between operations. Proving runs against
head − 12; state written moments ago is invisible to it for ~13 blocks (~5–6 minutes). Withdrawals share the user’s note channel, so the second compile must see the pool state the first one just changed. Submitting too early fails with the prover’sstate_too_freshhint (retry_after_secondsin the error body) — wait and retry.
state_too_fresh failure, tell the user
what already came back (it IS back), and retry the remaining token a few
minutes later. Money is never stranded: a failed withdrawal leaves the notes
in the pool, spendable on the next attempt.
