69 lines
3 KiB
Text
69 lines
3 KiB
Text
|
|
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));
|
||
|
|
}
|
||
|
|
}
|