Profiles

Standard packs

The ICRC-1, ICRC-2, ERC-20 and EIP-2612 packs, and the Permit2 and DAI permit profiles: what each matches and every annotation it carries.

A pack is a profile of kind: "pack" that describes a standard rather than one canister or contract. @meantjs/profile ships four packs and, beside them, two EVM contract profiles for well-known signing contracts, all copies of profile/packs/. Every annotation they carry is listed below. Since 0.4.0 each of the six also says what its calls and messages grant, in the grant of their effect (spec §5.3, D-056; below), so every profile_id changed with it. profile_id and the checked interface are the real output of the validator, run from profile/:

bash
cd profile
node scripts/validate.mjs
text
PASS packs/dai-permit.profile.json  (0 error, 0 warning, 0 info)
     profile_id   candid-profile:v0:sha256:942de20c5e311b6cd32b02c80c6aec2a80eeee50f9bcc5846120d23fac690dc8
     interface_id candid-profile:evm-abi:v0:sha256:3ed4a3aea8a76d5194413a0196ab608414c8bf9687592f591d8ecffb53255b46  [1 methods, 8 annotations]
PASS packs/erc20.profile.json  (0 error, 0 warning, 0 info)
     profile_id   candid-profile:v0:sha256:afc7d231076699406b2fc9f74da372f5da88465027ab1d46eca526e646368a81
     interface_id candid-profile:evm-abi:v0:sha256:81471b5efd3e34c464b5dda12ccdd454d8ca6e0d0c069871c8152bc6b154be4b  [7 methods, 23 annotations]
PASS packs/erc2612.profile.json  (0 error, 0 warning, 0 info)
     profile_id   candid-profile:v0:sha256:136de06daafff15af3dc5010c97929854d8f02f72bcb71ff1ebcbe7088a2b705
     interface_id candid-profile:evm-abi:v0:sha256:73c9721a3b8abd52830805c1ac5f848f1ef8918e9cf681b5d4e134333d0955ee  [6 methods, 18 annotations]
PASS packs/icrc1.profile.json  (0 error, 0 warning, 0 info)
     profile_id   candid-profile:v0:sha256:4eaa9312d60661248ff5627dba9906aa91240abe84cb1b1da8ca20a6fba2e3e6
     interface_id candid-core:interface:v1:sha256:7fae0dd330b20f53ae1e4e2cb1947c1c57791da0ae20299ceb499f63228d0689  [5 methods, 29 annotations]
PASS packs/icrc2.profile.json  (0 error, 0 warning, 0 info)
     profile_id   candid-profile:v0:sha256:f9d75e8bf5ff2edb0b167988feee494eeb54c386e6f62252c4bd96ac56346567
     interface_id candid-core:interface:v1:sha256:c7e94a82863faf90ec6906e7b35a1b917b94a34664bb3a54cc3fca979b3bda3f  [5 methods, 56 annotations]
PASS packs/permit2.profile.json  (0 error, 0 warning, 0 info)
     profile_id   candid-profile:v0:sha256:d85261a93989a3f9cdaf7b8f3a93a3a508a388c9d8e808696bf867ace920b0c6
     interface_id candid-profile:evm-abi:v0:sha256:e013c202cb76d4b7fefbb510ee6c1a806bbbdc1eee87dfb85c6e1a520c72d646  [4 methods, 40 annotations]

The validator's methods count takes an EIP-712 message as a method; the table splits them.

FileKindStandard or contractFunctionsMessagesAnnotations
icrc1packICRC-15029
icrc2packICRC-25056
erc20packERC-20 experimental7023
erc2612packERC-2612 experimental5118
permit2contract profileUniswap's Permit2 experimental0440
dai-permitcontract profileDAI's permit experimental018

The interface_id a pack is checked against is the identity of its own fragment: the validator compiles binds.requires (or reads binds.abi, with the struct types of binds.eip712) and resolves every annotation against it. It is not the identity of any ledger or token. A ledger that matches has its own interface_id, and the pack is applied to the ledger's types. A contract profile is checked against its own binds in the same way; the two shipped here bind EIP-712 types and no function.

How a pack matches#

A Candid pack applies to a canister whose service is a subtype of the fragment in binds.requires, with result variants allowed to be wider (D-019); an EVM pack applies to a contract that has every function of its binds.abi with the same selector, output types and kind of call, with no subtyping. A matched pack is applied to the service's own types, so a position the pack names that the service lacks is skipped (II_PACK_ANNOTATION_SKIPPED); the full rules are on Profiles, layers and binding.

An EVM pack may also carry EIP-712 messages (binds.eip712, spec §15.9). It still matches by its binds.abi, and its messages then join the contract's resolved form under messages. The contract profiles permit2 and dai-permit are not packs: no pack search tries them, and they apply when given as profiles (--profile, or profiles in resolveService).

What the packs grant#

A grant names who may take tokens because of a call or a signature, how many and until when, as positions of its arguments (Tags). A wallet reads it with the consent functions and checks it against terms the person confirmed for one task (Confirm terms for a task). These are every grant the six files carry, read from their effect annotations:

FileCall or messageKindWho may take itAmountEnds
icrc1icrc1_transfertransfer"to": "to"amountnone: when it is made
icrc2icrc2_approveapprove"spender": "spender"amountexpires_at; left out, never
erc20transfertransfer"to": "to"valuenone: when it is made
erc20approveapprove"spender": "spender"valuenone: never
erc2612Permit (message), permitapprove"spender": "spender"valuenone: never
permit2PermitSingle (message)approve"spender": "spender"details.amount, in details.tokenthe latest of details.expiration and sigDeadline
permit2PermitTransferFrom (message)transfer"spender": "spender"permitted.amount, in permitted.tokendeadline
dai-permitPermit (message)approve"spender": "spender"allowed, a bool: true unlimited, false nothingnone: never

The token is the one an amount.token names (Permit2's), else the ledger or contract called. An amount at or above its tag's threshold is unlimited, and an end at or above its tag's threshold never ends. EIP-2612's deadline and DAI's expiry bound only when the signature may be used, so they are not ends: those allowances never end (spec §5.3). The other transfers and approvals carry no grant: icrc2_transfer_from and ERC-20's transferFrom; and Permit2's PermitBatch and PermitBatchTransferFrom, which grant several tokens while a grant covers one. A request without a grant is not covered by terms and needs its confirmation. A pack whose grant does not hold in the service it is applied to keeps its effect and loses the grant (II_PACK_ANNOTATION_SKIPPED); in the ledger and token runs below every grant holds, and the resolved forms carry them all.

Using the packs#

On the command line, --packs tries every pack the package ships and applies those that match:

bash
npx candid-profile resolve ledger.did --packs > ledger.resolved.json

On the ICRC ledger's own .did (release ledger-suite-icrc-2026-03-09, the copy in profile/packages/resolver/test/fixtures/ledger.did) it applies two packs and reports on stderr:

text
candid-profile: packages/resolver/test/fixtures/ledger.did  candid-core:interface:v1:sha256:22eb4787e1a755acb9aabd0ed2121d754098bcd1cedb5d0ed8b5c4effb921846  [26 methods]
  applied     icrc1            pack         candid-profile:v0:sha256:4eaa9312d60661248ff5627dba9906aa91240abe84cb1b1da8ca20a6fba2e3e6
  applied     icrc2            pack         candid-profile:v0:sha256:f9d75e8bf5ff2edb0b167988feee494eeb54c386e6f62252c4bd96ac56346567
  0 error, 0 warning, 3 info
WayWhat it does
--packsTries every pack in the packs directory and applies those that match; the others get II_PACK_NOT_MATCHED (info). A pack for the other kind of interface is skipped: the EVM packs are never tried on a .did, nor the ICRC packs on an ABI. The contract profiles in the same directory are not packs and are never tried.
--packs-dir <dir>The packs directory (default: the standard packs the package ships).
--profile pack.jsonApplies the pack as given: it must match, or the resolve fails with EE_PACK_NOT_MATCHED. A contract profile, such as permit2.profile.json, is applied this way too.
loadPacks(dir?)Every *.profile.json of kind "pack" in a directory, sorted by file name; by default DEFAULT_PACKS_DIR, the shipped packs. Packs only: erc20, erc2612, icrc1 and icrc2, never the two contract profiles. Node entry.
STANDARD_PACKSThe same four packs as data, from @meantjs/profile/browser, for a bundle that cannot read files.
@meantjs/profile/packs/*The six files themselves, as a package export (./packs/* maps to the package's data/packs/*): the four packs, and the contract profiles @meantjs/profile/packs/permit2.profile.json and @meantjs/profile/packs/dai-permit.profile.json.
js
import { createSchemaValidator, loadPacks, resolveService } from "@meantjs/profile";
import { readFileSync } from "node:fs";

const validateSchema = await createSchemaValidator(); // the schema this package ships
const packs = await loadPacks(); // the packs it ships, never its contract profiles
packs.map((p) => p.doc.name); // ["erc20", "erc2612", "icrc1", "icrc2"]
const r = await resolveService({
  did: readFileSync("ledger.did", "utf8"),
  validateSchema,
  packs: packs.map(({ doc }) => ({ doc })), // tried; applied only if they match
});
jsprofile/packages/resolver/README.md
import icrc1 from "@meantjs/profile/packs/icrc1.profile.json" with { type: "json" };
const validateSchema = await createSchemaValidator(schema);                            // a document, not a path
const r = await resolveService({ did, validateSchema, packs: [{ doc: icrc1 }] });

On an EVM token with permit, the pack search applies both EVM packs, and the EIP-2612 Permit message joins the token's resolved form. token.abi.json here is a JSON array of the ten functions of a local test token: the seven of ERC-20 and permit, nonces and DOMAIN_SEPARATOR, as human-readable fragments.

bash
npx candid-profile resolve --abi token.abi.json --packs > token.resolved.json
text
candid-profile: token.abi.json  candid-profile:evm-abi:v0:sha256:adaf687bca6190caca23583655028c78a0ad4d9fca99fe1f1efdf96758b910eb  [10 methods]
  applied     erc20            pack         candid-profile:v0:sha256:afc7d231076699406b2fc9f74da372f5da88465027ab1d46eca526e646368a81
  applied     erc2612          pack         candid-profile:v0:sha256:136de06daafff15af3dc5010c97929854d8f02f72bcb71ff1ebcbe7088a2b705
  0 error, 0 warning, 2 info

The interface_id printed is the merged form's: the ten functions and the Permit struct the EIP-2612 pack adds. A contract profile is given with --profile. The Permit2 profile binds no function, so from profile/ it resolves against an empty ABI (empty.abi.json holds []):

bash
npx candid-profile resolve --abi empty.abi.json --profile packs/permit2.profile.json > permit2.resolved.json
text
candid-profile: empty.abi.json  candid-profile:evm-abi:v0:sha256:e013c202cb76d4b7fefbb510ee6c1a806bbbdc1eee87dfb85c6e1a520c72d646  [0 methods]
  applied     permit2          third_party  candid-profile:v0:sha256:d85261a93989a3f9cdaf7b8f3a93a3a508a388c9d8e808696bf867ace920b0c6
  0 error, 0 warning, 0 info

ICRC-1#

Fungible token ledgers: transfer and balance. Spec: ICRC-1. File: profile/packs/icrc1.profile.json.

  • profile_id: profile_idcandid-profile:v0:sha256:4eaa9312d60661248ff5627dba9906aa91240abe84cb1b1da8ca20a6fba2e3e6
  • checked against (the fragment): interface_idcandid-core:interface:v1:sha256:7fae0dd330b20f53ae1e4e2cb1947c1c57791da0ae20299ceb499f63228d0689
  • validator: 0 error, 0 warning, 0 info; 5 methods, 29 annotations

Required fragment#

candidprofile/packs/icrc1.profile.json
type Account = record { owner : principal; subaccount : opt blob };
type TransferArgs = record {
  from_subaccount : opt blob;
  to : Account;
  amount : nat;
  fee : opt nat;
  memo : opt blob;
  created_at_time : opt nat64;
};
type TransferError = variant {
  BadFee : record { expected_fee : nat };
  BadBurn : record { min_burn_amount : nat };
  InsufficientFunds : record { balance : nat };
  TooOld;
  CreatedInFuture : record { ledger_time : nat64 };
  Duplicate : record { duplicate_of : nat };
  TemporarilyUnavailable;
  GenericError : record { error_code : nat; message : text };
};
service : {
  icrc1_symbol : () -> (text) query;
  icrc1_decimals : () -> (nat8) query;
  icrc1_fee : () -> (nat) query;
  icrc1_balance_of : (Account) -> (nat) query;
  icrc1_transfer : (TransferArgs) -> (variant { Ok : nat; Err : TransferError });
}

Annotations#

AddressTagArgs
icrc1_transferintenttitle: "Send tokens"
template: "Send {amount} to {to}[ from subaccount {from_subaccount}][ with memo {memo}][ paying fee {fee}]"
icrc1_transfereffectkind: "transfer"
irreversible: true
grant: { "to": "to", "amount": "amount" }
icrc1_transferidempotencymode: "replay_same_payload"
stamp: "created_at_time"
icrc1_transferoutcomeclasses:
"Ok" → success
"Err.Duplicate" → already_done
"Err.TemporarilyUnavailable" → retry_same
"Err.TooOld" → retry_new
"Err.CreatedInFuture" → retry_new
"Err.BadFee" → fix_input
"Err.BadBurn" → fix_input
"Err.InsufficientFunds" → fail
"Err.GenericError" → fail
default: fail
icrc1_transfer(0).amountamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc1_transfer(0).amountdescribelabel: "Amount"
icrc1_transfer(0).toaccountencoding: "icrc1"
icrc1_transfer(0).todescribelabel: "Recipient"
icrc1_transfer(0).from_subaccountsubaccount{}
icrc1_transfer(0).from_subaccountdescribelabel: "From subaccount"
help: "Omit to send from the default subaccount."
icrc1_transfer(0).feeamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc1_transfer(0).feedefaultvalue: "null"
mode: "auto"
icrc1_transfer(0).feedescribelabel: "Fee"
help: "Leave empty to pay the ledger's current default fee. A wrong explicit fee fails with BadFee."
icrc1_transfer(0).memobytesencoding: "utf8"
icrc1_transfer(0).memodefaultvalue: "null"
mode: "suggest"
icrc1_transfer(0).memodescribelabel: "Memo"
help: "Transfers with different memos are never deduplicated against each other. Ledgers accept at least 32 bytes; longer memos may be rejected."
icrc1_transfer(0).created_at_timetimestampunit: "ns"
epoch: "unix"
icrc1_transfer(0).created_at_timedescribelabel: "Created at"
help: "Set once per intent by the tool layer; enables the ledger's duplicate protection."
icrc1_transfer(0).created_at_timeuihidden: true
icrc1_transfer->(0).Okdescribelabel: "Block index"
icrc1_transfer->(0).Err.BadFee.expected_feeamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc1_transfer->(0).Err.BadBurn.min_burn_amountamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc1_transfer->(0).Err.InsufficientFunds.balanceamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc1_transfer->(0).Err.CreatedInFuture.ledger_timetimestampunit: "ns"
epoch: "unix"
icrc1_transfer->(0).Err.Duplicate.duplicate_ofdescribelabel: "Existing block index"
help: "The same transfer was already executed in this block."
icrc1_balance_ofdescribelabel: "Balance of an account"
icrc1_balance_of(0)accountencoding: "icrc1"
icrc1_balance_of->(0)amountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc1_fee->(0)amountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }

ICRC-2#

Approve and transfer-from. ICRC-2 ledgers also implement ICRC-1, so both packs apply to them. Spec: ICRC-2. File: profile/packs/icrc2.profile.json.

  • profile_id: profile_idcandid-profile:v0:sha256:f9d75e8bf5ff2edb0b167988feee494eeb54c386e6f62252c4bd96ac56346567
  • checked against (the fragment): interface_idcandid-core:interface:v1:sha256:c7e94a82863faf90ec6906e7b35a1b917b94a34664bb3a54cc3fca979b3bda3f
  • validator: 0 error, 0 warning, 0 info; 5 methods, 56 annotations

Required fragment#

candidprofile/packs/icrc2.profile.json
type Account = record { owner : principal; subaccount : opt blob };
type ApproveArgs = record {
  from_subaccount : opt blob;
  spender : Account;
  amount : nat;
  expected_allowance : opt nat;
  expires_at : opt nat64;
  fee : opt nat;
  memo : opt blob;
  created_at_time : opt nat64;
};
type ApproveError = variant {
  BadFee : record { expected_fee : nat };
  InsufficientFunds : record { balance : nat };
  AllowanceChanged : record { current_allowance : nat };
  Expired : record { ledger_time : nat64 };
  TooOld;
  CreatedInFuture : record { ledger_time : nat64 };
  Duplicate : record { duplicate_of : nat };
  TemporarilyUnavailable;
  GenericError : record { error_code : nat; message : text };
};
type TransferFromArgs = record {
  spender_subaccount : opt blob;
  from : Account;
  to : Account;
  amount : nat;
  fee : opt nat;
  memo : opt blob;
  created_at_time : opt nat64;
};
type TransferFromError = variant {
  BadFee : record { expected_fee : nat };
  BadBurn : record { min_burn_amount : nat };
  InsufficientFunds : record { balance : nat };
  InsufficientAllowance : record { allowance : nat };
  TooOld;
  CreatedInFuture : record { ledger_time : nat64 };
  Duplicate : record { duplicate_of : nat };
  TemporarilyUnavailable;
  GenericError : record { error_code : nat; message : text };
};
type AllowanceArgs = record { account : Account; spender : Account };
service : {
  icrc1_symbol : () -> (text) query;
  icrc1_decimals : () -> (nat8) query;
  icrc2_approve : (ApproveArgs) -> (variant { Ok : nat; Err : ApproveError });
  icrc2_transfer_from : (TransferFromArgs) -> (variant { Ok : nat; Err : TransferFromError });
  icrc2_allowance : (AllowanceArgs) -> (record { allowance : nat; expires_at : opt nat64 }) query;
}

Annotations#

AddressTagArgs
icrc2_approveintenttitle: "Approve spending"
template: "Set {spender}'s allowance to {amount}, replacing any existing allowance[ (only if the current allowance is {expected_allowance})][ until {expires_at}][ from subaccount {from_subaccount}][ with memo {memo}]"
icrc2_approveeffectkind: "approve"
irreversible: false
grant: { "spender": "spender", "amount": "amount", "ends": ["expires_at"] }
icrc2_approveidempotencymode: "replay_same_payload"
stamp: "created_at_time"
icrc2_approveoutcomeclasses:
"Ok" → success
"Err.Duplicate" → already_done
"Err.TemporarilyUnavailable" → retry_same
"Err.TooOld" → retry_new
"Err.CreatedInFuture" → retry_new
"Err.BadFee" → fix_input
"Err.AllowanceChanged" → fix_input
"Err.Expired" → fix_input
"Err.InsufficientFunds" → fail
"Err.GenericError" → fail
default: fail
icrc2_approve(0).spenderaccountencoding: "icrc1"
icrc2_approve(0).spenderdescribelabel: "Spender"
icrc2_approve(0).amountamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_approve(0).amountdescribelabel: "Allowance"
help: "Maximum the spender may transfer. An approval replaces the previous allowance; it does not add to it."
icrc2_approve(0).expected_allowanceamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_approve(0).expected_allowancedescribelabel: "Expected current allowance"
help: "Optional safety check: the call fails with AllowanceChanged if the current allowance differs."
icrc2_approve(0).expires_attimestampunit: "ns"
epoch: "unix"
icrc2_approve(0).expires_atdescribelabel: "Expires at"
help: "Omit for an allowance that does not expire."
icrc2_approve(0).from_subaccountsubaccount{}
icrc2_approve(0).from_subaccountdescribelabel: "From subaccount"
icrc2_approve(0).feeamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_approve(0).feedefaultvalue: "null"
mode: "auto"
icrc2_approve(0).memobytesencoding: "utf8"
icrc2_approve(0).memodefaultvalue: "null"
mode: "suggest"
icrc2_approve(0).memodescribelabel: "Memo"
help: "Ledgers accept at least 32 bytes; longer memos may be rejected."
icrc2_approve(0).created_at_timetimestampunit: "ns"
epoch: "unix"
icrc2_approve(0).created_at_timeuihidden: true
icrc2_approve->(0).Okdescribelabel: "Block index"
icrc2_approve->(0).Err.BadFee.expected_feeamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_approve->(0).Err.InsufficientFunds.balanceamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_approve->(0).Err.AllowanceChanged.current_allowanceamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_approve->(0).Err.Expired.ledger_timetimestampunit: "ns"
epoch: "unix"
icrc2_approve->(0).Err.CreatedInFuture.ledger_timetimestampunit: "ns"
epoch: "unix"
icrc2_transfer_fromintenttitle: "Transfer using allowance"
template: "Move {amount} from {from} to {to} using an allowance[ (spender subaccount {spender_subaccount})][ with memo {memo}]"
icrc2_transfer_fromeffectkind: "transfer"
irreversible: true
icrc2_transfer_fromidempotencymode: "replay_same_payload"
stamp: "created_at_time"
icrc2_transfer_fromoutcomeclasses:
"Ok" → success
"Err.Duplicate" → already_done
"Err.TemporarilyUnavailable" → retry_same
"Err.TooOld" → retry_new
"Err.CreatedInFuture" → retry_new
"Err.BadFee" → fix_input
"Err.BadBurn" → fix_input
"Err.InsufficientFunds" → fail
"Err.InsufficientAllowance" → fail
"Err.GenericError" → fail
default: fail
icrc2_transfer_from(0).fromaccountencoding: "icrc1"
icrc2_transfer_from(0).fromdescribelabel: "From"
icrc2_transfer_from(0).toaccountencoding: "icrc1"
icrc2_transfer_from(0).todescribelabel: "Recipient"
icrc2_transfer_from(0).amountamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_transfer_from(0).amountdescribelabel: "Amount"
icrc2_transfer_from(0).spender_subaccountsubaccount{}
icrc2_transfer_from(0).spender_subaccountdescribelabel: "Spender subaccount"
icrc2_transfer_from(0).feeamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_transfer_from(0).feedefaultvalue: "null"
mode: "auto"
icrc2_transfer_from(0).memobytesencoding: "utf8"
icrc2_transfer_from(0).memodefaultvalue: "null"
mode: "suggest"
icrc2_transfer_from(0).memodescribelabel: "Memo"
help: "Ledgers accept at least 32 bytes; longer memos may be rejected."
icrc2_transfer_from(0).created_at_timetimestampunit: "ns"
epoch: "unix"
icrc2_transfer_from(0).created_at_timeuihidden: true
icrc2_transfer_from->(0).Okdescribelabel: "Block index"
icrc2_transfer_from->(0).Err.BadFee.expected_feeamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_transfer_from->(0).Err.BadBurn.min_burn_amountamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_transfer_from->(0).Err.InsufficientFunds.balanceamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_transfer_from->(0).Err.InsufficientAllowance.allowanceamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_transfer_from->(0).Err.CreatedInFuture.ledger_timetimestampunit: "ns"
epoch: "unix"
icrc2_allowance(0).accountaccountencoding: "icrc1"
icrc2_allowance(0).spenderaccountencoding: "icrc1"
icrc2_allowance->(0).allowanceamountdecimals: { "from": "query:icrc1_decimals" }
symbol: { "from": "query:icrc1_symbol" }
icrc2_allowance->(0).expires_attimestampunit: "ns"
epoch: "unix"

ERC-20#

Experimental

EVM support is spec §15, phase 1 and the first part of phase 2: profiles, consent text for calldata and for EIP-712 signing requests, and ERC-7730 export. See EVM contracts.

EIP-20 tokens on EVM chains. Spec: EIP-20. File: profile/packs/erc20.profile.json. Parameter names may differ from the pack's: they are matched by position, and the pack's addresses and placeholders are rewritten to the contract's names. name() is left out: no annotation reads it and EIP-20 makes it optional. fact EIP-20 makes decimals() and symbol() optional too, so a token without them matches no pack and needs a contract profile with literal values.

  • profile_id: profile_idcandid-profile:v0:sha256:afc7d231076699406b2fc9f74da372f5da88465027ab1d46eca526e646368a81
  • checked against (the ABI): interface_idcandid-profile:evm-abi:v0:sha256:81471b5efd3e34c464b5dda12ccdd454d8ca6e0d0c069871c8152bc6b154be4b
  • validator: 0 error, 0 warning, 0 info; 7 methods, 23 annotations

Required functions#

jsonprofile/packs/erc20.profile.json
[
  "function symbol() view returns (string)",
  "function decimals() view returns (uint8)",
  "function balanceOf(address owner) view returns (uint256)",
  "function allowance(address owner, address spender) view returns (uint256)",
  "function transfer(address to, uint256 value) returns (bool)",
  "function approve(address spender, uint256 value) returns (bool)",
  "function transferFrom(address from, address to, uint256 value) returns (bool)"
]

Annotations#

AddressTagArgs
transferintenttitle: "Send tokens"
template: "Send {value} to {to}"
transfereffectkind: "transfer"
irreversible: true
grant: { "to": "to", "amount": "value" }
transfer(0).valueamountdecimals: { "from": "query:decimals" }
symbol: { "from": "query:symbol" }
transfer(0).valuedescribelabel: "Amount"
transfer(0).todescribelabel: "Recipient"
approveintenttitle: "Approve spending"
template: "Set {spender}'s allowance to {value}, replacing any existing allowance"
approveeffectkind: "approve"
irreversible: false
grant: { "spender": "spender", "amount": "value" }
approve(0).spenderdescribelabel: "Spender"
approve(0).valueamountdecimals: { "from": "query:decimals" }
symbol: { "from": "query:symbol" }
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
message: "Unlimited"
approve(0).valuedescribelabel: "Allowance"
help: "Maximum the spender may transfer. An approval replaces the previous allowance; it does not add to it."
transferFromintenttitle: "Transfer using allowance"
template: "Move {value} from {from} to {to} using an allowance"
transferFromeffectkind: "transfer"
irreversible: true
transferFrom(0).fromdescribelabel: "From"
transferFrom(0).todescribelabel: "Recipient"
transferFrom(0).valueamountdecimals: { "from": "query:decimals" }
symbol: { "from": "query:symbol" }
transferFrom(0).valuedescribelabel: "Amount"
balanceOfdescribelabel: "Balance of an address"
balanceOf(0).ownerdescribelabel: "Owner"
balanceOf->(0)amountdecimals: { "from": "query:decimals" }
symbol: { "from": "query:symbol" }
allowancedescribelabel: "Allowance of a spender"
allowance(0).ownerdescribelabel: "Owner"
allowance(0).spenderdescribelabel: "Spender"
allowance->(0)amountdecimals: { "from": "query:decimals" }
symbol: { "from": "query:symbol" }
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
message: "Unlimited"

The threshold on approve(0).value and allowance->(0) is 2256 − 1, the largest uint256: an approval of that value displays as Unlimited and the symbol.

ERC-2612#

Experimental

EIP-712 messages are spec §15.9 (D-055), the first part of the EVM binding's phase 2: profiles, consent text for a signing request and an ERC-7730 file of the messages. Signing and checking signatures are not in it. See EVM contracts.

EIP-2612 permits on EVM chains: the signed Permit message that sets an ERC-20 allowance, and the permit function that submits one. Spec: EIP-2612. File: profile/packs/erc2612.profile.json. It applies to a token that has every function of its binds.abi with the same selector, outputs and kind of call, decimals() and symbol() included, since the amounts read them. A signing request matches Permit by its type hash, and its verifying contract is the token, so the query: refs are calls to that token. The pack binds no domain: each token's name and version are its own. fact (EIP-2612) "The deadline argument can be set to uint(-1) to create Permits that effectively never expire", so a deadline of 2256 − 1 reads Never.

  • profile_id: profile_idcandid-profile:v0:sha256:136de06daafff15af3dc5010c97929854d8f02f72bcb71ff1ebcbe7088a2b705
  • checked against (the ABI and the struct types): interface_idcandid-profile:evm-abi:v0:sha256:73c9721a3b8abd52830805c1ac5f848f1ef8918e9cf681b5d4e134333d0955ee
  • validator: 0 error, 0 warning, 0 info; 6 methods (5 functions and 1 message), 18 annotations

Required functions and types#

jsonprofile/packs/erc2612.profile.json
"abi": [
  "function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
  "function nonces(address owner) view returns (uint256)",
  "function DOMAIN_SEPARATOR() view returns (bytes32)",
  "function decimals() view returns (uint8)",
  "function symbol() view returns (string)"
],
"eip712": {
  "types": {
    "Permit": [
      {
        "name": "owner",
        "type": "address"
      },
      {
        "name": "spender",
        "type": "address"
      },
      {
        "name": "value",
        "type": "uint256"
      },
      {
        "name": "nonce",
        "type": "uint256"
      },
      {
        "name": "deadline",
        "type": "uint256"
      }
    ]
  }
}
Message and effectencode_type and type_hash
Permit
approve
Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)
0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9

Annotations#

AddressTagArgs
Permitintenttitle: "Permit token spending"
template: "Allow {spender} to spend up to {value} from {owner}. Signature expires: {deadline}."
Permiteffectkind: "approve"
irreversible: false
grant: { "spender": "spender", "amount": "value" }
Permit(0).valueamountdecimals: { "from": "query:decimals" }
symbol: { "from": "query:symbol" }
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
Permit(0).valuedescribelabel: "Amount"
Permit(0).deadlinetimestampunit: "s"
epoch: "unix"
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
message: "Never"
Permit(0).deadlinedescribelabel: "Signature expires"
Permit(0).ownerdescribelabel: "Owner"
Permit(0).spenderdescribelabel: "Spender"
Permit(0).noncedescribelabel: "Nonce"
permitintenttitle: "Submit a token permit"
template: "Set {spender}'s allowance from {owner} to {value} with {owner}'s signature. Signature expires: {deadline}."
permiteffectkind: "approve"
irreversible: false
grant: { "spender": "spender", "amount": "value" }
permit(0).valueamountdecimals: { "from": "query:decimals" }
symbol: { "from": "query:symbol" }
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
permit(0).valuedescribelabel: "Amount"
permit(0).deadlinetimestampunit: "s"
epoch: "unix"
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
message: "Never"
permit(0).deadlinedescribelabel: "Signature expires"
permit(0).ownerdescribelabel: "Owner"
permit(0).spenderdescribelabel: "Spender"
permit(0).vdescribelabel: "Signature v"

The threshold on both values and both deadlines is 2256 − 1: an amount of that value displays as Unlimited and the symbol (the default message), and a deadline of that value as Never. The grant of Permit and of permit names no ends: the deadline bounds only when the signature may be used, and the allowance it sets never ends (spec §5.3).

Permit2#

experimental Uniswap's Permit2, as a contract profile (kind: "contract", origin: "third_party"), not a pack. File: profile/packs/permit2.profile.json. It describes four messages and no function: the AllowanceTransfer messages PermitSingle and PermitBatch, which set allowances (approve), and the SignatureTransfer messages PermitTransferFrom and PermitBatchTransferFrom, which authorize one transfer (transfer irreversible). Each amount is in the token its message names (amount.token): a consumer supplies that token's decimals() and symbol(), and shows its address beside the symbol.

It is bound to the domain named Permit2, with no version, and to 0x0000…8BA3 on chains 1 and 42161, so a look-alike contract named "Permit2" gets none of these words (EE_DEPLOYMENT). fact ERC-7730's own Permit2 example lists these deployments (spec §15.9); its other chains are open item O10. fact (Uniswap permit2, Allowance.sol and AllowanceTransfer.sol) "If the inputted expiration is 0, the allowance only lasts the duration of the block", so an expiration of 0 reads Only in the block it is used; an allowance of type(uint160).max is never decremented, so it reads Unlimited. Witness messages (PermitWitnessTransferFrom) are left out: each protocol adds its own witness type (open O9, on Open questions).

  • profile_id: profile_idcandid-profile:v0:sha256:d85261a93989a3f9cdaf7b8f3a93a3a508a388c9d8e808696bf867ace920b0c6
  • checked against (its struct types): interface_idcandid-profile:evm-abi:v0:sha256:e013c202cb76d4b7fefbb510ee6c1a806bbbdc1eee87dfb85c6e1a520c72d646
  • validator: 0 error, 0 warning, 0 info; 4 methods (4 messages), 40 annotations

Domain and deployments#

jsonprofile/packs/permit2.profile.json
"eip712": {
  "domain": {
    "name": "Permit2"
  },
  "types": {
    …
  }
},
"deployments": [
  {
    "chain_id": 1,
    "address": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
  },
  {
    "chain_id": 42161,
    "address": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
  }
]

Messages#

Six struct types; the four that the profile tags with intent are messages, and PermitDetails and TokenPermissions are types inside them. Each encode_type lists the primary type, then the structs it refers to:

Message and effectencode_type and type_hash
PermitSingle
approve
PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)
0xf3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d0
PermitBatch
approve
PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)
0xaf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f79440863
PermitTransferFrom
transfer irreversible
PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)
0x939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d80106
PermitBatchTransferFrom
transfer irreversible
PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)
0xfcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b766

Annotations#

AddressTagArgs
PermitSingleintenttitle: "Permit2: allow token spending"
template: "Let {spender} spend up to {details.amount}. Allowance expires: {details.expiration}. Signature expires: {sigDeadline}."
PermitSingleeffectkind: "approve"
irreversible: false
grant: { "spender": "spender", "amount": "details.amount", "ends": ["details.expiration", "sigDeadline"] }
PermitSingle(0).details.tokendescribelabel: "Token"
PermitSingle(0).details.amountamounttoken: "details.token"
threshold: "1461501637330902918203684832716283019655932542975"
PermitSingle(0).details.amountdescribelabel: "Amount"
PermitSingle(0).details.expirationtimestampunit: "s"
epoch: "unix"
threshold: "281474976710655"
message: "Never"
labels: { "0": "Only in the block it is used" }
PermitSingle(0).details.expirationdescribelabel: "Allowance expires"
PermitSingle(0).details.noncedescribelabel: "Nonce"
PermitSingle(0).spenderdescribelabel: "Spender"
PermitSingle(0).sigDeadlinetimestampunit: "s"
epoch: "unix"
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
message: "Never"
PermitSingle(0).sigDeadlinedescribelabel: "Signature expires"
PermitBatchintenttitle: "Permit2: allow spending of several tokens"
template: "Let {spender} spend up to {details[].amount}. Allowances expire, in the same order: {details[].expiration}. Signature expires: {sigDeadline}."
PermitBatcheffectkind: "approve"
irreversible: false
PermitBatch(0).details[].tokendescribelabel: "Token"
PermitBatch(0).details[].amountamounttoken: "details[].token"
threshold: "1461501637330902918203684832716283019655932542975"
PermitBatch(0).details[].amountdescribelabel: "Amount"
PermitBatch(0).details[].expirationtimestampunit: "s"
epoch: "unix"
threshold: "281474976710655"
message: "Never"
labels: { "0": "Only in the block it is used" }
PermitBatch(0).details[].expirationdescribelabel: "Allowance expires"
PermitBatch(0).details[].noncedescribelabel: "Nonce"
PermitBatch(0).spenderdescribelabel: "Spender"
PermitBatch(0).sigDeadlinetimestampunit: "s"
epoch: "unix"
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
message: "Never"
PermitBatch(0).sigDeadlinedescribelabel: "Signature expires"
PermitTransferFromintenttitle: "Permit2: allow one transfer"
template: "Let {spender} transfer up to {permitted.amount} from you, once. Signature expires: {deadline}."
PermitTransferFromeffectkind: "transfer"
irreversible: true
grant: { "spender": "spender", "amount": "permitted.amount", "ends": ["deadline"] }
PermitTransferFrom(0).permitted.tokendescribelabel: "Token"
PermitTransferFrom(0).permitted.amountamounttoken: "permitted.token"
PermitTransferFrom(0).permitted.amountdescribelabel: "Up to"
PermitTransferFrom(0).spenderdescribelabel: "Spender"
PermitTransferFrom(0).noncedescribelabel: "Nonce"
PermitTransferFrom(0).deadlinetimestampunit: "s"
epoch: "unix"
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
message: "Never"
PermitTransferFrom(0).deadlinedescribelabel: "Signature expires"
PermitBatchTransferFromintenttitle: "Permit2: allow one transfer of several tokens"
template: "Let {spender} transfer up to {permitted[].amount} from you, once. Signature expires: {deadline}."
PermitBatchTransferFromeffectkind: "transfer"
irreversible: true
PermitBatchTransferFrom(0).permitted[].tokendescribelabel: "Token"
PermitBatchTransferFrom(0).permitted[].amountamounttoken: "permitted[].token"
PermitBatchTransferFrom(0).permitted[].amountdescribelabel: "Up to"
PermitBatchTransferFrom(0).spenderdescribelabel: "Spender"
PermitBatchTransferFrom(0).noncedescribelabel: "Nonce"
PermitBatchTransferFrom(0).deadlinetimestampunit: "s"
epoch: "unix"
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
message: "Never"
PermitBatchTransferFrom(0).deadlinedescribelabel: "Signature expires"

The thresholds are the largest value of each type: 2160 − 1 for an allowance (uint160), 248 − 1 for an expiration (uint48) and 2256 − 1 for a signature deadline (uint256). The two SignatureTransfer amounts have no threshold.

PermitSingle's grant ends at the latest of details.expiration and sigDeadline, and PermitTransferFrom's at its deadline; each grant's token is the one its message names. fact (Uniswap permit2 at main, read 2026-10-30, src/libraries/Allowance.sol, as spec §15.9 records) An expiration of 0 is stored as the time of the block the permit is used in, and that block may come as late as sigDeadline; the latest of the two is the end, so 0 needs no rule of its own. The batch messages carry no grant: a grant covers one token.

DAI permit#

experimental DAI's permit on Ethereum mainnet, older than EIP-2612 and different from it, as a contract profile (origin: "third_party"). File: profile/packs/dai-permit.profile.json. Only the message is described: DAI's functions are ERC-20's, and the ERC-20 pack covers them. It is bound to the domain Dai Stablecoin version 1 and to 0x6B17…1d0F on chain 1 (fact MCD_DAI in the Sky chainlog, read 2026-10-28, as spec §15.9 records).

fact (MakerDAO dss/src/dai.sol, as spec §15.9 records) An expiry of 0 never expires, allowed sets the allowance to uint(-1) or 0, and PERMIT_TYPEHASH is 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb, which the resolver's hash matches. So expiry reads Never at 0 (a label) and at 2256 − 1 (the threshold). DAI's Permit shares its name with EIP-2612's and not its type hash, and a request is matched by the hash. allowed reads through the template; a case-like reading of true and false is open item O12. The grant's amount is allowed itself: true grants an unlimited allowance and false grants nothing; expiry bounds only the signature, so the grant never ends (spec §5.3).

  • profile_id: profile_idcandid-profile:v0:sha256:942de20c5e311b6cd32b02c80c6aec2a80eeee50f9bcc5846120d23fac690dc8
  • checked against (its struct type): interface_idcandid-profile:evm-abi:v0:sha256:3ed4a3aea8a76d5194413a0196ab608414c8bf9687592f591d8ecffb53255b46
  • validator: 0 error, 0 warning, 0 info; 1 method (1 message), 8 annotations

Message#

Message and effectencode_type and type_hash
Permit
approve
Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)
0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb

Its binds is quoted whole on Profile and resolved form, under binds.eip712.

Annotations#

AddressTagArgs
Permitintenttitle: "Permit DAI spending"
template: "Set the DAI allowance {holder} gives {spender} to all of it if allowed is true, to none if false (allowed: {allowed}). Signature expires: {expiry}."
Permiteffectkind: "approve"
irreversible: false
grant: { "spender": "spender", "amount": "allowed" }
Permit(0).holderdescribelabel: "Holder"
Permit(0).spenderdescribelabel: "Spender"
Permit(0).alloweddescribelabel: "Allowed (true: all, false: none)"
Permit(0).expirytimestampunit: "s"
epoch: "unix"
threshold: "115792089237316195423570985008687907853269984665640564039457584007913129639935"
message: "Never"
labels: { "0": "Never" }
Permit(0).expirydescribelabel: "Signature expires"
Permit(0).noncedescribelabel: "Nonce"