--- eip: 8378 title: Parametric Token description: An extension of ERC-20 enabling fungible tokens with account-specific mutable or immutable parameters and sub-accounts. author: Alexander Zvezdin (@k2eno) discussions-to: https://ethereum-magicians.org/t/erc-8378-parametric-token/29385 status: Draft type: Standards Track category: ERC created: 2026-08-10 requires: 20, 165 --- ## Abstract This proposal introduces a standard for parametric ERC‑20 tokens—tokens that carry additional, account‑specific parameters beyond simple balances. Parameters may be **mutable** (updated during transfers according to deterministic mutation rules, e.g., weighted averages) or **immutable** (must remain identical across all tokens that can coexist in the same account). The standard defines an account model that supports **sub‑accounts** (partitions) within a single address, enabling a user to hold multiple parameter variants without requiring multiple wallets. It also extends the ERC‑20 allowance system to support sub‑account‑specific approvals with a one‑off consumption option. The standard is **interface-compatible** with ERC‑20: all standard functions are present. Where semantics differ, the standard provides built-in detection and query functions, making integration unambiguous. ## Motivation The parametric token standard is a primitive for the next generation of ERC-20 compatible DeFi and agentic finance: assets that carry state, liquidity that consolidates rather than fragments, value that can be containerized, and tokenomics engineered at the parameter level to support both human and autonomous, self-improving value transfer. ### Liquidity Consolidation Existing asset price predictions use a multi-binary approach: users bet yes or no across discrete outcome pools, which fragments liquidity and distorts the prediction itself, forcing users to select from preset buckets rather than simply quoting an expected price. Tokens with different mutable parameters (e.g., price predictions) can trade in the same environment because the contract manages parameter logic and allows tokens to merge in the same account. No need to split markets into separate pools per parameter value. This capability is unique to the parametric model. ### Velocity Control Parameters allow deliberate control of token turnover: - **Utility‑bearing tokens** (hedging instruments, access passes) benefit from higher velocity. Progressive age‑based fees stimulate higher turnover: users who no longer need the utility sell the token to avoid extra costs. - **Yield‑bearing tokens** (staking, governance, credentials, RWAs) benefit from user loyalty (i.e. lower velocity). Progressive age‑based rewards incentivise long‑term holding. Age‑weighted rewards or voting power align incentives with genuine commitment. ### Advanced Derivatives The Bundle construct (convex portfolio of WBTC and its inverse) demonstrates how parametric tokens enable sophisticated synthetic instruments out‑of‑the‑box, with robust, abuse-resistant mechanics. Token‑controlled parameters allow institutions to launch new derivative types, including RWA extensions. ### Agentic & Autonomous Systems Parametric tokens provide a secure evaluation and execution framework for autonomous agents. Agents implement logic, while parametric tokens serve as the rails - enabling agents to shape solutions, generate value, and channel it through the token's parameter topology on a competitive basis. Unlike static tokens, parametric tokens allow agents to keep track of these value flows, encoding strategy outcomes directly into the token state. The Bundle capability further extends this: agents can now containerize value, creating compound structures that represent complex strategies or aggregated positions. This aligns with the agentic economy's core principles—utility generation, capital efficiency, and just-in-time token usage—making parametric tokens an intrinsic enabler for the next generation of autonomous finance. ### UX Simplification Sub‑accounts let users hold multiple parameter variants in a single address – no need for multiple wallets. Fine‑grained permissions (sub‑account‑specific, one‑off allowances) give precise control over delegated spending. ## Specification The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174\. ### ⚠️ Core Design Principle: Separation of Ledger Accounting and Economic Value The standard treats parameters as abstract, uninterpreted numerical structures (`uint64[]`). It is entirely agnostic to economic meaning, game-theoretic impacts, or market valuation. Its sole invariant is deterministic preservation, partitioning, and mutation of the parameter matrix. The reference implementations are minimally complex execution proofs that intentionally abstract away all production layers: minting is permissionless, rewards are illustrative, and economic safeguards (e.g., inbound allowances, collateralization, oracle cross-checks) are left to external controllers. Implementers **MUST** build their own security and economic perimeters on top of this standard. ### Definitions - **Parameter**: An attribute associated with a **non-zero** token balance (account or sub‑account). Parameters are stored as `uint64` values (may represent timestamps, prices, or any numeric value). The token contract defines a fixed number of parameters via `NUMBER_OF_PARAMETERS`. - **Parameter Mutability**: - **Mutable**: The parameter value changes during token transfers according to token‑specific rules (e.g., weighted average, max operation). The token contract MUST implement the mutation logic inside its transfer functions. - **Immutable**: The parameter value never changes. Tokens with different immutable parameter values MUST NOT be allowed to coexist in the same account or sub‑account; the token contract SHALL revert any transfer that would cause such a conflict. - **Account**: An Ethereum address that holds tokens. Accounts can be of two types: - **Normal**: Holds a single balance and a single set of parameters. - **Super**: Holds multiple sub‑accounts, each with its own balance and parameters. - **Sub‑account**: A partition within a Super account, identified by a `uint48` index (starting from `0`). - **Default Sub‑account**: The sub‑account with index `0`, created automatically upon account conversion to Super. - **Allowance**: Standard allowance is divided into two components: - **General**: The part of total allowance that can be spent from any sub‑account except the one identified by `subId` in allowance record. Represented by (`total - sub`) expression. For Normal accounts and Super accounts with a single sub‑account, this is the only usable allowance. - **Specific**: The part of total allowance that is restricted to a particular sub‑account identified by `subId` in allowance record. Represented by the `sub` field. Non-zero Specific allowances SHALL only apply to sub-accounts with `subId > 0`. - **Transfer**: Two types of transfers are considered: - **Zero‑Sum**: A default parametric transfer where the amount debited from the sender's balance MUST equal the amount credited to the recipient's balance (`debitAmount == creditAmount`). - **Non‑Zero‑Sum (NZS)**: A transfer of a parametric token where the amount debited from the sender's balance MAY not equal the amount credited to the recipient's balance (`debitAmount != creditAmount`) is qualified as non-zero-sum (NZS) transfer. NZS tokens MUST implement the optional `IParametricTokenNzs` interface. ### Required Data Model Compliant implementations MUST maintain the following data structures to ensure consistent parameter tracking and sub‑account management. ```solidity struct ParamConfig { bytes32 name; // Human‑readable identifier (e.g., "mintTime", "anchor") uint8 decimals; // Number of decimals for display bool isMutable; // True if parameter changes during transfers } struct Account { AccountType accountType; // Normal or Super uint256 balance; // Total balance across all sub‑accounts uint64[NUMBER_OF_PARAMETERS] parameters; // Parameters for Normal accounts } struct SubAccount { uint256 balance; // Balance of this sub‑account uint64[NUMBER_OF_PARAMETERS] parameters; // Parameters for this sub‑account } struct SuperAccount { SubAccount[] subs; // Array of sub‑accounts uint48 subsCount; // Number of sub‑accounts } struct Allowance { uint256 total; // Total allowance across all sub‑accounts uint256 sub; // Allowance for subId uint48 subId; // Sub‑account this allowance applies to bool oneOff; // True if this is a one‑time allowance uint64 committedUntil; // If not 0, sub allowance can't be withdrawn until the deadline } ``` #### Required Storage Mappings The following mappings are REQUIRED: ```solidity mapping(address => Account) private _accounts; mapping(address => SuperAccount) private _supers; mapping(address => mapping(address => Allowance)) private _allowances; ``` - `_accounts[address]` holds the account data (type, balance, parameters), - `_supers[address]` holds the sub‑account array if the account is Super, - `_allowances[owner][spender]` holds the allowance record. #### Invariants - If `_accounts[addr].accountType == AccountType.Normal`, then `_supers[addr].subsCount` MUST be 0, - If `_accounts[addr].accountType == AccountType.Super`, then `_supers[addr].subsCount > 0` and `_supers[addr].subs.length == _supers[addr].subsCount`, - `_accounts[addr].balance == sum(_supers[addr].subs[i].balance)` for Super accounts, - `_accounts[addr].balance` MUST equal `balanceOf(addr)`. ### Interface #### Main Interface Every compliant Parametric Token MUST implement the following `IParametricToken` interface, in addition to the standard ERC‑20 interface (`IERC20`). ```solidity interface IParametricToken is IERC20 { // Constants enum AccountType { Normal, Super } // Events /** * @notice Emitted when a Normal account is converted to a Super account. * @dev This creates the default sub-account `0`, transferring the existing * balance and parameters to it. The account's type is permanently changed. * Can only be triggered by the account owner. * @param account The address of the account that was converted to Super */ event AccountConvertedToSuper(address indexed account); /** * @notice Emitted when a new sub-account is created within a Super account. * @dev The new sub-account is initialized with zero balance and default * parameters. Can only be triggered by the Super account owner. * @param superAccount The address of the Super account that owns the new sub-account * @param subId The index of the newly created sub-account */ event SubAccountCreated(address indexed superAccount, uint48 indexed subId); /** * @notice Emitted when a zero-sum parametric token transfer occurs. * @dev The standard ERC-20 `Transfer` event MUST also be emitted using the `amount`. * This event SHOULD NOT be emitted for NZS token transfers, instead `ParametricTransferNzs` * SHALL be emitted. * @param from The sender address * @param fromSubId The sender's sub-account * @param to The recipient address * @param toSubId The recipient's sub-account * @param amount The exact amount added to the recipient's balance * @param resultingParams The full parameter array of the receiver AFTER parameters update */ event ParametricTransfer( address indexed from, uint48 indexed fromSubId, address indexed to, uint48 toSubId, uint256 amount, uint64[] resultingParams ); /** * @notice Emitted when a sub-account specific allowance is set or updated. * @dev This allowance applies specifically to `subId`. If `oneOff` is `true`, * the allowance is consumed entirely after the first non-zero spend from * that sub-account. The general allowance (`total - sub`) is adjusted to * ensure `total >= sub`. Standard ERC-20 `Approval` events remain unaffected. * @param owner The address of the token owner * @param subId The sub-account for which the allowance is granted * @param spender The address authorized to spend the tokens * @param amount The specific allowance amount for the sub-account * @param oneOff `true` if the allowance is one-time use * @param committedUntil Future time if allowance is committed, otherwise 0 */ event ApprovalForSub( address indexed owner, uint48 indexed subId, address indexed spender, uint256 amount, bool oneOff, uint64 committedUntil ); // Interface detection /** * @notice ERC‑165 interface detection * @dev MUST return `true` for `type(IParametricToken).interfaceId` * @param interfaceId The interface identifier * @return bool True if the interface is supported */ function supportsInterface(bytes4 interfaceId) external view returns (bool); // Account settings /** * @notice Returns the total number of parameters defined by this token. * @dev MUST be a constant value. */ function NUMBER_OF_PARAMETERS() external view returns (uint8); //////////// /** * @notice Returns metadata for all parameters defined by this token * @return ParamConfig[] Array of metadata sets (name, decimals, isMutable) */ function paramConfig() external view returns (ParamConfig[] memory); // Account management /** * @notice Converts the caller's account from Normal to Super * @dev This creates sub-account 0 with the current balance and parameters, * and clears the Normal account parameters * @return true if the conversion succeeded */ function convertToSuper() external returns (bool); /** * @notice Creates a new sub-account for a Super account * @dev Creates new sub-account to the owner's Super account * with zero balance and initial (zero) parameters * @return uint48 The index of the newly created sub-account */ function createSubAccount() external returns (uint48); /** * @notice Returns true if the account is a Super account * @dev This is a compatibility helper for ERC‑20 wallets * @param account The address to query * @return bool True if the account is a Super account */ function isSuperAccount(address account) external view returns (bool); // Sub-account queries /** * @notice Returns the balance of a Normal account or specific sub-account * @dev For Normal accounts, subId must be 0. For Super accounts, * the subId must correspond to an existing sub-account * @param account The address of the account (Normal or Super) * @param subId The index of the sub-account (0 for Normal accounts) * @return uint256 The balance of the specified sub-account */ function parametricBalanceOf( address account, uint48 subId ) external view returns (uint256); /** * @notice Returns the number of sub-accounts for an account * @dev Returns 0 if the account is not Super * @param account The account to query * @return uint48 The number of sub-accounts */ function subsCountOf(address account) external view returns (uint48); /** * @notice Returns the value of a parameter for a given account or sub-account * @dev paramIndex must be less than NUMBER_OF_PARAMETERS * For Normal accounts, subId must be 0 * @param paramIndex The index of the parameter (0 to NUMBER_OF_PARAMETERS-1) * @param account The address of the account * @param subId The sub-account index (0 for Normal accounts) * @return uint64 The parameter value */ function parameterOf( uint8 paramIndex, address account, uint48 subId ) external view returns (uint64); /** * @notice Returns allowance settings for a given subId, owner and spender * @dev If the stored subId matches the queried subId, returns the sub-specific * allowance, one-off flag and commitment deadline. Otherwise, returns * the general allowance settings (total - sub, false, 0) * @param owner The address of the token owner * @param subId The sub-account index * @param spender The address of the spender * @return (uint256, bool, uint64) The allowance amount, one-off flag and commitment deadline */ function allowanceOf( address owner, uint48 subId, address spender ) external view returns (uint256, bool, uint64); /** * @notice Returns sub-allowance settings for a given owner and spender * @dev Returns (0, 0, false) for Normal accounts * @param owner The address of the token owner * @param spender The address of the spender * @return (uint48, uint256, bool) The allowance subId, sub amount and whether it is one-off */ function subAllowance( address owner, address spender ) external view returns (uint48, uint256, bool); // Sub-account approval /** * @notice Sets an allowance for a specific sub-account. * @dev Only callable by the owner of a Super account. The allowance applies * specifically to the given subId. If oneOff is true, the allowance is * consumed entirely after the first non-zero spend from that subId. * If committedUntil is set to a future timestamp, the owner * can't reduce sub allowance until that time * @param ownerSubId The sub-account for which the allowance is granted * @param spender The address authorized to spend * @param amount The allowance amount (sub-account-specific) * @param oneOff If true, the allowance is one-time use * @param committedUntil Future timestamp for allowance commitment deadline, otherwise 0 * @return true if the approval succeeded */ function approveForSub( uint48 ownerSubId, address spender, uint256 amount, bool oneOff, uint64 committedUntil ) external returns (bool); // Parametric transfers /** * @notice Transfers tokens from the caller's specified account/sub-account to a recipient's account/sub-account * @dev The caller must have sufficient balance in account/fromSubId. * The transfer will apply parameter mutation logic (weighted average, conflict checks) * before updating balances * @param fromSubId The sub-account to transfer from (0 for Normal accounts) * @param to The recipient address * @param toSubId The recipient's sub-account (0 for Normal accounts) * @param amount The number of tokens to transfer * @return true if the transfer succeeded */ function parametricTransfer( uint48 fromSubId, address to, uint48 toSubId, uint256 amount ) external returns (bool); /** * @notice Transfers tokens from a specified sub-account using an allowance * @dev This is the parametric equivalent of ERC-20 transferFrom. * The spender must have sufficient allowance for the specified fromSubId. * The transfer will apply parameter mutation logic before updating balances * @param from The token owner address * @param fromSubId The sub-account to transfer from (0 for Normal accounts) * @param to The recipient address * @param toSubId The recipient's sub-account (0 for Normal accounts) * @param amount The number of tokens to transfer * @return true if the transfer succeeded */ function parametricTransferFrom( address from, uint48 fromSubId, address to, uint48 toSubId, uint256 amount ) external returns (bool); } ``` #### Optional Extension: Non‑Zero‑Sum (NZS) Transfers Parametric NZS Tokens (where in general case `debitAmount != creditAmount`) MUST implement the following `IParametricTokenNzs` interface: ```solidity interface IParametricTokenNzs is IParametricToken { // Events /** * @notice Emitted when a non-zero-sum parametric token transfer occurs * @dev The standard ERC-20 `Transfer` event MUST also be emitted using the `creditAmount`. * The base `ParametricTransfer` event SHOULD NOT be emitted for NZS token transfers * @param from The sender address * @param fromSubId The sender's sub-account * @param to The recipient address * @param toSubId The recipient's sub-account * @param debitAmount The exact amount deducted from the sender's balance * @param creditAmount The exact amount added to the recipient's balance * @param incomingParams The full incoming parameter array * @param resultingParams The full resulting parameter array */ event ParametricTransferNzs( address indexed from, uint48 indexed fromSubId, address indexed to, uint48 toSubId, uint256 debitAmount, uint256 creditAmount, uint64[] incomingParams, uint64[] resultingParams ); // Functions /// @notice Returns true if the token implements non‑zero‑sum transfers. function isNonZeroSum() external pure returns (bool); } ``` #### Interface Detection (ERC‑165) All tokens implementing `IParametricToken` interface MUST support ERC‑165 and return `true` for `supportsInterface(type(IParametricToken).interfaceId)`. Tokens implementing `IParametricTokenNzs` interface MUST also return `true` for `supportsInterface(type(IParametricTokenNzs).interfaceId)`. Off‑chain indexers SHOULD use `supportsInterface(type(IParametricTokenNzs).interfaceId)` to determine whether to listen to `ParametricTransferNzs` events. If false, they MUST rely on the base `ParametricTransfer` event. ### Parameter Configuration The token contract MUST define: - `NUMBER_OF_PARAMETERS` – a **public** constant `uint8` indicating the total number of parameters. - A parameter configuration struct (see below in Data Model section). The contract MUST implement: - `paramConfig() external view returns (ParamConfig[] memory)` – returns array of metadata for each parameter, including its mutability. - `parameterOf(uint8 paramIndex, address account, uint48 subId) external view returns (uint64)` to return the current parameter value for a given account/sub‑account. For Normal accounts, `subId` MUST be `0`; for Super accounts, the caller MUST provide a valid `subId`. ### Account Types and Sub‑accounts - **Normal accounts** hold a single balance and one set of parameters. - **Super accounts** hold multiple sub‑accounts, each with its own balance and parameters. The aggregate balance of a Super account is the sum of all its sub‑account balances (accessible via `balanceOf`). - An account is initially Normal. The owner MAY convert it to Super by calling `convertToSuper()`. This creates Default sub‑account with the current balance and parameters, and clears the Normal parameters. - Additional sub‑accounts can be created by the owner via `createSubAccount()`, which returns the new sub‑account index. ### Parameter Semantics #### Immutable Parameters - Tokens with different immutable parameter values MUST NOT be merged into the same account/sub‑account. - If the recipient’s balance is zero, the incoming tokens’ immutable parameters are accepted (the account inherits them). - If the recipient already holds tokens, any transfer that would result in a mix of different immutable parameter values MUST revert. - This check applies to both standard ERC‑20 transfers and parametric transfers. #### Mutable Parameters - When tokens are transferred, the recipient’s mutable parameters MUST be updated according to token‑specific rules. - The token contract MUST implement the mutation logic inside its internal transfer functions (e.g. `weightedAverage`, `maximum` etc). - The mutation logic for each mutable parameter MUST be implemented as a **deterministic transition function** whose result is **fully reconstructible and verifiable** from - parameters of the source account, - parameters of the destination account, - the transfer value, - pre‑transfer balance value of the destination account, - other state variables or blockchain parameters (e.g., `block.timestamp`), and - external data that is committed to the blockchain in a verifiable manner (e.g., oracle price feeds). - Context factors that are not publicly reconstructible (e.g., off‑chain values without timestamped on‑chain commitment) MUST NOT be used, as they would make the transition non‑auditable and break the standard's reproducibility guarantee. - When the balance of an account or sub‑account becomes zero, its parameters MUST be reset to **zero**. #### Resulting Balance - The post‑transfer balance of the destination account MAY be calculated using logic other than a simple sum of the pre‑transfer balance and the transfer amount. If such custom logic is used, it MUST be implemented as a **deterministic transition function** whose result is **fully reconstructible and verifiable** from - parameters of the source account, - parameters of the destination account, - the transfer value, - pre‑transfer balance value of the destination account, - other state variables or blockchain parameters (e.g., `block.timestamp`), and - external data that is committed to the blockchain in a verifiable manner (e.g., oracle price feeds). - Like in case of parameters mutation, context factors that are not publicly reconstructible (e.g., off‑chain values without timestamped on‑chain commitment) MUST NOT be used for balance calculation, as they would make the transition non‑auditable and break the standard's reproducibility guarantee. **A. Zero‑Sum Transfers (Default)** If token transfers imply that the computed `creditAmount` always equals the `debitAmount` (the amount deducted from the sender), the transfer is a standard zero‑sum transfer. The token MUST emit the base `ParametricTransfer` event. **B. Non‑Zero‑Sum Transfers (NZS)** If the computed `creditAmount` MAY differ from the `debitAmount` (i.e., `creditAmount != debitAmount`), the token transfer is qualified as a Non‑Zero‑Sum (NZS) transfer. In this case: - The token MUST implement the `IParametricTokenNzs` optional extension. - The token MUST emit the `ParametricTransferNzs` event with the exact `debitAmount`, `creditAmount`, and the **incoming** (pre-transfer of sender) and **resulting** (post-transfer of recipient) parameter states. - The token MUST NOT emit the base `ParametricTransfer` event to avoid ambiguous amount semantics. - The token MUST still emit the standard ERC‑20 `Transfer` event using the `creditAmount`. _Example_: See Bundle Token implementation for a NZS transfer sample. #### Parameter Initialization Parameter initialization (setting initial values when tokens are minted) is **outside the scope of this standard**, as minting is typically controlled by an external engine contract rather than the token itself. However, compliant implementations MUST ensure that every mint operation results in all parameters being set to well‑defined initial values. The initialization mechanism MAY be implemented in one of the following ways (or a combination thereof): - The minting engine provides the initial parameter values as arguments to the mint function. - The minting user (the recipient) selects the initial parameter values at the time of minting (e.g., choosing a prediction price for a resolution time they believe will prevail). - The token contract derives initial values from deterministic blockchain parameters (e.g., `block.timestamp` for a mintTime parameter). Regardless of the mechanism, the token contract MUST NOT allow a mint operation to complete with uninitialized or default‑zero parameters unless zero is explicitly intended as a valid initial value. ### Allowances and Approvals The standard extends the ERC‑20 allowance system to support both **General** allowances (usable across sub‑accounts) and **Specific** allowances (restricted to a particular sub‑account). #### Data Structure Each allowance record is represented by: ```solidity struct Allowance { uint256 total; // Total allowance for all sub‑accounts (general + specific) uint256 sub; // Specific allowance for the sub‑account identified by subId uint48 subId; // Sub‑account to which the specific allowance applies bool oneOff; // If true, the specific allowance is consumed entirely after one use uint64 committedUntil; // If not 0, sub allowance can't be withdrawn until the deadline } ``` #### Invariants - `total >= sub` MUST always hold - `(total - sub)` represents the General allowance available for any sub‑account **except** the one specified by `subId`. - Default sub-account MUST always use General allowance, even if the allowance record has `subId == 0`. - If `total == 0`, then `sub` MUST be `0`, `subId` MUST be `0`, and `oneOff` MUST be `false`. - If `subId == 0`, then `sub` MUST be `0`, and `oneOff` MUST be `false`. - If `sub == 0`, then `committedUntil` MUST be `0`. - If `block.timestamp > committedUntil`, the commitment is expired and MUST be treated as void for all restriction purposes. Implementations SHOULD reset `committedUntil` to `0` when it expires. #### Normal Accounts and Super Accounts with a Single Sub‑account - MUST NOT use Specific allowances, only the General allowance (`total`) is available for spending. - `subId` MUST be `0`, `sub` MUST be `0`, and `oneOff` MUST be `false`. #### Standard ERC‑20 Approvals - `approve(address spender, uint256 amount)`: - `amount` MUST exceed `sub` if `block.timestamp <= committedUntil`. - Sets `total` to `amount`. - If `sub` exceeds `amount`, it is capped to `amount`. - If `amount == 0`, then `subId` MUST be `0`, `oneOff` MUST be `false`, `committedUntil` MUST be `0`. - This function does **not** affect `subId`, `oneOff` and `committedUntil` for non‑zero amounts. #### Sub‑account‑specific Approvals - `approveForSub(uint48 ownerSubId, address spender, uint256 amount, bool oneOff_, uint64 committedUntil_)`: - **Only callable** by the owner of a Super account with at least **two** sub-accounts (`subsCount > 1`). - If `committedUntil == 0` or `block.timestamp > committedUntil`: - If `ownerSubId == 0` (i.e., Default sub-account): `amount` MUST be `0`, `oneOff_` MUST be `false`, `committedUntil_` MUST be `0`. Resulting state implies the account has no Specific allowance. - If `ownerSubId > 0` and `oneOff_ == true`, then `amount` MUST be `> 0`. - If `block.timestamp <= committedUntil`: - `amount` MUST exceed `sub` - `ownerSubId` MUST equal `subId` - `oneOff_` and `committedUntil_` values are ignored and MUST NOT change `oneOff` and `committedUntil` settings. - MUST implement conservative allowance logic: - If not otherwise declared above, sets `subId` to `ownerSubId`, `sub` to `amount`, `oneOff` to `oneOff_`, and `committedUntil` to `committedUntil_`. - **increase** in sub-specific allowance MUST happen at the expense of General allowance (`total - sub`). - **reduction** in sub-specific allowance MUST not change General allowance (`total - sub`). - If `amount > total`, `total` is raised to `amount` (maintaining `total >= sub`). - Emits `ApprovalForSub`. #### Spending Allowances #### A. For Normal Accounts (any transfer): - `transferFrom` and `parametricTransferFrom` MUST spend from General allowance (`total`). #### B. For Super Accounts: - `transferFrom` (standard ERC‑20, uses `ownerSubId = 0`): | Condition | Allowance Used | Effect | | ---------------------------------------- | ----------------------- | ---------------------------------- | | `subId == 0` (no Specific allowance) | General (`total - sub`) | `total` decreases; `sub` unchanged | | `subId != 0` (Specific allowance exists) | General (`total - sub`) | `total` decreases; `sub` unchanged | ⚠️ `transferFrom` never spends from the Specific allowance (even if `subId == 0`), because sub‑account `0` cannot have a Specific allowance. - `parametricTransferFrom` (uses `ownerSubId` from caller): | Condition | Allowance Used | Effect | | ------------------------------------------ | ----------------------- | ------------------------------------------------------------- | | `ownerSubId == subId` and `ownerSubId > 0` | Specific (`sub`) | `sub` and `total` decrease; general (`total - sub`) unchanged | | `ownerSubId != subId` or `ownerSubId == 0` | General (`total - sub`) | `total` decreases; `sub` unchanged | - In all cases, insufficient allowance MUST trigger a revert. #### One‑off Allowance Behavior - If `oneOff` is `true` and the allowance is spent using the exact `subId` stored in the allowance record: - `total` MUST be reduced by `sub`, `sub` MUST be set to `0` - `oneOff` flag MUST be set to `false` - `committedUntil` MUST be set to `0`. - If the allowance is spent from a different `subId`, the `oneOff` flag is ignored. - Standard `approve()` cannot set `oneOff`; calling it with `amount == 0` (only if `committedUntil` is not representing a valid commitment) clears the flag. ### Optional (RECOMMENDED) Functionality #### Sub‑account Validation The standard defines a helper modifier (RECOMMENDED): ```solidity modifier onlyValidSub(address account, uint48 subId) { if (subId > 0) require(_accounts[account].accountType == AccountType.Super, "Not super account"); if (subId > 0) require(subId < _supers[account].subsCount, "Sub-account doesn't exist"); _; } ``` This ensures that `subId` is valid for the given account. #### All Parameters Getter The following getter function is RECOMMENDED (especially for tokens with multiple parameters): ```solidity /** * @notice Returns all parameter values for a given account/sub-account as an array * @dev This is a convenience getter. The array length MUST equal NUMBER_OF_PARAMETERS * @param account The address of the account * @param subId The sub-account index (0 for Normal accounts) * @return uint64[] The full parameter array */ function allParametersOf( address account, uint48 subId ) external view returns (uint64[] memory); ``` #### Permissions To protect accounts/sub-accounts against parameter poisoning (where an attacker transfers tokens with unfavourable parameter values), tokens MAY implement permissions to restrict inbound transfers based on parameter value ranges. If permissions are implemented, - limitations imposed by permissions MUST be enforced by checking the incoming parameters against the stored permissions before applying any mutation logic. If a transfer violates any permission, it MUST revert. - permissions MUST have following data structure, interfaces and rules. #### Permissions Data Structure and Mappings ```solidity struct Permission { bool enabled; // Enabled flag uint64 min; // Minimum acceptable value (inclusive) uint64 max; // Maximum acceptable value (inclusive) bool soft; // If true, owner is exempt; if false, applies to all } struct IndexedPermission { uint8 paramIndex; // Parameter index bool enabled; // Enabled flag uint64 min; // Minimum acceptable value (inclusive) uint64 max; // Maximum acceptable value (inclusive) bool soft; // If true, owner is exempt; if false, applies to all } mapping(address => mapping(uint48 => mapping(uint8 => Permission)) private _permissions; ``` - `_permissions[account][subId][paramIndex]` holds a permission record for inbound transfers. #### Permissions Interfaces ```solidity // Events /** * @notice Emitted when a permission is created or updated * @param owner The address of the account owner * @param subId The sub-account for which the permission is set * @param index The index of parameter for which the permission is set * @param enabled The enabled flag * @param min The minimum for incoming parameter value * @param max The maximum for incoming parameter value * @param soft `true` to skip checks for transfers from account owner */ event PermissionForSub( address indexed owner, uint48 indexed subId, uint8 indexed index, bool enabled, uint64 min, uint64 max, bool soft ) // Functions /** * @notice Sets or updates a permission for inbound transfers to a sub-account * @dev Only callable by the account owner (ownerSubId = 0 for Normal accounts). * If the index corresponds to an immutable parameter: MUST revert. * If enabled == true: MUST revert if min > max, or (max == 0 and soft == true) * If enabled == false: MUST revert if (min, max, soft) are not (0, 0, false) * @param paramIndex The parameter index (0 to NUMBER_OF_PARAMETERS-1) * @param ownerSubId The sub-account to apply the permission to * @param enabled The enabled flag * @param min Minimum acceptable value (inclusive) * @param max Maximum acceptable value (inclusive) * @param soft If true, owner is exempt; if false, applies to all * @return true if the permission setting succeeded */ function permitForSub( uint8 paramIndex, uint48 ownerSubId, bool enabled, uint64 min, uint64 max, bool soft ) external returns (bool); /** * @notice Returns the current permission for a specific parameter and sub-account * @dev MUST revert if index corresponds to an immutable parameter * @param paramIndex The parameter index (0 to NUMBER_OF_PARAMETERS-1) * @param account The address of the account * @param subId The sub-account index * @return Permission The current permission (min, max, soft). * If no permission is set, returns (0, 0 , false) */ function permissionOf( uint8 paramIndex, address account, uint48 subId ) external view returns (Permission memory); /** * @notice Returns all permissions for a sub-account, sorted by index * @dev If no permissions are enabled, returns an empty array. * Only enabled permissions for mutable parameters are returned * @param account The address of the account * @param subId The sub-account index * @return IndexedPermission[] An array of all permissions, sorted ascending by index */ function allPermissionsOf( address account, uint48 subId ) external view returns (IndexedPermission[] memory); ``` #### Permissions Rules - Permission records CAN be created for every sub-account and every mutable parameter. - `min <= max` MUST always hold. - If `enabled == true`, the incoming parameter value MUST satisfy the limits defined by the corresponding `min` and `max` values (inclusive); any inbound transfer that violates a permission MUST revert. - If `enabled == false`, `soft` MUST be `false`; any call to `permitForSub` that violates this MUST revert. - If `soft == false`, limitations apply to all inbound transfers. - If `soft == true`, limitations apply to all inbound transfers **except** those initiated by the account owner themselves. - `allPermissionsOf(address account, uint48 subId)` returns an array of all active indexed permissions; resulting array MUST be sorted by `paramIndex` ascending; all permission records with `enabled == false` MUST be omitted. - The `PermissionForSub` event MUST be emitted whenever a permission is created or updated. ### ERC‑20 Compatibility This standard is interface‑compatible with ERC‑20: all standard functions (transfer, transferFrom, balanceOf, allowance, approve, totalSupply, name, symbol, decimals) are present and behave as defined in ERC‑20 for **Normal accounts and for Super accounts where only the Default sub‑account holds tokens**. However, for Super accounts with multiple funded sub‑accounts, the semantics of certain ERC‑20 functions diverge from the vanilla ERC‑20 model: - `balanceOf(address account)` returns the aggregate balance across all sub‑accounts for Super accounts, not the balance of a single spendable pool - `transfer` and `transferFrom` always operate on the Default sub‑account (`subId == 0`), not on the aggregate balance. This means that for a Super account, `balanceOf` may show a larger amount than is actually transferable via the standard `transfer` function. This is an intentional design trade‑off: preserving the ERC‑20 interface while enabling sub‑account functionality. #### Integrations Integrators are not left to guess. The standard provides two tools to eliminate any operational uncertainty: | Tool | Purpose | | --------------------------------------------------------- | ----------------------------------------------------- | | `supportsInterface( type(IParametricToken).interfaceId )` | Detects whether a token implements this standard | | `isSuperAccount(address)` | Detects whether a specific account is a Super account | Using these tools, wallets and protocols can: - Treat the token as vanilla ERC‑20 if `supportsInterface` returns `false`. - Use standard ERC‑20 semantics if `isSuperAccount(account)` returns `false`. If `isSuperAccount(account)` returns `true`, query `parametricBalanceOf(account, 0)` to obtain the amount transferable via `transfer`, and use `parametricTransfer` when sub‑account precision is needed. The compatibility risk is therefore educational, not operational – the standard provides all necessary detection and query functions to handle Super accounts correctly. #### Specific ERC‑20 Function Mappings | ERC‑20 Function | Parametric Equivalent / Behavior | | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `transfer(address to, uint256 amount)` | MUST be equivalent to `parametricTransfer(0, to, 0, amount)` - transfers from the Default sub‑account | | `transferFrom(address from, address to, uint256 amount)` | MUST be equivalent to `parametricTransferFrom(from, 0, to, 0, amount)` – spends from the Default sub‑account, using the General allowance | | `balanceOf(address account)` | MUST return the aggregate balance across all sub‑accounts for Super accounts, or the balance for Normal accounts | | `allowance(address owner, address spender)` | MUST return the General allowance (`total - sub`); for Normal accounts it MUST equal `total`. Equivalent to `allowanceOf(owner, 0, spender)` | | For Non‑Zero‑Sum (NZS) tokens | The `amount` parameter of the standard `Transfer` event MUST equal the `creditAmount` (the amount credited to the recipient's balance) | ## Rationale ### Why Sub‑accounts? Sub‑accounts allow a single address to hold tokens with different immutable parameters (or simply different parameter histories) without requiring multiple wallets. This simplifies user experience and reduces the need for key management. Sub‑account `0` serves as the default for ERC‑20 compatibility, ensuring that existing wallets and tools work without modification. ### Why Separate `total` and `sub` Allowance? The split between `total` and `sub` provides flexibility: an owner can grant a specific allowance for a particular sub‑account (e.g., to a trading bot that should only access that sub‑account) while still allowing a larger general allowance for other sub‑accounts. This is crucial in scenarios where different sub‑accounts represent different strategies or risk profiles. ### Why One‑off Allowance? One‑off allowances are useful for atomic operations where the spender should only be able to use the allowance once (e.g., for a single redemption or swap). The standard ensures that after any positive spend from the designated sub‑account, the entire sub‑allowance is consumed, preventing accidental or malicious reuse. The invariant is preserved by adjusting `total` to reflect the remaining general allowance. ### Why `uint64` for Parameters? `uint64` is sufficient for most use cases (timestamps, prices scaled by `10^decimals`, etc.) and is gas‑efficient. The `decimals` field in `ParamConfig` allows for human‑readable formatting. ### Why `uint48` for Sub‑account Index? `uint48` is enough to support an enormous number of sub‑accounts (over 2.8e14) while being storage‑efficient when packed with other fields. ### Why Parametric Tokens Are Agentic‑Friendly? Deterministic transition functions (unambiguously reproducible state changes), sub‑account isolation, and composable allowances make parametric tokens a natural fit for autonomous agents. Predictable (reproducible) state transitions, independent strategy management, and secure atomic workflows — the features agents need - are already built in. ### Why Non‑Zero‑Sum Transfers? Advanced financial instruments, such as tokenized containers (see Bundle Token example), might require that the value of a position is not a linear function of the underlying tokens. When two holdings with different parameter values (e.g., different `anchor` prices) are merged, the resulting position may have a different total balance than the simple arithmetic sum of the inputs due to the convexity of the payoff function. Providing an optional NZS extension allows the standard to support such sophisticated derivatives without forcing every parametric token to implement complex accounting logic. The extension gives indexers and wallets a clear, standardised way to detect and correctly track non‑linear balance updates, ensuring that off‑chain systems can accurately reflect on‑chain state. ### Relation to Existing Standards Existing token standards define parameters or state at different levels, but none support **account‑specific**, **updatable parameters** while preserving fungibility and ERC‑20 compatibility. #### Comparison A: Fungibility and Liquidity | Standard | State / Parameter Location | Fungibility | Liquidity | | ------------------------------- | ---------------------------------- | -------------------------- | ----------------- | | **ERC‑20** | None (only balance) | ✅ Fungible | Consolidated | | **ERC‑721** | Per `tokenId` (metadata) | ❌ Non‑fungible | Highly fragmented | | **ERC‑1155** | Per `id` (token class) | 🔶 Mixed | Fragmented | | **ERC‑3525** | Per `slot` (token‑level container) | 🔶 Mixed | Highly fragmented | | **ERC‑4626** | Global (vault‑level) | ✅ Fungible | Consolidated | | **ERC‑8378 (Parametric Token)** | **Per account / sub‑account** | ✅ Conditional fungibility | Consolidated | In ERC‑1155, different parameter values require distinct `id`s, fragmenting liquidity into separate pools. ERC‑3525 introduces slots, but these are token‑level constructs that complicate standard ERC‑20 integration. ERC‑4626 parameters are global to the vault, not individualised per user. #### Comparison B: Parameters Initialization, Mutability and Partitioning | Standard | Initialization | Transfer Mutability | Fungible Partition | | ------------------------------- | -------------------------- | ----------------------- | ------------------ | | **ERC‑20** | ❌ None | ❌ None | ❌ None | | **ERC‑721** | ✅ Minter-Defined | ❌ None | ❌ None | | **ERC‑1155** | ✅ Minter-Defined | ❌ None | ❌ None | | **ERC‑3525** | ✅ Minter-Defined | ❌ None | ❌ None | | **ERC‑4626** | 🔶 Auto (contract defined) | ❌ None | ❌ None | | **ERC‑8378 (Parametric Token)** | ✅ Minter-Defined OR Auto | ✅ Auto (deterministic) | ✅ Yes | The Parametric Token standard fills this gap by attaching parameters **directly to the account** (or sub‑account), with deterministic or user-defined parameters initialization at mint and strictly deterministic verifiable mutation logic applied on transfers, while remaining **fully compatible with ERC‑20**. This enables novel applications like scalar prediction markets, convex stablecoins, and tokens with velocity‑based economics without the limitations of existing standards. ## Backwards Compatibility - This standard is **interface‑compatible** with ERC‑20: all standard functions are present and behave as defined for Normal accounts and Super accounts with only the Default sub‑account in use. For Super accounts with multiple funded sub‑accounts, `balanceOf` returns the aggregate balance, while `transfer` and `transferFrom` operate only on the Default sub‑account. This distinction is fully detectable via `supportsInterface(type(IParametricToken).interfaceId)` and `isSuperAccount(address)`. - The optional NZS extension requires integrators to use both `debitAmount` and `creditAmount` from the `ParametricTransferNzs` event for accurate balance tracking; the standard `Transfer` event alone is insufficient for NZS tokens. - Existing wallets and exchanges that implement only ERC‑20 will function with Parametric Tokens, displaying aggregate balances and total allowances. However, they may show a `balanceOf` amount that is not fully transferable via `transfer` for Super accounts. This is not a security risk – it is a semantic difference that integrators can resolve using the standard's detection and query functions. - Advanced features (sub‑accounts, parameters, permissions) are accessed via additional functions; they do not interfere with standard ERC‑20 operations. All allowance logic is implemented conservatively to preserve ERC‑20 spending semantics where applicable. - The standard introduces additional implementation risks arising from parameter state, deterministic transitions, sub-accounts, and optional non-zero-sum accounting. These risks are distinct from, and in addition to, standard ERC-20 risks such as allowance misuse and reentrancy. Implementers should refer to the Security Considerations section for specific mitigations. ## Test Cases A comprehensive test suite for the reference implementation is available in the `/test` directory of the [Foundry repository](https://github.com/K2eno/parametric-token). The suite includes over 100 unit tests covering all core functions, parameter mutation logic, sub‑account transitions, allowance spending, NZS mints/transfers, permissions and revert conditions for Normal and Super accounts. ## Reference Implementation [Foundry repository](https://github.com/K2eno/parametric-token) includes scripts in `/script` demonstrating three implementations of this ERC: Prediction, Tenure and Bundle tokens with respective engines - in all cases scripts cover deploy and trading scenarios. ## Security Considerations ### Parameter Conflict Checks Implementations MUST verify immutable parameter compatibility before executing any transfer. Failure to do so could allow tokens with different immutable parameters to mix, breaking the intended semantics. ### Allowance Manipulation As with standard ERC‑20, large allowances should be handled with care. This standard adds two mitigations for Specific allowances: `oneOff` consumes the allowance after the first spend, and `committedUntil` prevents owner reduction until a deadline. Standard `approve` remains subject to the same risks as ERC‑20. ### Reentrancy All external functions that modify state (transfers, approvals, sub‑account creation) SHOULD be protected with reentrancy guards, especially when they call external contracts (e.g., during transfers that may trigger hooks). ### Super Accounts and ERC‑20 Wallet Compatibility Wallets and integrators should be aware that for Super accounts `balanceOf(account)` returns the aggregate balance across all sub‑accounts. For this reason, usage of `balanceOf(account)` to determine the transferable amount for standard ERC‑20 `transfer` and `transferFrom` functions is misleading and may lead to transaction reverts, because these functions are implementing transfer from Default sub-account, representing only partial balance. Instead, `parametricBalanceOf(account, 0)` getter shall be used to determine transferable amount. ### Sub‑account Ownership Only the account owner can convert it to Super or create sub‑accounts. However, once an account is Super, the owner must manage sub‑account indices carefully; there is no mechanism to delete sub‑accounts, so the number of sub‑accounts should be bounded. ### Parameter Mutation The mutation logic for mutable parameters must be carefully designed to avoid overflow or underflow. Since parameters are `uint64`, all arithmetic should be checked (or use Solidity’s built‑in overflow checks). The weighted average calculation, for example, should use `uint256` intermediate values to prevent overflow. ### Parameters Poisoning & Manipulation In parametric tokens, an attacker may transfer tokens with unfavourable parameter values to degrade a victim's aggregate state (e.g., lowering a weighted-average prediction). Protection can be implemented via account-specific **permissions** (preset parameter ranges, see Specification for optional implementation) or **post-transfer validation** (reverting if resulting parameters fall outside bounds). These mechanisms preserve economic interest, as a parametric token's value is intrinsically bound to its parameter state. Flash-loan-based parameter manipulation is theoretically possible but economically deterred by the fundamental utility‑bearing nature of parametric tokens and the associated **utility vs reasonable cost** trade-off. Consequently, the cost of manipulation will generally exceed the extractable value. ### Protection Against Unauthorised Engine Access In encapsulated token systems (like the Inverse Token example), the engine contract may have special privileges. The standard does not mandate such a mechanism, but if implemented (e.g., via `onlyEngine` modifiers), the contract must ensure that the engine cannot bypass user approvals without explicit consent. ### One‑off Allowance Reset The reset logic (zeroing remaining sub and adjusting total) must be performed atomically within the same transaction to avoid race conditions. The implementation must not allow the allowance to be spent in two separate transactions after the first use. ### Non‑Zero‑Sum Transfer Accounting Implementers of NZS transfers must be acutely aware that the `Transfer` event carries the `creditAmount`, not the `debitAmount`. Off‑chain systems that rely on deriving sender balances from `Transfer` events will compute an incorrect `balanceOf(from)` if they attempt to subtract the `creditAmount` from the sender. Implementers MUST document this behaviour prominently and off‑chain indexers MUST detect NZS tokens via ERC‑165 and subtract the `debitAmount` from `ParametricTransferNzs` event when updating the sender's balance. Failure to do so will result in persistent accounting mismatches. ## Copyright Copyright and related rights waived via [CC0](https://eips.ethereum.org/LICENSE). ## Citation Please cite this document as: ```bibtex @article{ERC-Parametric-Token, title = {ERC-8378: Parametric Token}, author = {Alexander Zvezdin}, url = {https://github.com/k2eno/parametric-token/blob/main/ERCS/erc-8378.md}, year = {2026} } ```