Features Solutions Technology Tokenomics Docs About Launch App
Docs / Smart Contracts / ABI Reference

ABI Reference

The function signatures and events of the TAG IT contracts deployed on Base Sepolia, taken from the Solidity source.

Testnet, unaudited

Everything on this page refers to the Base Sepolia testnet deployment (chainId 84532). There is no Base Mainnet deployment. The contracts have not completed a third-party security audit — see Audit Reports. Do not put anything of value behind them.

Getting the ABIs

This site does not host prebuilt ABI JSON files. Generate them from the contract source, which is the same source these signatures were read from:

git clone https://github.com/TAG-IT-NETWORK/tagit-contracts.git
cd tagit-contracts
forge build

# ABI artifacts land here (the "abi" key of each JSON file):
#   out/TAGITCore.sol/TAGITCore.json
#   out/TAGITAccess.sol/TAGITAccess.json
#   out/TAGITToken.sol/TAGITToken.json

Building requires Foundry. The Solidity sources are browsable at tagit-contracts/src.

Rather than reproduce whole ABI files here — which drift the moment the contracts change — this page documents the signatures you are most likely to need, each one confirmed present in the deployed bytecode.

TAGITCore

0x3aDc7EFDb58Ae85483eFf5D4966D916185f31d1D — the Digital Twin NFT. An ERC-721 whose transfers are disabled at the token level: transferFrom and safeTransferFrom revert with TransferDisabled(). Ownership only moves through the lifecycle functions below.

Lifecycle states

Every asset holds one of seven states. The State enum is ABI-encoded as uint8.

Value State Meaning
0NONEDefault — the asset does not exist
1MINTEDNFT exists, no NFC tag bound yet
2BOUNDNFC tag cryptographically linked
3ACTIVATEDQA passed, ready for distribution
4CLAIMEDOwned by an end consumer
5FLAGGEDLost, stolen, or recalled
6RECYCLEDEnd of life — terminal, no transitions out

State-changing functions

All of these except transferAsset and updateMetadataHash are gated on a BIDGES capability badge, checked through the TAGITAccess controller. Calling one without the badge reverts with MissingCapability(address account, uint256 capabilityId) — that error is declared on ITAGITAccess and thrown inside TAGITAccess, so decode the revert data against the TAGITAccess ABI; its selector is not in the TAGITCore ABI. TAGITCore's own Unauthorized(address caller, uint256 requiredCapability) is raised by updateMetadataHash only.

Signature Transition Gate
mint(address,bytes32) NONE → MINTED MINTER
batchMint(address[],bytes32[]) NONE → MINTED MINTER
bindTag(uint256,bytes32,bytes,bytes) MINTED → BOUND BINDER
batchBind(uint256[],bytes32[],bytes[],bytes) MINTED → BOUND BINDER
activate(uint256) BOUND → ACTIVATED ACTIVATOR
batchActivate(uint256[]) BOUND → ACTIVATED ACTIVATOR
claim(uint256,address) ACTIVATED → CLAIMED CLAIMER
flag(uint256) BOUND, ACTIVATED or CLAIMED → FLAGGED FLAGGER
batchFlag(uint256[]) BOUND, ACTIVATED or CLAIMED → FLAGGED FLAGGER
approveResolve(uint256,address) records one approval, no state change RESOLVER
resolve(uint256,address) FLAGGED → the exact pre-flag state RESOLVER
recycle(uint256) any live state → RECYCLED RECYCLER
transferAsset(uint256,address) CLAIMED → CLAIMED (owner changes) asset owner
updateMetadataHash(uint256,bytes32) no state change asset owner or MINTER

Batch calls are capped at MAX_BATCH_SIZE() = 100 and revert atomically; an oversized batch reverts with BatchTooLarge and an empty one with EmptyBatch. resolve requires RESOLVE_QUORUM() = 2 independent approveResolve calls. Fewer than two reverts with QuorumNotReached(uint256 tokenId, uint256 current, uint256 required); a newOwner that does not match the recipient the approvers agreed on reverts with RecipientMismatch instead. Both constants are readable on-chain.

The capability names are exposed as bytes32 constants — MINTER_CAPABILITY(), BINDER_CAPABILITY(), ACTIVATOR_CAPABILITY(), CLAIMER_CAPABILITY(), FLAGGER_CAPABILITY(), RESOLVER_CAPABILITY(), RECYCLER_CAPABILITY(), VIEWER_CAPABILITY() and AUDITOR_CAPABILITY() — each the keccak256 of its name.

Binding needs an oracle signature

bindTag takes the NFC chip's challengeResponse and an ECDSA oracleSignature from the address returned by trustedOracle(). The signed digest is keccak256(abi.encodePacked(tokenId, tagHash, challengeResponse)) wrapped in the EIP-191 prefix — the contract applies toEthSignedMessageHash before ECDSA.recover, so sign the message with personal_sign / signMessage, not the raw digest.

batchBind uses a different, domain-separated digest and one signature covering the whole batch: keccak256(abi.encode(BATCH_BIND_DOMAIN, block.chainid, address(this), tokenIds, tagHashes, responseHashes)), where responseHashes[i] = keccak256(challengeResponses[i]), again EIP-191 wrapped. A bindTag signature will not satisfy batchBind.

A binding cannot be forged without a physical scan attested by that oracle. A signature that does not recover to trustedOracle() reverts with InvalidOracleSignature(); calling either function before an oracle is configured reverts with OracleNotSet().

getAsset

The main read. Returns the packed asset record for a token in a single call.

// src/core/TAGITCore.sol
function getAsset(uint256 tokenId)
    external
    view
    returns (address assetOwner, uint64 timestamp, State state, uint8 flags, uint16 reserved)

Returns:

{
  "name": "getAsset",
  "type": "function",
  "stateMutability": "view",
  "inputs": [
    { "name": "tokenId", "type": "uint256" }
  ],
  "outputs": [
    { "name": "assetOwner", "type": "address" },
    { "name": "timestamp", "type": "uint64" },
    { "name": "state", "type": "uint8" },
    { "name": "flags", "type": "uint8" },
    { "name": "reserved", "type": "uint16" }
  ]
}

A token that was never minted returns the zero address and state 0 rather than reverting, so check assetOwner != address(0) before trusting the rest.

Other view functions

function totalSupply() external view returns (uint256);
function getTokenByTag(bytes32 tagHash) external view returns (uint256);
function getTagByToken(uint256 tokenId) external view returns (bytes32);
function getResolveApprovalStatus(uint256 tokenId)
    external
    view
    returns (uint256 approvalCount, address recipient, bool quorumReached);
function metadataHash(uint256 tokenId) external view returns (bytes32);
function tokenURI(uint256 tokenId) public view returns (string memory);

getTokenByTag is the scan-to-asset lookup: hash the NFC tag UID with keccak256 and it returns the bound token ID, or 0 if that tag is not bound to anything. getTagByToken is the inverse and returns the zero hash for an unbound token.

tokenURI is authorization-gated for ITAR compliance: the asset owner and holders of VIEWER_CAPABILITY or AUDITOR_CAPABILITY get the full URI, everyone else gets a redacted URI. It is a plain view call, so the caller it checks is the from address of your eth_call.

Events

These are the events TAGITCore emits. Index them to follow an asset through its life.

event AssetMinted(uint256 indexed tokenId, address indexed to, bytes32 metadata);

event TagBound(uint256 indexed tokenId, bytes32 indexed tagHash);

event StateChanged(uint256 indexed tokenId, State from, State to, address actor);

event AssetResold(uint256 indexed tokenId, address indexed from, address indexed to);

event CustodyTransfer(
    uint256 indexed assetId,
    uint8 fromState,
    uint8 toState,
    address indexed fromOwner,
    address indexed toOwner,
    uint256 timestamp,
    bytes32 prevStateHash
);

event ResolveApproved(uint256 indexed tokenId, address indexed approver, uint256 approvalCount);

event MetadataHashUpdated(
    uint256 indexed tokenId,
    bytes32 previousHash,
    bytes32 newHash,
    address indexed updater
);

StateChanged fires on every transition. CustodyTransfer fires alongside it and carries prevStateHash = keccak256(abi.encode(assetId, uint8(fromState), fromOwner, block.number - 1)), which chains each custody record to the one before it.

{
  "name": "StateChanged",
  "type": "event",
  "inputs": [
    { "name": "tokenId", "type": "uint256", "indexed": true },
    { "name": "from", "type": "uint8", "indexed": false },
    { "name": "to", "type": "uint8", "indexed": false },
    { "name": "actor", "type": "address", "indexed": false }
  ]
}

TAGITAccess

0xb56A1D91995C212342FaA843468F03521340A1D6 — the BIDGES access controller TAGITCore consults. Capability and identity IDs are passed as uint256.

function hasCapability(address account, uint256 capabilityId) external view returns (bool);
function requireCapability(address account, uint256 capabilityId) external view;
function hasIdentity(address account, uint256 identityId) external view returns (bool);
function requireIdentity(address account, uint256 identityId) external view;
function identityBadge() external view returns (address);
function capabilityBadge() external view returns (address);

The has* pair returns a boolean; the require* pair reverts instead. Use hasCapability to decide whether to show a write action in a UI before the user pays gas for a revert. The two badge getters return the IdentityBadge and CapabilityBadge contract addresses listed on the Contract Addresses page.

TAGITToken

0x5f98B83cD7Aef769cc51D2FB739BA49D561170DETAG IT Token (TAGIT), 18 decimals. A standard ERC-20, so the usual ABI works: balanceOf, transfer, approve, allowance, transferFrom, totalSupply.

It also carries three OpenZeppelin extensions and two contract-specific functions:

mint(address,uint256) exists but is callable only by the configured emissions contract; any other caller reverts with OnlyEmissionsCanMint.

Using an ABI with ethers.js

Read an asset's state, check a capability before offering a write, and follow transitions as they happen:

import { ethers } from 'ethers';

// Built with `forge build` — see "Getting the ABIs" above
import TAGITCoreABI from './abis/TAGITCore.json';
import TAGITAccessABI from './abis/TAGITAccess.json';

// Base Sepolia, chainId 84532
const provider = new ethers.JsonRpcProvider('https://sepolia.base.org');

const core = new ethers.Contract(
  '0x3aDc7EFDb58Ae85483eFf5D4966D916185f31d1D',
  TAGITCoreABI,
  provider
);

const access = new ethers.Contract(
  '0xb56A1D91995C212342FaA843468F03521340A1D6',
  TAGITAccessABI,
  provider
);

const STATE = ['NONE', 'MINTED', 'BOUND', 'ACTIVATED', 'CLAIMED', 'FLAGGED', 'RECYCLED'];

// Read an asset
async function readAsset(tokenId) {
  const [assetOwner, timestamp, state] = await core.getAsset(tokenId);
  if (assetOwner === ethers.ZeroAddress) return null; // never minted

  return {
    owner: assetOwner,
    state: STATE[Number(state)],
    lastChange: new Date(Number(timestamp) * 1000)
  };
}

// Resolve a scanned NFC tag UID to its token ID
async function tokenForTag(tagUid) {
  const tagHash = ethers.keccak256(ethers.toUtf8Bytes(tagUid));
  const tokenId = await core.getTokenByTag(tagHash);
  return tokenId === 0n ? null : tokenId; // 0 means "not bound"
}

// Check a badge before offering a gated action
async function canMint() {
  const capability = await core.MINTER_CAPABILITY(); // keccak256("MINTER")
  return await access.hasCapability('0xYOUR_WALLET_ADDRESS', BigInt(capability));
}

// Secondary-market resale — owner-gated, needs a signer
async function resell(tokenId, buyer) {
  const signer = await new ethers.BrowserProvider(window.ethereum).getSigner();
  const tx = await core.connect(signer).transferAsset(tokenId, buyer);
  return await tx.wait();
}

// Follow lifecycle transitions
core.on('StateChanged', (tokenId, from, to, actor) => {
  console.log(
    `#${tokenId}: ${STATE[Number(from)]} -> ${STATE[Number(to)]} by ${actor}`
  );
});
Tip

On ethers.js v6 use ethers.BrowserProviderethers.providers.Web3Provider was removed in v6. Enum returns arrive as BigInt, so wrap them in Number() before indexing.

Substitute 0xYOUR_WALLET_ADDRESS with the account you want to check. The contract addresses above are the real Base Sepolia deployments and are listed on the Contract Addresses page.

Edit this page on GitHub
Type to search documentation...