Skip to content

Make pay_agent actually transfer CSPR — payer session code + verified testnet settlement - #12

Open
syedhassan-aifinpay wants to merge 4 commits into
agent/casper-v2-atomic-settlementfrom
fix/casper-v2-session-transfer
Open

Make pay_agent actually transfer CSPR — payer session code + verified testnet settlement#12
syedhassan-aifinpay wants to merge 4 commits into
agent/casper-v2-atomic-settlementfrom
fix/casper-v2-session-transfer

Conversation

@syedhassan-aifinpay

@syedhassan-aifinpay syedhassan-aifinpay commented Aug 4, 2026

Copy link
Copy Markdown
Member

What this fixes

Casper settlement must be submitted as payer session ModuleBytes. A direct stored-contract call cannot access the payer's main purse safely. The session creates a temporary purse, funds the exact amount and calls pay_agent atomically.

The verifier now accepts only a successful deploy containing the exact payer-session ModuleBytes and rejects direct stored-contract settlement.

Integrated surfaces

  • workspace CI builds contract and payer-session Wasm;
  • deployment manifest binds both artifact hashes;
  • bridge, MCP and demos construct the payer-session deploy;
  • replay, forged payer, zero amount, self-payment, unknown recipient and malformed request IDs remain rejected.

Evidence

Candidate SHA: bd6db3320df9d0cc4387d2df9f31329a0d465d3b

GitHub CI run 31280849717 — success.

Local: both Wasm builds, clippy with -D warnings, 14 tests.

  • contract Wasm SHA-256: 7a2f18b0c4745e548a701fe6a11cb57d3258baa771fdec7fc77ccc9fa58d2b04
  • payer-session Wasm SHA-256: f7c05b2a73cd0d6a446d4ce5f43d9e032e1921107407db6855c95c9c8304bcc8

Release boundary

The manifest intentionally remains source-only. No mainnet deploy is included; settlement stays disabled until a verified deployment binds these exact hashes.

… code

The v2 branch replaced receipt-only bookkeeping with system::transfer_to_account,
which is the right intent but cannot work. Proven on testnet, not argued: the call
reverts with "Forged reference: URef(...)". Under EntryPointType::Called the
contract resolves the account main purse, which is not in its access rights, and
the contract has no purse of its own — call() never creates one. Passing the main
purse in as an argument does not rescue it either; Casper attenuates a main purse
handed to a contract down to deposit-only, and the transfer fails with
ERR_TRANSFER_FAILED. Both failures are recorded on testnet.

So the funds have to be placed in a purse the contract may legitimately debit, and
that has to happen inside the same transaction or the settlement is not atomic —
which is the whole point of this work. pay_agent now takes that purse as an
argument, and the new session/ crate is the payer-side half: it creates a purse,
moves exactly the payment into it from the payer's main purse, and calls pay_agent
handing it over. One deploy, so either all of it happens or none does. The
temporary purse holds exactly the payment, so the contract can never reach the
rest of the payer's balance.

Verified end to end on testnet. A real 2.5 CSPR settlement moved the provider
balance by exactly 2.5000, payment_count went to 1, and the settlement record
reads back SETTLED. Six adversarial cases were then rejected on chain with the
right error codes: replay 102, forged payer 103, zero amount 106, self-payment
107, unregistered recipient 101, malformed identifier 105. None moved value.

demo/testnet-verify.js and demo/testnet-negative.js reproduce both runs. Key
directories and the node URL come from the environment; no key material is
committed.
coinsecuritiescompany and others added 3 commits August 9, 2026 00:52
…erclaim

Two gaps in the v2 release gate.

There were no negative on-chain tests. testnet-verify.js proves the happy path
moves CSPR; nothing proved the paths that must not. demo/testnet-negative.js
covers forged payer, zero amount, self-payment, malformed identifier,
unregistered recipient and replay, then settles a real payment and checks the
recipient's balance delta is exactly the amount.

Each case asserts the specific User error from src/main.rs rather than just
"the deploy failed". Accepting any revert would also accept an out-of-gas
run, which would report a broken guard as working. The valid payment is
included for the same reason: a suite that rejects everything, including what
it should accept, would otherwise look like a pass.

Second, deployments/casper-v2.json is what trusted-contract.js consults before
allowing a payment, so every field in it is a security claim — and it could be
filled in by hand. scripts/deployment-manifest.mjs is now the sanctioned way
to write it and refuses claims it cannot substantiate: stage builds and records
both Wasm digests against the exact source commit, refusing to run on a dirty
worktree because the commit would not then describe the built bytes. It leaves
status "built" with the contract and deploy hashes null, since only a real
deployment may fill those. verify additionally requires the chain to confirm
the contract exists, and fails closed when the node is unreachable — an
unreachable node is not evidence of a good deployment.

check is the CI gate and now runs on every build: 7 tests cover advancing the
status without build evidence, claiming a deployment with no hashes, claiming
verified with no timestamp, a truncated contract hash, an unknown status and
the wrong contract version.

21 demo tests passing, was 14. Manifest stays source_only, so Casper payments
remain quarantined. Nothing deployed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants