UNPKG

@btc-vision/btc-runtime

Version:

Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.

1,029 lines (845 loc) 37.2 kB
// THIS STANDARD IS EXPERIMENTAL AND SHOULDN'T BE USED IN REAL PROJECTS // CONTRACTS USING THIS COULD BREAK IN THE FUTURE import { u256 } from '@btc-vision/as-bignum/assembly'; import { BytesWriter } from '../buffer/BytesWriter'; import { Blockchain } from '../env'; import { sha256 } from '../env/global'; import { EMPTY_POINTER } from '../math/bytes'; import { AddressMemoryMap } from '../memory/AddressMemoryMap'; import { MapOfMap } from '../memory/MapOfMap'; import { StoredString } from '../storage/StoredString'; import { StoredU256 } from '../storage/StoredU256'; import { StoredU256Array } from '../storage/arrays/StoredU256Array'; import { Calldata } from '../types'; import { Address } from '../types/Address'; import { Revert } from '../types/Revert'; import { SafeMath } from '../types/SafeMath'; import { ADDRESS_BYTE_LENGTH, BOOLEAN_BYTE_LENGTH, SELECTOR_BYTE_LENGTH, U256_BYTE_LENGTH, U32_BYTE_LENGTH, U64_BYTE_LENGTH, U8_BYTE_LENGTH, } from '../utils'; import { IOP721 } from './interfaces/IOP721'; import { OP721InitParameters } from './interfaces/OP721InitParameters'; import { ReentrancyGuard } from './ReentrancyGuard'; import { StoredMapU256 } from '../storage/maps/StoredMapU256'; import { MAX_URI_LENGTH, OP721ApprovedForAllEvent, URIEvent } from '../events/predefined'; import { ON_OP721_RECEIVED_SELECTOR, OP712_DOMAIN_TYPE_HASH, OP712_VERSION_HASH, OP721_APPROVAL_FOR_ALL_TYPE_HASH, OP721_APPROVAL_TYPE_HASH, } from '../constants/Exports'; import { ExtendedAddress } from '../types/ExtendedAddress'; import { OP721TransferredEvent } from '../events/predefined/OP721TransferredEvent'; import { OP721ApprovedEvent } from '../events/predefined/OP721ApprovedEvent'; import { OP721MintedEvent } from '../events/predefined/OP721MintedEvent'; import { OP721BurnedEvent } from '../events/predefined/OP721BurnedEvent'; const stringPointer: u16 = Blockchain.nextPointer; const totalSupplyPointer: u16 = Blockchain.nextPointer; const maxSupplyPointer: u16 = Blockchain.nextPointer; const ownerOfMapPointer: u16 = Blockchain.nextPointer; const tokenApprovalMapPointer: u16 = Blockchain.nextPointer; const operatorApprovalMapPointer: u16 = Blockchain.nextPointer; const balanceOfMapPointer: u16 = Blockchain.nextPointer; const tokenURIMapPointer: u16 = Blockchain.nextPointer; const nextTokenIdPointer: u16 = Blockchain.nextPointer; const ownerTokensMapPointer: u16 = Blockchain.nextPointer; const tokenIndexMapPointer: u16 = Blockchain.nextPointer; const initializedPointer: u16 = Blockchain.nextPointer; const tokenURICounterPointer: u16 = Blockchain.nextPointer; const approveNonceMapPointer: u16 = Blockchain.nextPointer; export abstract class OP721 extends ReentrancyGuard implements IOP721 { protected readonly _name: StoredString; protected readonly _symbol: StoredString; protected readonly _baseURI: StoredString; protected readonly _banner: StoredString; protected readonly _icon: StoredString; protected readonly _description: StoredString; protected readonly _website: StoredString; protected readonly _totalSupply: StoredU256; protected readonly _maxSupply: StoredU256; protected readonly _nextTokenId: StoredU256; protected readonly _initialized: StoredU256; protected readonly _tokenURICounter: StoredU256; protected readonly ownerOfMap: StoredMapU256; protected readonly tokenApprovalMap: StoredMapU256; protected readonly balanceOfMap: AddressMemoryMap; protected readonly operatorApprovalMap: MapOfMap<u256>; protected readonly _approveNonceMap: AddressMemoryMap; // Token URI storage - stores index to StoredString array protected readonly tokenURIIndices: StoredMapU256; protected readonly tokenURIStorage: Map<u32, StoredString> = new Map(); // Enumerable extension - owner -> array of token IDs protected readonly ownerTokensMap: Map<Address, StoredU256Array> = new Map(); // Token ID -> index in owner's array protected readonly tokenIndexMap: StoredMapU256; public constructor() { super(); this._name = new StoredString(stringPointer, 0); this._symbol = new StoredString(stringPointer, 2); this._baseURI = new StoredString(stringPointer, 3); this._banner = new StoredString(stringPointer, 4); this._icon = new StoredString(stringPointer, 5); this._description = new StoredString(stringPointer, 6); this._website = new StoredString(stringPointer, 7); this._totalSupply = new StoredU256(totalSupplyPointer, EMPTY_POINTER); this._maxSupply = new StoredU256(maxSupplyPointer, EMPTY_POINTER); this._nextTokenId = new StoredU256(nextTokenIdPointer, EMPTY_POINTER); this._initialized = new StoredU256(initializedPointer, EMPTY_POINTER); this._tokenURICounter = new StoredU256(tokenURICounterPointer, EMPTY_POINTER); this.ownerOfMap = new StoredMapU256(ownerOfMapPointer); this.tokenApprovalMap = new StoredMapU256(tokenApprovalMapPointer); this.balanceOfMap = new AddressMemoryMap(balanceOfMapPointer); this.operatorApprovalMap = new MapOfMap<u256>(operatorApprovalMapPointer); // Initialize separate nonce maps this._approveNonceMap = new AddressMemoryMap(approveNonceMapPointer); this.tokenURIIndices = new StoredMapU256(tokenURIMapPointer); this.tokenIndexMap = new StoredMapU256(tokenIndexMapPointer); } public get name(): string { return this._name.value; } public get symbol(): string { return this._symbol.value; } public get icon(): string { return this._icon.value; } public get baseURI(): string { return this._baseURI.value; } public get totalSupply(): u256 { return this._totalSupply.value; } public get maxSupply(): u256 { return this._maxSupply.value; } public get banner(): string { return this._banner.value; } public get description(): string { return this._description.value; } public get website(): string { return this._website.value; } public instantiate( params: OP721InitParameters, skipDeployerVerification: boolean = false, ): void { if (!this._initialized.value.isZero()) throw new Revert('Already initialized'); if (!skipDeployerVerification) this.onlyDeployer(Blockchain.tx.sender); if (params.name.length == 0) throw new Revert('Name cannot be empty'); if (params.symbol.length == 0) throw new Revert('Symbol cannot be empty'); if (params.maxSupply.isZero()) throw new Revert('Max supply cannot be zero'); this._name.value = params.name; this._symbol.value = params.symbol; this._baseURI.value = params.baseURI; this._maxSupply.value = params.maxSupply; this._nextTokenId.value = u256.One; this._initialized.value = u256.One; this._tokenURICounter.value = u256.Zero; this._banner.value = params.banner; this._icon.value = params.icon; this._description.value = params.description; this._website.value = params.website; } @method('name') @returns({ name: 'name', type: ABIDataTypes.STRING }) public fn_name(_: Calldata): BytesWriter { const name = this.name; const w = new BytesWriter(String.UTF8.byteLength(name) + 4); w.writeStringWithLength(name); return w; } @method('symbol') @returns({ name: 'symbol', type: ABIDataTypes.STRING }) public fn_symbol(_: Calldata): BytesWriter { const symbol = this.symbol; const w = new BytesWriter(String.UTF8.byteLength(symbol) + 4); w.writeStringWithLength(symbol); return w; } @method('maxSupply') @returns({ name: 'maxSupply', type: ABIDataTypes.UINT256 }) public fn_maxSupply(_: Calldata): BytesWriter { const w = new BytesWriter(U256_BYTE_LENGTH); w.writeU256(this.maxSupply); return w; } @method({ name: 'tokenId', type: ABIDataTypes.UINT256 }) @returns({ name: 'uri', type: ABIDataTypes.STRING }) public tokenURI(calldata: Calldata): BytesWriter { const tokenId = calldata.readU256(); if (!this._exists(tokenId)) throw new Revert('Token does not exist'); // Check if custom URI exists const uriIndex = this.tokenURIIndices.get(tokenId); let uri: string; if (!uriIndex.isZero()) { // Get custom URI from storage const index = uriIndex.toU32(); if (!this.tokenURIStorage.has(index)) { // Lazy load from storage const storedURI = new StoredString(tokenURIMapPointer, index); this.tokenURIStorage.set(index, storedURI); } uri = this.tokenURIStorage.get(index).value; } else { // Return baseURI + tokenId uri = this.baseURI + tokenId.toString(); } const w = new BytesWriter(String.UTF8.byteLength(uri) + 4); w.writeStringWithLength(uri); return w; } @method( 'changeMetadata', { name: 'icon', type: ABIDataTypes.STRING }, { name: 'banner', type: ABIDataTypes.STRING }, { name: 'description', type: ABIDataTypes.STRING }, { name: 'website', type: ABIDataTypes.STRING }, ) public changeMetadata(calldata: Calldata): BytesWriter { this.onlyDeployer(Blockchain.tx.sender); const icon: string = calldata.readStringWithLength(); const banner: string = calldata.readStringWithLength(); const description: string = calldata.readStringWithLength(); const website: string = calldata.readStringWithLength(); if (icon.length == 0) throw new Revert('Icon cannot be empty'); if (banner.length == 0) throw new Revert('Banner cannot be empty'); if (description.length == 0) throw new Revert('Description cannot be empty'); if (website.length == 0) throw new Revert('Website cannot be empty'); this._icon.value = icon; this._banner.value = banner; this._description.value = description; this._website.value = website; return new BytesWriter(0); } @method('totalSupply') @returns({ name: 'totalSupply', type: ABIDataTypes.UINT256 }) public fn_totalSupply(_: Calldata): BytesWriter { const w = new BytesWriter(U256_BYTE_LENGTH); w.writeU256(this.totalSupply); return w; } @method({ name: 'owner', type: ABIDataTypes.ADDRESS }) @returns({ name: 'balance', type: ABIDataTypes.UINT256 }) public balanceOf(calldata: Calldata): BytesWriter { const owner = calldata.readAddress(); const balance = this._balanceOf(owner); const w = new BytesWriter(U256_BYTE_LENGTH); w.writeU256(balance); return w; } @method({ name: 'tokenId', type: ABIDataTypes.UINT256 }) @returns({ name: 'owner', type: ABIDataTypes.ADDRESS }) public ownerOf(calldata: Calldata): BytesWriter { const tokenId = calldata.readU256(); const owner = this._ownerOf(tokenId); const w = new BytesWriter(ADDRESS_BYTE_LENGTH); w.writeAddress(owner); return w; } @method( { name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'tokenId', type: ABIDataTypes.UINT256 }, ) @emit('Transferred') public transfer(calldata: Calldata): BytesWriter { const to = calldata.readAddress(); const tokenId = calldata.readU256(); this._transfer(Blockchain.tx.sender, to, tokenId); return new BytesWriter(0); } @method( { name: 'from', type: ABIDataTypes.ADDRESS }, { name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'tokenId', type: ABIDataTypes.UINT256 }, ) @emit('Transferred') public transferFrom(calldata: Calldata): BytesWriter { const from = calldata.readAddress(); const to = calldata.readAddress(); const amount = calldata.readU256(); this._transfer(from, to, amount); return new BytesWriter(0); } @method( { name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'tokenId', type: ABIDataTypes.UINT256 }, { name: 'data', type: ABIDataTypes.BYTES }, ) @emit('Transferred') public safeTransfer(calldata: Calldata): BytesWriter { const to = calldata.readAddress(); const tokenId = calldata.readU256(); const data = calldata.readBytesWithLength(); this._safeTransfer(Blockchain.tx.sender, to, tokenId, data); return new BytesWriter(0); } @method( { name: 'from', type: ABIDataTypes.ADDRESS }, { name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'tokenId', type: ABIDataTypes.UINT256 }, { name: 'data', type: ABIDataTypes.BYTES }, ) @emit('Transferred') public safeTransferFrom(calldata: Calldata): BytesWriter { const from = calldata.readAddress(); const to = calldata.readAddress(); const tokenId = calldata.readU256(); const data = calldata.readBytesWithLength(); this._safeTransfer(from, to, tokenId, data); return new BytesWriter(0); } @method( { name: 'operator', type: ABIDataTypes.ADDRESS }, { name: 'tokenId', type: ABIDataTypes.UINT256 }, ) @emit('Approved') public approve(calldata: Calldata): BytesWriter { const operator = calldata.readAddress(); const tokenId = calldata.readU256(); this._approve(operator, tokenId); return new BytesWriter(0); } @method({ name: 'tokenId', type: ABIDataTypes.UINT256 }) public getApproved(calldata: Calldata): BytesWriter { const tokenId = calldata.readU256(); if (!this._exists(tokenId)) throw new Revert('Token does not exist'); const approved = this._addressFromU256(this.tokenApprovalMap.get(tokenId)); const w = new BytesWriter(ADDRESS_BYTE_LENGTH); w.writeAddress(approved); return w; } @method( { name: 'operator', type: ABIDataTypes.ADDRESS }, { name: 'approved', type: ABIDataTypes.BOOL }, ) @emit('ApprovedForAll') public setApprovalForAll(calldata: Calldata): BytesWriter { const operator = calldata.readAddress(); const approved = calldata.readBoolean(); if (operator === Blockchain.tx.sender) throw new Revert('Cannot approve self'); this._setApprovalForAll(Blockchain.tx.sender, operator, approved); return new BytesWriter(0); } @method( { name: 'owner', type: ABIDataTypes.ADDRESS }, { name: 'operator', type: ABIDataTypes.ADDRESS }, ) @returns({ name: 'approved', type: ABIDataTypes.BOOL }) public isApprovedForAll(calldata: Calldata): BytesWriter { const owner = calldata.readAddress(); const operator = calldata.readAddress(); const approved: boolean = this._isApprovedForAll(owner, operator); const w = new BytesWriter(U8_BYTE_LENGTH); w.writeBoolean(approved); return w; } @method( { name: 'owner', type: ABIDataTypes.BYTES32 }, { name: 'ownerTweakedPublicKey', type: ABIDataTypes.BYTES32 }, { name: 'operator', type: ABIDataTypes.ADDRESS }, { name: 'tokenId', type: ABIDataTypes.UINT256 }, { name: 'deadline', type: ABIDataTypes.UINT64 }, { name: 'signature', type: ABIDataTypes.BYTES }, ) @emit('Approved') public approveBySignature(calldata: Calldata): BytesWriter { const ownerAddress = calldata.readBytesArray(ADDRESS_BYTE_LENGTH); const ownerTweakedPublicKey = calldata.readBytesArray(ADDRESS_BYTE_LENGTH); const owner = new ExtendedAddress(ownerTweakedPublicKey, ownerAddress); const operator = calldata.readAddress(); const tokenId = calldata.readU256(); const deadline = calldata.readU64(); const signature = calldata.readBytesWithLength(); // Verify ownership const tokenOwner = this._ownerOf(tokenId); if (tokenOwner !== owner) throw new Revert('Not token owner'); this._verifyApproveSignature(owner, operator, tokenId, deadline, signature); this._approve(operator, tokenId); return new BytesWriter(0); } @method( { name: 'owner', type: ABIDataTypes.BYTES32 }, { name: 'ownerTweakedPublicKey', type: ABIDataTypes.BYTES32 }, { name: 'operator', type: ABIDataTypes.ADDRESS }, { name: 'approved', type: ABIDataTypes.BOOL }, { name: 'deadline', type: ABIDataTypes.UINT64 }, { name: 'signature', type: ABIDataTypes.BYTES }, ) @emit('Approved') public setApprovalForAllBySignature(calldata: Calldata): BytesWriter { const ownerAddress = calldata.readBytesArray(ADDRESS_BYTE_LENGTH); const ownerTweakedPublicKey = calldata.readBytesArray(ADDRESS_BYTE_LENGTH); const owner = new ExtendedAddress(ownerTweakedPublicKey, ownerAddress); const operator = calldata.readAddress(); const approved = calldata.readBoolean(); const deadline = calldata.readU64(); const signature = calldata.readBytesWithLength(); if (owner === operator) throw new Revert('Cannot approve self'); this._verifySetApprovalForAllSignature(owner, operator, approved, deadline, signature); this._setApprovalForAll(owner, operator, approved); return new BytesWriter(0); } @method({ name: 'tokenId', type: ABIDataTypes.UINT256 }) @emit('Burned') public burn(calldata: Calldata): BytesWriter { const tokenId = calldata.readU256(); this._burn(tokenId); return new BytesWriter(0); } @method() @returns({ name: 'domainSeparator', type: ABIDataTypes.BYTES32 }) public domainSeparator(_: Calldata): BytesWriter { const w = new BytesWriter(32); w.writeBytes(this._buildDomainSeparator()); return w; } @method( { name: 'owner', type: ABIDataTypes.ADDRESS }, { name: 'index', type: ABIDataTypes.UINT256 }, ) @returns({ name: 'tokenId', type: ABIDataTypes.UINT256 }) public tokenOfOwnerByIndex(calldata: Calldata): BytesWriter { const owner = calldata.readAddress(); const index = calldata.readU256(); const balance = this._balanceOf(owner); if (index >= balance) throw new Revert('Index out of bounds'); const tokenArray = this._getOwnerTokenArray(owner); const tokenId = tokenArray.get(index.toU32()); const w = new BytesWriter(U256_BYTE_LENGTH); w.writeU256(tokenId); return w; } @method({ name: 'owner', type: ABIDataTypes.ADDRESS }) @returns({ name: 'nonce', type: ABIDataTypes.UINT256 }) public nonceOf(calldata: Calldata): BytesWriter { const owner = calldata.readAddress(); const nonce = this._approveNonceMap.get(owner); const w = new BytesWriter(U256_BYTE_LENGTH); w.writeU256(nonce); return w; } @method({ name: 'baseURI', type: ABIDataTypes.STRING }) @emit('URI') public setBaseURI(calldata: Calldata): BytesWriter { this.onlyDeployer(Blockchain.tx.sender); const baseURI: string = calldata.readStringWithLength(); if (baseURI.length == 0) throw new Revert('Base URI cannot be empty'); if (<u32>baseURI.length > MAX_URI_LENGTH) { throw new Revert('Base URI exceeds maximum length'); } this._setBaseURI(baseURI); return new BytesWriter(0); } @method() @returns( { name: 'name', type: ABIDataTypes.STRING }, { name: 'symbol', type: ABIDataTypes.STRING }, { name: 'icon', type: ABIDataTypes.STRING }, { name: 'banner', type: ABIDataTypes.STRING }, { name: 'description', type: ABIDataTypes.STRING }, { name: 'website', type: ABIDataTypes.STRING }, { name: 'totalSupply', type: ABIDataTypes.UINT256 }, { name: 'domainSeparator', type: ABIDataTypes.BYTES32 }, ) public metadata(_: Calldata): BytesWriter { const name = this.name; const symbol = this.symbol; const icon = this.icon; const banner = this.banner; const description = this.description; const website = this.website; const domainSeparator = this._buildDomainSeparator(); const nameLength = String.UTF8.byteLength(name); const symbolLength = String.UTF8.byteLength(symbol); const iconLength = String.UTF8.byteLength(icon); const bannerLength = String.UTF8.byteLength(banner); const descriptionLength = String.UTF8.byteLength(description); const websiteLength = String.UTF8.byteLength(website); const totalSize = U32_BYTE_LENGTH * 6 + nameLength + symbolLength + iconLength + bannerLength + descriptionLength + websiteLength + U256_BYTE_LENGTH * 2 + U32_BYTE_LENGTH + domainSeparator.length; const w = new BytesWriter(totalSize); w.writeStringWithLength(name); w.writeStringWithLength(symbol); w.writeStringWithLength(icon); w.writeStringWithLength(banner); w.writeStringWithLength(description); w.writeStringWithLength(website); w.writeU256(this.totalSupply); w.writeU256(this.maxSupply); w.writeBytesWithLength(domainSeparator); return w; } protected _mint(to: Address, tokenId: u256): void { if (to === Address.zero()) { throw new Revert('Cannot mint to zero address'); } if (this._exists(tokenId)) { throw new Revert('Token already exists'); } if (!this._maxSupply.value.isZero() && this._totalSupply.value >= this._maxSupply.value) { throw new Revert('Max supply reached'); } // Set owner this.ownerOfMap.set(tokenId, this._u256FromAddress(to)); // Add to enumeration this._addTokenToOwnerEnumeration(to, tokenId); // Update balance const currentBalance = this.balanceOfMap.get(to); this.balanceOfMap.set(to, SafeMath.add(currentBalance, u256.One)); // Update total supply this._totalSupply.value = SafeMath.add(this._totalSupply.value, u256.One); this.createMintedEvent(to, tokenId); } protected _burn(tokenId: u256): void { const owner = this._ownerOf(tokenId); // Check authorization if ( owner !== Blockchain.tx.sender && !this._isApprovedForAll(owner, Blockchain.tx.sender) ) { const approved = this._addressFromU256(this.tokenApprovalMap.get(tokenId)); if (approved !== Blockchain.tx.sender) { throw new Revert('Not authorized to burn'); } } // Clear approvals this.tokenApprovalMap.delete(tokenId); // Remove from enumeration this._removeTokenFromOwnerEnumeration(owner, tokenId); // Update balance const currentBalance = this.balanceOfMap.get(owner); this.balanceOfMap.set(owner, SafeMath.sub(currentBalance, u256.One)); // Remove owner this.ownerOfMap.delete(tokenId); // Clear custom URI if exists const uriIndex = this.tokenURIIndices.get(tokenId); if (!uriIndex.isZero()) { this.tokenURIIndices.delete(tokenId); } // Update total supply this._totalSupply.value = SafeMath.sub(this._totalSupply.value, u256.One); this.createBurnedEvent(owner, tokenId); } protected _transfer(from: Address, to: Address, tokenId: u256): void { if (from === Address.zero()) { throw new Revert('Invalid sender'); } if (to === Address.zero()) { throw new Revert('Invalid receiver'); } const owner = this._ownerOf(tokenId); if (owner !== from) { throw new Revert('Transfer from incorrect owner'); } // Check authorization const sender = Blockchain.tx.sender; if (sender !== from && !this._isApprovedForAll(from, sender)) { const approved = this._addressFromU256(this.tokenApprovalMap.get(tokenId)); if (approved !== sender) { throw new Revert('Not authorized to transfer'); } } // Clear approval this.tokenApprovalMap.delete(tokenId); // Remove from old owner enumeration this._removeTokenFromOwnerEnumeration(from, tokenId); // Add to new owner enumeration this._addTokenToOwnerEnumeration(to, tokenId); // Update balances const fromBalance = this.balanceOfMap.get(from); this.balanceOfMap.set(from, SafeMath.sub(fromBalance, u256.One)); const toBalance = this.balanceOfMap.get(to); this.balanceOfMap.set(to, SafeMath.add(toBalance, u256.One)); // Transfer ownership this.ownerOfMap.set(tokenId, this._u256FromAddress(to)); this.createTransferredEvent(from, to, tokenId); } protected _safeTransfer(from: Address, to: Address, tokenId: u256, data: Uint8Array): void { this._transfer(from, to, tokenId); if (Blockchain.isContract(to)) { this._checkOnOP721Received(from, to, tokenId, data); } } protected _approve(operator: Address, tokenId: u256): void { // Validate to address if (operator === Address.zero()) throw new Revert('Cannot approve zero address'); const owner = this._ownerOf(tokenId); if (operator === owner) throw new Revert('Approval to current owner'); if ( owner !== Blockchain.tx.sender && !this._isApprovedForAll(owner, Blockchain.tx.sender) ) { throw new Revert('Not authorized to approve'); } this.tokenApprovalMap.set(tokenId, this._u256FromAddress(operator)); this.createApprovedEvent(owner, operator, tokenId); } protected _setApprovalForAll(owner: Address, operator: Address, approved: boolean): void { const operatorMap = this.operatorApprovalMap.get(owner); operatorMap.set(operator, approved ? u256.One : u256.Zero); this.operatorApprovalMap.set(owner, operatorMap); this.createApprovedForAllEvent(owner, operator, approved); } protected _isApprovedForAll(owner: Address, operator: Address): boolean { const operatorMap = this.operatorApprovalMap.get(owner); const approval = operatorMap.get(operator); return !approval.isZero(); } protected _exists(tokenId: u256): bool { const owner = this.ownerOfMap.get(tokenId); return !owner.isZero(); } protected _ownerOf(tokenId: u256): Address { const ownerU256 = this.ownerOfMap.get(tokenId); if (ownerU256.isZero()) { throw new Revert('Token does not exist'); } return this._addressFromU256(ownerU256); } protected _balanceOf(owner: Address): u256 { if (owner === Address.zero()) { throw new Revert('Invalid address'); } return this.balanceOfMap.get(owner); } protected _setTokenURI(tokenId: u256, uri: string): void { if (!this._exists(tokenId)) throw new Revert('Token does not exist'); if (<u32>uri.length > MAX_URI_LENGTH) { throw new Revert('URI exceeds maximum length'); } // Use incremental counter for URI storage const currentIndex = this._tokenURICounter.value.toU32(); const uriStorage = new StoredString(tokenURIMapPointer, currentIndex); uriStorage.value = uri; // Store index reference this.tokenURIIndices.set(tokenId, u256.fromU32(currentIndex)); // Increment counter for next URI this._tokenURICounter.value = SafeMath.add(this._tokenURICounter.value, u256.One); // Cache in memory this.tokenURIStorage.set(currentIndex, uriStorage); this.emitEvent(new URIEvent(uri, tokenId)); } protected _checkOnOP721Received( from: Address, to: Address, tokenId: u256, data: Uint8Array, ): void { const calldata = new BytesWriter( SELECTOR_BYTE_LENGTH + ADDRESS_BYTE_LENGTH * 2 + U256_BYTE_LENGTH + U32_BYTE_LENGTH + data.length, ); calldata.writeSelector(ON_OP721_RECEIVED_SELECTOR); calldata.writeAddress(Blockchain.tx.sender); calldata.writeAddress(from); calldata.writeU256(tokenId); calldata.writeBytesWithLength(data); const response = Blockchain.call(to, calldata); if (response.data.byteLength < SELECTOR_BYTE_LENGTH) { throw new Revert('Transfer rejected by recipient'); } const retVal = response.data.readSelector(); if (retVal !== ON_OP721_RECEIVED_SELECTOR) { throw new Revert('Transfer rejected by recipient'); } } protected _verifyApproveSignature( owner: ExtendedAddress, spender: Address, tokenId: u256, deadline: u64, signature: Uint8Array, ): void { if (signature.length !== 64) { throw new Revert('Invalid signature length'); } if (Blockchain.block.number > deadline) { throw new Revert('Signature expired'); } const nonce = this._approveNonceMap.get(owner); const structWriter = new BytesWriter( 32 + ADDRESS_BYTE_LENGTH * 2 + U256_BYTE_LENGTH * 2 + U64_BYTE_LENGTH, ); structWriter.writeBytesU8Array(OP721_APPROVAL_TYPE_HASH); structWriter.writeAddress(owner); structWriter.writeAddress(spender); structWriter.writeU256(tokenId); structWriter.writeU256(nonce); structWriter.writeU64(deadline); const structHash = sha256(structWriter.getBuffer()); this._verifySignature(structHash, owner, signature, nonce); } protected _verifySetApprovalForAllSignature( owner: ExtendedAddress, spender: Address, approved: boolean, deadline: u64, signature: Uint8Array, ): void { if (signature.length !== 64) { throw new Revert('Invalid signature length'); } if (Blockchain.block.number > deadline) { throw new Revert('Signature expired'); } const nonce = this._approveNonceMap.get(owner); const structWriter = new BytesWriter( 32 + ADDRESS_BYTE_LENGTH * 2 + BOOLEAN_BYTE_LENGTH + U256_BYTE_LENGTH + U64_BYTE_LENGTH, ); structWriter.writeBytesU8Array(OP721_APPROVAL_FOR_ALL_TYPE_HASH); structWriter.writeAddress(owner); structWriter.writeAddress(spender); structWriter.writeBoolean(approved); structWriter.writeU256(nonce); structWriter.writeU64(deadline); const structHash = sha256(structWriter.getBuffer()); this._verifySignature(structHash, owner, signature, nonce); } protected _verifySignature( structHash: Uint8Array, owner: ExtendedAddress, signature: Uint8Array, nonce: u256, ): void { const messageWriter = new BytesWriter(2 + 32 + 32); messageWriter.writeU16(0x1901); messageWriter.writeBytes(this._buildDomainSeparator()); messageWriter.writeBytes(structHash); const hash = sha256(messageWriter.getBuffer()); if (!Blockchain.verifySignature(owner, signature, hash)) { throw new Revert('Invalid signature'); } this._approveNonceMap.set(owner, SafeMath.add(nonce, u256.One)); } protected _setBaseURI(baseURI: string): void { this._baseURI.value = baseURI; } protected override _buildDomainSeparator(): Uint8Array { const writer = new BytesWriter(32 * 5 + ADDRESS_BYTE_LENGTH); writer.writeBytesU8Array(OP712_DOMAIN_TYPE_HASH); // Hash the name string for domain separator const nameBytes = Uint8Array.wrap(String.UTF8.encode(this.name)); writer.writeBytes(sha256(nameBytes)); writer.writeBytesU8Array(OP712_VERSION_HASH); writer.writeBytes(Blockchain.chainId); writer.writeBytes(Blockchain.protocolId); writer.writeAddress(this.address); return sha256(writer.getBuffer()); } // Enumeration helpers protected _addTokenToOwnerEnumeration(to: Address, tokenId: u256): void { const tokenArray = this._getOwnerTokenArray(to); const newIndex = tokenArray.getLength(); tokenArray.push(tokenId); this.tokenIndexMap.set(tokenId, u256.fromU32(newIndex)); tokenArray.save(); } protected _removeTokenFromOwnerEnumeration(from: Address, tokenId: u256): void { const tokenArray = this._getOwnerTokenArray(from); const arrayLength = tokenArray.getLength(); // Check for empty array if (arrayLength == 0) { throw new Revert('Token array is empty'); } const lastIndex = arrayLength - 1; const tokenIndex = this.tokenIndexMap.get(tokenId).toU32(); if (tokenIndex != lastIndex) { // Move last token to removed token's position const lastTokenId = tokenArray.get(lastIndex); tokenArray.set(tokenIndex, lastTokenId); this.tokenIndexMap.set(lastTokenId, u256.fromU32(tokenIndex)); } // Remove last element tokenArray.deleteLast(); this.tokenIndexMap.delete(tokenId); tokenArray.save(); } /** * SECURITY NOTICE: * * This function uses a 30-byte truncation of addresses for storage pointer generation. * While this may appear to introduce collision risks, it is secure within the OP_NET * protocol context because: * * 1. All addresses in OP_NET are tweaked public keys (32-byte elliptic curve points) * 2. Tweaked public keys are uniformly distributed across the secp256k1 curve space * 3. Finding two public keys with identical 30-byte prefixes (240 bits) requires * approximately 2^120 operations due to the birthday paradox * 4. The probability of accidentally generating colliding addresses through normal * key generation is cryptographically negligible * * The truncation from 32 to 30 bytes is a space optimization that does not * meaningfully impact security given the uniform distribution of elliptic curve points. */ protected _getOwnerTokenArray(owner: Address): StoredU256Array { // Truncate the 32-byte address to 30 bytes for the storage pointer // This is safe due to the uniform distribution of tweaked public keys const truncatedAddress = new Uint8Array(30); for (let i: i32 = 0; i < 30; i++) { truncatedAddress[i] = owner[i]; } if (!this.ownerTokensMap.has(owner)) { const array = new StoredU256Array(ownerTokensMapPointer, truncatedAddress); this.ownerTokensMap.set(owner, array); } return this.ownerTokensMap.get(owner); } // Helper functions for 32-byte address conversions protected _u256FromAddress(addr: Address): u256 { // OP_NET addresses are already 32 bytes (tweaked public keys) // Direct conversion from 32-byte address to u256 return u256.fromUint8ArrayBE(addr); } protected _addressFromU256(value: u256): Address { // Convert u256 back to 32-byte address const bytes = value.toUint8Array(true); // Returns 32 bytes in BE const addr = new Address([]); // Direct copy since both are 32 bytes for (let i: i32 = 0; i < 32; i++) { addr[i] = bytes[i]; } return addr; } // Event creation helpers protected createTransferredEvent(from: Address, to: Address, tokenId: u256): void { this.emitEvent(new OP721TransferredEvent(Blockchain.tx.sender, from, to, tokenId)); } protected createApprovedEvent(owner: Address, approved: Address, tokenId: u256): void { this.emitEvent(new OP721ApprovedEvent(owner, approved, tokenId)); } protected createMintedEvent(to: Address, tokenId: u256): void { this.emitEvent(new OP721MintedEvent(to, tokenId)); } protected createBurnedEvent(from: Address, tokenId: u256): void { this.emitEvent(new OP721BurnedEvent(from, tokenId)); } protected createApprovedForAllEvent( owner: Address, operator: Address, approved: boolean, ): void { this.emitEvent(new OP721ApprovedForAllEvent(owner, operator, approved)); } }