The Bitcoin (Cash) VM has had both relative and absolute timelocks for over a decade.
Early dapps that use this functionality are HODL plugin by @mainnet_pat, and the Last Will and Mecenas plugins by @Licho.
Contracts employing relative timelocks can also return some balance to enable recurring payments. With introspection, PUSH style seemingly automatic functionality is possible by making the contract parameters public for optimistic anyone-can-spend MEV execution.
Updating early timelocking contracts with introspection, it’s possible to create an ecosystem where payments, wills, and vaults seemingly automatically happen on schedule for both tokens and coin value.
Absolute timelock (BIP65) HODL-style vault (with CashToken support).
Many utxos may be spent “in parallel”. No early withdraws!
// Locktime: lock funds until a certain block time.
//
// - Allow spending after a predefined staticblocktime (BIP65).
// - Allow chaining many utxos in parallel, with each utxo contributing to the fee.
//
// L,3,<Locktime>,<receiptLockingBytecode>,<contractBytecode>
//
contract Locktime(
// length of time to lock contract, blocks
int locktime,
// LockingBytecode of the beneficiary, the address receiving payments
bytes recipientLockingBytecode
) {
function execute() {
// Check that time has passed and that time locks are enabled
// OP_CHECKLOCKTIMEVERIFY OP_DROP
require(tx.time >= locktime);
// Check that each output sends to the recipient
// OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY
require(tx.outputs[this.activeInputIndex].lockingBytecode == recipientLockingBytecode);
// Check that each output sends the balance minus an executor allowance
// OP_INPUTINDEX OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE c409 OP_SUB OP_GREATERTHANOREQUAL
require(tx.outputs[this.activeInputIndex].value >= tx.inputs[this.activeInputIndex].value - 2500);
// Require tokens go forward
// OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY
require(tx.outputs[this.activeInputIndex].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
// OP_INPUTINDEX OP_OUTPUTTOKENAMOUNT OP_INPUTINDEX OP_UTXOTOKENAMOUNT OP_NUMEQUALVERIFY
require(tx.outputs[this.activeInputIndex].tokenAmount == tx.inputs[this.activeInputIndex].tokenAmount);
// OP_INPUTINDEX OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUAL
require(tx.outputs[this.activeInputIndex].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);
}
}
Relative timelock (BIP68) deadman-switch or Last Will vault (with CashToken support)
Many utxos may be spent “in parallel”. Refresh is accomplished via NFT baton.
// Will: an automatic "deadman" switch with variable timeout.
//
// - Allow forwarding utxos after a predefined interval (BIP68).
// - Allow chaining many utxos in parallel, with each utxo contributing to the fee.
// W,3,<authCat>,<timeout>,<receiptLockingBytecode>,<contractBytecode>
contract Will(
// Category of the authenticating baton
// The auth baton is managed by the wallet of the user,
// The contract is agnostic of the token paid to the receipt.
bytes32 authCat,
// length of time (blocks) to lock utxos
int timeout,
// LockingBytecode of the beneficiary, the address receiving payments
bytes recipientLockingBytecode
) {
// OP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF
function execute() {
// Check that time has passed and that time locks are enabled
// OP_SWAP OP_CHECKSEQUENCEVERIFY OP_DROP
require(tx.age >= timeout);
// Check that each output sends to the recipient
// OP_INPUTINDEX OP_OUTPUTBYTECODE OP_ROT OP_EQUALVERIFY
require(tx.outputs[this.activeInputIndex].lockingBytecode == recipientLockingBytecode);
// Check that each output sends the balance minus an executor allowance
// OP_INPUTINDEX OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE c409 OP_SUB OP_GREATERTHANOREQUAL
require(tx.outputs[this.activeInputIndex].value >= tx.inputs[this.activeInputIndex].value - 2500);
// Require tokens go forward
// OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY
require(tx.outputs[this.activeInputIndex].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
// OP_INPUTINDEX OP_OUTPUTTOKENAMOUNT OP_INPUTINDEX OP_UTXOTOKENAMOUNT OP_NUMEQUALVERIFY
require(tx.outputs[this.activeInputIndex].tokenAmount == tx.inputs[this.activeInputIndex].tokenAmount);
// OP_INPUTINDEX OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_EQUAL
require(tx.outputs[this.activeInputIndex].nftCommitment == tx.inputs[this.activeInputIndex].nftCommitment);
} // OP_NIP OP_NIP
// Allow refreshing, or withdraw, with the authentication baton
// OP_ELSE OP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY
function refresh() {
// Authentication failed, script fails.
// OP_0 OP_UTXOTOKENCATEGORY OP_SWAP OP_2 OP_CAT OP_EQUAL
require(tx.inputs[0].tokenCategory == authCat + 0x02);
} // OP_NIP OP_NIP OP_ENDIF
}
Subscriptions, Share Vesting, Revocable Token Annuities
A contract for fixed MEV-powered token payments, managed by NFT baton.
Example applications:
- Vestment of shares at regular intervals.
- Monthly or weekly “stable” coin denominated subscriptions.
- Revocable bitcoin annuities via WBCH.
Ownership of the subscription balance is managed with NFT authentication. Subscription ownership transfers with the minting baton.
If a token balance doesn’t permit an installment, the remaining tokens are sent to the intended receipt; the utxo value can be claimed by the executor.
// S,3,<authCat>,<period>,<receiptLockingBytecode>,<installment>,<contractBytecode>
// ...
contract Subscription(
// Category of the authenticating baton
// The auth baton is managed by the wallet of the user,
// The contract is agnostic of the token paid to the receipt.
bytes32 authCat,
// payment interval (blocks)
int period,
// LockingBytecode of the beneficiary, the address receiving payments
bytes recipientLockingBytecode,
// Amount of tokens being vested each period
int installment
) {
// OP_4 OP_PICK OP_0 OP_NUMEQUAL OP_IF
function execute() {
// Require version 2 for BIP68 support
// OP_TXVERSION OP_2 OP_NUMEQUALVERIFY
require(tx.version == 2);
// Require a single utxo input
// OP_TXINPUTCOUNT OP_1 OP_NUMEQUALVERIFY
require(tx.inputs.length == 1);
// Require a rolling timelock is satisfied
// OP_SWAP OP_CHECKSEQUENCEVERIFY OP_DROP
require(tx.age >= period);
// Require minimum token dust
// OP_0 OP_OUTPUTVALUE 2003 OP_NUMEQUALVERIFY
require(tx.outputs[0].value == 800);
// Require payment in the same token
// OP_0 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY
require(tx.outputs[0].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
// Require that each output sends to the intended recipient
// OP_0 OP_OUTPUTBYTECODE OP_ROT OP_EQUALVERIFY
require(tx.outputs[0].lockingBytecode == recipientLockingBytecode);
// If not enough tokens remain to fulfill an installment,
// OP_INPUTINDEX OP_UTXOTOKENAMOUNT OP_2 OP_PICK OP_LESSTHANOREQUAL OP_IF
if(tx.inputs[this.activeInputIndex].tokenAmount <= installment){
// require token liquidation
// OP_0 OP_OUTPUTTOKENAMOUNT OP_INPUTINDEX OP_UTXOTOKENAMOUNT OP_NUMEQUALVERIFY
require(tx.outputs[0].tokenAmount == tx.inputs[this.activeInputIndex].tokenAmount);
// utxo sats are unencumbered in the final installment
}
// OP_ELSE
else{
// Require that installment paid
// OP_0 OP_OUTPUTTOKENAMOUNT OP_2 OP_PICK OP_NUMEQUALVERIFY
require(tx.outputs[0].tokenAmount == installment);
// Require the executor fee is not excessive
// OP_1 OP_OUTPUTVALUE OP_INPUTINDEX OP_UTXOVALUE f00a OP_SUB OP_GREATERTHANOREQUAL OP_VERIFY
require(tx.outputs[1].value >= tx.inputs[this.activeInputIndex].value - 2800);
// Require that the token remainder after installment is returned
// OP_1 OP_OUTPUTTOKENAMOUNT OP_INPUTINDEX OP_UTXOTOKENAMOUNT OP_3 OP_PICK OP_SUB OP_GREATERTHANOREQUAL OP_VERIFY
require(tx.outputs[1].tokenAmount >= tx.inputs[this.activeInputIndex].tokenAmount - installment);
// Require the token category is identical
// OP_1 OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_EQUALVERIFY
require(tx.outputs[1].tokenCategory == tx.inputs[this.activeInputIndex].tokenCategory);
// Require the second output match the active bytecode
// OP_1 OP_OUTPUTBYTECODE OP_INPUTINDEX OP_UTXOBYTECODE OP_EQUALVERIFY
require(tx.outputs[1].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode);
} // OP_ENDIF
} // OP_2DROP OP_DROP
// Withdraw or adjust balances.
// OP_1 OP_ELSE OP_4 OP_ROLL OP_1 OP_NUMEQUALVERIFY
function withdraw() {
// Transactions beginning with a minting auth baton are unrestricted
// OP_0 OP_UTXOTOKENCATEGORY OP_SWAP OP_2 OP_CAT OP_EQUAL
require(tx.inputs[0].tokenCategory == authCat + 0x02);
} // OP_NIP OP_NIP OP_NIP
} // OP_ENDIF