deviant/Parameters/Pari_P2P/contracts/GamePoolEscrowFlatV2.cash
walerikus 35076606c7 chore: checkpoint in-flight work across shell, contracts, and docs
Snapshot of the shared working tree's outstanding changes (multiple
concurrent workstreams), excluding generated runtime state files
(.football-tote-state.json, .tote-platform-state.json, .usdm-historical-chart.html).

Spans: wallet shell (connect/, keystore MultiKeyStore+vault, screens,
currency picker, sign-message), verify artifacts, new Potidaea/DRA/tote
contracts + regtest suites, cross-chain/T6 + WalletConnect Parameters,
a telegram-orderbook subproject, design mockups, and docs. See
shells/app/WALLET-COORDINATION.md for the per-file ownership split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 02:02:05 +02:00

166 lines
8.2 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

pragma cashscript ^0.12.0;
// GamePoolEscrowFlatV2 — flat-fee escrow with per-side refund.
//
// Identical to GamePoolEscrowFlat (existing artifact) except for ONE new
// resolution path: `refundParty(int side)`. This enables the Pari_p2p
// **Flow A** (independent deposits) safely — a single-side deposit can be
// reclaimed by its owner after the deadline if the opponent never joined.
// Without this, a Flow A deposit by A with B-never-shows leaves A's funds
// permanently stuck at the escrow address (existing refund() requires a
// combined pool UTXO with poolAmount FTs).
//
// All other paths (resolveArbiter, resolveHashlock, refund) are byte-for-
// byte identical to GamePoolEscrowFlat and validate the same output
// structure. They work transparently with either:
// - one combined pool UTXO (Flow B atomic co-sign), OR
// - two separate deposit UTXOs (Flow A independent deposits)
// because the covenant only constrains outputs, not the input layout.
//
// IMPORTANT: in Flow A, EACH deposit UTXO is its own covenant instance
// spending; resolution txs (resolveArbiter / refund) spend BOTH UTXOs as
// inputs in the same tx and consensus enforces the constraints on each.
//
// Status: CHIPNET-VALIDATED by scripts/chipnet-pari-p2p-test.mjs across four
// scenarios on live chipnet consensus. Latest run (with cancel()) — CHE
// category ebd7c46a…0be36: mint+stake atomic (db444c0d…, 729578e5…),
// resolveArbiter on 2 separate deposits (a73c008e…), refundParty(0) on stuck
// single-side after deadline (ba66a060…), cancel(0, sigA, pubA, sigArbiter)
// immediate user-initiated reclaim with no deadline wait (391fdb18…). Full
// reference: Parameters/Pari_p2p/.
contract GamePoolEscrowFlatV2(
bytes tokenCategory, // the MBA being staked
int poolAmount, // total pooled FTs = 2 × stakeAmount
int poolSats, // total backing = 2 × stakeSats
int stakeAmount, // per-player stake (for refund / refundParty)
int stakeSats, // per-player backing
bytes20 partyA, // player A pkh (refund recipient)
bytes20 partyB, // player B pkh (refund recipient)
pubkey arbiterPk, // arbiter for resolveArbiter (+ fee recipient)
bytes32 secretHash, // sha256(secret) for resolveHashlock
int deadline, // CLTV height/time for refund + refundParty
int feeAmount // flat arbiter fee in FTs; 0..<poolAmount
) {
// ---- Arbiter declares the winner: pool − feeAmount → winner, feeAmount → arbiter ----
function resolveArbiter(sig arbiterSig, bytes winnerLockingBytecode) {
require(checkSig(arbiterSig, arbiterPk));
int rate = poolSats / poolAmount;
require(feeAmount < poolAmount);
int winnerAmount = poolAmount - feeAmount;
require(tx.outputs[0].lockingBytecode == winnerLockingBytecode);
require(tx.outputs[0].tokenCategory == tokenCategory);
require(tx.outputs[0].tokenAmount == winnerAmount);
require(tx.outputs[0].value == winnerAmount * rate);
if (feeAmount > 0) {
require(feeAmount * rate >= 546);
require(tx.outputs[1].lockingBytecode == new LockingBytecodeP2PKH(hash160(arbiterPk)));
require(tx.outputs[1].tokenCategory == tokenCategory);
require(tx.outputs[1].tokenAmount == feeAmount);
require(tx.outputs[1].value == feeAmount * rate);
}
}
// ---- Winner reveals the committed secret: entire pool → revealer ----
function resolveHashlock(bytes secret, bytes winnerLockingBytecode) {
require(sha256(secret) == secretHash);
require(tx.outputs[0].lockingBytecode == winnerLockingBytecode);
require(tx.outputs[0].tokenCategory == tokenCategory);
require(tx.outputs[0].tokenAmount == poolAmount);
require(tx.outputs[0].value == poolSats);
}
// ---- Combined refund: both stakes back to A and B (Flow B or paired Flow A) ----
function refund() {
require(tx.time >= deadline);
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(partyA));
require(tx.outputs[0].tokenCategory == tokenCategory);
require(tx.outputs[0].tokenAmount == stakeAmount);
require(tx.outputs[0].value == stakeSats);
require(tx.outputs[1].lockingBytecode == new LockingBytecodeP2PKH(partyB));
require(tx.outputs[1].tokenCategory == tokenCategory);
require(tx.outputs[1].tokenAmount == stakeAmount);
require(tx.outputs[1].value == stakeSats);
}
// ---- per-side refund (Pari_p2p Flow A safety net) ----
//
// After the deadline, a single-side deposit can be reclaimed by its
// owner. Permissionless — anyone can trigger it (they just pay the
// miner fee), but the output is structurally forced to the specified
// side's P2PKH. The input must be a SINGLE-STAKE-SIZED UTXO; this
// prevents calling refundParty on a combined pool UTXO that the
// existing refund() should handle.
//
// `side` selects which party's P2PKH receives:
// side = 0 → partyA
// side = 1 → partyB
function refundParty(int side) {
require(tx.time >= deadline);
require(side == 0 || side == 1);
// Constrain the input to a single-stake-sized deposit. This
// closes off a perverse spend where someone tries to refundParty
// a combined pool UTXO (which would otherwise pay only one side
// their stake's worth and "lose" the other side's stake to fees).
require(tx.inputs[this.activeInputIndex].tokenAmount == stakeAmount);
require(tx.inputs[this.activeInputIndex].value == stakeSats);
if (side == 0) {
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(partyA));
} else {
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(partyB));
}
require(tx.outputs[0].tokenCategory == tokenCategory);
require(tx.outputs[0].tokenAmount == stakeAmount);
require(tx.outputs[0].value == stakeSats);
}
// ---- USER-SIGNED EARLY CANCEL — both depositor and arbiter must sign ----
//
// Lets a single-side depositor reclaim BEFORE the deadline, without waiting,
// when the arbiter (game server) co-signs an attestation that the match
// was indeed cancelled / no opponent joined.
//
// Why both signatures are required:
// - depositor's sig alone → A could grief mid-game by cancelling after B
// deposited (the covenant cannot see other UTXOs at this address).
// - arbiter's sig alone → server could rug deposits unilaterally.
// - both together → arbiter attests cancellation conditions, depositor
// confirms they want their funds back. Server cooperation is required,
// and refundParty(side) after the deadline remains the no-server fallback.
//
// Caller passes `partyPk` (the depositor's compressed pubkey) so the contract
// can verify checkSig(partySig, partyPk) AND that hash160(partyPk) matches
// the right party's pkh stored at construction.
function cancel(int side, sig partySig, pubkey partyPk, sig arbiterSig) {
require(side == 0 || side == 1);
// Arbiter co-signs the cancellation attestation.
require(checkSig(arbiterSig, arbiterPk));
// Verify partyPk hashes to the right party's stored pkh, AND that this
// party signed.
if (side == 0) {
require(hash160(partyPk) == partyA);
} else {
require(hash160(partyPk) == partyB);
}
require(checkSig(partySig, partyPk));
// Same single-stake-input guard as refundParty.
require(tx.inputs[this.activeInputIndex].tokenAmount == stakeAmount);
require(tx.inputs[this.activeInputIndex].value == stakeSats);
if (side == 0) {
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(partyA));
} else {
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(partyB));
}
require(tx.outputs[0].tokenCategory == tokenCategory);
require(tx.outputs[0].tokenAmount == stakeAmount);
require(tx.outputs[0].value == stakeSats);
}
}