pragma cashscript ^0.13.0; // USD-pegged sale of a BCNR name certificate (CashTokens NFT). // // The certificate sits in this covenant while it is for sale. Anyone may // buy it by paying the seller the BCH equivalent of `targetCents` at the // oracle's current price; the seller may reclaim it at any time. // // Oracle message (40 bytes): timestamp as uint32 LE, price in USD cents per // BCH as uint32 LE, then the 32-byte txid of the listing's own outpoint, // signed by `oraclePk` (Schnorr data signature over the sha256 of the // message, as checkDataSig expects). // // What the chain can and cannot enforce: script can require a transaction // to be mined no EARLIER than a time (nLocktime), never no later, so a quote // cannot expire on-chain. The [floorSats, ceilSats] band the seller chooses // is therefore the real guarantee: whatever quote a buyer presents, the // payout stays inside it. Binding the quote to the listing's outpoint means // cancelling and relisting voids every quote ever issued. The locktime rule // below still stops a quote from being used before the oracle issued it. contract UsdListing( pubkey oraclePk, bytes20 sellerPkh, int targetCents, int floorSats, int ceilSats, int maxAgeSecs ) { // Buyer path. Output 0 pays the seller; output 1 carries the certificate // to wherever the buyer wants it. Remaining outputs are the buyer's. function buy(bytes oracleMsg, datasig oracleSig) { require(oracleMsg.length == 40); require(checkDataSig(oracleSig, oracleMsg, oraclePk)); bytes tsBytes, bytes rest = oracleMsg.split(4); bytes priceBytes, bytes quotedTxid = rest.split(4); int ts = int(tsBytes); int priceCents = int(priceBytes); require(priceCents > 0); require(quotedTxid == tx.inputs[this.activeInputIndex].outpointTransactionHash); // Not before the quote: nLocktime is checked against median-time-past // (about an hour behind the clock), so the buyer sets it a couple of // hours behind the quote and the contract allows that slack. require(tx.locktime + 10800 >= ts); require(tx.locktime <= ts + maxAgeSecs); int need = targetCents * 100000000 / priceCents; if (need < floorSats) { need = floorSats; } if (need > ceilSats) { need = ceilSats; } // Seller is paid first, in plain BCH. require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(sellerPkh)); require(tx.outputs[0].value >= need); require(tx.outputs[0].tokenCategory == 0x); // The certificate leaves intact: same category, same commitment. require(tx.outputs[1].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory); require(tx.outputs[1].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment); } // Seller path: reclaim the certificate (cancel the listing, or re-list // at a new target). function cancel(pubkey pk, sig s) { require(hash160(pk) == sellerPkh); require(checkSig(s, pk)); } }