Getting your Trinity Audio player ready...

This post was first published on Medium. Read Part 1 here, Part 2 here, and Part 3 here.

Control Distribution of BSV-20 Tokens

In our last article, we have shown smart contracts can control transfers of BSV-20 tokens after minting. Today, we demonstrate how to control distribution/issuance of such tokens.

Tickerless mode

BSV-20 introduces a tickerless mode in V2 and takes a different approach from V1.

Deploy
To deploy and mint a token with a supply of 21000000, you inscribe the following JSON (ContentType: application/bsv-20):

{
"p": "bsv-20",
"op": "deploy+mint",
"amt": "21000000",
"dec": "10"
}
view raw deployV2.json hosted with ❤ by GitHub

Note, unlike V1, there is no designated tick field (thus tickerless).

Issue

To issue 10000 tokens above, you create a transfer inscription with the following JSON.

{
"p": "bsv-20",
"op": "transfer",
"id": "3b313338fa0555aebeaf91d8db1ffebd74773c67c8ad5181ff3d3f51e21e0000_1"
"amt": "10000",
}
view raw transferV2.json hosted with ❤ by GitHub

Instead of a ticker, a token is identified by an id field, made of the transaction ID and output index where the token was deployed, formatted as <txid>_<vout>.

Also the first issuance transaction must spend from deployment transaction since the whole supply is minted at once, while minting transaction does not spend from it and they are separate in V1. That means every transaction of a token can traceback to that token’s genesis deployment, and each is in a DAG (Directed Acyclic Graph) rooted at genesis transaction. This allows BSV-20 indexer to scale more efficiently since it does not have to scan the entire blockchain and order minting transactions, to enforce “first is first minting.

For more details on how BSV-20 token V2 works, please read the official documentation.

Fair launch

A notable characteristic of BSV- 20 V1 tokens is fair launch, contrasting with ERC-20 tokens. Specifically, once someone deploys a transaction of the tokens on BSV-20, everyone has the same chance to claim the tokens. Issuers cannot reserve a portion for free, i.e., there is no pre-mine.

If a token’s total supply is minted at once when deployed in V2 tickerless mode, is it possible to maintain fair launch?

The answer is yes. Instead of locking the whole supply in a standard issuer address (P2PKH script) when deployed, we lock it into a smart contract. The smart contract can be called by anyone and any distribution policy can be enforced in it.

issuance transactions
Chain of issuance transactions

In the diagram above, each box represents a token UTXO and stacked UTXOs are in the same transaction. The second transaction spends the UTXO of the first deployment transaction, indicated by the first arrow, and creates two UTXOs:

  • a spawned copy of the same contract in genesis, but with reduced remaining supply
  • newly issued tokens.

The chain of transactions goes on till the whole token supply is issued. Note the contract can be called by anyone.

We list a few policies as examples.

Rate limit

Under this policy, anyone can claim tokens as long as it is more than, say, 5 minutes away from the last claim. Contract is listed below.

export class BSV20Mint extends SmartContract {
// ...
@method()
public mint(dest: Addr, amount: bigint) {
assert(
this.ctx.locktime >= this.lastUpdate + this.timeDelta,
'locktime has not yet expired'
)
// Update last mint timestamp.
this.lastUpdate = this.ctx.locktime
// Check mint amount doesn't exceed maximum.
assert(amount <= this.maxMintAmount, 'mint amount exceeds maximum')
// If first mint, parse token id and store it in a state var
if (this.isFirstMint) {
this.tokenId =
BSV20Mint.txId2Ascii(this.ctx.utxo.outpoint.txid) +
toByteString('_', true) +
BSV20Mint.int2Ascii(this.ctx.utxo.outpoint.outputIndex)
this.isFirstMint = false
}
// Check if tokens still available.
assert(
this.totalSupply - this.alreadyMinted >= amount,
'not enough tokens left to mint'
)
// Update already minted amount.
this.alreadyMinted += amount
let outputs = toByteString('')
if (this.alreadyMinted != this.totalSupply) {
// If there are still tokens left, then
// build state output inscribed with leftover tokens.
const leftover = this.totalSupply - this.alreadyMinted
const transferInscription = BSV20Mint.getTransferInsciption(
this.tokenId,
leftover
)
const stateScript = slice(
this.getStateScript(),
this.prevInscriptionLen
) // Slice prev inscription
outputs += Utils.buildOutput(transferInscription + stateScript, 1n)
// Store next inscription length, so we know how much to slice in the next iteration.
this.prevInscriptionLen = len(transferInscription)
}
// Build P2PKH output to dest paying specified amount of tokens.
const script1 =
BSV20Mint.getTransferInsciption(this.tokenId, amount) +
Utils.buildPublicKeyHashScript(dest)
outputs += Utils.buildOutput(script1, 1n)
// Build change output.
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
}
view raw bsv20Mint.ts hosted with ❤ by GitHub
Contract

Line 6–12 enforce rate limiting. Line 26–30 ensure supply is not exceeded. If yes, Line 38–52 create an output containing the same contract but with updated state: remaining supply. Line 55–58 issue tokens to a destination address.

mini PoW

The policy ensures anyone can claim tokens, as long as she finds a nonce meeting some specified difficulty requirement, as in Bitcoin’s Proof of Work (PoW).

export class Pow20 extends SmartContract {
// ...
@method(SigHash.ANYONECANPAY_ALL)
public mint(
nonce: ByteString,
lock: ByteString,
trailingOutputs: ByteString
) {
if (len(this.id) == 0n) {
this.id =
this.ctx.utxo.outpoint.txid +
int2ByteString(this.ctx.utxo.outpoint.outputIndex, 4n)
}
this.pow = this.validatePOW(nonce)
const reward = this.calculateReward()
this.supply -= reward
let stateOutput = toByteString('')
if (this.supply > 0n) {
stateOutput = this.buildStateOutput(1n)
}
const rewardOutput = Utils.buildOutput(
this.buildInscription(lock, reward),
1n
)
const outputs: ByteString = stateOutput + rewardOutput + trailingOutputs
assert(
hash256(outputs) == this.ctx.hashOutputs,
`invalid outputs hash ${stateOutput} ${rewardOutput} ${trailingOutputs}`
)
}
@method()
calculateReward(): bigint {
let reward = this.reward
if (this.supply < this.reward) {
reward = this.supply
}
return reward
}
@method()
validatePOW(nonce: ByteString): ByteString {
const pow = hash256(this.pow + nonce)
const test = rshift(Utils.fromLEUnsigned(pow), 256n - this.difficulty)
assert(test == 0n, pow + ' invalid pow')
return pow
}
}
view raw miniPow.ts hosted with ❤ by GitHub
Credit: David Case

ICO

A policy can be implemented so that anyone can receive tokens by sending bitcoin to a specific address in a trustless way, similar to an initial coin offering (ICO). In the diagram above, a third output is added for bitcoin payment, which is validated in a contract.

export class BSV20Mint extends SmartContract {
// ...
@method()
public mint(dest: Addr, amount: bigint) {
// If first mint, parse token id and store it in a state var
if (this.isFirstMint) {
this.tokenId =
BSV20Mint.txId2Ascii(this.ctx.utxo.outpoint.txid) +
toByteString('_', true) +
BSV20Mint.int2Ascii(this.ctx.utxo.outpoint.outputIndex)
this.isFirstMint = false
}
// Check if tokens still available.
assert(
this.totalSupply - this.alreadyMinted >= amount,
'not enough tokens left to mint'
)
// Update already minted amount.
this.alreadyMinted += amount
let outputs = toByteString('')
if (this.alreadyMinted != this.totalSupply) {
// If there are still tokens left, then
// build state output inscribed with leftover tokens.
const leftover = this.totalSupply - this.alreadyMinted
const transferInscription = BSV20Mint.getTransferInsciption(
this.tokenId,
leftover
)
const stateScript = slice(
this.getStateScript(),
this.prevInscriptionLen
) // Slice prev inscription
outputs += Utils.buildOutput(transferInscription + stateScript, 1n)
// Store next inscription length, so we know how much to slice in the next iteration.
this.prevInscriptionLen = len(transferInscription)
}
// Build P2PKH output to dest paying specified amount of tokens.
const script1 =
BSV20Mint.getTransferInsciption(this.tokenId, amount) +
Utils.buildPublicKeyHashScript(dest)
outputs += Utils.buildOutput(script1, 1n)
// Calculate total price for minted amount and enforce payment to ICO address.
const priceTotal = this.icoPrice * amount
outputs += Utils.buildPublicKeyHashOutput(this.icoAddr, priceTotal)
// Build change output.
outputs += this.buildChangeOutput()
assert(hash256(outputs) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
}
view raw ico.ts hosted with ❤ by GitHub

Watch The Bitcoin Masterclasses #3 Day 2 – Afternoon Session: Accounting and mapping transaction on-chain

Recommended for you

Singapore turns to quantum solutions in tackling cyber threats
Together with the use of AI, Singapore utilizes quantum solutions to address digital threats; meanwhile, researchers are changing the game...
May 6, 2025
India showcases first ‘Made in India’ AI server
India has announced the launch of its first fully designed AI server, named “Adipoli," which is manufactured by VVDN Technologies.
May 5, 2025
Advertisement
Advertisement
Advertisement