Confidential payroll rails for Safe{Wallet} multisigs.
β Live on Ethereum Sepolia β a real 2-of-3 Safe runs the full confidential-payroll lifecycle on-chain: sealed amounts, an over-cap line paying encrypted zero, a publicly-auditable cap flag. 195 tests green. Not a mock.
Owners approve the cap. Nobody reads the salaries.
The problem. Safe is the standard for DAO/startup treasuries, but it's radically transparent: every payout amount is public in the queue before execution and on-chain forever after. Contributor comp becomes a poaching list. Teams either accept the leak or exfiltrate payroll to a custodial processor β abandoning the multisig guarantees they chose Safe for.
The approach. NoxSafe keeps the Safe exactly as it is β same contracts, same owners, same signing
queue β and routes only the amounts through Nox confidential tokens (ERC-7984). One multisig batch wraps
the payroll float (USDC β cUSD) and grants a time-bound, revocable ERC-7984 operator to the
PayrollRail. Owners approve who gets paid and a public budget cap; individual amounts stay
encrypted end-to-end, enforced against the cap in encrypted space (le+select), visible only to each
recipient β and an auditor if the DAO chooses. Delivered as a Safe App inside app.safe.global.
The same roster, three truths: sealed to the public queue Β· one line to each recipient Β· all lines to the auditor, on one on-chain grant.
The rail holds zero Safe execution rights. It is a time-bound ERC-7984 operator
(cUSD.setOperator(rail, quarterEnd)), revocable with one setOperator(rail, 0) from the queue. Its
worst-case blast radius is the wrapped float β which the app sets equal to the approved cap (float β‘ cap),
so the worst case is the approved spend. The Safe's unwrapped USDC is untouchable. We didn't ask the
Safe to trust a module with its treasury; we asked it to delegate a capped, expiring token authority.
The full quarter lifecycle is proven on Ethereum Sepolia, governed by a real 2-of-3 Safe (created via protocol-kit): the onboarding batch and every approval were routed through the multisig (two owners sign, the flow executes); the designer decrypted exactly 4,200 from the live Nox gateway; the over-cap probe paid encrypted zero; the six cap-compliance flags decrypt to trueΓ5 + falseΓ1; the auditor decrypted all six lines (26,000 proposed, 24,000 actually paid β the over-cap 2,000 line sealed to zero, so spend stays under the 25,000 cap); and the accounting proof published "spend β€ budget = true".
| Demo Safe (2-of-3) | 0x3Bd273B4f90829C0fA5d2aFa296b02E2AFaF9642 |
| PayrollRail | 0xE7158dAE72C94D6396ed73636d9E5Fe4B5370ED8 |
| ConfidentialUSD (cUSD) | 0x82C281D7403e44d61968c2F49751a56877468991 (reused) |
| Onboarding batch tx | 0x682907a5β¦f72e1 (one Safe multisig batch) |
| executePayroll tx | 0x62bf1ca4β¦b093d2 (6 lines, 310,617 gas/payout) |
| Tests | 195 green locally β 67 Hardhat contract + 128 @noxsafe/rail-sdk unit (npm run test:all); own contracts (PayrollRail / ConfidentialUSD / DemoUSD) at 100% statements / branches / functions / lines, enforced by npm run coverage:check |
| Bench | concurrent encrypt Γ10 = 2.6s (264 ms/line) Β· decrypt p50 814ms / p95 2.0s Β· per-payout 310,617 gas |
| Package | @noxsafe/rail-sdk (MIT) + @noxsafe/payroll-kit CLI |
Reproduce: npm run e2e:safe (full multisig lifecycle) Β· npm run e2e:local (zero-gas local mirror) Β·
npm run bench. Full proof + tx hashes in fixtures/e2e-result.json.
Etherscan source-verify is one zero-gas command β npm run verify:contracts (ETHERSCAN_API_KEY is set in
.env); DemoUSD + cUSD are already verified.
npm install
npm run compile
npm run test:all # 67 contract + 128 unit = 195 tests green (local, zero gas)
# See the whole thing run locally (zero gas, no gateway):
npm run node # terminal 1
npm run e2e:local # terminal 2 β deploy β onboard β propose β approve β execute β audit
# Offline CLI: sealed roster + the live encrypted cap check
npm run cli -- roster preview --csv fixtures/roster.csv --cap 25000SAFE_ADDRESS=<demoSafe> npm run deploy # deploys ONLY PayrollRail (reuses DemoUSD + cUSD)
npm run verify:contracts # Etherscan source-verify the rail
npm run e2e # designer decrypts 4,200; cap flags trueΓ5 + falseΓ1
npm run bench # rosterΓ10 encrypt wall-time + per-payout gas + decrypt p50/p95Estimated Sepolia ETH for the funded run: ~0.02β0.03 ETH (1 deploy + ~15 lifecycle txs).
Copy the template and fill it in (.env is gitignored β never commit real keys; use throwaway keys, Sepolia only):
cp .env.example .env| Variable | What it is | How to obtain |
|---|---|---|
DEPLOYER_ADDRESS, DEPLOYER_PRIVATE_KEY, PRIVATE_KEY |
Throwaway EOA that deploys the PayrollRail, wraps the payroll float, and signs demo txs (PRIVATE_KEY mirrors the deployer key). |
Generate a key: openssl rand -hex 32 (prefix 0x). Derive its address: node -e "console.log(new (require('ethers').Wallet)('0x<hex>').address)". Fund ~0.03 Sepolia ETH from sepoliafaucet.com or the Alchemy faucet. |
SEPOLIA_RPC_URL |
Sepolia JSON-RPC endpoint. | Default public node needs no signup (rate-limited). For reliable e2e, get a free key at Alchemy or Infura β Ethereum β Sepolia. |
CHAIN_ID |
Fixed 11155111 (Sepolia). |
Do not change. |
NOX_PROTOCOL_ADDRESS |
iExec Nox protocol contract (NoxCompute β TEE ACL + proof validation). | Fixed 0x24efβ¦77bf; re-verify from the Nox docs /networks page if it redeploys. No Nox account or API key is required β the Handle Gateway is self-serve. |
ETHERSCAN_API_KEY |
Optional β only for npm run verify:contracts (publishes contract source). |
Free, ~1 min at etherscan.io/myapikey β Add β copy the ~34-char key (no 0x). |
DEMO_MNEMONIC |
Throwaway BIP-39 phrase the e2e derives the 2-of-3 Safe owners, contributors, and the compliance auditor from. | Generate your own: node -e "console.log(require('ethers').Wallet.createRandom().mnemonic.phrase)". |
External services (all free, testnet-only):
- iExec Nox Handle Gateway β encrypts/decrypts payroll amounts inside Intel TDX; self-serve, no signup.
- Safe{Wallet} (protocol-kit) β the real 2-of-3 multisig that governs the onboarding batch and approvals on Sepolia.
- Ethereum Sepolia β the chain everything deploys to.
- Etherscan (Sepolia) β contract source verification only.
Deployed contract addresses for this app (2-of-3 Safe, PayrollRail, cUSD) are in the Status table under LIVE on Sepolia above.
The full lifecycle and the encrypted budget invariant are in SPEC.md; the stack + diagram in
ARCHITECTURE.md; the demo script in DEMO.md.
// executePayroll, per line β cap enforcement entirely in encrypted space:
ebool ok = Nox.le(Nox.add(spent, amt), Nox.toEuint256(cap)); // cap PUBLIC, amt SEALED
euint256 pay = Nox.select(ok, amt, Nox.toEuint256(0)); // over-cap -> encrypted zero
Nox.allowPublicDecryption(ok); // anyone audits compliance, sees no amount
Nox.allowTransient(pay, address(cUSD));
cUSD.confidentialTransferFrom(safe, recipient, pay); // operator-pull from the Safeselect-based encrypted enforcement (over-cap pays encrypted zero, on-chain indistinguishable) Β·
per-handle viewer ACLs for recipient/auditor disclosure Β· allow (admin) vs addViewer (viewer) for the
compliance-officer/auditor split Β· allowPublicDecryption for a "spend β€ budget" flag that leaks no line Β·
setOperator's time-bound authority Β· the ERC20ToERC7984Wrapper on/off ramp. Remove Nox and you'd need
an FHE coprocessor, a disclosure registry, a custodial payroll processor, and a bespoke audited Safe
module β four systems and a worse trust story. See docs/SPONSOR_DEFENSE.md.
- The treasurer sees plaintext amounts client-side (inherent to composing payroll).
- The operator grant covers any amount until expiry (ERC-7984 semantics) β mitigated by short expiry + wrapping only the float (float β‘ cap).
- The total wrapped float and recipient addresses are public; only line amounts are the protected asset.
- Beta SDK
0.1.0-beta.13pinned; our 15 dated frictions are infeedback.md.
Not a weekend prototype β the repo ships a production-grade harness. A 7-stage
GitHub Actions pipeline (.github/workflows/ci.yml) gates every PR:
Quality β Security β Build β E2E β Performance β Deploy β Release, with concurrency
cancellation and a Node matrix on main.
# ββ Contracts + SDK (repo root) βββββββββββββ
npm run compile # Hardhat, solc 0.8.35 viaIR
npm run test:contracts # 67 Hardhat contract tests
npm test # 128 @noxsafe/rail-sdk unit tests (Vitest)
npm run test:all # all 195, zero gas
npm run coverage:check # gate: own contracts must be 100% on every metric
# ββ Frontend (web/) βββββββββββββββββββββββββ
cd web
npm run lint # next lint
npm run typecheck # tsc --noEmit
npm run build # Next.js production build
npm run e2e # Playwright E2E (demo mode β no wallet, no env)
npm run lighthouse # Lighthouse CI (perf / a11y / SEO)
# ββ Security (repo root) ββββββββββββββββββββ
make security-scan # npm audit + license check| Layer | Tool | Status |
|---|---|---|
| Contract quality | Hardhat + Solidity viaIR |
β 65 tests |
| SDK unit testing | Vitest | β 128 tests |
| Frontend quality | ESLint (next lint) + TypeScript strict |
β |
| E2E testing | Playwright β 3 suites Γ 2 devices (demo mode) | β 54 checks |
| Security (SAST) | CodeQL | β |
| Security (SCA) | Dependabot + npm audit + license-checker |
β |
| Secret scanning | TruffleHog | β |
| Performance | Lighthouse CI (a11y β₯ 0.9 hard gate) | β |
E2E runs headless with no wallet and no env vars: it drives the Safe App
tabs, the recipient/auditor disclosure portals and /verify straight off the
deterministic demo fixture, asserting the confidentiality UX renders correctly.
Built during the WTF!! Hackathon (iExec Nox). The confidential-token skeleton (ConfidentialUSD wrapper,
DemoUSD, the handle/ACL integration patterns, and the deploy/bench/readiness script shapes) is a shared
skeleton with my sibling entry NoxSend and is disclosed as reused; the
PayrollRail contract, the @noxsafe/rail-sdk roster/cap logic, the Safe-App integration, and all tests
are NoxSafe's own. Nothing is reused from any Vibe Coding Challenge project. Throwaway Sepolia keys only;
never mainnet.
Brand assets in docs/ β synthwave theme (treasury emerald
#10B981 + encrypted violet #8B5CF6). "Safe" referenced nominatively; no Safe logo used.