@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
972 lines (866 loc) • 32.5 kB
text/typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import { BytesWriter } from '../buffer/BytesWriter';
import {
ALLOWANCE_DECREASE_TYPE_HASH,
ALLOWANCE_INCREASE_TYPE_HASH,
ALLOWANCE_SELECTOR,
BALANCE_OF_SELECTOR,
DECIMALS_SELECTOR,
DOMAIN_SEPARATOR_SELECTOR,
ICON_SELECTOR,
MAXIMUM_SUPPLY_SELECTOR,
METADATA_SELECTOR,
NAME_SELECTOR,
NONCE_OF_SELECTOR,
ON_OP20_RECEIVED_SELECTOR,
OP712_DOMAIN_TYPE_HASH,
OP712_VERSION_HASH,
SYMBOL_SELECTOR,
TOTAL_SUPPLY_SELECTOR,
} from '../constants/Exports';
import { Blockchain } from '../env';
import { sha256, sha256String } from '../env/global';
import { OP20ApprovedEvent, OP20BurnedEvent, OP20MintedEvent, OP20TransferredEvent } from '../events/predefined';
import { Selector } from '../math/abi';
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 { Calldata } from '../types';
import { Address } from '../types/Address';
import { Revert } from '../types/Revert';
import { SafeMath } from '../types/SafeMath';
import {
ADDRESS_BYTE_LENGTH,
SELECTOR_BYTE_LENGTH,
U256_BYTE_LENGTH,
U32_BYTE_LENGTH,
U64_BYTE_LENGTH,
U8_BYTE_LENGTH,
} from '../utils';
import { IOP20 } from './interfaces/IOP20';
import { OP20InitParameters } from './interfaces/OP20InitParameters';
import { ReentrancyGuard, ReentrancyLevel } from './ReentrancyGuard';
import { ExtendedAddress } from '../types/ExtendedAddress';
const namePointer: u16 = Blockchain.nextPointer;
const symbolPointer: u16 = Blockchain.nextPointer;
const iconPointer: u16 = Blockchain.nextPointer;
const decimalsPointer: u16 = Blockchain.nextPointer;
const totalSupplyPointer: u16 = Blockchain.nextPointer;
const maxSupplyPointer: u16 = Blockchain.nextPointer;
const balanceOfMapPointer: u16 = Blockchain.nextPointer;
const allowanceMapPointer: u16 = Blockchain.nextPointer;
const nonceMapPointer: u16 = Blockchain.nextPointer;
/**
* OP20 Token Standard Implementation for OP_NET.
*
* This abstract class implements the OP20 token standard, providing a complete
* fungible token implementation with advanced features including:
* - EIP-712 style typed data signatures for gasless approvals
* - Safe transfer callbacks for receiver contracts
* - Reentrancy protection for security
* - Quantum-resistant signature support (Schnorr now, ML-DSA future)
* - Unlimited approval optimization (u256.Max)
*
* @remarks
* OP20 is OP_NET's equivalent of ERC20, adapted for Bitcoin's UTXO model with
* additional security features. All storage uses persistent pointers for
* cross-transaction state management. The contract includes built-in protection
* against common attack vectors including reentrancy and integer overflow.
*
* Inheriting contracts must implement deployment verification logic and can
* extend with additional features like minting permissions, pausability, etc.
*
* @example
* ```typescript
* class MyToken extends OP20 {
* constructor() {
* super();
* const params: OP20InitParameters = {
* name: "My Token",
* symbol: "MTK",
* decimals: 18,
* maxSupply: u256.fromU64(1000000000000000000000000), // 1M tokens
* icon: "https://example.com/icon.png"
* };
* this.instantiate(params);
* }
* }
* ```
*/
export abstract class OP20 extends ReentrancyGuard implements IOP20 {
/**
* Total supply of tokens currently in circulation.
* Intentionally public for inherited classes to implement custom minting/burning logic.
*/
public _totalSupply: StoredU256;
/**
* Reentrancy protection level for this contract.
* Set to CALLBACK to allow single-depth callbacks for safeTransfer operations.
*/
protected override readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.CALLBACK;
/**
* Nested mapping of owner -> spender -> allowance amount.
* Tracks approval amounts for transferFrom operations.
*/
protected readonly allowanceMap: MapOfMap<u256>;
/**
* Mapping of address -> balance.
* Stores token balances for all holders.
*/
protected readonly balanceOfMap: AddressMemoryMap;
/** Maximum supply that can ever be minted. */
protected readonly _maxSupply: StoredU256;
/** Number of decimal places for token display. */
protected readonly _decimals: StoredU256;
/** Human-readable token name. */
protected readonly _name: StoredString;
/** Token icon URL for display in wallets/explorers. */
protected readonly _icon: StoredString;
/** Token ticker symbol. */
protected readonly _symbol: StoredString;
/**
* Mapping of address -> nonce for EIP-712 signatures.
* Prevents signature replay attacks.
*/
protected readonly _nonceMap: AddressMemoryMap;
/**
* Initializes the OP20 token with storage pointers.
* Sets up all persistent storage mappings and variables.
*/
public constructor() {
super();
this.allowanceMap = new MapOfMap<u256>(allowanceMapPointer);
this.balanceOfMap = new AddressMemoryMap(balanceOfMapPointer);
this._nonceMap = new AddressMemoryMap(nonceMapPointer);
this._totalSupply = new StoredU256(totalSupplyPointer, EMPTY_POINTER);
this._maxSupply = new StoredU256(maxSupplyPointer, EMPTY_POINTER);
this._decimals = new StoredU256(decimalsPointer, EMPTY_POINTER);
this._name = new StoredString(namePointer);
this._symbol = new StoredString(symbolPointer);
this._icon = new StoredString(iconPointer);
}
/**
* Initializes token parameters. Can only be called once.
*
* @param params - Token initialization parameters
* @param skipDeployerVerification - If true, skips deployer check (use with caution)
*
* @throws {Revert} If already initialized
* @throws {Revert} If decimals > 32
* @throws {Revert} If caller is not deployer (unless skipped)
*
* @remarks
* This method sets immutable token parameters and should be called in the
* constructor of inheriting contracts. The maximum of 32 decimals is enforced
* to prevent precision issues with u256 arithmetic.
*/
public instantiate(
params: OP20InitParameters,
skipDeployerVerification: boolean = false,
): void {
if (!this._maxSupply.value.isZero()) throw new Revert('Already initialized');
if (!skipDeployerVerification) this.onlyDeployer(Blockchain.tx.sender);
if (params.decimals > 32) throw new Revert('Decimals > 32');
this._maxSupply.value = params.maxSupply;
this._decimals.value = u256.fromU32(u32(params.decimals));
this._name.value = params.name;
this._symbol.value = params.symbol;
this._icon.value = params.icon;
}
/**
* Returns the token name.
*
* @returns Token name as string
*/
@method()
@returns({ name: 'name', type: ABIDataTypes.STRING })
public name(_: Calldata): BytesWriter {
const w = new BytesWriter(String.UTF8.byteLength(this._name.value) + 4);
w.writeStringWithLength(this._name.value);
return w;
}
/**
* Returns the token symbol.
*
* @returns Token symbol as string
*/
@method()
@returns({ name: 'symbol', type: ABIDataTypes.STRING })
public symbol(_: Calldata): BytesWriter {
const w = new BytesWriter(String.UTF8.byteLength(this._symbol.value) + 4);
w.writeStringWithLength(this._symbol.value);
return w;
}
/**
* Returns the token icon URL.
*
* @returns Icon URL as string
*/
@method()
@returns({ name: 'icon', type: ABIDataTypes.STRING })
public icon(_: Calldata): BytesWriter {
const w = new BytesWriter(String.UTF8.byteLength(this._icon.value) + 4);
w.writeStringWithLength(this._icon.value);
return w;
}
/**
* Returns the number of decimals used for display.
*
* @returns Number of decimals (0-32)
*/
@method()
@returns({ name: 'decimals', type: ABIDataTypes.UINT8 })
public decimals(_: Calldata): BytesWriter {
const w = new BytesWriter(1);
w.writeU8(<u8>this._decimals.value.toU32());
return w;
}
/**
* Returns the total supply of tokens in circulation.
*
* @returns Current total supply as u256
*/
@method()
@returns({ name: 'totalSupply', type: ABIDataTypes.UINT256 })
public totalSupply(_: Calldata): BytesWriter {
const w = new BytesWriter(U256_BYTE_LENGTH);
w.writeU256(this._totalSupply.value);
return w;
}
/**
* Returns the maximum supply that can ever exist.
*
* @returns Maximum supply cap as u256
*/
@method()
@returns({ name: 'maximumSupply', type: ABIDataTypes.UINT256 })
public maximumSupply(_: Calldata): BytesWriter {
const w = new BytesWriter(U256_BYTE_LENGTH);
w.writeU256(this._maxSupply.value);
return w;
}
/**
* Returns the EIP-712 domain separator for signature verification.
*
* @returns 32-byte domain separator hash
*
* @remarks
* The domain separator includes chain ID, protocol ID, and contract address
* to prevent cross-chain and cross-contract signature replay attacks.
*/
@method()
@returns({ name: 'domainSeparator', type: ABIDataTypes.BYTES32 })
public domainSeparator(_: Calldata): BytesWriter {
const w = new BytesWriter(32);
w.writeBytes(this._buildDomainSeparator());
return w;
}
/**
* Returns the token balance of an address.
*
* @param calldata - Contains the address to query
* @returns Balance as u256
*/
@method({ name: 'owner', type: ABIDataTypes.ADDRESS })
@returns({ name: 'balance', type: ABIDataTypes.UINT256 })
public balanceOf(calldata: Calldata): BytesWriter {
const bal = this._balanceOf(calldata.readAddress());
const w = new BytesWriter(U256_BYTE_LENGTH);
w.writeU256(bal);
return w;
}
/**
* Returns the current nonce for an address (for signature verification).
*
* @param calldata - Contains the address to query
* @returns Current nonce as u256
*/
@method({ name: 'owner', type: ABIDataTypes.ADDRESS })
@returns({ name: 'nonce', type: ABIDataTypes.UINT256 })
public nonceOf(calldata: Calldata): BytesWriter {
const current = this._nonceMap.get(calldata.readAddress());
const w = new BytesWriter(U256_BYTE_LENGTH);
w.writeU256(current);
return w;
}
/**
* Returns the amount an address is allowed to spend on behalf of another.
*
* @param calldata - Contains owner and spender addresses
* @returns Remaining allowance as u256
*/
@method(
{ name: 'owner', type: ABIDataTypes.ADDRESS },
{ name: 'spender', type: ABIDataTypes.ADDRESS },
)
@returns({ name: 'remaining', type: ABIDataTypes.UINT256 })
public allowance(calldata: Calldata): BytesWriter {
const w = new BytesWriter(U256_BYTE_LENGTH);
const rem = this._allowance(calldata.readAddress(), calldata.readAddress());
w.writeU256(rem);
return w;
}
/**
* Transfers tokens from sender to recipient.
*
* @param calldata - Contains recipient address and amount
* @emits Transferred event
*
* @throws {Revert} If sender has insufficient balance
* @throws {Revert} If recipient is zero address
*/
@method(
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
)
@emit('Transferred')
public transfer(calldata: Calldata): BytesWriter {
this._transfer(Blockchain.tx.sender, calldata.readAddress(), calldata.readU256());
return new BytesWriter(0);
}
/**
* Transfers tokens on behalf of another address using allowance.
*
* @param calldata - Contains from address, to address, and amount
* @emits Transferred event
*
* @throws {Revert} If insufficient allowance
* @throws {Revert} If from has insufficient balance
*/
@method(
{ name: 'from', type: ABIDataTypes.ADDRESS },
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
)
@emit('Transferred')
public transferFrom(calldata: Calldata): BytesWriter {
const from = calldata.readAddress();
const to = calldata.readAddress();
const amount = calldata.readU256();
this._spendAllowance(from, Blockchain.tx.sender, amount);
this._transfer(from, to, amount);
return new BytesWriter(0);
}
/**
* Safely transfers tokens and calls onOP20Received on recipient if it's a contract.
*
* @param calldata - Contains recipient, amount, and optional data
* @emits Transferred event
*
* @throws {Revert} If recipient contract rejects the transfer
* @remarks
* Prevents tokens from being permanently locked in contracts that can't handle them.
*/
@method(
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
{ name: 'data', type: ABIDataTypes.BYTES },
)
@emit('Transferred')
public safeTransfer(calldata: Calldata): BytesWriter {
this._safeTransfer(
Blockchain.tx.sender,
calldata.readAddress(),
calldata.readU256(),
calldata.readBytesWithLength(),
);
return new BytesWriter(0);
}
/**
* Safely transfers tokens on behalf of another address with callback.
*
* @param calldata - Contains from, to, amount, and optional data
* @emits Transferred event
*
* @throws {Revert} If insufficient allowance or balance
* @throws {Revert} If recipient contract rejects
*/
@method(
{ name: 'from', type: ABIDataTypes.ADDRESS },
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
{ name: 'data', type: ABIDataTypes.BYTES },
)
@emit('Transferred')
public safeTransferFrom(calldata: Calldata): BytesWriter {
const from = calldata.readAddress();
const to = calldata.readAddress();
const amount = calldata.readU256();
const data = calldata.readBytesWithLength();
this._spendAllowance(from, Blockchain.tx.sender, amount);
this._safeTransfer(from, to, amount, data);
return new BytesWriter(0);
}
/**
* Increases the allowance granted to a spender.
*
* @param calldata - Contains spender address and amount to increase
* @emits Approved event
*
* @remarks
* Preferred over setting allowance directly to avoid race conditions.
* If overflow would occur, sets to u256.Max (unlimited).
*/
@method(
{ name: 'spender', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
)
@emit('Approved')
public increaseAllowance(calldata: Calldata): BytesWriter {
const owner: Address = Blockchain.tx.sender;
const spender: Address = calldata.readAddress();
const amount: u256 = calldata.readU256();
this._increaseAllowance(owner, spender, amount);
return new BytesWriter(0);
}
/**
* Decreases the allowance granted to a spender.
*
* @param calldata - Contains spender address and amount to decrease
* @emits Approved event
*
* @remarks
* If decrease would cause underflow, sets allowance to zero.
*/
@method(
{ name: 'spender', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
)
@emit('Approved')
public decreaseAllowance(calldata: Calldata): BytesWriter {
const owner: Address = Blockchain.tx.sender;
const spender: Address = calldata.readAddress();
const amount: u256 = calldata.readU256();
this._decreaseAllowance(owner, spender, amount);
return new BytesWriter(0);
}
/**
* Increases allowance using an EIP-712 typed signature (gasless approval).
*
* @param calldata - Contains owner, spender, amount, deadline, and signature
* @emits Approved event
*
* @throws {Revert} If signature is invalid or expired
*
* @remarks
* Enables gasless approvals where a third party can submit the transaction.
* Uses Schnorr signatures now, will support ML-DSA after quantum transition.
*/
@method(
{ name: 'owner', type: ABIDataTypes.BYTES32 },
{ name: 'ownerTweakedPublicKey', type: ABIDataTypes.BYTES32 },
{ name: 'spender', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
{ name: 'deadline', type: ABIDataTypes.UINT64 },
{ name: 'signature', type: ABIDataTypes.BYTES },
)
@emit('Approved')
public increaseAllowanceBySignature(calldata: Calldata): BytesWriter {
const ownerAddress = calldata.readBytesArray(ADDRESS_BYTE_LENGTH);
const ownerTweakedPublicKey = calldata.readBytesArray(ADDRESS_BYTE_LENGTH);
const owner = new ExtendedAddress(ownerTweakedPublicKey, ownerAddress);
const spender: Address = calldata.readAddress();
const amount: u256 = calldata.readU256();
const deadline: u64 = calldata.readU64();
const signature = calldata.readBytesWithLength();
this._increaseAllowanceBySignature(owner, spender, amount, deadline, signature);
return new BytesWriter(0);
}
/**
* Decreases allowance using an EIP-712 typed signature.
*
* @param calldata - Contains owner, spender, amount, deadline, and signature
* @emits Approved event
*
* @throws {Revert} If signature is invalid or expired
*/
@method(
{ name: 'owner', type: ABIDataTypes.BYTES32 },
{ name: 'ownerTweakedPublicKey', type: ABIDataTypes.BYTES32 },
{ name: 'spender', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
{ name: 'deadline', type: ABIDataTypes.UINT64 },
{ name: 'signature', type: ABIDataTypes.BYTES },
)
@emit('Approved')
public decreaseAllowanceBySignature(calldata: Calldata): BytesWriter {
const ownerAddress = calldata.readBytesArray(ADDRESS_BYTE_LENGTH);
const ownerTweakedPublicKey = calldata.readBytesArray(ADDRESS_BYTE_LENGTH);
const owner = new ExtendedAddress(ownerTweakedPublicKey, ownerAddress);
const spender: Address = calldata.readAddress();
const amount: u256 = calldata.readU256();
const deadline: u64 = calldata.readU64();
const signature = calldata.readBytesWithLength();
this._decreaseAllowanceBySignature(owner, spender, amount, deadline, signature);
return new BytesWriter(0);
}
/**
* Burns tokens from the sender's balance.
*
* @param calldata - Contains amount to burn
* @emits Burned event
*
* @throws {Revert} If sender has insufficient balance
*
* @remarks
* Permanently removes tokens from circulation, decreasing total supply.
*/
@method({ name: 'amount', type: ABIDataTypes.UINT256 })
@emit('Burned')
public burn(calldata: Calldata): BytesWriter {
this._burn(Blockchain.tx.sender, calldata.readU256());
return new BytesWriter(0);
}
/**
* Returns all token metadata in a single call.
*
* @returns Combined metadata including name, symbol, icon, decimals, totalSupply, and domain separator
*
* @remarks
* Optimization for wallets/explorers to fetch all token info in one call.
*/
@method()
@returns(
{ name: 'name', type: ABIDataTypes.STRING },
{ name: 'symbol', type: ABIDataTypes.STRING },
{ name: 'icon', type: ABIDataTypes.STRING },
{ name: 'decimals', type: ABIDataTypes.UINT8 },
{ name: 'totalSupply', type: ABIDataTypes.UINT256 },
{ name: 'domainSeparator', type: ABIDataTypes.BYTES32 },
)
public metadata(_: Calldata): BytesWriter {
const name = this._name.value;
const symbol = this._symbol.value;
const icon = this._icon.value;
const domainSeparator = this._buildDomainSeparator();
const nameLength = String.UTF8.byteLength(name);
const symbolLength = String.UTF8.byteLength(symbol);
const iconLength = String.UTF8.byteLength(icon);
const totalSize =
U32_BYTE_LENGTH * 4 +
U256_BYTE_LENGTH * 2 +
nameLength +
symbolLength +
iconLength +
domainSeparator.length +
U8_BYTE_LENGTH;
const w = new BytesWriter(totalSize);
w.writeStringWithLength(name);
w.writeStringWithLength(symbol);
w.writeStringWithLength(icon);
w.writeU8(<u8>this._decimals.value.toU32());
w.writeU256(this._totalSupply.value);
w.writeBytesWithLength(domainSeparator);
return w;
}
/**
* Internal: Gets balance of an address.
* @protected
*/
protected _balanceOf(owner: Address): u256 {
if (!this.balanceOfMap.has(owner)) return u256.Zero;
return this.balanceOfMap.get(owner);
}
/**
* Internal: Gets allowance between owner and spender.
* @protected
*/
protected _allowance(owner: Address, spender: Address): u256 {
const senderMap = this.allowanceMap.get(owner);
return senderMap.get(spender);
}
/**
* Internal: Executes token transfer logic.
* @protected
*/
protected _transfer(from: Address, to: Address, amount: u256): void {
if (from === Address.zero()) {
throw new Revert('Invalid sender');
}
if (to === Address.zero()) {
throw new Revert('Invalid receiver');
}
const balance: u256 = this.balanceOfMap.get(from);
if (balance < amount) {
throw new Revert('Insufficient balance');
}
this.balanceOfMap.set(from, SafeMath.sub(balance, amount));
const toBal: u256 = this.balanceOfMap.get(to);
this.balanceOfMap.set(to, SafeMath.add(toBal, amount));
this.createTransferredEvent(Blockchain.tx.sender, from, to, amount);
}
/**
* Internal: Safe transfer with receiver callback.
* @protected
*/
protected _safeTransfer(from: Address, to: Address, amount: u256, data: Uint8Array): void {
this._transfer(from, to, amount);
if (Blockchain.isContract(to)) {
// In CALLBACK mode, the guard allows depth up to 1
// In STANDARD mode, the guard blocks all reentrancy
this._callOnOP20Received(from, to, amount, data);
}
}
/**
* Internal: Spends allowance for transferFrom.
* @protected
*/
protected _spendAllowance(owner: Address, spender: Address, amount: u256): void {
if (owner.equals(spender)) return;
const ownerMap = this.allowanceMap.get(owner);
const allowed: u256 = ownerMap.get(spender);
if (allowed === u256.Max) return;
if (allowed < amount) {
throw new Revert('Insufficient allowance');
}
ownerMap.set(spender, SafeMath.sub(allowed, amount));
this.allowanceMap.set(owner, ownerMap);
}
/**
* Internal: Calls onOP20Received on receiver contract.
* @protected
*/
protected _callOnOP20Received(
from: Address,
to: Address,
amount: u256,
data: Uint8Array,
): void {
const operator = Blockchain.tx.sender;
const calldata = new BytesWriter(
SELECTOR_BYTE_LENGTH +
ADDRESS_BYTE_LENGTH * 2 +
U256_BYTE_LENGTH +
U32_BYTE_LENGTH +
data.length,
);
calldata.writeSelector(ON_OP20_RECEIVED_SELECTOR);
calldata.writeAddress(operator);
calldata.writeAddress(from);
calldata.writeU256(amount);
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_OP20_RECEIVED_SELECTOR) {
throw new Revert('Transfer rejected by recipient');
}
}
/**
* Internal: Processes signature-based allowance increase.
* @protected
*/
protected _increaseAllowanceBySignature(
owner: ExtendedAddress,
spender: Address,
amount: u256,
deadline: u64,
signature: Uint8Array,
): void {
this._verifySignature(
ALLOWANCE_INCREASE_TYPE_HASH,
owner,
spender,
amount,
deadline,
signature,
);
this._increaseAllowance(owner, spender, amount);
}
/**
* Checks if a selector should bypass reentrancy guards.
* @protected
*/
protected override isSelectorExcluded(selector: Selector): boolean {
if (
selector === BALANCE_OF_SELECTOR ||
selector === ALLOWANCE_SELECTOR ||
selector === TOTAL_SUPPLY_SELECTOR ||
selector === NAME_SELECTOR ||
selector === SYMBOL_SELECTOR ||
selector === DECIMALS_SELECTOR ||
selector === NONCE_OF_SELECTOR ||
selector === DOMAIN_SEPARATOR_SELECTOR ||
selector === METADATA_SELECTOR ||
selector === MAXIMUM_SUPPLY_SELECTOR ||
selector === ICON_SELECTOR
) {
return true;
}
return super.isSelectorExcluded(selector);
}
/**
* Internal: Processes signature-based allowance decrease.
* @protected
*/
protected _decreaseAllowanceBySignature(
owner: ExtendedAddress,
spender: Address,
amount: u256,
deadline: u64,
signature: Uint8Array,
): void {
this._verifySignature(
ALLOWANCE_DECREASE_TYPE_HASH,
owner,
spender,
amount,
deadline,
signature,
);
this._decreaseAllowance(owner, spender, amount);
}
/**
* Internal: Verifies EIP-712 typed signatures.
* @protected
*/
protected _verifySignature(
typeHash: u8[],
owner: ExtendedAddress,
spender: Address,
amount: 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._nonceMap.get(owner);
const structWriter = new BytesWriter(
32 + ADDRESS_BYTE_LENGTH * 2 + U256_BYTE_LENGTH * 2 + U64_BYTE_LENGTH,
);
structWriter.writeBytesU8Array(typeHash);
structWriter.writeAddress(owner);
structWriter.writeAddress(spender);
structWriter.writeU256(amount);
structWriter.writeU256(nonce);
structWriter.writeU64(deadline);
const structHash = sha256(structWriter.getBuffer());
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._nonceMap.set(owner, SafeMath.add(nonce, u256.One));
}
/**
* Internal: Builds EIP-712 domain separator.
* @protected
*/
protected override _buildDomainSeparator(): Uint8Array {
const writer = new BytesWriter(32 * 5 + ADDRESS_BYTE_LENGTH);
writer.writeBytesU8Array(OP712_DOMAIN_TYPE_HASH);
writer.writeBytes(sha256String(this._name.value));
writer.writeBytesU8Array(OP712_VERSION_HASH);
writer.writeBytes(Blockchain.chainId);
writer.writeBytes(Blockchain.protocolId);
writer.writeAddress(this.address);
return sha256(writer.getBuffer());
}
/**
* Internal: Increases allowance with overflow protection.
* @protected
*/
protected _increaseAllowance(owner: Address, spender: Address, amount: u256): void {
if (owner === Address.zero()) {
throw new Revert('Invalid approver');
}
if (spender === Address.zero()) {
throw new Revert('Invalid spender');
}
const senderMap = this.allowanceMap.get(owner);
const previousAllowance = senderMap.get(spender);
let newAllowance: u256 = u256.add(previousAllowance, amount);
// If it overflows, set to max
if (newAllowance < previousAllowance) {
newAllowance = u256.Max;
}
senderMap.set(spender, newAllowance);
this.createApprovedEvent(owner, spender, newAllowance);
}
/**
* Internal: Decreases allowance with underflow protection.
* @protected
*/
protected _decreaseAllowance(owner: Address, spender: Address, amount: u256): void {
if (owner === Address.zero()) {
throw new Revert('Invalid approver');
}
if (spender === Address.zero()) {
throw new Revert('Invalid spender');
}
const senderMap = this.allowanceMap.get(owner);
const previousAllowance = senderMap.get(spender);
let newAllowance: u256;
// If it underflows, set to zero
if (amount > previousAllowance) {
newAllowance = u256.Zero;
} else {
newAllowance = SafeMath.sub(previousAllowance, amount);
}
senderMap.set(spender, newAllowance);
this.createApprovedEvent(owner, spender, newAllowance);
}
/**
* Internal: Mints new tokens to an address.
* @protected
*
* @throws {Revert} If exceeds max supply
*/
protected _mint(to: Address, amount: u256): void {
if (to === Address.zero()) {
throw new Revert('Invalid receiver');
}
const toBal: u256 = this.balanceOfMap.get(to);
this.balanceOfMap.set(to, SafeMath.add(toBal, amount));
// @ts-expect-error AssemblyScript valid
this._totalSupply += amount;
if (this._totalSupply.value > this._maxSupply.value) {
throw new Revert('Max supply reached');
}
this.createMintedEvent(to, amount);
}
/**
* Internal: Burns tokens from an address.
* @protected
*/
protected _burn(from: Address, amount: u256): void {
if (from === Address.zero()) {
throw new Revert('Invalid sender');
}
const balance: u256 = this.balanceOfMap.get(from);
const newBalance: u256 = SafeMath.sub(balance, amount);
this.balanceOfMap.set(from, newBalance);
// @ts-expect-error AssemblyScript valid
this._totalSupply -= amount;
this.createBurnedEvent(from, amount);
}
/** Event creation helpers */
protected createBurnedEvent(from: Address, amount: u256): void {
this.emitEvent(new OP20BurnedEvent(from, amount));
}
protected createApprovedEvent(owner: Address, spender: Address, amount: u256): void {
this.emitEvent(new OP20ApprovedEvent(owner, spender, amount));
}
protected createMintedEvent(to: Address, amount: u256): void {
this.emitEvent(new OP20MintedEvent(to, amount));
}
protected createTransferredEvent(
operator: Address,
from: Address,
to: Address,
amount: u256,
): void {
this.emitEvent(new OP20TransferredEvent(operator, from, to, amount));
}
}