一键导入
token-standard-integration
ICRC-1/ICRC-2/ERC-20 準拠トークンシステム設計 — TheWorld/InstanceRegistry SQLiteスキーマ、トークン正規化
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
ICRC-1/ICRC-2/ERC-20 準拠トークンシステム設計 — TheWorld/InstanceRegistry SQLiteスキーマ、トークン正規化
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
A-Life 自律開発システムの監視者ガイド — ガバナンス優先介入原則、TMUX直接操作の緊急手順
EVM/ICP デプロイの安全手順とロールバック — InstanceFactory/TheWorld デプロイ、canister install、コントラクトデプロイ
etherclaw dump-s — ETHERCLAW.toml の [[lazy]] 全パッケージの仕様を集約出力
EtherClaw InstanceFactory/TheWorld cross-chain architecture - Optimistic Upgrader system spanning EVM (Base Mainnet) and ICP, ERC-7546, auditor approval flow
etherclaw daemon --standalone mode — ETHERCLAW.toml format, Architect/Executor role split, DogFeeding workflow
EtherClaw Tier-based hierarchical agent system - TaskTree depth から自動生成される Manager/Worker 階層
| name | token-standard-integration |
| description | ICRC-1/ICRC-2/ERC-20 準拠トークンシステム設計 — TheWorld/InstanceRegistry SQLiteスキーマ、トークン正規化 |
| triggers | ["ICRC-1","ICRC-2","ERC-20","token standard","トークン標準","token design","トークン設計","InstanceRegistry","token table","トークンテーブル","colony token","ETHERCLAW token"] |
ICRC-1/ICRC-2/ERC-20 準拠のトークンシステム設計とER統合
このSkillは、A-Lifeプロジェクトのトークン設計がICP (ICRC-1/ICRC-2) およびEVM (ERC-20) の標準規格に準拠するための知識を提供します。TheWorld/InstanceRegistry CanisterのSQLiteスキーマ設計において、トークン関連テーブルの正規化と標準インターフェース実装をガイドします。
A-Lifeは2層のトークン構造を持ちます:
ETHERCLAW (グローバルトークン)
$t^i$ (インスタンストークン)
┌─────────────────────────────────────────────┐
│ ETHERCLAW (token_type='etherclaw') │
│ - Global governance (c_gov) │
│ - Colony/Auditor stakes │
│ - Total supply: fixed │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Instance Tokens ($t^i$) │
│ token_type='instance', instance_id FK │
│ - Instance-specific governance (c^i_share) │
│ - Economic incentive alignment │
│ - Total supply: per-instance configured │
└─────────────────────────────────────────────┘
必須メタデータ:
icrc1_name() → tokens.token_nameicrc1_symbol() → tokens.token_symbolicrc1_decimals() → tokens.decimals (通常8)icrc1_total_supply() → tokens.total_supplyicrc1_fee() → tokens.fee (transfer手数料)icrc1_metadata() → tokens.metadata (JSON)Account モデル:
type Account = record {
owner : principal;
subaccount : opt blob; -- 32 bytes or NULL
};
Transfer操作:
type TransferArgs = record {
from_subaccount : opt blob;
to : Account;
amount : nat;
fee : opt nat;
memo : opt blob;
created_at_time : opt nat64; -- idempotency key
};
ERC-20の approve() / transferFrom() 相当:
type ApproveArgs = record {
from_subaccount : opt blob;
spender : Account;
amount : nat;
expires_at : opt nat64;
fee : opt nat;
memo : opt blob;
created_at_time : opt nat64;
};
icrc2_approve : (ApproveArgs) -> variant { Ok : nat; Err : ApproveError };
icrc2_transfer_from : (TransferFromArgs) -> variant { Ok : nat; Err : TransferFromError };
icrc2_allowance : (AllowanceArgs) -> Allowance query;
基本インターフェース:
interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
CREATE TABLE tokens (
token_id TEXT PRIMARY KEY,
token_type TEXT NOT NULL CHECK(token_type IN ('etherclaw', 'instance')),
-- ICRC-1 / ERC-20 共通メタデータ
token_name TEXT NOT NULL,
token_symbol TEXT NOT NULL,
decimals INTEGER NOT NULL DEFAULT 8,
total_supply TEXT NOT NULL, -- BigInt as TEXT
fee TEXT NOT NULL DEFAULT '10000', -- 0.0001 with 8 decimals
-- Instance token
instance_id TEXT NULL,
-- EVM deployment (multi-chain support)
chain_id INTEGER NULL,
contract_address TEXT NULL,
-- ICRC-1 metadata (JSON)
metadata TEXT, -- [{"key": "logo", "value": "data:image/svg+xml;base64,..."}]
created_at INTEGER NOT NULL,
FOREIGN KEY (instance_id) REFERENCES instances(instance_id) ON DELETE CASCADE,
FOREIGN KEY (chain_id) REFERENCES chains(chain_id),
CHECK (
(token_type = 'etherclaw' AND instance_id IS NULL)
OR
(token_type = 'instance' AND instance_id IS NOT NULL)
),
UNIQUE(instance_id) WHERE token_type = 'instance'
);
Design Rationale:
total_supply / fee は TEXT 型 (SQLiteにBigInt型がないため、文字列としてnubber-bigintで扱う)metadata はJSON (ICRC-1 vec record { text; Value } をシリアライズ)chain_id / contract_address はEVMブリッジ用 (NULLable)CREATE TABLE token_balances (
balance_id INTEGER PRIMARY KEY AUTOINCREMENT,
token_id TEXT NOT NULL,
-- ICRC-1 Account
owner_principal TEXT NOT NULL,
subaccount BLOB NULL, -- 32 bytes or NULL (default subaccount)
balance TEXT NOT NULL, -- BigInt as TEXT
-- Staking (ETHERCLAW のみ使用)
staked_amount TEXT NOT NULL DEFAULT '0',
stake_locked_until INTEGER NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (token_id) REFERENCES tokens(token_id) ON DELETE CASCADE,
UNIQUE(token_id, owner_principal, subaccount)
);
CREATE INDEX idx_balances_owner ON token_balances(owner_principal);
CREATE INDEX idx_balances_token ON token_balances(token_id);
Design Rationale:
subaccount がNULLの場合はデフォルトサブアカウントstaked_amount / stake_locked_until はETHERCLAWのStaking用 (instance tokenでは未使用)CREATE TABLE token_transactions (
tx_id INTEGER PRIMARY KEY AUTOINCREMENT,
token_id TEXT NOT NULL,
tx_type TEXT NOT NULL
CHECK(tx_type IN ('transfer', 'mint', 'burn', 'stake', 'unstake', 'reward', 'approve', 'transfer_from')),
-- From Account (ICRC-1)
from_owner_principal TEXT NULL,
from_subaccount BLOB NULL,
-- To Account (ICRC-1)
to_owner_principal TEXT NULL,
to_subaccount BLOB NULL,
amount TEXT NOT NULL,
fee TEXT NULL,
memo BLOB NULL,
created_at_time INTEGER NULL, -- ICRC-1 idempotency key
-- Context
related_proposal_id INTEGER NULL,
timestamp INTEGER NOT NULL,
FOREIGN KEY (token_id) REFERENCES tokens(token_id) ON DELETE CASCADE,
FOREIGN KEY (related_proposal_id) REFERENCES proposals(proposal_id),
CHECK (
(tx_type = 'mint' AND from_owner_principal IS NULL)
OR
(tx_type = 'burn' AND to_owner_principal IS NULL)
OR
(tx_type NOT IN ('mint', 'burn'))
)
);
CREATE INDEX idx_tx_token ON token_transactions(token_id);
CREATE INDEX idx_tx_from ON token_transactions(from_owner_principal);
CREATE INDEX idx_tx_to ON token_transactions(to_owner_principal);
CREATE INDEX idx_tx_timestamp ON token_transactions(timestamp);
-- ICRC-1 idempotency: created_at_time が同じなら重複排除
CREATE UNIQUE INDEX idx_tx_idempotency
ON token_transactions(token_id, from_owner_principal, from_subaccount, created_at_time)
WHERE created_at_time IS NOT NULL;
Design Rationale:
created_at_time はICRC-1のidempotency key (同一リクエストの重複実行を防ぐ)mint は from が NULL、burn は to が NULL (CHECK制約で保証)related_proposal_id はガバナンス起因のtransfer追跡用CREATE TABLE token_allowances (
allowance_id INTEGER PRIMARY KEY AUTOINCREMENT,
token_id TEXT NOT NULL,
-- Owner (spender に転送を許可する側)
owner_principal TEXT NOT NULL,
owner_subaccount BLOB NULL,
-- Spender (転送を許可される側)
spender_principal TEXT NOT NULL,
spender_subaccount BLOB NULL,
-- Allowance
amount TEXT NOT NULL, -- BigInt as TEXT
expires_at INTEGER NULL, -- ICRC-2 expiration
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (token_id) REFERENCES tokens(token_id) ON DELETE CASCADE,
UNIQUE(token_id, owner_principal, owner_subaccount, spender_principal, spender_subaccount)
);
CREATE INDEX idx_allowances_owner ON token_allowances(token_id, owner_principal, owner_subaccount);
CREATE INDEX idx_allowances_spender ON token_allowances(token_id, spender_principal, spender_subaccount);
Design Rationale:
expires_at をサポート (ERC-20にはない機能)// can.did に追加
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 : {
// Multi-token ICRC-1 (token_id で切り替え)
icrc1_name : (token_id : text) -> (text) query;
icrc1_symbol : (token_id : text) -> (text) query;
icrc1_decimals : (token_id : text) -> (nat8) query;
icrc1_total_supply : (token_id : text) -> (nat) query;
icrc1_fee : (token_id : text) -> (nat) query;
icrc1_balance_of : (token_id : text, account : Account) -> (nat) query;
icrc1_transfer : (token_id : text, args : TransferArgs) -> (variant { Ok : nat; Err : TransferError });
icrc1_metadata : (token_id : text) -> (vec record { text; Value }) query;
// ICRC-2 Extension (optional)
icrc2_approve : (token_id : text, args : ApproveArgs) -> (variant { Ok : nat; Err : ApproveError });
icrc2_transfer_from : (token_id : text, args : TransferFromArgs) -> (variant { Ok : nat; Err : TransferFromError });
icrc2_allowance : (token_id : text, args : AllowanceArgs) -> (Allowance) query;
}
icrc1_balance_of(token_id, account) の実装:
icrc1BalanceOf : TokenId -> Account -> InstanceRegistryM Nat
icrc1BalanceOf tokenId (MkAccount owner subacct) = do
rows <- execQuery "SELECT balance FROM token_balances WHERE token_id = ? AND owner_principal = ? AND subaccount IS ?"
[tokenId, owner, subacct]
case rows of
[balance] => pure (parseNat balance)
[] => pure 0
_ => throwError "Multiple balances found"
icrc1_transfer(token_id, args) の実装:
icrc1Transfer : TokenId -> TransferArgs -> InstanceRegistryM (Either TransferError Nat)
icrc1Transfer tokenId args = do
-- 1. Check idempotency
whenJust args.created_at_time $ \ts => do
existing <- execQuery "SELECT tx_id FROM token_transactions WHERE token_id = ? AND created_at_time = ?" [tokenId, show ts]
whenJust existing $ \txId => pure (Left $ Duplicate txId)
-- 2. Check balance
fromBalance <- icrc1BalanceOf tokenId (MkAccount args.from args.from_subaccount)
let totalAmount = args.amount + fromMaybe defaultFee args.fee
when (fromBalance < totalAmount) $ pure (Left $ InsufficientFunds fromBalance)
-- 3. Execute transfer
txId <- insertTransaction tokenId "transfer" args
updateBalance tokenId (args.from, args.from_subaccount) (negate totalAmount)
updateBalance tokenId (args.to.owner, args.to.subaccount) args.amount
pure (Right txId)
-- ETHERCLAW を EVM にミント (HTTP Outcall経由)
syncTokenToEVM : TokenId -> ChainId -> Account -> Nat -> InstanceRegistryM ()
syncTokenToEVM tokenId chainId recipient amount = do
-- 1. Get EVM contract address
contractAddr <- getContractAddress tokenId chainId
-- 2. Build ERC-20 mint calldata
let calldata = encodeMintCall recipient amount
-- 3. HTTP Outcall to execute transaction
httpOutcall $ MkEvmTxRequest {
chainId = chainId,
to = contractAddr,
data = calldata,
gasLimit = 100000
}
-- 4. Log sync event
insertEvent "TokenSyncToEVM" $ show (tokenId, chainId, recipient, amount)
-- EVM Transfer イベントを ICP に反映
indexEvmTransferEvents : ChainId -> BlockNumber -> InstanceRegistryM ()
indexEvmTransferEvents chainId fromBlock = do
-- 1. HTTP Outcall to get ERC-20 Transfer events
events <- httpOutcall $ MkEvmGetLogsRequest {
chainId = chainId,
fromBlock = fromBlock,
topics = ["Transfer(address,address,uint256)"]
}
-- 2. Parse and insert to token_transactions
forM_ events $ \event => do
let (from, to, amount) = parseTransferEvent event
insertTransaction tokenId "transfer" $ MkTransferArgs {
from_owner_principal = evmAddressToPrincipal from,
to = MkAccount (evmAddressToPrincipal to) Nothing,
amount = amount,
memo = Just (encode event.transactionHash)
}
-- tokens テーブル拡張
ALTER TABLE tokens ADD COLUMN decimals INTEGER NOT NULL DEFAULT 8;
ALTER TABLE tokens ADD COLUMN fee TEXT NOT NULL DEFAULT '10000';
ALTER TABLE tokens ADD COLUMN metadata TEXT;
-- token_balances に subaccount 追加
ALTER TABLE token_balances ADD COLUMN subaccount BLOB NULL;
-- UNIQUE制約の再作成
DROP INDEX IF EXISTS idx_balances_unique;
CREATE UNIQUE INDEX idx_balances_unique ON token_balances(token_id, owner_principal, subaccount);
-- token_transactions に created_at_time 追加
ALTER TABLE token_transactions ADD COLUMN created_at_time INTEGER NULL;
-- idempotency index
CREATE UNIQUE INDEX idx_tx_idempotency
ON token_transactions(token_id, from_owner_principal, from_subaccount, created_at_time)
WHERE created_at_time IS NOT NULL;
-- token_allowances テーブル新規作成
CREATE TABLE token_allowances (
allowance_id INTEGER PRIMARY KEY AUTOINCREMENT,
token_id TEXT NOT NULL,
owner_principal TEXT NOT NULL,
owner_subaccount BLOB NULL,
spender_principal TEXT NOT NULL,
spender_subaccount BLOB NULL,
amount TEXT NOT NULL,
expires_at INTEGER NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (token_id) REFERENCES tokens(token_id) ON DELETE CASCADE,
UNIQUE(token_id, owner_principal, owner_subaccount, spender_principal, spender_subaccount)
);
service : {
+ // ICRC-1 Standard
+ icrc1_name : (token_id : text) -> (text) query;
+ icrc1_symbol : (token_id : text) -> (text) query;
+ icrc1_decimals : (token_id : text) -> (nat8) query;
+ icrc1_total_supply : (token_id : text) -> (nat) query;
+ icrc1_fee : (token_id : text) -> (nat) query;
+ icrc1_balance_of : (token_id : text, account : Account) -> (nat) query;
+ icrc1_transfer : (token_id : text, args : TransferArgs) -> (variant { Ok : nat; Err : TransferError });
+ icrc1_metadata : (token_id : text) -> (vec record { text; Value }) query;
}
SQLiteにBigInt型がないため、TEXT型で保存してIdris2の Data.BigInt で処理:
-- TEXT → BigInt
parseBalance : String -> BigInt
parseBalance str = fromMaybe 0 (parseBigInt str)
-- BigInt → TEXT
showBalance : BigInt -> String
showBalance = show
ICRC-1のsubaccountは32 bytesだが、NULLの場合はデフォルトサブアカウント:
normalizeSubaccount : Maybe Blob -> Maybe Blob
normalizeSubaccount Nothing = Nothing
normalizeSubaccount (Just b) =
if length b == 32 then Just b else Nothing
ICRC-1では fee はoptional。未指定の場合はtokens.feeを使用:
calculateFee : TokenId -> Maybe Nat -> InstanceRegistryM Nat
calculateFee tokenId (Just fee) = pure fee
calculateFee tokenId Nothing = do
rows <- execQuery "SELECT fee FROM tokens WHERE token_id = ?" [tokenId]
case rows of
[feeStr] => pure (parseNat feeStr)
_ => throwError "Token not found"
created_at_time が同じリクエストは重複実行を防ぐ:
checkIdempotency : TokenId -> Maybe Nat64 -> InstanceRegistryM (Maybe TxId)
checkIdempotency tokenId Nothing = pure Nothing
checkIdempotency tokenId (Just ts) = do
rows <- execQuery "SELECT tx_id FROM token_transactions WHERE token_id = ? AND created_at_time = ?"
[tokenId, show ts]
case rows of
[txId] => pure (Just txId)
[] => pure Nothing
icrc1_name() / icrc1_symbol() がtokensテーブルから正しく取得icrc1_decimals() が8 (またはカスタム値) を返すicrc1_total_supply() が全balanceの合計と一致icrc1_balance_of() がsubaccount対応icrc1_transfer() がidempotency対応 (同じcreated_at_timeで重複実行しない)icrc1_metadata() がJSON → Candid Value変換icrc2_approve() がtoken_allowancesに正しく記録icrc2_transfer_from() がallowanceを消費icrc2_allowance() がexpires_at考慮このSkillは以下の状況で発動:
tokens, token_balances, token_transactions, token_allowances テーブル編集時can.did にICRC-1/ICRC-2インターフェース追加時lazy dfx init --token コマンド実行時自動発動トリガー:
**/Token/**/*.idr, **/can.didCREATE TABLE token, ALTER TABLE tokenicrc1_, icrc2_