ABI Reference
The function signatures and events of the TAG IT contracts deployed on Base Sepolia, taken from the Solidity source.
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 |
|---|---|---|
0 | NONE | Default — the asset does not exist |
1 | MINTED | NFT exists, no NFC tag bound yet |
2 | BOUND | NFC tag cryptographically linked |
3 | ACTIVATED | QA passed, ready for distribution |
4 | CLAIMED | Owned by an end consumer |
5 | FLAGGED | Lost, stolen, or recalled |
6 | RECYCLED | End 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.
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:
assetOwner- current owner addresstimestamp- block timestamp of the last state changestate- lifecycle state, ABI-encoded asuint8(see the table above)flags- bit flags, reserved for future usereserved- reserved for future metadata
{
"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
0x5f98B83cD7Aef769cc51D2FB739BA49D561170DE — TAG 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:
- ERC-2612 Permit —
permit(address,address,uint256,uint256,uint8,bytes32,bytes32)andnonces(address)for gasless approvals - ERC20Votes —
delegate(address),getVotes(address)and the checkpoint reads used byTAGITGovernor - ERC20Burnable —
burn(uint256)andburnFrom(address,uint256) version()— returns the contract version stringisEmissionsConfigured()— whether the emissions address has been set
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}`
);
});
On ethers.js v6 use ethers.BrowserProvider — ethers.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.