Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0eda8f4
update cashscript and cashc version, change contract names, add decay…
kiok46 Jul 26, 2025
2060c53
remove reverse from auction and factory
kiok46 Jul 27, 2025
af6f3e4
Ensure that the lockingbytecode of the bidders are enforced to be p2pkh
kiok46 Jul 27, 2025
add6e59
complete implementation of dual decay
kiok46 Jul 27, 2025
dbbf6ad
Remove OP_RETURN outputs from auction contract, update readme to have…
kiok46 Jul 28, 2025
5d04016
Add invalid name penalisation in Name contract, fix tld length in fac…
kiok46 Jul 28, 2025
e916b3e
include messages in require statements, and update tests
kiok46 Jul 29, 2025
0a9ad42
add more tests to registry and add a dummy contract artifact
kiok46 Jul 29, 2025
ddb61f2
Add comment about immutable nft commitment check for input and output
kiok46 Jul 29, 2025
eaa0621
Complete major tests for registry
kiok46 Jul 29, 2025
c01eaec
Add more tests for auction
kiok46 Jul 30, 2025
142c18d
Test auction price decay with different values
kiok46 Jul 30, 2025
e03557a
complete all tests for auction and registry
kiok46 Jul 31, 2025
7e653de
add test case for overflow of registration id
kiok46 Jul 31, 2025
9787c35
Complete tests for bid
kiok46 Aug 1, 2025
9db1ea4
complete tests for name enforcer
kiok46 Aug 2, 2025
4134931
Complete tests for conflict resolver and update messaging in the erro…
kiok46 Aug 3, 2025
d8a089f
Add message against require for all contracts
kiok46 Aug 3, 2025
7b2fc93
start ownership guard tests
kiok46 Aug 3, 2025
0921bfc
start tests for accumulator and factory
kiok46 Aug 3, 2025
7705504
restructure tests
kiok46 Aug 4, 2025
8c49f65
move import to unit as well
kiok46 Aug 4, 2025
ce8ba16
Add more factory tests and move startin auction price inside the cont…
kiok46 Aug 6, 2025
b39b3d7
update bid tests and move minBidPercentage to contract
kiok46 Aug 6, 2025
130b367
move a few config from name and ownership guard to inside the contrac…
kiok46 Aug 6, 2025
235ddbc
Add restriction of tokencategory in bid contract's output
kiok46 Aug 6, 2025
7b6f54d
Add pure bch restriction to output of the active index of all contracts
kiok46 Aug 6, 2025
6f87f5a
update cashscript and cashc version, add new tests and update utils
kiok46 Aug 7, 2025
dcdb121
complete factory tests
kiok46 Aug 9, 2025
1e6272d
move contracts form unit and e2e to just under test/contracts
kiok46 Aug 9, 2025
420e3cd
complete use auth tests
kiok46 Aug 9, 2025
74dba4a
complete tests for penalising invalid name inside name contract
kiok46 Aug 9, 2025
104667d
new tests for the penalization on registration conflict in name contr…
kiok46 Aug 10, 2025
4502b9f
remove unnecessary util functions
kiok46 Aug 10, 2025
da1582d
complete test for burn in name contract
kiok46 Aug 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 103 additions & 108 deletions README.md

Large diffs are not rendered by default.

Binary file modified architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 31 additions & 25 deletions contracts/Accumulator.cash
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
pragma cashscript 0.11.2;
pragma cashscript 0.11.4;

contract Accumulator() {
/**
* Once enough auctions have happened, there might come a time when the counterNFT's tokenAmount is not enough.
* Once enough auctions have happened, there will come a time when the counterNFT's tokenAmount is not enough.
* Since the amount would be accumulating in the thread NFTs, this function can be used to transfer them back to the
* Counter NFT to keep the system functioning smoothly.
*
Expand All @@ -21,49 +21,55 @@ contract Accumulator() {
* - Output4: Change BCH
*/
function call(){
require(tx.inputs.length == 5);
require(tx.outputs.length == 5);
require(tx.inputs.length == 5, "Transaction: must have exactly 5 inputs");
require(tx.outputs.length == 5, "Transaction: must have exactly 5 outputs");

// This contract can only be used at input1 and it should return the input1 back to itself.
require(this.activeInputIndex == 1);
require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);
require(this.activeInputIndex == 1, "Input 1: accumulator contract UTXO must be at this index");
require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Input 1: locking bytecode must match output 1");
// Restriction on output category is important as minting NFT is used in this transaction.
require(tx.outputs[this.activeInputIndex].tokenCategory == 0x);
require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, "Output 1: must not have any token category (pure BCH only)");

// This contract can only be used with the 'lockingbytecode' used in the 0th input.
// Note: This contract can be used with any contract that fulfills these conditions, and that is fine
// because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will
// be manipulating their own utxos.
bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);
require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode);

// Enforce input 2 and 3 are from the registry
require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, "Input 2: locking bytecode does not match registry input's locking bytecode");
require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode, "Input 3: locking bytecode does not match registry input's locking bytecode");

require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);
require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode);
// Enforce output 2 and 3 are returning to the registry
require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, "Output 2: locking bytecode does not match registry input's locking bytecode");
require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode, "Output 3: locking bytecode does not match registry input's locking bytecode");

require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory);
require(tx.outputs[3].tokenCategory == tx.inputs[3].tokenCategory);
// Enforce NFT transfer preserves token categories
require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory, "Output 2: token category does not match input 2");
require(tx.outputs[3].tokenCategory == tx.inputs[3].tokenCategory, "Output 3: token category does not match input 3");

// authorizedThreadNFTs are immutable — must match registry input category
bytes registryInputCategory = tx.inputs[0].tokenCategory;
require(tx.inputs[3].tokenCategory == registryInputCategory, "Input 3: token category does not match registry (immutable NFT check)");

// authorizedThreadNFTs are immutable
require(tx.inputs[3].tokenCategory == registryInputCategory);

// Split counter token category and capability
bytes counterCategory, bytes counterCapability = tx.inputs[2].tokenCategory.split(32);
require(counterCategory == registryInputCategory);
require(counterCapability == 0x02); // Minting
require(counterCategory == registryInputCategory, "Input 2: token category prefix does not match registry");
// Minting
require(counterCapability == 0x02, "Input 2: counter capability must be minting capability (0x02)");

// Locking bytecode of the authorized contract is 35 bytes long.
require(tx.inputs[3].nftCommitment.length == 35);
require(tx.inputs[3].nftCommitment.length == 35, "Input 3: NFT commitment length must be 35 bytes (authorized contract locking bytecode)");

// Since the nftCommitment of counterNFT is registrationID so it must not be null
// as the DomainMintingNFT has no nftCommitment nor tokenAmount
require(tx.inputs[2].nftCommitment != 0x);
require(tx.inputs[2].tokenAmount > 0); // Ensure that the counter minting NFT is used.
require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount + tx.inputs[3].tokenAmount);
// as the NameMintingNFT has no nftCommitment nor tokenAmount
require(tx.inputs[2].nftCommitment != 0x, "Input 2: counter NFT must have a non-empty commitment (registration ID)");
// Ensure that the counter minting NFT is used.
require(tx.inputs[2].tokenAmount > 0, "Input 2: counter NFT must have token amount greater than 0");
require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount + tx.inputs[3].tokenAmount, "Output 2: token amount must equal input 2 + input 3 amounts (accumulation)");

// Pure BCH input and output.
require(tx.inputs[4].tokenCategory == 0x);
require(tx.outputs[4].tokenCategory == 0x);
require(tx.inputs[4].tokenCategory == 0x, "Input 4: must be pure BCH (no token category)");
require(tx.outputs[4].tokenCategory == 0x, "Output 4: must be pure BCH (no token category)");
}
}
105 changes: 62 additions & 43 deletions contracts/Auction.cash
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
pragma cashscript 0.11.2;
pragma cashscript 0.11.4;

/**
* @param minStartingBid The minimum starting bid for the auction.
*/
contract Auction(int minStartingBid) {
contract Auction() {
/**
* Starts a new domain registration auction.
* Starts a new name registration auction.
* @param name The name being registered.
*
* The function creates a new auction with:
Expand All @@ -28,73 +25,95 @@ contract Auction(int minStartingBid) {
* - Output1: Input1 back to this contract without any change.
* - Output2: Minting CounterNFT going back to the Registry contract.
* - Output3: auctionNFT to the Registry contract.
* - Output4: OP_RETURN output containing the name.
* - Output5: Optional change in BCH.
* - Output4: Optional change in BCH.
*/
function call(bytes name) {
require(tx.inputs.length == 4);
require(tx.outputs.length <= 6);
require(tx.inputs.length == 4, "Transaction: must have exactly 4 inputs");
require(tx.outputs.length <= 5, "Transaction: must have at most 5 outputs");

// This contract can only be used at input1 and it should return the input1 back to itself.
require(this.activeInputIndex == 1);
require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);
// Ensure that the domainCategory in not minted here.
require(tx.outputs[this.activeInputIndex].tokenCategory == 0x);
require(this.activeInputIndex == 1, "Input 1: auction contract UTXO must be at this index");
require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Input 1: locking bytecode must match output 1");
// Ensure that no tokenCategory is minted here.
require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, "Output 1: must not have any token category (pure BCH only)");

// This contract can only be used with the 'lockingbytecode' used in the 0th input.
// Note: This contract can be used with any contract that fulfills these conditions, and that is fine
// because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will
// be manipulating their own utxos.
bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);
require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);
require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode);
require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, "Input 2: locking bytecode does not match registry input's locking bytecode");
require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, "Output 2: locking bytecode does not match registry input's locking bytecode");
require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode, "Output 3: locking bytecode does not match registry input's locking bytecode");

// Registration ID increases by 1 with each transaction.
int prevRegistrationId = int(tx.inputs[2].nftCommitment.reverse());
int nextRegistrationId = int(tx.outputs[2].nftCommitment.reverse());
require(nextRegistrationId == prevRegistrationId + 1);
int prevRegistrationId = int(tx.inputs[2].nftCommitment);
int currentRegistrationId = int(tx.outputs[2].nftCommitment);
require(currentRegistrationId == prevRegistrationId + 1, "Output 2: registration ID must increase by 1");

// Reduce the tokenAmount in the counterNFT as some amount is going to auctionNFT
require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount - nextRegistrationId);
require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount - currentRegistrationId, "Output 2: counter NFT token amount must decrease by currentRegistrationId");
// tokenAmount in the auctionNFT is the registrationId.
require(tx.outputs[3].tokenAmount == nextRegistrationId);
require(tx.outputs[3].tokenAmount == currentRegistrationId, "Output 3: auction NFT token amount must equal currentRegistrationId");

// Every auction begins with a min base value of at least minStartingBid satoshis.
require(tx.outputs[3].value >= minStartingBid);
// Dual Decay mechanism, auction price decays linearly with the step.
// To facilitate higher precisions and since decimals do not exist in VM, we multiply
// it by 1e6 (1000000) and call the units as points.

// TODO: make this 1000000 (0.01 BCH)
int constant minStartingBid = 10000;
// 1. Decay points (0.0003% per step)
int decayPoints = minStartingBid * currentRegistrationId * 3;
// 2. Get auction price points
int currentPricePoints = minStartingBid * 1e6;
// 3. Subtract price points by decay points to get the current auction price.
int currentAuctionPrice = (currentPricePoints - decayPoints) / 1e6;

// Set the minimum auction price to 20000 satoshis.
currentAuctionPrice = max(currentAuctionPrice, 20000);

// Every auction begins with a min base value of at least currentAuctionPrice satoshis.
require(tx.outputs[3].value >= currentAuctionPrice, "Output 3: auction price must be at least minimum calculated price");
// Funding UTXO/ Bid UTXO
require(tx.inputs[3].tokenCategory == 0x);
require(tx.inputs[3].tokenCategory == 0x, "Input 3: funding UTXO must be pure BCH");

// Ensure that the funding happens from a P2PKH UTXO.
require(tx.inputs[3].lockingBytecode.length == 25);
// Ensure that the funding happens from a P2PKH UTXO because there will be no way to know the locking bytecode as
// name can be of any length.
require(tx.inputs[3].lockingBytecode.length == 25, "Input 3: locking bytecode must be 25 bytes (P2PKH)");

// Extract the PKH from the lockingBytecode of the Funding UTXO.
// <pkh> + name > 20 bytes
bytes pkh = tx.inputs[3].lockingBytecode.split(3)[1].split(20)[0];
require(tx.outputs[3].nftCommitment == pkh + name);
bytes pkhLockingBytecodeHead, bytes pkhLockingBytecodeBody = tx.inputs[3].lockingBytecode.split(3);
// OP_DUP OP_HASH160 Push 20-byte
require(pkhLockingBytecodeHead == 0x76a914, "Input 3: locking bytecode must start with OP_DUP OP_HASH160 (0x76a914)");
bytes pkh, bytes pkhLockingBytecodeTail = pkhLockingBytecodeBody.split(20);
// OP_EQUALVERIFY OP_CHECKSIG
require(pkhLockingBytecodeTail == 0x88ac, "Input 3: locking bytecode must end with OP_EQUALVERIFY OP_CHECKSIG (0x88ac)");
require(tx.outputs[3].nftCommitment == pkh + name, "Output 3: NFT commitment must match bidder PKH + name");

// Ensure that the name is not too long, as of 2025 upgrade, the nftcommitment is 40 bytes.
// 20 bytes pkh + 16 bytes name + 4 bytes TLD
require(name.length <= 16, "Name: length must be at most 16 characters");

// CounterNFT should keep the same category and capability.
require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory);
require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory, "Output 2: counter NFT token category must match input 2");

// All the token categories in the transaction should be the same.
bytes registryInputCategory = tx.inputs[0].tokenCategory;

// CounterNFT should be minting and of the 'domainCategory' i.e registryInputCategory
// CounterNFT should be minting and of the 'nameCategory' i.e registryInputCategory
bytes counterCategory, bytes counterCapability = tx.outputs[2].tokenCategory.split(32);
require(counterCategory == registryInputCategory);
require(counterCapability == 0x02); // Minting
require(counterCategory == registryInputCategory, "Output 2: counter NFT token category prefix must match registry");
// Minting
require(counterCapability == 0x02, "Output 2: counter NFT capability must be minting (0x02)");

// AuctionNFT should be mutable and of the 'domainCategory' i.e registryInputCategory
// AuctionNFT should be mutable and of the 'nameCategory' i.e registryInputCategory
bytes auctionCategory, bytes auctionCapability = tx.outputs[3].tokenCategory.split(32);
require(auctionCategory == registryInputCategory);
require(auctionCapability == 0x01); // Mutable

// Enforce an OP_RETURN output that contains the name.
require(tx.outputs[4].lockingBytecode == new LockingBytecodeNullData([name]));
require(auctionCategory == registryInputCategory, "Output 3: auction NFT token category prefix must match registry");
// Mutable
require(auctionCapability == 0x01, "Output 3: auction NFT capability must be mutable (0x01)");

if (tx.outputs.length == 6) {
if (tx.outputs.length == 5) {
// If any change, then it must be pure BCH.
require(tx.outputs[5].tokenCategory == 0x);
require(tx.outputs[4].tokenCategory == 0x, "Output 4: change must be pure BCH (no token category)");
}
}
}
Loading