はじめに
『DApps開発入門』という本や色々記事を書いているかるでねです。
今回は、現実資産をトークン化したときに必要となる法令順守の確認、移転制御、強制執行などの仕組であるuRWA(Universal Real World Asset Interface)を標準化する提案しているERC7943についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。
概要
ERC7943は、ブロックチェーン上で「現実資産(Real World Assets / RWA)」をトークン化するときに必要になる共通のインターフェースを定める「uRWA(Universal RWA)標準」を提案しています。 RWAには、証券、不動産、商品(コモディティ)、その他の実物資産や金融資産が含まれます。
RWAは、通常のトークンにはあまり求められない法令順守の機能を必要とします。 例えば、資産の凍結、当局や裁判所の命令に沿った強制的な名義変更(法令順守のための移転実行)、許可されたユーザーだけに移転を認める仕組みなどです。 uRWA標準は、こうした機能を最低限の形で追加できるように、既存の一般的なトークン標準(ERC20、ERC721、ERC1155)を拡張します。 同時に、実装の細部には口を出さない「非干渉(オピニオンレス)」な設計方針をとるため、各プロジェクトの事情に合わせた柔軟な実装が可能です。
これにより、DeFi(分散型金融)のプロトコルやアプリケーションは、RWAトークンと一貫した方法で連携できます。 具体的には、あるアカウントが送受信してよいかを事前に確認したり、凍結中の資産を正しく扱ったり、内部のコンプライアンスロジックがどうであれ統一的なインターフェースでRWAにアクセスできるようになります。 さらに、コントラクトが備える機能を外部から判定できる仕組み(ERC165によるイントロスペクション)を採用します。
ERC165については以下の記事を参考にしてください。
動機
RWAは、伝統的な金融とDeFiをつなぐ大きな可能性をもっています。 不動産、社債、コモディティ、アート、証券などをトークン化することで、少額からの分割保有、プログラム可能なコンプライアンス、従来は流動性が低かった資産の二次市場での流通拡大、分散型プロトコルとの統合といった利点が生まれます。
一方で、RWAのトークン化には、純粋なデジタル資産では一般的でない要件が付きまといます。 具体的には、利用者の許可リスト(アロウリスト)の管理、移転制限、資産凍結、法執行機関の要請に応じた手続きなどです。 既存のERC20、ERC721、ERC1155は、それ自体としてはこれらのコンプライアンス機能を想定していません。
過去のRWA向け標準化の試みには、あらゆるケースをカバーしようとして機能が膨らみ、簡素なユースケースでも余計なガスコストや複雑さを招いた例がありました。 例えば、きめ細かなロールベース権限管理を標準で強制したり、オンチェーンの許可リストや特定のオンチェーン身元確認(ID)方式、メタデータ管理の方式まで固定したりすることで、開発や運用の自由度が損なわれることがありました。
RWAには多様な種類があり、単一の万能標準では現実にそぐわないという前提もあります。 そこで、uRWA標準は「最小限の必須インターフェース」を定義し、コンプライアンスやコントロールに関する共通の土台だけを提供します。 これにより、ベースとなるトークン実装は軽量さを保てますし、RWA特有のやり取りに必要な標準的な関数だけが共通化されます。 最終的な狙いは、RWAを扱うDeFiコンポーネントが組み合わせ可能(コンポーザブル)になり、どの資産タイプでも同じ作法で法令順守を確認しながら連携できるようにすることです。
用語
| 用語 | 説明 |
|---|---|
| EIP | Ethereum Improvement Proposalの略です。イーサリアムに関する改良提案の文書で、仕様や標準を定めます。 |
| RWA(Real World Assets) | 現実世界の資産を指します。不動産、証券、コモディティなどをブロックチェーン上のトークンとして表現します。 |
| uRWA(Universal RWA)標準 | RWAトークンが最低限備えるべき法令順守関連のインターフェースを定め、既存のトークン標準を拡張する提案です。 |
| ERC20 / ERC721 / ERC1155 | それぞれ、同質トークン、非代替性トークン、複数トークン対応のイーサリアム標準です。 |
| ERC165 | コントラクトがどのインターフェースに対応しているかを外部から判定できる仕組み(イントロスペクション)を提供する標準です。 |
| 資産の凍結 | 規制やトラブル対応のため、一時的にトークンの移転や使用を止める機能です。 |
| 強制的な移転(enforcement transfer) | 法令や裁判所の命令に基づき、当事者の意思に関わらずトークンの名義や保有先を変更する手続きです。 |
| 許可リスト(allowlist) | 送受信など特定の操作を許可されたアドレスの一覧です。規制対応やKYCの運用に使われます。 |
| 非干渉(オピニオンレス) | 実装方法や具体的な運用手順を標準で固定しない設計方針です。各プロジェクトが事情に合わせて選べます。 |
| コンポーザビリティ | 異なるプロトコルやアプリが、同じ前提で組み合わせられる性質です。標準化されたインターフェースが鍵になります。 |
仕様
ERC7943 の基本構造
ERC7943 は、以下のいずれかのトークン標準を基盤として拡張されます。
| トークン種別 | 基盤となる標準 | 対応インターフェース |
|---|---|---|
| 同質トークン(Fungible Token) | ERC20 | IERC7943Fungible |
| 非代替トークン(Non-Fungible Token) | ERC721 | IERC7943NonFungible |
| 複数トークン対応(Multi Token) | ERC1155 | IERC7943MultiToken |
これらはいずれもERC165を実装し、サポートするインターフェースを識別できるようにします。
ERC20ベースのインターフェース(IERC7943Fungible)
/// @notice Interface for ERC-20 based implementations.
interface IERC7943Fungible is IERC165 {
/// @notice Emitted when tokens are taken from one address and transferred to another.
/// @param from The address from which tokens were taken.
/// @param to The address to which seized tokens were transferred.
/// @param amount The amount seized.
event ForcedTransfer(address indexed from, address indexed to, uint256 amount);
/// @notice Emitted when `setFrozenTokens` is called, changing the frozen `amount` of tokens for `user`.
/// @param user The address of the user whose tokens are being frozen.
/// @param amount The amount of tokens frozen after the change.
event Frozen(address indexed user, uint256 amount);
/// @notice Error reverted when a user is not allowed to interact.
/// @param account The address of the user which is not allowed for interactions.
error ERC7943NotAllowedUser(address account);
/// @notice Error reverted when a transfer is attempted from `user` with an `amount` less or equal than its balance, but greater than its unfrozen balance.
/// @param user The address holding the tokens.
/// @param amount The amount being transferred.
/// @param unfrozen The amount of tokens that are unfrozen and available to transfer.
error ERC7943InsufficientUnfrozenBalance(address user, uint256 amount, uint256 unfrozen);
/// @notice Takes tokens from one address and transfers them to another.
/// @dev Requires specific authorization. Used for regulatory compliance or recovery scenarios.
/// @param from The address from which `amount` is taken.
/// @param to The address that receives `amount`.
/// @param amount The amount to force transfer.
function forcedTransfer(address from, address to, uint256 amount) external;
/// @notice Changes the frozen status of `amount` tokens belonging to a `user`.
/// This overwrites the current value, similar to an `approve` function.
/// @dev Requires specific authorization. Frozen tokens cannot be transferred by the user.
/// @param user The address of the user whose tokens are to be frozen/unfrozen.
/// @param amount The amount of tokens to freeze/unfreeze.
function setFrozenTokens(address user, uint256 amount) external;
/// @notice Checks if a specific user is allowed to interact according to token rules.
/// @dev This is often used for allowlist/KYC/KYB/AML checks.
/// @param user The address to check.
/// @return allowed True if the user is allowed, false otherwise.
function isUserAllowed(address user) external view returns (bool allowed);
/// @notice Checks the frozen status/amount.
/// @param user The address of the user.
/// @return amount The amount of tokens currently frozen for `user`.
function getFrozenTokens(address user) external view returns (uint256 amount);
/// @notice Checks if a transfer is currently possible according to token rules. It enforces validations on the frozen tokens.
/// @dev This may involve checks like allowlists, blocklists, transfer limits and other policy-defined restrictions.
/// @param from The address sending tokens.
/// @param to The address receiving tokens.
/// @param amount The amount being transferred.
/// @return allowed True if the transfer is allowed, false otherwise.
function canTransfer(address from, address to, uint256 amount) external view returns (bool allowed);
}ForcedTransfer
event ForcedTransfer(address indexed from, address indexed to, uint256 amount);特定のアドレスから他のアドレスへ、強制的にトークンを移転した時に発行されるイベント。 強制移転(forcedTransfer)が発生した時に発行されます。 法令遵守や資産回収など、特別な許可を持つアカウントのみが行える操作です。
パラメータ
from- トークンを差し押さえられたアドレス。
to- トークンが送られたアドレス。
amount- 強制移転されたトークン数量。
Frozen
event Frozen(address indexed user, uint256 amount);特定のユーザーのトークンが凍結または解除された時に発行されるイベント。
setFrozenTokens 関数によってユーザーのトークン凍結状態が変更されたときに発行されます。
ユーザーのトークンの一部または全部を一時的に移転できなくする用途に使われます。
パラメータ
user- 凍結対象のユーザーアドレス。
amount- 凍結後のトークン総量。
ERC7943NotAllowedUser
error ERC7943NotAllowedUser(address account);ユーザーが許可されていない場合に発生するエラー。 KYC(本人確認)や許可リストの制約を満たしていないユーザーが操作を行った場合に発生します。
パラメータ
account- 許可されていないユーザーアドレス。
ERC7943InsufficientUnfrozenBalance
error ERC7943InsufficientUnfrozenBalance(address user, uint256 amount, uint256 unfrozen);凍結されていない残高が不足している場合に発生するエラー。 送金額が凍結されていない残高よりも多い場合に発生します。 残高全体は十分でも、凍結分を除いた利用可能残高が不足しているケースを想定しています。
パラメータ
user- トークン保有者。
amount- 送金しようとした金額。
unfrozen- 現在利用可能な(凍結されていない)残高。
forcedTransfer
function forcedTransfer(address from, address to, uint256 amount) external;管理者などの特別な権限を持つアカウントが、強制的にトークンを移転する関数。 規制対応や資産回収、誤送信修正などのために利用されます。 この関数は通常の移転制限を無視し、残高を直接操作して移転を行います。 呼び出しには特別な権限が必要です。
引数
from- トークンを引き出すアドレス。
to- トークンを受け取るアドレス。
amount- 強制移転する数量。
setFrozenTokens
function setFrozenTokens(address user, uint256 amount) external;指定したユーザーのトークン凍結量を設定する関数。 ユーザーが保有するトークンのうち、指定量を凍結します。 凍結されたトークンはユーザー自身では移転できなくなります。 権限を持つアカウントのみが呼び出せます。
引数
user- 凍結対象のアドレス。
amount- 凍結後の総凍結量。
isUserAllowed
function isUserAllowed(address user) external view returns (bool allowed);指定したユーザーがトークン操作を許可されているかを確認する関数。 KYC、KYB(法人確認)、AML(マネーロンダリング対策)などのポリシーに基づき、ユーザーが許可リストに含まれるかを確認します。 トランザクション実行の前に確認されることが多いです。
引数
user- チェック対象のユーザー。
戻り値
allowed- 許可されていれば true、禁止されていれば false。
getFrozenTokens
function getFrozenTokens(address user) external view returns (uint256 amount);ユーザーが凍結中のトークン量を確認する関数。 現在の凍結状態を外部から確認できます。 コントラクト内部のストレージを変更することはありません。
引数
user- チェック対象のアドレス。
戻り値
amount- 凍結されているトークン量。
canTransfer
function canTransfer(address from, address to, uint256 amount) external view returns (bool allowed);トークンの送金が許可されているかを確認する関数。 送金可能かどうかを判断します。 凍結状態や許可リスト、ブロックリスト、取引制限などの内部ルールを考慮して判定します。 コントラクトが一時停止(pause)されている場合は false を返します。
引数
from- 送金元アドレス。
to- 送金先アドレス。
amount- 送金する数量。
戻り値
allowed- 送金可能なら
true、不可能ならfalse。
- 送金可能なら
ERC721ベースのインターフェース(IERC7943NonFungible)
非代替トークン(NFT)版の仕様です。
構造はERC20版とほぼ同じですが、数量ではなく tokenId を扱います。
/// @notice Interface for ERC-721 based implementations.
interface IERC7943NonFungible is IERC165 {
/// @notice Emitted when `tokenId` is taken from one address and transferred to another.
/// @param from The address from which `tokenId` is taken.
/// @param to The address to which seized `tokenId` is transferred.
/// @param tokenId The ID of the token being transferred.
event ForcedTransfer(address indexed from, address indexed to, uint256 indexed tokenId);
/// @notice Emitted when `setFrozenTokens` is called, changing the frozen status of `tokenId` for `user`.
/// @param user The address of the user whose `tokenId` is subjected to freeze/unfreeze.
/// @param tokenId The ID of the token subjected to freeze/unfreeze.
/// @param frozenStatus Whether `tokenId` has been frozen or unfrozen.
event Frozen(address indexed user, uint256 indexed tokenId, bool indexed frozenStatus);
/// @notice Error reverted when a user is not allowed to interact.
/// @param account The address of the user which is not allowed for interactions.
error ERC7943NotAllowedUser(address account);
/// @notice Error reverted when a transfer is attempted from `user` with a `tokenId` which has been previously frozen.
/// @param user The address holding the tokens.
/// @param tokenId The ID of the token being frozen.
error ERC7943FrozenTokenId(address user, uint256 tokenId);
/// @notice Takes `tokenId` from one address and transfers it to another.
/// @dev Requires specific authorization. Used for regulatory compliance or recovery scenarios.
/// @param from The address from which `tokenId` is taken.
/// @param to The address that receives `tokenId`.
/// @param tokenId The ID of the token being transferred.
function forcedTransfer(address from, address to, uint256 tokenId) external;
/// @notice Changes the frozen status of `tokenId` belonging to a `user`.
/// This overwrites the current value, similar to an `approve` function.
/// @dev Requires specific authorization. Frozen tokens cannot be transferred by the user.
/// @param user The address of the user whose tokens are to be frozen/unfrozen.
/// @param tokenId The ID of the token to freeze/unfreeze.
/// @param frozenStatus whether `tokenId` is being frozen or not.
function setFrozenTokens(address user, uint256 tokenId, bool frozenStatus) external;
/// @notice Checks if a specific user is allowed to interact according to token rules.
/// @dev This is often used for allowlist/KYC/KYB/AML checks.
/// @param user The address to check.
/// @return allowed True if the user is allowed, false otherwise.
function isUserAllowed(address user) external view returns (bool allowed);
/// @notice Checks the frozen status of a specific `tokenId`.
/// @param user The address of the user.
/// @param tokenId The ID of the token.
/// @return frozenStatus Whether `tokenId` is currently frozen for `user`.
function getFrozenTokens(address user, uint256 tokenId) external view returns (bool frozenStatus);
/// @notice Checks if a transfer is currently possible according to token rules. It enforces validations on the frozen tokens.
/// @dev This may involve checks like allowlists, blocklists, transfer limits and other policy-defined restrictions.
/// @param from The address sending tokens.
/// @param to The address receiving tokens.
/// @param tokenId The ID of the token being transferred.
/// @return allowed True if the transfer is allowed, false otherwise.
function canTransfer(address from, address to, uint256 tokenId) external view returns (bool allowed);
}主な相違点
| 項目 | ERC20版 | ERC721版 |
|---|---|---|
| 管理単位 | 数量(amount) | トークンID(tokenId) |
| 凍結対象 | 一定数量 | 個別トークン単位 |
| 特有のエラー | ERC7943FrozenTokenId | ERC7943InsufficientUnfrozenBalance |
ERC1155ベースのインターフェース(IERC7943MultiToken)
マルチトークン(複数トークン)対応版です。 各トークンIDごとに数量を管理し、凍結や強制移転を制御します。 ERC20とERC721の中間的な性質を持ちます。
/// @notice Interface for ERC-1155 based implementations.
interface IERC7943MultiToken is IERC165 {
/// @notice Emitted when tokens are taken from one address and transferred to another.
/// @param from The address from which tokens were taken.
/// @param to The address to which seized tokens were transferred.
/// @param tokenId The ID of the token being transferred.
/// @param amount The amount seized.
event ForcedTransfer(address indexed from, address indexed to, uint256 indexed tokenId, uint256 amount);
/// @notice Emitted when `setFrozenTokens` is called, changing the frozen `amount` of `tokenId` tokens for `user`.
/// @param user The address of the user whose tokens are being frozen.
/// @param tokenId The ID of the token being frozen.
/// @param amount The amount of tokens frozen after the change.
event Frozen(address indexed user, uint256 indexed tokenId, uint256 amount);
/// @notice Error reverted when a user is not allowed to interact.
/// @param account The address of the user which is not allowed for interactions.
error ERC7943NotAllowedUser(address account);
/// @notice Error reverted when a transfer is attempted from `user` with an `amount` of `tokenId` less or equal than its balance, but greater than its unfrozen balance.
/// @param user The address holding the tokens.
/// @param tokenId The ID of the token being transferred.
/// @param amount The amount being transferred.
/// @param unfrozen The amount of tokens that are unfrozen and available to transfer.
error ERC7943InsufficientUnfrozenBalance(address user, uint256 tokenId, uint256 amount, uint256 unfrozen);
/// @notice Takes tokens from one address and transfers them to another.
/// @dev Requires specific authorization. Used for regulatory compliance or recovery scenarios.
/// @param from The address from which `amount` is taken.
/// @param to The address that receives `amount`.
/// @param tokenId The ID of the token being transferred.
/// @param amount The amount to force transfer.
function forcedTransfer(address from, address to, uint256 tokenId, uint256 amount) external;
/// @notice Changes the frozen status of `amount` of `tokenId` tokens belonging to a `user`.
/// This overwrites the current value, similar to an `approve` function.
/// @dev Requires specific authorization. Frozen tokens cannot be transferred by the user.
/// @param user The address of the user whose tokens are to be frozen/unfrozen.
/// @param tokenId The ID of the token to freeze/unfreeze.
/// @param amount The amount of tokens to freeze/unfreeze.
function setFrozenTokens(address user, uint256 tokenId, uint256 amount) external;
/// @notice Checks if a specific user is allowed to interact according to token rules.
/// @dev This is often used for allowlist/KYC/KYB/AML checks.
/// @param user The address to check.
/// @return allowed True if the user is allowed, false otherwise.
function isUserAllowed(address user) external view returns (bool allowed);
/// @notice Checks the frozen status/amount of a specific `tokenId`.
/// @param user The address of the user.
/// @param tokenId The ID of the token.
/// @return amount The amount of `tokenId` tokens currently frozen for `user`.
function getFrozenTokens(address user, uint256 tokenId) external view returns (uint256 amount);
/// @notice Checks if a transfer is currently possible according to token rules. It enforces validations on the frozen tokens.
/// @dev This may involve checks like allowlists, blocklists, transfer limits and other policy-defined restrictions.
/// @param from The address sending tokens.
/// @param to The address receiving tokens.
/// @param tokenId The ID of the token being transferred.
/// @param amount The amount being transferred.
/// @return allowed True if the transfer is allowed, false otherwise.
function canTransfer(address from, address to, uint256 tokenId, uint256 amount) external view returns (bool allowed);
}追加仕様
| 要件 | 内容 |
|---|---|
| ERC165の実装 | supportsInterface 関数を実装し、以下の識別子を返す必要があります。 |
| Fungible(同質) | 0x20e30a04 |
| Non-Fungible(非代替) | 0xa1264b3e |
| Multi Token(複数) | 0x5ffc456d |
また、forcedTransfer や setFrozenTokens はアクセス制御(例:onlyOwner やロールベースアクセス制御)によって保護する必要があります。
具体的な制御方法は標準では指定されません。
トランスファー時の挙動
| 操作 | 要件 |
|---|---|
公開移転 (transfer, transferFrom, safeTransferFrom など) |
canTransfer または isUserAllowed が false の場合は実行してはいけません。 |
| ミント | isUserAllowed が false のアカウントにはミントしてはいけません。 |
| Burn | 通常は制限不要。ただし凍結済み資産を含む場合は凍結状態を更新し、Frozen イベントを発行する必要があります。 |
エラー処理ポリシー
一般的な拒否には ERC7943NotAllowedUser を使用できます。
より詳細なケースでは、ERC7943InsufficientUnfrozenBalance または ERC7943FrozenTokenId を使用することが推奨されます。
もし送金額が残高を超えている、またはトークンIDがユーザー所有でない場合は、ベース標準(ERC20/ERC721/ERC1155)のエラーを使います。
補足
全体像
| 重点領域 | ねらい | 具体化の方法 |
|---|---|---|
| 最小主義(Minimalism) | RWAに必要な最低限の制御に絞り、過剰な機能強制を避ける。 | forcedTransfer、setFrozenTokens、isUserAllowed、canTransfer、getFrozenTokens と、そのためのイベント/エラーのみを標準化。 |
| 柔軟なコンプライアンス | 実装内部のチェック方法は縛らず、外部から一貫した確認手段を提供する。 | 読み取り関数である isUserAllowed、canTransfer、getFrozenTokens を定義。内部ロジックは各実装に委ねる。 |
| 互換性(Compatibility) | 既存のERC20 / ERC721 / ERC1155 と共存。 | ベース標準に「インターフェース層」を追加するだけの形にする。 |
| 強制執行ルール | 規制対応・回復手続きに必要な操作を標準化。 | forcedTransfer と setFrozenTokens を必須関数として定義し、アクセス制御を要求。 |
| ERC165 | 実装がインターフェース対応を明示できるようにする。 | supportsInterface で対応可否を通知。 |
最小主義
ERC7943は、RWAで一般的に必要となる機能だけを定義します。
余計なロール設計や特定のID方式、メタデータ仕様などは標準では強制しません。
定義されるエラーも、導入した機能と一対になる最低限のものに絞っています。
具体的には、ユーザー許可に対する ERC7943NotAllowedUser、凍結を考慮した残高不足に対する ERC7943InsufficientUnfrozenBalance を用意します。
これにより、「どの条件で、なぜ失敗したか」をコントラクト外部が正確に把握しやすくなります。
イベント Frozen と ForcedTransfer は、通常とは異なる移転(差し押さえ等)や凍結の状態変化を外部に確実に通知するためにあります。
特にオフチェーンのインデクサが、差し押さえや凍結量の正確な会計処理を行えるよう、ベースとなるトークンの Transfer イベントとの発行順序に注意を払う設計になっています。
柔軟なコンプライアンス
isUserAllowed、canTransfer、getFrozenTokens は、コントラクトのコンプライアンスポリシーや凍結状態を外部から確認するための読み取り関数です。
どうやって許可判定するかは各実装の自由であり、オンチェーン/オフチェーンのKYC、ブロックリスト、移転制限、時限ルールなど、さまざまな戦略を取れます。
互換性
ERC7943は既存のERC20、ERC721、ERC1155に重ねる「インターフェース層」です。
既存のウォレットやブロックエクスプローラは、ベース標準の関数をそのまま扱えます。
isUserAllowed・canTransfer・getFrozenTokens による制約は実装側が課しますが、ERC7943の専用機能を使い切るには、ウォレットやプロトコル側で明示的に統合する必要があります。
必要な強制執行ルール
RWAの現実要件として、強制移転と凍結/解凍は避けて通れません。
そこで forcedTransfer と setFrozenTokens を標準化し、アクセス制御を必須としています。
例えば、forcedTransfer は通常の移転チェックをバイパスできます。
ただし、凍結状態を変更する場合は、凍結変更(Frozen)→ ベース Transfer イベント の順でイベントを出す、といった順序が求められます。
ERC165の採用
ERC165を実装することで、外部からERC7943への対応有無を安全に検出できます。
プロトコルは supportsInterface を確認するだけで、RWA対応のトークンとしての扱いを切り替えられます。
実運用のイメージ
自動マーケットメイカー(AMM)やレンディングのようなDeFiプロトコルは、ERC7943に対応したERC20を相手に、isUserAllowed や canTransfer を用いて事前チェックを行えます。
執行系の操作(forcedTransfer、setFrozenTokens)は、第三者の運用主体や外部プロトコルが自動化し、プログラマブルなコンプライアンスを構築できます。
加えて、各トークンに対して、オンチェーンID連携、(配当等の)履歴ベース機能、半同質トークン化のためのメタデータなど、資産タイプ固有の拡張を重ねられます。
拡張性
ERC7943は規制資産に必要な原始的操作(プリミティブ)を提供しますが、それ以外の機能は拡張(Extension)として自由に追加できます。 例えば、setFrozenTokens のような管理操作を行う際に、法的な証拠(legal proof)を添付することを必須にしたい場合は、別の関数でその証拠を一緒に処理(バッチング)する設計を取ることができます。
ComplianceOperator
contract ComplianceOperator is IERC7943MultiToken {
// ... 必要な状態変数やコンストラクタ、アクセス制御などを実装 ...
function setFrozenTokensWithProof(
address user,
uint256 tokenId,
uint256 amount,
bytes calldata legalProof
) public {
/// legalProof の検証・記録などをここで実施
token.setFrozenToken(user, tokenId, amount);
}
}拡張コントラクトの一部で、凍結操作に法的根拠(legalProof)を添付して実行する例。
setFrozenTokens と同等の操作を、追加的な証憑データを伴って行います。
外部の規制遵守プロセスと連携しやすくし、例えば署名付き命令書の検証や、証拠のオフチェーン保管のためのハッシュ記録などを組み合わせられます。
パラメータ
user- 凍結対象のアドレス。
tokenId- 対象トークンID。
amount- 凍結後の数量。
legalProof- 法的根拠データ(署名済み命令書等)のバイト列。
setFrozenTokensWithProof
function setFrozenTokensWithProof(
address user,
uint256 tokenId,
uint256 amount,
bytes calldata legalProof
) public {
/// do anything with `legalProof`
token.setFrozenToken(user, tokenId, amount);
}凍結量の設定を、法的根拠付きで実施する関数。
規制当局や裁判所命令に紐づくデータを legalProof として受け取り、検証・記録したうえで実際の凍結操作を委譲します。
実装では、アクセス制御(ロールベース等)や token.setFrozenTokens 呼び出しの可用性、イベント発行、エラー処理などを適切に組み込みます。
引数
user- 凍結対象のアドレス。
tokenId- 凍結対象のトークンID。
amount- 設定する凍結数量。
legalProof- 法的根拠データ。
- 検証や監査記録に使用します。
マルチコールや一時停止(Pausable)との併用
ERC6357で示されたようなマルチコール・パターンを使えば、ERC7943のプリミティブ(forcedTransfer、setFrozenTokens など)と追加機能を1トランザクションにまとめることができます。
さらに、プラットフォーム全体の安全性を高めるために、modifierや関数内に一時停止(Pausable)を組み込み、canTransfer の返り値(false)と整合する形で停止状態を明確に伝える運用も可能です。
命名に関する考え方
命名は、RWA領域での意味の一貫性と中立性を重視しています。 既存標準との整合も考慮され、学習コストの低減や相互運用性に寄与します。
| 名称 | 採用理由 |
|---|---|
forcedTransfer |
動機(没収・回復など)に依存しない、中立的な動作名。「資産を強制的に移す」事実のみを表します。ERC3643 に合わせた命名で、RWA標準間の整合性を確保します。 |
canTransfer |
ERC3643、ERC7518 で広く使われる命名に揃えることで、実装・統合の認知負荷を下げます。 |
setFrozenTokens / getFrozenTokens |
移転制限の管理を素直に表す名称で、ERC3643 の命名パターンと整合させています。 |
| 凍結関連の統合 | freeze/unfreeze を分けず、1つの設定関数(上書き型)と1つのイベント(Frozen)に収束。仕様をシンプルに保ちます。 |
| 「Frozen」用語選定 | 同質資産(数量)と非代替資産(状態)のいずれにも中立に適用でき、表現がブレません。 |
ERC7943InsufficientUnfrozenBalance |
「不足」と「未解凍」の組み合わせで事象を正確に伝えます。ERC6093 のガイドラインに則り、プレフィクスや主語が明確です。 |
イベントの順序と具体例
forcedTransfer が凍結状態を更新しながら移転する場合、凍結の変更(Frozen)→ ベース標準の転送イベント の順にイベントを出すことが重要です。
例えば、ERC721 / ERC1155のように受取側で外部呼び出しが起きる可能性がある場合、未凍結量が先に更新されていることが再入可能性(reentrancy)対策の観点でも有利です。
オフチェーンのインデクサも、先に凍結量の変動を確定させてから転送を取り込めるため、会計が正確になります。
Frozen
event Frozen(address indexed user, uint256 amount);ユーザーの凍結量が設定・変更されたときに発行されるイベント。
setFrozenTokens により、ユーザーごとの凍結量が上書きされたことを通知します。オフチェーンの会計や監査で、時点の凍結量を追跡するのに役立ちます。
パラメータ
user- 対象ユーザーのアドレス。
amount- 変更後の凍結総量。
ForcedTransfer
event ForcedTransfer(address indexed from, address indexed to, uint256 amount);強制移転が行われたときに発行されるイベント。 通常のユーザー発トランスファーではなく、権限者による強制的な資産移転が行われたことを通知します。 差し押さえや回復など、特殊なケースの識別に使います。
パラメータ
from- 引き落とし元アドレス。
to- 受取先アドレス。
amount- 強制移転された数量。
互換性
ERC7943は新しいインターフェース標準であり、ERC20、ERC721、ERC1155 そのものを書き換えるものではありません。
既存のウォレットやエクスプローラは、従来どおりベース標準の機能にアクセスできます。
そのうえで、実装側が isUserAllowed、canTransfer、getFrozenTokens の結果を内部で反映させ、transfer やミントが不許可となる条件を施します。
ただし、ERC7943の追加機能(強制移転・凍結量設定・詳細エラーなど)をフルに活用するには、統合側(ウォレット、プロトコル、インデクサ)で明示的な対応が必要です。
例えば、AMMやレンディングは、流動性提供や借入時に isUserAllowed / canTransfer を呼び出し、事前に可否判定を組み込みます。
インデクサは Frozen / ForcedTransfer とベース標準のイベントを順序どおりに取り込み、凍結量や差し押さえ履歴を正確に再構築します。
代表的関数の再整理
isUserAllowed
function isUserAllowed(address user) external view returns (bool allowed);ユーザーが操作を許可されているかを返す関数。 KYC/KYB/AMLや許可・ブロックリストなどの内部ポリシーに照らして判定します。 読み取り関数であり、ストレージは変更しません。
引数
user- 判定対象のアドレス。
戻り値
allowed- 許可なら
true、不許可ならfalse。
- 許可なら
canTransfer
function canTransfer(address from, address to, uint256 amountOrTokenId) external view returns (bool allowed);指定の送金が現在可能かを返す関数。 凍結量の検証(未凍結量の範囲内か)、許可ユーザーかどうか、ポーズ状態かどうか、その他の制限(リミットやブロックリスト等)を判定に含めます。 読み取り関数であり、ストレージは変更しません。
引数
from- 送金元アドレス。
to- 送金先アドレス。
amountOrTokenId- ERC20/ERC1155では数量、ERC721では
tokenId。
- ERC20/ERC1155では数量、ERC721では
戻り値
allowed- 可能なら
true、不可能ならfalse。
- 可能なら
getFrozenTokens
function getFrozenTokens(address user[, uint256 tokenId]) external view returns (uint256 amountOrStatus);ユーザー(および必要に応じて tokenId)の凍結状態を返す関数。
ERC20/ERC1155では数量(未解凍量との差分を判断可能)、ERC721ではフラグ(凍結中かどうか)を返します。
読み取り関数であり、ストレージは変更しません。
引数
user- 対象ユーザー。
tokenId(ERC721/1155 の場合)- 対象トークンID。
戻り値
amountOrStatus- ERC20/ERC1155は凍結数量、ERC721は凍結フラグ。
setFrozenTokens
function setFrozenTokens(address user[, uint256 tokenId], uint256 amountOrStatus) external;凍結量(または凍結フラグ)を設定する関数。
上書き型の設定を行います。
権限者のみが呼び出せます。実行時に Frozen イベントを発行します。
引数
user- 対象ユーザー。
tokenId(ERC721/1155 の場合)- 対象トークンID。
amountOrStatus- ERC20/ERC1155は数量、ERC721は真偽値。
forcedTransfer
function forcedTransfer(address from, address to[, uint256 tokenId], uint256 amountOrOne) external;権限者が強制的に資産を移す関数。
内部残高・保有者を直接操作して移転します。
canTransfer の検査はバイパスします。
必要に応じて凍結状態を先に更新し、Frozen → ベース転送イベント → ForcedTransfer の順でイベントを適切に発行します。
to に対しては isUserAllowed の検査を必ず行います。
引数
from- 引き落とし元アドレス。
to- 受取先アドレス。
tokenId(ERC721/1155 の場合)- 対象トークンID。
amountOrOne- ERC20/ERC1155は数量、ERC721は1(単一トークン)。
参考実装
ERC20
pragma solidity ^0.8.29;
/* required imports ... */
contract uRWA20 is Context, ERC20, AccessControlEnumerable, IERC7943Fungible {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
bytes32 public constant ENFORCER_ROLE = keccak256("ENFORCER_ROLE");
bytes32 public constant WHITELIST_ROLE = keccak256("WHITELIST_ROLE");
mapping(address user => bool whitelisted) public isWhitelisted;
mapping(address user => uint256 amount) internal _frozenTokens;
event Whitelisted(address indexed account, bool status);
error NotZeroAddress();
constructor(string memory name, string memory symbol, address initialAdmin) ERC20(name, symbol) {
/* give initialAdmin necessary roles ...*/
}
function canTransfer(address from, address to, uint256 amount) public virtual view returns (bool allowed) {
if (balanceOf(from) < _frozenTokens[from]) return;
if (amount > balanceOf(from) - _frozenTokens[from]) return;
if (!isUserAllowed(from) || !isUserAllowed(to)) return;
allowed = true;
}
function isUserAllowed(address user) public virtual view returns (bool allowed) {
if (isWhitelisted[user]) allowed = true;
}
function getFrozenTokens(address user) external view returns (uint256 amount) {
amount = _frozenTokens[user];
}
function changeWhitelist(address account, bool status) external onlyRole(WHITELIST_ROLE) {
require(account != address(0), NotZeroAddress());
isWhitelisted[account] = status;
emit Whitelisted(account, status);
}
/* standard mint and burn functions with access control ...*/
function setFrozenTokens(address user, uint256 amount) public onlyRole(ENFORCER_ROLE) {
_frozenTokens[user] = amount;
emit Frozen(user, amount);
}
function forcedTransfer(address from, address to, uint256 amount) public onlyRole(ENFORCER_ROLE) {
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
_excessFrozenUpdate(from, amount);
super._update(from, to, amount);
emit ForcedTransfer(from, to, amount);
}
function _excessFrozenUpdate(address user, uint256 amount) internal {
uint256 unfrozenBalance = _unfrozenBalance(user);
if(amount > unfrozenBalance && amount <= balanceOf(user)) {
// Protect from underflow: if amount > balanceOf(user) the call will revert in super._update with insufficient balance error
_frozenTokens[user] -= amount - unfrozenBalance; // Reduce by excess amount
emit Frozen(user, _frozenTokens[user]);
}
}
function _unfrozenBalance(address user) internal view returns(uint256 unfrozenBalance) {
unfrozenBalance = balanceOf(user) < _frozenTokens[user] ? 0 : balanceOf(user) - _frozenTokens[user];
}
function _update(address from, address to, uint256 amount) internal virtual override {
if (from != address(0) && to != address(0)) { // Transfer
require(amount <= balanceOf(from), IERC20Errors.ERC20InsufficientBalance(from, balanceOf(from), amount));
uint256 unfrozenBalance = _unfrozenBalance(from);
require(amount <= unfrozenBalance, ERC7943InsufficientUnfrozenBalance(from, amount, unfrozenBalance));
require(canTransfer(from, to, amount), "ERC7943: transfer not allowed");
} else if (from == address(0)) { // Mint
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
} else { // Burn
_excessFrozenUpdate(from, amount);
}
super._update(from, to, amount);
}
function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControlEnumerable, IERC165) returns (bool) {
return interfaceId == type(IERC7943Fungible).interfaceId ||
interfaceId == type(IERC20).interfaceId ||
super.supportsInterface(interfaceId);
}
}ERC721
pragma solidity ^0.8.29;
/* required imports ... */
contract uRWA721 is Context, ERC721, AccessControlEnumerable, IERC7943NonFungible {
/* same definitions, constructor and changeWhitelist function as before ...*/
mapping(address user => mapping(uint256 tokenId => bool frozen)) internal _frozenTokens;
function isUserAllowed(address user) public view virtual override returns (bool allowed) {
if (isWhitelisted[user]) allowed = true;
}
function canTransfer(address from, address to, uint256 tokenId) public view virtual override returns (bool allowed) {
address owner = _ownerOf(tokenId);
if (owner != from || owner == address(0)) return;
if (!isUserAllowed(from) || !isUserAllowed(to)) return;
if (_frozenTokens[from][tokenId]) return;
allowed = true;
}
function getFrozenTokens(address user, uint256 tokenId) external view returns (bool frozenStatus) {
frozenStatus = _frozenTokens[user][tokenId];
}
function setFrozenTokens(address user, uint256 tokenId, bool frozenStatus) public onlyRole(ENFORCER_ROLE) {
require(user == ownerOf(tokenId), IERC721Errors.ERC721InvalidOwner(user));
_frozenTokens[user][tokenId] = frozenStatus;
emit Frozen(user, tokenId, frozenStatus);
}
function forcedTransfer(address from, address to, uint256 tokenId) public virtual override onlyRole(ENFORCER_ROLE) {
require(to != address(0), ERC721InvalidReceiver(address(0)));
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
_excessFrozenUpdate(from , tokenId);
super._update(to, tokenId, address(0)); // Skip _update override
ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, "");
emit ForcedTransfer(from, to, tokenId);
}
function _excessFrozenUpdate(address from, uint256 tokenId) internal {
_validateCorrectOwner(from, tokenId);
if(_frozenTokens[from][tokenId]) {
_frozenTokens[from][tokenId] = false; // Unfreeze the token if it was frozen
emit Frozen(from, tokenId, false);
}
}
function _validateCorrectOwner(address claimant, uint256 tokenId) internal view {
address currentOwner = ownerOf(tokenId);
require(currentOwner == claimant, ERC721IncorrectOwner(claimant, tokenId, currentOwner));
}
/* standard mint function with access control ...*/
function burn(uint256 tokenId) external virtual onlyRole(BURNER_ROLE) {
address previousOwner = _update(address(0), tokenId, _msgSender());
if (previousOwner == address(0)) revert ERC721NonexistentToken(tokenId);
}
function _update(address to, uint256 tokenId, address auth) internal virtual override returns(address) {
address from = _ownerOf(tokenId);
if (auth != address(0)) {
_checkAuthorized(from, auth, tokenId);
}
if (from != address(0) && to != address(0)) { // Transfer
_validateCorrectOwner(from, tokenId);
require(!_frozenTokens[from][tokenId], ERC7943FrozenTokenId(from, tokenId));
require(canTransfer(from, to, tokenId), "ERC7943: transfer not allowed");
} else if (from == address(0)) { // Mint
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
} else { // Burn
_excessFrozenUpdate(from, tokenId);
}
return super._update(to, tokenId, auth);
}
function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControlEnumerable, ERC721, IERC165) returns (bool) {
return interfaceId == type(IERC7943NonFungible).interfaceId ||
super.supportsInterface(interfaceId);
}
}ERC1155
pragma solidity ^0.8.29;
/* required imports ... */
contract uRWA1155 is Context, ERC1155, AccessControlEnumerable, IERC7943MultiToken {
/* same definitions, constructor and changeWhitelist function as before ...*/
mapping(address user => mapping(uint256 tokenId => uint256 amount)) internal _frozenTokens;
function canTransfer(address from, address to, uint256 tokenId, uint256 amount) public view virtual override returns (bool allowed) {
if (balanceOf(from, tokenId) < _frozenTokens[from][tokenId]) return;
if (balanceOf(from, tokenId) < amount) return;
if (!isUserAllowed(from) || !isUserAllowed(to)) return;
if (amount > balanceOf(from, tokenId) - _frozenTokens[from][tokenId]) return;
allowed = true;
}
function isUserAllowed(address user) public view virtual override returns (bool allowed) {
if (isWhitelisted[user]) allowed = true;
}
function getFrozenTokens(address user, uint256 tokenId) external view returns (uint256 amount) {
amount = _frozenTokens[user][tokenId];
}
function setFrozenTokens(address user, uint256 tokenId, uint256 amount) public onlyRole(ENFORCER_ROLE) {
_frozenTokens[user][tokenId] = amount;
emit Frozen(user, tokenId, amount);
}
function forcedTransfer(address from, address to, uint256 tokenId, uint256 amount) public onlyRole(ENFORCER_ROLE) {
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
// Reimplementing _safeTransferFrom to avoid the check on _update
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
_excessFrozenUpdate(from, tokenId, amount);
uint256[] memory ids = new uint256[](1);
uint256[] memory values = new uint256[](1);
ids[0] = tokenId;
values[0] = amount;
super._update(from, to, ids, values);
if (to != address(0)) {
address operator = _msgSender();
if (ids.length == 1) {
uint256 id = ids[0];
uint256 value = values[0];
ERC1155Utils.checkOnERC1155Received(operator, from, to, id, value, "");
} else {
ERC1155Utils.checkOnERC1155BatchReceived(operator, from, to, ids, values, "");
}
}
emit ForcedTransfer(from, to, tokenId, amount);
}
function _excessFrozenUpdate(address user, uint256 tokenId, uint256 amount) internal {
uint256 unfrozenBalance = _unfrozenBalance(user, tokenId);
if(amount > unfrozenBalance && amount <= balanceOf(user, tokenId)) {
// Protect from underflow: if amount > balanceOf(user) the call will revert in super._update with insufficient balance error
_frozenTokens[user][tokenId] -= amount - unfrozenBalance; // Reduce by excess amount
emit Frozen(user, tokenId, _frozenTokens[user][tokenId]);
}
}
/* standard mint and burn functions with access control ...*/
function _unfrozenBalance(address user, uint256 tokenId) internal view returns(uint256 unfrozenBalance) {
unfrozenBalance = balanceOf(user, tokenId) < _frozenTokens[user][tokenId] ? 0 : balanceOf(user, tokenId) - _frozenTokens[user][tokenId];
}
function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal virtual override {
if (ids.length != values.length) {
revert ERC1155InvalidArrayLength(ids.length, values.length);
}
if (from != address(0) && to != address(0)) { // Transfer
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids[i];
uint256 value = values[i];
require(value <= balanceOf(from, id), ERC1155InsufficientBalance(from, balanceOf(from, id), value, id));
uint256 unfrozenBalance = _unfrozenBalance(from, id);
require(value <= unfrozenBalance, ERC7943InsufficientUnfrozenBalance(from, id, value, unfrozenBalance));
require(canTransfer(from, to, id, value), "ERC7943: transfer not allowed");
}
}
if (from == address(0)) { // Mint
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
} else if (to == address(0)) { // Burn
for (uint256 j = 0; j < ids.length; ++j) {
_excessFrozenUpdate(from, ids[j], values[j]);
}
}
super._update(from, to, ids, values);
}
function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControlEnumerable, ERC1155, IERC165) returns (bool) {
return interfaceId == type(IERC7943MultiToken).interfaceId ||
super.supportsInterface(interfaceId);
}
}セキュリティ
forcedTransfer と setFrozenTokens におけるアクセス制御
forcedTransfer や setFrozenTokens は、通常のトークン移転とは異なり、ユーザーの意思を介さずに資産を操作できる特権関数です。
したがって、これらの関数へのアクセス制御は極めて重要です。
アクセス権限の管理を誤ると、第三者が任意に資産を移転・凍結・解除できる状態(資産の窃取や不正操作)を招きます。 そのため、以下のような安全設計パターンを採用することが推奨されます。
| 推奨設計 | 内容 |
|---|---|
| マルチシグ(multisig) | 複数の署名者によって承認が必要な構造を採用し、単一の鍵紛失や乗っ取りによる操作を防ぐ。 |
| タイムロック(timelock) | 操作実行までに一定時間の遅延を設け、誤操作や不正操作に対して人間の監視による介入余地を確保する。 |
これらの制御を設けることで、「不正アクセスによる強制移転」や「勝手な凍結設定」といった重大なリスクを回避できます。
setFrozenTokens に対するフロントラン攻撃(Front-running)の懸念
setFrozenTokens 関数は、ERC20の approve 関数と同様に、フロントラン(先回り取引)攻撃の影響を受ける可能性があります。
攻撃者は、凍結される直前に自分の資産を送金し逃げることで、凍結処理を無効化する行動を取ることが考えられます。 これを防ぐために、標準仕様では「保有量以上の凍結も許可する」という設計が提案されています。 もしこの提案を採用しない場合、つまり「所有している分だけしか凍結できない」とすると、攻撃者が保有資産を急いで動かし、凍結が間に合わないリスクが高まります。
また、より安全な設計として、凍結状態を段階的に増減させる(インクリメンタル方式)機能の追加も考えられます。 たとえば、凍結を即時反映ではなく一定ブロック後に有効化する、あるいは時間的に段階的にロックする、といった拡張設計が効果的です。
| 対応策 | 内容 |
|---|---|
| 凍結上限を保有量に限定しない | 一時的に保有量以上を凍結できるようにし、逃避行動を抑止する。 |
| 段階的凍結機能を導入 | 凍結を漸進的に強化・解除する設計を取り入れることで、フロントランの影響を最小化する。 |
実装ロジックの正確性
isUserAllowed、canTransfer、forcedTransfer、getFrozenTokens などのインターフェース関数は、資産移転の可否や法令順守の中核となるロジックを担います。
これらの関数の内部実装に不備があると、意図しない挙動が発生します。
- 不適切な条件分岐により、本来禁止される移転が許可される。
- 逆に、合法な取引が誤って拒否される。
- 凍結量の更新ミスにより、残高不整合やダブルスパンド(同一資産の二重利用)が起こる。
したがって、各関数の設計・テスト時には、想定されるあらゆるパターンでのシミュレーションが必須です。
特に canTransfer の戻り値は、DeFiプロトコルやウォレットの挙動に直接影響するため、慎重な検証が求められます。
コントラクト全体における一般的なセキュリティ対策
ERC7943を実装するコントラクトも、一般的なスマートコントラクトの安全設計に従う必要があります。 特に、外部コールが多いトークン転送系関数では、再入可能性攻撃(reentrancy attack)への対策が重要です。
| 推奨パターン | 内容 |
|---|---|
| 再入防止ガード(Reentrancy Guard) | 外部呼び出し後に同じ関数が再度実行されるのを防ぐ。 |
| Checks-Effects-Interactions パターン | 変数の状態更新(effects)を外部コール(interactions)の前に行い、副作用を最小化する。 |
| 外部呼び出しの安全管理 | 特にERC721やERC1155のように、トークン受取時に onERC721Received などのフックを呼ぶトークンは注意が必要。フック内で意図しない再実行や外部呼び出しが起こり得ます。 |
再入防止ガードを導入することで、これらのフック呼び出しに起因する再入攻撃や状態改ざんを効果的に防げます。
特に forcedTransfer や setFrozenTokens のようにストレージを書き換える関数では、操作順序(チェック→更新→呼び出し)を守ることが重要です。
引用
Dario Lo Buglio (@xaler5), "ERC-7943: uRWA - Universal Real World Asset Interface [DRAFT]," Ethereum Improvement Proposals, no. 7943, June 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7943.
最後に
今回は「現実資産をトークン化したときに必要となる法令順守の確認、移転制御、強制執行などの仕組であるuRWA(Universal Real World Asset Interface)を標準化する提案しているERC7943」についてまとめてきました! いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!


