Skip to main content

Lock-To-Mint

The LockToMint contract is a type of contract that locks a specified CAT20 token for a certain period to mint a CAT721 token. The detailed operations of the contract are as follows:

  1. Use the CAT721/deploy feature to deploy the CAT721 token.
  2. Use the LockToMint/getLockToMintLockingScriptHash feature to retrieve the LockToMint contract hash.
  3. Use the CAT721/mint feature to mint a CAT721 token into the LockToMint contract.
  4. Use the LockToMint/lockToMint feature to complete the locking of CAT20 tokens and the minting of CAT721 tokens.
  5. Use the LockToMint/redeemCAT20 feature to retrieve the locked CAT20 tokens back to your wallet after the lock period expires.

Get Lock-To-Mint Contract's Hash

Feature API

/**
*
* @param lockToMintCovenant a {@link LockToMintCovenant}
* @returns Ripemd160
*/
export function getLockToMintLockingScriptHash(
lockToMintCovenant: LockToMintCovenant
): Ripemd160 {
...
}

The function has only one parameter, lockToMintCovenant, which is a Covenant object encapsulating the LockToMint contract. It returns the result of performing a hash160 operation on the lockingScript of the LockToMint contract.

Design

In the diagram, addr: LockToMint is generated using this method.

Dive into details

When initially generating the CAT721 token, you need to retrieve the contract hash of LockToMint and then mint the CAT721 token to the contract hash.

Lock To Mint

Feature API

/**
*
* @param signer a signer, such as {@link DefaultSigner} or {@link UnisatSigner}
* @param cat20Covenant
* @param cat721Covenant a {@link CAT721Covenant}
* @param nftReceiver an address which user receive nft
* @param lockToMintCovenant
* @param utxoProvider a {@link UtxoProvider}
* @param chainProvider a {@link ChainProvider}
* @param cat721Utxo a {@link Cat721Utxo}
* @param cat20Utxo a {@link Cat20Utxo}
* @param feeRate specify the fee rate for constructing transactions
* @param serviceFeeAddress an address which service fee pay to
* @param serviceFee the number of satoshi pay to service
* @returns
*/
export async function lockToMint(
signer: Signer,
cat20Covenant: CAT20Covenant,
cat721Covenant: CAT721Covenant,
nftReceiver: string,
lockToMintCovenant: LockToMintCovenant,
utxoProvider: UtxoProvider,
chainProvider: ChainProvider,
cat721Utxo: Cat721Utxo,
cat20Utxo: Cat20Utxo,
feeRate: number,
serviceFeeAddress: string,
serviceFee: number
) {
...
}

Design

Dive into details

constructor(
cat721Script: ByteString,
cat20Script: ByteString,
lockTokenAmount: int32,
nonce: ByteString,
lockedBlocks: bigint
) {
super(...arguments)
this.cat721Script = cat721Script
this.cat20Script = cat20Script
this.lockTokenAmount = lockTokenAmount
this.nonce = nonce
this.lockedBlocks = lockedBlocks
}
  • cat721Script: The script for the contract that locks the NFT series.
  • cat20Script: The token that users need to pay when unlocking the contract.
  • lockTokenAmount: The amount of tokens users must pay to unlock the contract.
  • nonce: A random value used to construct the time-lock contract, preventing users from pre-building a general-purpose time-lock script to front-run the process.
  • lockedBlocks: The value for relative time-lock, which is effective for 16 bits. If the 23rd bit is set to true, it specifies the time unit. For example:
    • 0x00000017: Locks for 23 blocks.
    • 0x403b54: Locks for 15188 * 512s.

Unlock logic

Check the contract context.

// Check sighash preimage.
assert(
this.checkSig(
SigHashUtils.checkSHPreimage(shPreimage),
SigHashUtils.Gx
),
'preimage check error'
)
// check ctx
SigHashUtils.checkPrevoutsCtx(
prevoutsCtx,
shPreimage.hashPrevouts,
shPreimage.inputIndex
)
SigHashUtils.checkSpentScriptsCtx(
spentScriptsCtx,
shPreimage.hashSpentScripts
)

Check if the 0th input is CAT721, used for authentication.

// ensure input 0 is nft input
assert(spentScriptsCtx[0] == this.cat721Script)

Check if the 1st input is the locked CAT20, used for authentication.

// ensure input 1 is token input
assert(spentScriptsCtx[1] == this.cat20Script)

Verify the lock address of the token on the 1st input, and the provided public key. Perform a checksig to ensure the public key can sign. The public key will later be used as a constructor parameter for the CATTimeLock contract. Once it is confirmed that the user can sign, it indicates that the user will be able to redeem the locked CAT20 after the time lock expires.

// verify cat20 state
const catTx20Txid = TxProof.getTxIdFromPreimg2(cat20Tx)
TxUtil.checkIndex(cat20OutputVal, cat20OutputIndex)
assert(catTx20Txid + cat20OutputIndex == prevoutsCtx.prevouts[1])
// verifyPreStateHash
StateUtils.verifyPreStateHash(
cat20TxStatesInfo,
CAT20Proto.stateHash(cat20State),
cat20Tx.outputScriptList[STATE_OUTPUT_INDEX],
cat20OutputVal
)

// verify pubkey can sig, exec in taproot bvm runtime, schnorr sig
const pubkey = cat20OwnerPubKeyPrefix + cat20OwnerPubkeyX
assert(hash160(pubkey) == cat20State.ownerAddr)
this.checkSig(cat20OwnerPubkeySig, cat20OwnerPubkeyX)

Construct the CATTimeLock contract by using the user’s public key to create a P2WSH (Pay-to-Witness-Script-Hash) CATTimeLock contract.

// build catTimeLock p2wsh
const timeLockScript = LockToMint.buildCatTimeLockP2wsh(
pubkey,
this.nonce,
this.lockedBlocks
)

Since it’s not possible to compute P2TR (Pay-to-Taproot) scripts directly within the contract, the P2WSH (Pay-to-Witness-Script-Hash) method must be used for contract deployment.

Construct a CAT721 output to the recipient.

// nft to user
curStateHashes += hash160(CAT721Proto.stateHash(nftReceiver))
const nftOutput = TxUtil.buildOutput(
this.cat721Script,
contractSatoshis
)

Transfer CAT20 tokens to the time-lock contract.

// token to lock contract address
curStateHashes += hash160(
CAT20Proto.stateHash({
amount: this.lockTokenAmount,
ownerAddr: hash160(timeLockScript),
})
)
const tokenOutput = TxUtil.buildOutput(
this.cat20Script,
contractSatoshis
)

Return the change of CAT20 tokens to the address of the CAT20 input owner.

// if change token amount more than 0, change to user
let tokenChangeOutput = toByteString('')
if (cat20Change > 0n) {
// cat20State
curStateCnt += 1n
curStateHashes += hash160(
CAT20Proto.stateHash({
ownerAddr: cat20State.ownerAddr,
amount: cat20Change,
})
)
tokenChangeOutput = TxUtil.buildOutput(
this.cat20Script,
contractSatoshis
)
}

Generate the time-lock UTXO output.

// time lock output
const catTimeLockOutput = TxUtil.buildOutput(
timeLockScript,
contractSatoshis
)

Generate the fee change output.

// change satoshi
const changeOutput = TxUtil.getChangeOutput(changeInfo)

Redeem CAT20 Token

Feature API

/**
* Redeem CAT20 tokens to user wallet.
* @param signer a signer, such as {@link DefaultSigner} or {@link UnisatSigner}
* @param lockToMintCovenant {@link LockToMintCovenant}
* @param utxoProvider a {@link UtxoProvider}
* @param chainProvider a {@link ChainProvider}
* @param minterAddr the minter address of the CAT20 token
* @param inputTokenUtxos CAT20 token utxos to be sent
* @param receivers the recipient's address and token amount
* @param tokenChangeAddress the address to receive change CAT20 tokens
* @param feeRate the fee rate for constructing transactions
* @returns the guard transaction, the send transaction and the CAT20 token outputs
*/

Design

Dive into details

import {
ByteString,
PubKey,
Sig,
SmartContract,
assert,
method,
prop,
} from 'scrypt-ts'

export class CATTimeLock extends SmartContract {
@prop()
pubkey1: PubKey

@prop()
pubkey2: PubKey

@prop()
nonce: ByteString

@prop()
lockedBlocks: bigint

constructor(
pubkey1: PubKey,
pubkey2: PubKey,
nonce: ByteString,
lockedBlocks: bigint
) {
super(...arguments)
this.pubkey1 = pubkey1
this.pubkey2 = pubkey2
this.nonce = nonce
this.lockedBlocks = lockedBlocks
}

@method()
public unlock(sig: Sig) {
this.csv(this.lockedBlocks)
assert(this.checkMultiSig([sig], [this.pubkey1, this.pubkey2]))
}

@method()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private csv(lockedBlocks: bigint): void {
// ... Gets substituted for OP_CSV w/ inline assembly hook
// TODO: Rm once OP_CSV is added to compiler.
assert(true)
}
}
  • pubkey1: The first possible user public key, in the format 02 + xonly (where 02 indicates the public key is compressed and xonly is the x-coordinate of the elliptic curve point).
  • pubkey2: The second possible user public key, in the format 03 + xonly (where 03 indicates the public key is compressed and xonly is the x-coordinate of the elliptic curve point).
  • nonce: A random value used to construct the time-lock contract, which prevents users from creating a generic time-lock script to front-run the contract.
  • lockedBlocks: The value for the relative time-lock, which is effective for 16 bits. The 23rd bit being set to true indicates the time unit. Examples include:
    • 0x00000017: Locks for 23 blocks.
    • 0x403b54: Locks for 15188 * 512s (15188 blocks multiplied by 512 seconds).

Summary

  • Decentralized Locking: The CAT20 tokens are locked in a contract controlled by the public key of the paying user. All users remain the payers in this system.
  • High-Concurrency Performance: For example, in the case of a 10k collection, if 10k users simultaneously construct and broadcast transactions, all can enter the memory pool at the same time and wait for the transactions to be added to the blockchain. This ensures that the system can handle a large number of concurrent transactions efficiently.