// LpManager — Phase B of silent-iron-keystone v4. // // PURPOSE // ───────── // Wraps LP_MINT / LP_BURN / LP_LOCK in a typed-argv DSOL contract. // Same bug-class-fix property as IonSwapDeposit: every parameter // (positionId, pool_id, amount, blinding) is positional in the ABI // and CANNOT be silently dropped. // // PRIVACY MODEL // ───────────── // LP positions are commitment-based today (`pos-` → Pedersen // commit + blinding held client-side in localStorage). This contract // preserves that pattern: the OPENING (amount, blinding) stays in // the user's wallet; the contract holds only the commitment hash. // // BUG-CLASS FIX // ───────────── // The v1.2.144 stuck-LP regression came from `provider.lpMint({ // commitment: null, positionId: null })`. With typed argv, calls // missing `commitment` or `positionId` are rejected at the ABI // decoder before any state mutation. Result: structurally // impossible to mint a stuck LP position. // // EMITTED EVENTS // ────────────── // mint() → LP_MINT_V1 syscall, byte-equivalent to legacy attestation // burn() → LP_BURN_V1 syscall, byte-equivalent to legacy attestation // lock() → LP_LOCK event (no syscall — pure on-contract state) dark contract LpManager { // commitment_hex hex-encoded as a sentinel — non-empty means present. public mapping(bytes => bytes) positionPool; // positionId → pool_id public mapping(bytes => bytes) positionCommit; // positionId → Pedersen commit public mapping(bytes => uint64) positionMintBlock; // positionId → block minted public mapping(bytes => uint64) positionLockUntil; // positionId → lock-until block (0 = unlocked) public mapping(bytes => bool) positionBurned; // positionId → burned flag public mapping(bytes => bool) spentNullifiers; @direct entry mint(bytes positionId, bytes pool_id, bytes commitment) { require(positionMintBlock[positionId] == 0, "LP_DUPLICATE_POSITION"); positionPool[positionId] = pool_id; positionCommit[positionId] = commitment; positionMintBlock[positionId] = block.number; positionLockUntil[positionId] = 0; positionBurned[positionId] = false; syscall(LP_MINT_V1, ctx.txHash); } @direct entry burn(bytes positionId, uint64 amount, bytes nullifier) { require(positionMintBlock[positionId] != 0, "LP_UNKNOWN_POSITION"); require(!positionBurned[positionId], "LP_ALREADY_BURNED"); require(block.number >= positionLockUntil[positionId], "LP_STILL_LOCKED"); require(!spentNullifiers[nullifier], "LP_REPLAY"); require(amount > 0, "LP_ZERO_AMOUNT"); positionBurned[positionId] = true; spentNullifiers[nullifier] = true; syscall(LP_BURN_V1, ctx.txHash); } @direct entry lock(bytes positionId, uint64 unlock_block) { require(positionMintBlock[positionId] != 0, "LP_UNKNOWN_POSITION"); require(!positionBurned[positionId], "LP_ALREADY_BURNED"); require(unlock_block > block.number + 6, "LP_UNLOCK_TOO_SOON"); require(unlock_block > positionLockUntil[positionId], "LP_LOCK_NOT_EXTENSION"); positionLockUntil[positionId] = unlock_block; } }