Minimal repo for a hackathon prototype of Proof-of-Clean-Funds using strkBTC on Starknet.
- On-chain Cairo contracts (
contracts/) for:PolicyRegistryProofVerifierGateAdapterMerkleUtilshelper libraryexternal_verifierstubmock_erc20(local strkBTC)
- Protostar tests (
tests/) and scripts (scripts/) - Next.js + TypeScript dApp (
frontend/) with:- Wallet connect (Argent X / Braavos via
@starknet-react/core) /generate→ simulate proof generation/deposit→ verify & deposit flow/dashboard→ compliance view (stubbed events)
- Wallet connect (Argent X / Braavos via
NOTE: Contracts are written in legacy StarkNet Cairo (
%lang starknet) to keep the devnet / Protostar story simple and copy‑paste friendly for hackathon use.
contracts/policy_registry.cairoproof_verifier.cairogate_adapter.cairomerkle_utils.cairoexternal_verifier.cairo(off-chain verifier stub)mocks/mock_erc20.cairo(local strkBTC token)
tests/test_proof_verifier.pytest_gate_adapter.py
scripts/deploy_testnet.shrun_local_dev.sh
protostar.tomlfrontend/(Next.js app)
- Storage:
policy_commitment(policy_id: felt) -> feltowner() -> felt
- Functions:
constructor(owner_address: felt)– sets OWNER_ADDRESS.register_policy(policy_id: felt, commitment: felt)– owner-only.get_policy_commitment(policy_id: felt) -> felt
- Storage:
used_nullifier(nullifier: felt) -> felt(1 if used)
- Event:
ProofVerified(proof_id: felt, policy_id: felt, tier: felt)
- External:
verify_and_register(proof_id, policy_id, public_inputs_len, public_inputs_ptr, proof_blob_ptr, nullifier, expiry_ts) -> (ok)
- Behaviour:
- Checks expiry:
expiry_ts >= get_block_timestamp() - Checks
used_nullifier(nullifier) == 0 - Calls stub
call_external_verifier(proof_blob_ptr, public_inputs_ptr, public_inputs_len)fromexternal_verifier.cairo - Marks
used_nullifier(nullifier) = 1 - Emits
ProofVerified(proof_id, policy_id, tier)withtier = public_inputs_ptr(first public input / tier).
- Checks expiry:
- Storage:
strk_btc_address() -> felt(ERC20)proof_verifier_address() -> feltbalances(user: felt) -> Uint256(demo balances)
- Interfaces:
IProofVerifier.verify_and_register(...) -> (ok)IERC20.transferFrom(sender, recipient, amount: Uint256) -> (success)
- External:
constructor(strk_btc_addr: felt, proof_verifier_addr: felt)deposit(policy_id, proof_id, proof_blob_ptr, public_inputs_ptr, nullifier, expiry_ts, amount: Uint256)get_balance(user) -> Uint256
- Behaviour:
- Calls
IProofVerifier.verify_and_registerwith:public_inputs_len = 1public_inputs_ptr = tier(first public input)
- Requires
ok == 1 - Calls
IERC20.transferFrom(sender=user, recipient=this, amount) - Updates
balances[user] += amount
- Calls
%lang starknethelper lib:verify_merkle_proof(leaf, proof_ptr, proof_len, root) -> (ok)- Uses
hash2to iteratively hash leaf with proof siblings.
%lang cairohelper:call_external_verifier(proof_blob_ptr, public_inputs_ptr, public_inputs_len) -> (ok)- Returns
1iffproof_blob_ptr == 0x1, else0.
Install (example):
pip install protostar==0.9.1Check:
protostar --versionFrom repo root:
protostar buildThere are three key tests:
test_verify_register_oktest_replay_nullifiertest_deposit_flow
Run them all:
protostar testThis uses starknet-devnet + Protostar.
From repo root:
chmod +x scripts/run_local_dev.sh
./scripts/run_local_dev.shThe script will:
- Start
starknet-devnetonhttp://127.0.0.1:5050(if not running) - Compile contracts
- Deploy:
mock_erc20(mock strkBTC) →MOCK_STRKBTC_ADDRESSproof_verifier→PROOF_VERIFIER_ADDRESSgate_adapterwired with the above →GATE_ADAPTER_ADDRESS
At the end it prints addresses and copy‑paste lines to configure the frontend .env.
Variables to fill (contracts side)
STRKBTC_ADDRESS: use the real strkBTC on testnet orMOCK_STRKBTC_ADDRESSfrom devnetOWNER_ADDRESS: passed toPolicyRegistry.constructorwhen deploying
For alpha-goerli or other testnet, adjust the network name and inputs in:
scripts/deploy_testnet.sh
Then:
chmod +x scripts/deploy_testnet.sh
./scripts/deploy_testnet.shReplace:
STRKBTC_ADDRESSwith the real token addressOWNER_ADDRESSwith your compliance owner account
The Next.js app lives in frontend/.
cd frontend
npm installCopy the example:
cd frontend
cp .env.example .env.localEdit .env.local:
NEXT_PUBLIC_STARKNET_RPC_URL=http://127.0.0.1:5050 # or your devnet/testnet RPC
NEXT_PUBLIC_GATE_ADAPTER_ADDRESS=0xGATE_ADAPTER_ADDRESS
NEXT_PUBLIC_PROOF_VERIFIER_ADDRESS=0xPROOF_VERIFIER_ADDRESS
NEXT_PUBLIC_STRKBTC_ADDRESS=0xSTRKBTC_ADDRESS
NEXT_PUBLIC_POLICY_REGISTRY_ADDRESS=0xPOLICY_REGISTRY_ADDRESSUse the addresses printed by run_local_dev.sh or deploy_testnet.sh.
From frontend/:
npm run devOpen http://localhost:3000 in a browser with Argent X or Braavos installed.
Scripts:
npm run dev– start dev servernpm run build– production buildnpm run start– run production servernpm run test– run minimal Jest test for/api/generate-proof
- Connect / disconnect wallet (Argent X / Braavos via
@starknet-react/core) - Shows connected address
- Links to:
/generate/deposit/dashboard
- Form:
- Policy selector – loads from
PolicyRegistryvia/api/policies - Validity –
24hor7d - Amount (optional, used only for demo / UI)
- Policy selector – loads from
- On Generate Proof:
- Calls
POST /api/generate-proofwith{ walletAddress, policy_id, validity, amount } - Endpoint returns:
{ "proof_id": "...", "policy_id": "...", "tier": 1, "proof_blob_hex": "0x1", "public_inputs": ["1"], "nullifier": "0x...", "expiry_ts": 1234567890, "amount": "..." } - Uses:
proof_blob_hex = "0x1"→ activates stubcall_external_verifiertier = public_inputs[0]nullifier = keccak-like SHA-256(wallet || policy || nonce)
- Calls
- UI:
- Shows proof details (ID, policy, tier, expiry, nullifier, amount)
- Button Use in dApp → stores proof in
localStoragefor/deposit
- Auto-populates from stored proof (if present)
- Button Verify & Deposit:
- Builds a
depositcall toGateAdapterwith:policy_idfrom proofproof_idfrom proofproof_blob_ptr = 0x1(stub accepted value)public_inputs_ptr = tier(first public input)nullifierfrom proofexpiry_tsfrom proofamountas simple Uint256 (derived from proof amount or default)
- Sends tx via
@starknet-react/core/useSendTransaction - Shows tx hash / status message (Metamask-style error if something fails)
- Builds a
On-chain behaviour:
GateAdapter.depositalways callsProofVerifier.verify_and_registerfirst and reverts ifok != 1.
- Shows simple stats:
verified_count(number of events, currently stubbed)blocked_count(simulated)
- Table of
ProofVerifiedevents is currently a stub; replacefetcherinapp/dashboard/page.tsxwith agetEventsRPC call for real dashboards.
- Input JSON:
{
"walletAddress": "0x...",
"policy_id": "1",
"validity": "24h | 7d",
"amount": "optional string"
}- Behaviour:
- Computes
expiry_tsbased on validity - Generates random
proof_id - Derives
nullifier = keccak(wallet || policy || nonce)using SHA-256 for demo - Returns:
proof_idpolicy_idtier(fixed1for demo)proof_blob_hex = "0x1"(accepted by stub)public_inputs = ["1"]nullifierexpiry_tsamount
- Computes
- Reads policies via RPC from
PolicyRegistry:- Uses a small ABI for
get_policy_commitment - Queries a fixed set of policy IDs:
["1", "2", "3"] - Returns:
- Uses a small ABI for
{
"policies": [
{ "id": "1", "commitment": "0x..." },
{ "id": "2", "commitment": "0x..." },
{ "id": "3", "commitment": "0x..." }
]
}Assuming:
starknet-devnetrunning (./scripts/run_local_dev.sh)- Contracts deployed (addresses wired into
frontend/.env.local) npm run devrunning infrontend/
- Abrir
http://localhost:3000 - En la Home:
- Click en Connect ArgentX o Connect Braavos
- Ver la dirección conectada en la UI
- Ir a
/generate - Seleccionar política, por ejemplo
policy_id = "1"(standard) - Elegir Validity: 24h
- (Opcional) Especificar
Amount, por ejemplo0.5 - Click en Generate Proof
- Ver en la UI:
Proof IDPolicy IDTier(1)ExpiryNullifier
- Click en Use in dApp (save locally) para guardar la prueba en
localStorage
- Ir a
/deposit - Confirmar que el
Proof IDaparece auto-completado y los demás datos se ven en la tarjeta. - Asegurarse que el usuario tiene strkBTC mock y que el
GateAdapterestá aprobado (en un entorno real haríasapprovedesde el wallet/token UI; en este prototipo el approve se hace desde el test y desde un flujo separado si se desea extender). - Click en Verify & Deposit
- Firmar la transacción en ArgentX / Braavos
- Ver en la UI el hash de la transacción (
txid) y, opcionalmente, abrir el explorador (agregar el link de explorer adecuado según la red) - En la cadena:
- El contrato
ProofVerifieremiteProofVerified - El
GateAdapteraumenta el balance interno del usuario (get_balance)
- El contrato
- Ir a
/dashboard - Ver:
Verified proofsincrementado (simulado / a conectar con eventos reales)Blocked attempts(simulado)
- Contar la historia de compliance: cada depósito está ligado a un
ProofVerifiedconpolicy_idytier, sin revelar el historial completo del usuario.
- STRKBTC_ADDRESS:
- En devnet: usar el
MOCK_STRKBTC_ADDRESSdel scriptrun_local_dev.sh - En testnet: remplazar por el address real de strkBTC
- En devnet: usar el
- OWNER_ADDRESS:
- Address del account que controla
PolicyRegistry.register_policy
- Address del account que controla
- El stub
external_verifier.cairopermite activar/verificar cualquier prueba conproof_blob_ptr == 0x1, lo que simplifica la demo sin un ZK verifier real.