An incentivized scheme for fixed token Future markets

TL;DR

There are two groups of users: A) those who want more coins, B) those who want a higher price (less BCH liquidity). Together they can create a market where they work together toward their individual goals.


Introduction

With the advent of CashTokens, it is now possible to control and track tokens from smart contracts such as the Wrapped BCH contract by @dagurval .

Similar to a wBCH token, creating timelocked futures tokens would also be trivial by modifying the wrapOrUnwrap() function of WrapBCH into placeOrRedeem() to control Bitcoin Cash Futures fungible tokens, locked until some blocktime.

A Futures issuer could send a supply of 21M fungible tokens each representing one coin locked until some block height in the future to the above contract and let anyone encumber coins by swapping them into the contract in return for a tradable token―redeemable at a later date.


A complicated token placement to market loop

It stands to reason, from an intuitive market perspective, that beyond the initial novelty, a Future should eventually sell at a discount to a normal liquid coin―it being less useful immediately. So the person who encumbered the coin will immediately have created a token that will probably then sell for less than what was input. The coin locker might always expect to lose money in the locking process itself.

From the buyer’s perspective however, they’d essentially be buying BCH at a discount; there’s financial incentive for a buyer to buy futures below the face value of the locked coin.

However, the locker that releases tokens from the FutureBCH holding contract however would have a real asset at some price, so they could presumably receive some proceeds from the sale.

A lone locker, or the general practice locking coins to release the Futures, could create a liquid market where the price of a Future token started out below 100%, maybe 90% face value, to gradually increase reaching parity when the locktime was reached.

So the locker could loop some liquidity repeatedly in a geometric series where the sum of the amount locked would be the proportional to one minus the discount.

The amount that could be locked given a sacrificial amount at some market price would be a the sum of a geometric series:

image

image

So a locker with 1 coin, selling Futures at a rate of 0.9 BCH per futureBCH could cause a total of 10 coins to be locked.

A locker with 1 coin able to sell futures at a price of 0.95 BCH per futureBCH could cause 20 coins to be locked… etc.

At 0.99 BCH per futureBCH, one BCH could incentivize locking of 100 BCH. So although it’s literally throwing away coins on leverage, the effect on market liquidity could have an outsized impact if the discount was thin (if buyers were willing to buy futureBCH very near the face value of the underlying coin).

The net result of the extreme case is that the locker paid the buyer a coupon of 1 BCH to lock 100 BCH over the course of hundreds of trades through loops. If the net result is that someone will get paid a coupon for locking a large amount of coins, it’s possible to skip the looping of a geometric series, and the market and just create contracts holding come coupon funds where holders could harvest the coupon incentives at various rates.


So to place BCH and release Futures tokens, there is unnecessary complexity in the amount of “looping” involved, and the margins on the tokens would have to be very tight (+~98%) making trading the Futures on current dexes a non-starter because the fees (~0.3%) should be several hundred sats, or in the thousands when the futures near maturity or the holder will lose their incentive to trade such a narrowly priced asset.


A simplified approach; Skipping the dex and discount loop

If the party incentivizing the creation of the encumbered futures can come to terms with the fact they’re essentially just paying people to save, then the whole rigmarole, the looping, as well as a necessary stop on the open market can be skipped, and Alice can just go take a coupon in one transaction with one tiny transaction fee:

Input Output
FutureBCH(900000).function.placeOrRedeem(false) 1 FutureBCH => 1 BCH
aliceTemplate.unlockP2PKH() 99.8M sats => 1 FutureBCH
Coupon(900000).function.apply() 200k sats -

One really simple and extremely flexible way to specify an arbitrary coupon down to the millionth of a percent would be to just have the FutureCoupon contract only spend a single output and restrict locking to whole coins.

So if someone wanted to offer a coupon of 100k sat for locking 100M sats, they could send a utxo (or stream of them) at the Coupon contract, and they’d eventually get harvested as maturity date for the FutureContract approached.

For a CashScript 0.10.0-next generated Bitauth dubugging URL authenticaion template click HERE

In CashScript, using BIP65’s OP_CHECKLOCKTIMEVERIFY for a fixed locktime in blocks.

pragma cashscript ^0.10.0;

// Future BCH fungible token vault
//
//      Inputs: 00-covenant
//      Outputs: 00-covenant
//
//     [WIP] 2024-02-19 
//
//      enforcing token Category prevents coupons from being 
//        claimed on from random tokens sent to contract
// 

contract FutureBCH(int locktime, bytes tokenCategory) {

    function placeOrRedeem(bool isRedeem) {

        //  Flow
        //  00 contract    ->  00 contract
        //  01 userPkh     =>  01 userPkh
        //  02 coupons     ^

        // enforce BIP65 timelocks and the direction of the swap 
        if(isRedeem){
          // tokens may be redeemed in any amount after the future has matured
          require(tx.time >= locktime);
          require(tx.inputs[1].tokenCategory == tokenCategory);
        } else{
          // otherwise, only whole coins may be "placed" or locked
          // And the token id must match a pre-configured token
          require(tx.inputs[0].tokenAmount == 100000000);
          require(tx.inputs[0].tokenCategory == tokenCategory);
          require(tx.inputs[1].value == 100000000);
          require(tx.outputs[1].tokenCategory == tokenCategory);
        }

        // Enforce that this contract lives on
        require(
          tx.outputs[this.activeInputIndex].lockingBytecode 
          == 
          tx.inputs[this.activeInputIndex].lockingBytecode, 
          "locking bytecode index mismatch"
          );

        require(
          tx.inputs[this.activeInputIndex].tokenAmount + 
          tx.inputs[this.activeInputIndex].value 
          == 
          tx.outputs[this.activeInputIndex].tokenAmount + 
          tx.outputs[this.activeInputIndex].value,
         "summation mismatch"
         );
    }
}

A coupon contract isn’t necessarily specific to being used for futures. To restrict spending of a utxo toward a single contract seems straight-forward:

pragma cashscript ^0.10.0;

// Restrict output of any utxo to a preconfigured address

contract Coupon(
  // Locking bytecode the coupon will be applied to
  bytes destinationLockingBytecode
){
  function apply() {
    // Require all utxos be spent atomically
    require(tx.inputs.length == 1);
    require(tx.outputs.length == 1);

    // assure at the entire amount minus a transaction fee
    // goes to the intended output 
    int appliedAmount = tx.inputs[0].value - 1500;
    require(tx.outputs[0].value >= appliedAmount);

    // Check that the first output 
    // sends to the intended recipient. 
    require(
      tx.outputs[0].lockingBytecode 
      == destinationLockingBytecode
    );
  }
}

Given the bitcoin reward schedule, it might make sense to align contract expiration with intervals pertaining to block rewards. i.e. 210,000 cycle multi-year periods, 1000 block increments for weeklies, etc.


Market Example

Bob sends a small output as a coupon for Alice to place coins in a timelock. The coupon acts as a standing irrevocable bid in an open ended auction terminating at contract expiration. Once Alice places her coin in the vault, it may only be withdrawn after expiration for a corresponding amount of tokens. However, if Alice wants liquidity in the meantime, she may then sell her dated futures tokens on the open market.

Bob may send 1000 sats toward a coupon to lock 100M sats for 1000 blocks (1 week). This is not incentive for Alice to open a wallet or move coins, so the coupon is left unused.

Bob, frustrated and impatient, might dispense an array of UTXOs to the coupon contract in increments of 10k (20k, 30k, … 200k) until Alice decides that 0.2M sats is sufficient incentive to place 100M sats for one week. The process of locking is easy enough, she takes the 190k sat coupon too.

BANG! Price discovery. Alice has decided with the help of Bob, that the prevailing spot rate of interest on Bitcoin Cash futures will be 0.19% on the week, or an annualized rate of 10.374%.

The following day, with 900 blocks until expiration, the 180k and 170k coupons are exceed the 0.19% rate, and Charlie locks two coins taking those coupons.

With this simple standing auction, Alice and Charlie locked a total of 400M sats for a nominal interest rate of 10% APR, and Bob has cause 400M coins to become locked until payday for the low low price of 2.1M sats.

It is conceivable, if the week continues, that new buyers may collect the smaller coupons closer to expiration and Bob may cause 21 BCH to be locked with his 2.1M sats by the end of the week. If not, unclaimed coupons may be collected after the contract lock time has been exceeded.


This is an adaptation of some thoughts of how to do futures (w/o tokens)… @emergent_reasons.

Anyway, I’ve tried to go through the potential market dynamics and game theory. I think the quickest way to deep liquidity with these assets is to skip the DEX and load the incentive explicitly as a coupon.

3 Likes

Coupon incentive failure and recovery with standing irrevocable bids.

The coupon approach leads to a novel effect as the time to redemption approaches.

Presume Bob, or the coupon issuer, is doing a one dollar lulz to incentivize Alice to lock a whole coin. Once a coupon issuer sends an output to the coupon contract, it cannot be undone or retracted. A coupon that was too low cannot be revised upward, the coupon incentive auction fails.

However, since the number of blocks to expiration is decreasing, the effective rate of return on every coupon is increasing inversely proportional to the number of blocks remaining. Eventually, the incentive to lock a coin is simply the coupon over one block, then, after the specified blocktime, there is no timelock necessary and the coupon is entirely MEV or anyone-can-spend―as long as it passes through the Vault contract.

For example, there are 100M sats in a coin, and about 53k block in a year―so a annualized rate of 1% translates to a coupon rate of about 19 sats per coin per block. Suppose the market price of one coin is $300, and one dollar is about 330k sats.

If Bob were to issue a coupon for a contract expiring a year from now, a coupon offering 0.3% return may not incentivize Alice (or anyone) to lock a coin. Or another way to put it, a coupon with a rate of 6.3 sats per coin per block may be too low to incentivize the market.

However, eight months on, assuming no one claimed it, Bob’s 330k coupon would still exist as a “coupon” for futures redeemable in about four months. The rate of return on the same “coupon” has tripled to about 19 sats per coin per block and it’s headed northward. Two months, a week, one day prior to expiration, the rate would be 38, 330 and 12.3k sats per coin per block, respectively. One block prior to expiration, the “coupon no one wanted” would pay 330k sats for someone to lock one BCH for a single block.

Throughout the irrevocable bid lulz process, Bob never has to interact or do anything to incrementally boost the rate of return on his coupon bid. Once the coupon is placed, it becomes a matter of when, rather than if, it will be claimed.

1 Like

This is a super fantastic idea. I would use it on both sides probably.

I’m happy to donate a few sats to incentivise rising BCH TVL, and incentivise people to learn & practice self custody.

I’d also love to get a yield just for locking coins that are going to sit anyway.

What kind of UI is going to come with the Flipstarter proposal? Hopefully something snazzy, the idea is already great but if it has a nice UI and a cool progress bar showing TVL rising and stuff I’ll be super impressed.

Also, this will be a killer app to get onto DeFi Llama tracking once it is ready.

1 Like

There’s a couple different roles, and the UI kind of starts at the contract level.

For degens:

People are going to open some wallet connected webapp and be presented with a table of free money. With some potentially stupid rates at the outset of the app.

They’ll be able to toggle between their preferred rate display method (APR, compounded APR*, sats per block etc.) Degens will be able to pick coupons up manually, or set a rate to collect any coupon at a certain threshold.

Once we get a degen or two into auto-degen couponing, they’re going to start looking at the rate history chart and realize there are other people more or less degen than them, and they can work out together what the market rate should be for coupon takers.

This is just a data table or a list, sorted by rate with some warnings to keep them from locking their coins a crazy amount of time accidentally. Every contract expiration will be shown with the blocktime, and the estimated calendar date of maturation.

They’re going to get something like a yield curve chart and table to stare at, with the latest rate for each expiration in one place.

For coupon writers:

Coupon writers drop sats onto a separate contract to incentivze locking a specific amount in a specific future. They’re spending pennies, but they’re important people. And it has to be clear how much they’re spending and how that relates to the current rate.

Currently, I’m taking the discrete value constraint out of the Vault and moving it to the Coupon. With that UI, I think people can write coupons for different placement amounts (0.1 BCH, 1, 10, 100, etc) all for the same Future contract. So that will make the coupon writing a bit more interesting.

Since every coupon contract is identical except for the date and the placement amount, we don’t need to have any sort of record keeping for the parameters of the coupons.

Since the return on coupons gradually increases over time, it’s really cheap to just lowball everything and let the rate of return double as the time halves. Whales are busy. So they might want batch actions to:

  • write 10 or 100 coupons at a particular date,
  • write at a certain rate across a range of dates,
  • or write a range of rates for one date.

We have to see what tools whales want. But they probably want a bill to review confirmation with fiat amounts.

For Futures Set writers:

Futures set writers have to decide which dates contracts are set to (blocks). And assure that the 21M supply of an FT is funded to the contract, and that it matches the tokenId for the contract.

@bitcoincashautist came up with a clever way to commission tokens from a blueprint contract. Ideally, it would be as simple as anyone calling a function that would commission the contract for the next period and fund the FTs “automatically”.

I’d like writing a new futures set to be as easy as someone seeing the futures only go up to block 1,050,000 and clicking ‘next set’ commission the set for block 1,060,000, but I’m not sure if that’s possible.

There’s legal and security issues that will go away forever if it’s just all on chain. If that’s possible, since only one or two people would need to write futures, that’d be a no UI UI. i.e. A cli and a cron job to “click” next. With a backup “next set” button on the webapp.

Part of the utility of the futures the standardness and regularity. Designers have to consider liquidity offerings would have to create a healthy market.

The sets have to get sparser as they get further out. A set might mature every 10k blocks, and another parallel set might be going every 1000 blocks “weeklies”. So if someone wanted to hodl til the next halving, the 10k chain might be up to block 1,050k, but weeklies wouldn’t need to be generated that high up for another three years.

Ideally, someone competent who understood the staging would start one of these machine sets (1k/10k) and not have to ever do set writing again. If everyone is just using the same dates, it will concentrate liquidity.

1 Like

As an update, below is the draft final Vault, whittled down to warpped BCH with an extra timelock condition if placing tokens back.

pragma cashscript ^0.10.0;

// Vault - Store coins locked for tokens until maturation date. 
//
//  inputs              outputs
//  [0] contract    ->  [0] contract
//  [1] userPkh     =>  [1] userPkh
//  [2] coupon?     -^
//

contract Vault(int locktime) {

    function swap() {
        
        // If tokens are flowing back into this contract
        if(tx.outputs[this.activeInputIndex].tokenAmount > tx.inputs[this.activeInputIndex].tokenAmount){
        // OP_INPUTINDEX OP_OUTPUTTOKENAMOUNT OP_INPUTINDEX OP_UTXOTOKENAMOUNT OP_GREATERTHAN OP_IF 

            // Enforce a BIP65 timelock 
            // Note, intended for use with block height based locks 
            // (where:  locktime < 500M).
            require(tx.time >= locktime);
            // OP_0 OP_PICK OP_CHECKLOCKTIMEVERIFY OP_DROP

        } // OP_ENDIF 
         
        // 
        // Inspired by wrapped.cash c. Nov 2023
        // Author: Dagur Valberg Johannsson <dagurval@pvv.ntnu.no> 
        // License: MIT
        //
        // ensure the token in and out matches
        require(
          tx.inputs[this.activeInputIndex].tokenCategory 
          == 
          tx.outputs[this.activeInputIndex].tokenCategory
          );
          // OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_EQUAL OP_VERIFY 

        // Enforce that this contract lives on
        require(
          tx.outputs[this.activeInputIndex].lockingBytecode 
          == 
          tx.inputs[this.activeInputIndex].lockingBytecode
          );
          // OP_INPUTINDEX OP_OUTPUTBYTECODE OP_INPUTINDEX OP_UTXOBYTECODE OP_EQUAL OP_VERIFY

        // ensure the sum of sats and tokens in 
        // matches the sum of sats and tokens out.
        require(
          tx.inputs[this.activeInputIndex].tokenAmount + 
          tx.inputs[this.activeInputIndex].value 
          == 
          tx.outputs[this.activeInputIndex].tokenAmount + 
          tx.outputs[this.activeInputIndex].value
         );
         // OP_INPUTINDEX OP_UTXOTOKENAMOUNT OP_INPUTINDEX OP_UTXOVALUE OP_ADD
         // OP_INPUTINDEX OP_OUTPUTTOKENAMOUNT OP_INPUTINDEX OP_OUTPUTVALUE OP_ADD 
         // OP_NUMEQUAL
         // OP_NIP
    }

}

As an incentive to lock coins in the vault, below is a generic coupon, which is not token or application specific. When used with the above vault, it could incentivize someone locking BCH for a fungible token, either the official tokens, or any other fungible token someone wanted to lock in the timed vault on a 1:1 basis.

//
// Coupon - apply* utxo coupons by spending at least <amount> on <lock>
//
contract Coupon(
  // Minimum spent (sats) to claim each coupon utxo.
  int amount,
  
  // Contract holding the logic.
  bytes lock
){

  function apply() {
    
    // assure at the minium amount is sent to the intended contract
    // OP_0 OP_OUTPUTVALUE OP_0 OP_UTXOVALUE OP_SUB OP_1 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY
    require((tx.outputs[0].value - tx.inputs[0].value) >= amount);


    // Check that the Coupon is interacting with an existing Vault instance 
    // OP_0 OP_UTXOBYTECODE OP_1 OP_ROLL OP_EQUAL OP_VERIFY
    require(tx.inputs[0].lockingBytecode == lock);

    // The coupon must be spent as the last input, 
    //   therefore only coupon may be spent at a time.
    // OP_INPUTINDEX OP_1 OP_ADD OP_TXINPUTCOUNT OP_NUMEQUAL
    require(this.activeInputIndex+1 == tx.inputs.length);
  }

}

Vaults can be positioned and funded automatically with a “Gantry” contract:

// Gantry - Create vault contracts with fungible tokens in a uniform way. 
//
// 2024-06-05
//
// From: Future Bitcoin Cash
//
// Author: 2qx <2qx_in_the_future@small.neomailbox.ch>
//
// NFT commentment stores the next series locktime in 32-bit LE
//
// [ ] Require the minting baton in the input
// [ ] Get the current step increment for the chain of futures
// [ ] Get the current vault locktime to be printed.
//
//   either
// [ ] Mint an array of FT utxos, 
// [ ] send them off to a Vault
//
//   or
// [ ] skip every 10th print.
//
// [ ] increment locktime height value stored on NFT baton
// [ ] assure NFT baton is returned
//
//
//  Gantry i/o Flow:
//
//  Inputs              Outputs
//  [0] NFT mintBaton   ->  [0] NFT mintBaton
//  [1] topup sats?     =>  [1] FTs Vault
//                      =>  [2] FTs Vault
//                      =>  [3] FTs Vault
//                      =>  [4] FTs Vault
//                      =>  [5] FTs Vault
//                      =>  [6] FTs Vault
//                      =>  [7] FTs Vault
//                          [8] OP_RETURN SMP0 1000 FBCH <locktime> 08
//  
//  ... but skip every 10th token print, 
//   which will be printed by the gantry of the next order.
//  [0] NFT mintBaton   =>  [0] NFT mintBaton
//


contract Gantry(
    int step, 
    bytes vaultUnlockingBytecode
    ) {

    function execute() {

        // Gantry covenant and the associated NFT baton must be spent as index 0
        // input and passed on to index 0 output, funded with some dust BCH in order
        // to avoid griefing by someone with access to hashrate
        require(this.activeInputIndex == 0);
        require(tx.inputs[this.activeInputIndex].lockingBytecode ==
            tx.outputs[this.activeInputIndex].lockingBytecode);
        require(tx.inputs[this.activeInputIndex].tokenCategory ==
            tx.outputs[this.activeInputIndex].tokenCategory);
        require(tx.outputs[this.activeInputIndex].value > 800);

        int locktime = int(tx.inputs[this.activeInputIndex].nftCommitment);

        // Locktime stored in mutable NFT commitment MUST be incremented by <step>
        // and stored as bytes4 LE uint again.
        require(tx.outputs[this.activeInputIndex].nftCommitment ==
            bytes4(locktime + step));

        // Every 10th step, skip creating Vault and just increment the commitment
        if((locktime / step) % 10 == 0) { 
            require(tx.outputs.length == 1);
        } else {
            // Construct redeem bytecode for the Vault instance being created
            bytes theVault = 
                bytes(bytes(locktime).length) + bytes(locktime) + // int locktime
                vaultUnlockingBytecode;
            // Construct P2SH32 locking bytecode from redeem bytecode
            bytes vaultLockingBytecode = 0xaa20 + hash256(theVault) + 0x87;

            // Verify creation of Vault genesis outputs
            require(tx.outputs[1].lockingBytecode == vaultLockingBytecode);
            require(tx.outputs[1].tokenCategory == tx.inputs[0].outpointTransactionHash);
            require(tx.outputs[1].nftCommitment == 0x);
            require(tx.outputs[1].tokenAmount == 2100000000000000);
            require(tx.outputs[1].value == 1000);

            require(tx.outputs[2].lockingBytecode == vaultLockingBytecode);       
            require(tx.outputs[2].tokenCategory == tx.inputs[0].outpointTransactionHash);
            require(tx.outputs[2].nftCommitment == 0x);
            require(tx.outputs[2].tokenAmount == tx.outputs[1].tokenAmount);     
            require(tx.outputs[2].value == 1000);

            require(tx.outputs[3].lockingBytecode == vaultLockingBytecode);       
            require(tx.outputs[3].tokenCategory == tx.inputs[0].outpointTransactionHash);
            require(tx.outputs[3].nftCommitment == 0x);
            require(tx.outputs[3].tokenAmount == tx.outputs[1].tokenAmount);     
            require(tx.outputs[3].value == 1000);

            require(tx.outputs[4].lockingBytecode == vaultLockingBytecode);       
            require(tx.outputs[4].tokenCategory == tx.inputs[0].outpointTransactionHash);
            require(tx.outputs[4].nftCommitment == 0x);
            require(tx.outputs[4].tokenAmount == tx.outputs[1].tokenAmount);     
            require(tx.outputs[4].value == 1000);

            require(tx.outputs[5].lockingBytecode == vaultLockingBytecode);       
            require(tx.outputs[5].tokenCategory == tx.inputs[0].outpointTransactionHash);
            require(tx.outputs[5].nftCommitment == 0x);
            require(tx.outputs[5].tokenAmount == tx.outputs[1].tokenAmount);     
            require(tx.outputs[5].value == 1000);

            require(tx.outputs[6].lockingBytecode == vaultLockingBytecode);       
            require(tx.outputs[6].tokenCategory == tx.inputs[0].outpointTransactionHash);
            require(tx.outputs[6].nftCommitment == 0x);
            require(tx.outputs[6].tokenAmount == tx.outputs[1].tokenAmount);     
            require(tx.outputs[6].value == 1000);

            require(tx.outputs[7].lockingBytecode == vaultLockingBytecode);       
            require(tx.outputs[7].tokenCategory == tx.inputs[0].outpointTransactionHash);
            require(tx.outputs[7].nftCommitment == 0x);
            require(tx.outputs[7].tokenAmount == tx.outputs[1].tokenAmount);     
            require(tx.outputs[7].value == 1000);

            require(tx.outputs[8].lockingBytecode == vaultLockingBytecode);       
            require(tx.outputs[8].tokenCategory == tx.inputs[0].outpointTransactionHash);
            require(tx.outputs[8].nftCommitment == 0x);
            require(tx.outputs[8].tokenAmount == tx.outputs[1].tokenAmount);     
            require(tx.outputs[8].value == 1000);

            // Tag FT metadata for indexers 
            //
            // 6a              OP_RETURN
            // 04 53 4d 50 30  SMP0
            // 02 10 00        Genesis/ticker; from input 0
            // 04 46 42 43 48  FBCH
            // 03 90 05 10     <locktime>
            // 01 08           Decimal seperator
            bytes announcement =  0x6a04534d50300210000446424348 +
                                  bytes(bytes(locktime).length) +  bytes(locktime) +
                                  0x0108;
            require(tx.outputs[9].lockingBytecode == announcement);
            require(tx.outputs[9].tokenCategory == 0x);
            require(tx.outputs[9].value == 0);

            // Ensure no other outputs can be created
            require(tx.outputs.length == 10);  

        }     
        
    }
}

Gantries to position Vaults can be deployed with a battery of gratuitous engineering:

pragma cashscript ^0.10.0;

// Battery - Spawn an array of vault deploying gantries from a single utxo.
//
// 2024-06-05
//
// From: Future Bitcoin Cash
//
// Author: 2qx <2qx_in_the_future@small.neomailbox.ch>
//
// A Battery releases a series of Gantries at small powers of 10 that 
// go on to create Futures Vaults on those respective intervals.
//
// Given a minting NFT with the commitment containing a power of 10, 
// mint a sequence of NFTs with minting capability
// sending mutable batons NFTs to the corresponting Gantry.
//
//  execute():
//
//  inputs                           outputs
//  [0] Battery + NFT 0x40420F00 10ᴇ6 ->  [0] Battery    + NFT  0xA0860100
//                                    =>  [1] Gantry10ᴇ6 + NFT* <startTime>
//
//  [0] Battery + NFT 0xA0860100 10ᴇ5 ->  [0] Battery    + NFT  0x10270000
//                                    =>  [1] Gantry10ᴇ5 + NFT* <startTime>
//
//  ... 0x10270000 10ᴇ4 ... 0xE8030000 10ᴇ3 ... 0x64000000 10ᴇ2
//
//  [0] Battery + NFT 0x<end>        ->  [0]  Burn NFT, sats are unencumbered.
//                                       [1]  Gantry10ᴇ2 + NFT* <startTime>
//
// 

contract Battery(

    // Correct contract initialization will have minting NFT's commitment
    // set to <step> from the NFT commitment, which will be the step set for 1st minted gantry,
    // and will then get decremented for the next one until end step is reached.

    // The end is the smallest power of 10 to create a Gantry for.
    int endStep,

    // Base time from which to calculate each Gantry's starting point, e.g.:
    // --| baseTime
    //   |------------------------------------| gantry 0 start
    //   |------------| gantry 1 start
    //   |----| gantry 2 start
    int baseTime,

    // Redeem bytecode tail of the gantry contracts
    bytes gantryReedemBytecodeTail,

    // Redeem bytecode tail of the vault contracts
    bytes vaultReedemBytecodeTail,

) {

    function execute() {

        // Get the current step, we will mint a Gantry for this step
        bytes stepBytes = tx.inputs[this.activeInputIndex].nftCommitment;
        int step = int(stepBytes);

        // Set the gantry's starting time at correct offset from baseTime
        bytes4 gantryStart = bytes4(baseTime - (baseTime % step) + step);
        require(tx.outputs[1].nftCommitment == gantryStart);

        // Construct the full redeem bytecode for the Gantry instance
        bytes gantryRedeemBytecode =
            bytes(vaultReedemBytecodeTail.length) + vaultReedemBytecodeTail +
            bytes(bytes(step).length)             + bytes(step)   + 
            gantryReedemBytecodeTail;

        require(
            // The second output must have the P2SH32 of the gantry redeem bytecode
            0xaa20 + hash256(gantryRedeemBytecode) + 0x87
            == tx.outputs[1].lockingBytecode
        );

        // Ensure that Gantry inherits a mutable NFT so that it may update the
        // commitment as it mints its Vaults.
        bytes gantryCategory =
            tx.inputs[this.activeInputIndex].tokenCategory.split(32)[0] +
            0x01;
        require(tx.outputs[1].tokenCategory == gantryCategory);

        // Exactly 2 outputs, so token state or BCH can't leak out.
        require(tx.outputs.length == 2);

        // Enforce exact dust amount on the Gantry so that remainder must go
        // back into Battery or pure BCH change at index 0.
        require(tx.outputs[1].value == 800);

        // Fee allowance = 1000
        require(tx.outputs[0].value >
            tx.inputs[this.activeInputIndex].value -
            1800);

        if(step > endStep) {
            // Calculate and enforce next baton's step,
            require(tx.outputs[0].nftCommitment == bytes4(step / 10));
            // token category & capability (pass on minting NFT),
            require(tx.outputs[0].tokenCategory ==
                tx.inputs[this.activeInputIndex].tokenCategory);
            // and contract code.
            require(tx.outputs[0].lockingBytecode ==
                tx.inputs[this.activeInputIndex].lockingBytecode);
        } else {
            // Burn the minting baton while allowing any remaining BCH
            // to be extracted to output 0.
            require(tx.outputs[0].tokenCategory == 0x);

            // Note: output 1 still mints a Gantry in this same TX,
            // and it will be the last one to get minted.
        }
    }
}
2 Likes

@bitcoincashautist has completed an audit here:

3 Likes