argus/contracts/subscription.cash
Local Dev e0eca1190e covenants: close the holes an adversarial review found
migrate let the owner supply the new state, so an owner could put a script
of their own in front of the approved code and take the certificate out of
the covenant, or rewrite owner and expiry on the way. The contract now
carries its own state over and the owner supplies only the code. The
steward's approval covers source and destination code, so it cannot be
replayed in another tier or network.

The delivery escrow could be collected with a second protocol message after
the agreed one; it now allows at most four outputs, the fourth plain change.
A subscription draw could pay its fee allowance to a third output; it now
has one or two outputs and pays the host exactly the price.

Smaller: the commit salt is exactly 32 bytes (one box could serve several
names), expiry stays a block height, and relative-lock terms are range
checked in script. Escrow quotes are refused for co-sign TLDs, where a
delivery would satisfy the contract and register nothing.
2026-10-04 16:53:56 +02:00

72 lines
3.4 KiB
Text

pragma cashscript ^0.13.0;
// Subscription — a prepaid balance the host can draw from once per period,
// and the buyer can take back at any time.
// (Argus/src/lib/plans.js SUBSCRIPTION_ROADMAP; ROADMAP-MAINNET.md §9 step 4.)
//
// STATUS: chipnet draft. NOT REVIEWED.
//
// The buyer funds this script. Three things then hold, and the buyer can check
// each from the script alone:
//
// · the balance stays theirs — cancel() needs only their signature, no
// notice, no cooperation, and returns whatever is left;
// · the host cannot take more than one period's price per period — pull()
// pays the host's lock exactly `priceSats`, sends the rest back to this
// same script, and can only run on an output that is `periodBlocks` old,
// which the re-created output is not;
// · nobody else gains anything — pull() needs no signature, so the host does
// not have to be online with a key, but it has at most two outputs: the
// price to the host, exactly, and the rest back to this script. Whatever
// the fee allowance does not use goes to the miner, not to whoever built
// the transaction.
//
// ONE BALANCE, ONE OUTPUT. The once-per-period limit is a property of an
// output, not of an address. A buyer who pays the same address twice has two
// balances, and each can be drawn every period. To add funds: cancel and fund
// again, in one transaction if you like.
//
// A pull spends this input ALONE. With two inputs, one "price to the host"
// output and one "rest back to the script" output could satisfy both, and the
// host would collect two balances for one payment. One input means the fee
// comes out of the balance too, bounded by `maxFeeSats`.
//
// The first period is not special-cased: the balance is drawn one period
// after it is funded. A host that wants payment up front takes the first
// period in the funding transaction itself.
contract Subscription(bytes20 buyerPkh, bytes hostLock, int priceSats, int periodBlocks, int maxFeeSats) {
// One period's payment to the host.
function pull() {
// A relative lock is a 16-bit block count. Anything outside that range
// would be read as something else entirely: only its low bits, a
// number of seconds, or no lock at all.
require(within(periodBlocks, 1, 65536));
require(this.age >= periodBlocks);
require(tx.inputs.length == 1);
require(tx.outputs.length <= 2);
int balance = tx.inputs[0].value;
require(tx.outputs[0].lockingBytecode == hostLock);
require(tx.outputs[0].value == priceSats);
require(tx.outputs[0].tokenCategory == 0x);
// Everything else stays in the subscription, less a bounded fee. When
// what would remain is too small to be an output, it is the fee.
int rest = balance - priceSats - maxFeeSats;
if (rest >= 546) {
require(tx.outputs[1].lockingBytecode == tx.inputs[0].lockingBytecode);
require(tx.outputs[1].value >= rest);
require(tx.outputs[1].tokenCategory == 0x);
} else {
// Nothing is returned, so there must be nothing else: the leftover
// is the fee, not a second output for whoever built the draw.
require(tx.outputs.length == 1);
}
}
// The buyer ends it and takes the balance. No conditions on where it goes.
function cancel(pubkey pk, sig s) {
require(hash160(pk) == buyerPkh);
require(checkSig(s, pk));
}
}