原文

ERC-TBA: Prediction Market CTF Wrapper — martchelo_eth (2026-07-27)

要約

この提案は、Conditional Tokens Framework (CTF) のポジション、すなわちERC-1155の予測市場アウトカムシェアを、ERC-20トークンとして表現することを標準化します。

2つのインターフェースが定義されています。ICTFWrapperに準拠するコントラクトは、単一のCTFポジションを1対1でラップするERC-20トークンであり、wrapunwrap、およびそのファクトリーへのポインターを公開します。ICTFWrapperFactoryに準拠するコントラクトは、1つのConditional Tokensコントラクトと1つの担保トークンにバインドされており、ラッパーをデプロイして登録し、各ラッパーのポジションパラメータを記録し、完全セット操作であるsplitmergeを公開します。

動機

予測市場アウトカムシェアは、主にERC-1155トークンとして表現されます。主要な会場は、GnosisのConditional Tokens Frameworkを通じてポジションを発行しており、各アウトカムはERC-1155のpositionIdです。

これらのポジションにおけるオンチェーン活動は増加しており、DeFiプロトコル(レンディング市場、ボールト、ストラクチャードプロダクト、レバレッジシステムなど)の幅広いセットが、予測市場シェアを第一級資産として利用したいと考えています。これらの会場はERC-20を扱い、ERC-1155ポジションIDを扱うものはほとんどありません。現在存在するラッパーは、2つの極端なタイプに分かれています。

汎用ラッパー(例:GnosisのWrapped1155Factory)は、ポジションIDのみを公開しますが、それ自体ではそのポジションがどの市場に属し、どのアウトカムを表し、どの担保で償還されるかについては何も明らかになりません。また、担保を完全セットに分割したり、セットを担保に戻したりする方法も提供されていません。インテグレーターはConditional Tokensコントラクトを直接操作し、すべてのIDを自分で再導出する必要があります。

プロトコルネイティブなアウトカムごとのERC-20は、これらすべてのパラメータとロジックを保持していますが、それは1つのプロトコルの独自の型、ゲッター、および市場コントラクトを通じてのみであり、ある会場向けに書かれた統合は他の会場には転用できません。

この2つの間には、ビルダーがCTFベースの会場で必要とする読み取り可能なポジションパラメータと完全セット操作を提供する共通のスキーマが存在しません。

仕様

このドキュメントにおけるキーワード「MUST」、「MUST NOT」、「REQUIRED」、「SHALL」、「SHALL NOT」、「SHOULD」、「SHOULD NOT」、「RECOMMENDED」、「NOT RECOMMENDED」、「MAY」、「OPTIONAL」は、RFC 2119およびRFC 8174に記述されている通りに解釈されます。

定義

  • ファクトリー: ICTFWrapperFactoryに準拠するコントラクト。

  • ラッパー: ファクトリーによってデプロイおよび登録される、ICTFWrapperに準拠するコントラクト。

  • CTF: ファクトリーconditionalTokens()によって返されるConditional Tokensコントラクト。

  • COLLATERAL: ファクトリーcollateralToken()によって返されるトークン。

  • ポジションID: CTFおよびCOLLATERALの下でのタプル(parentCollectionId, conditionId, indexSet)のERC-1155 IDであり、以下のように計算されます。

    positionId = CTF.getPositionId(
        COLLATERAL,
        CTF.getCollectionId(parentCollectionId, conditionId, indexSet)
    )
    
  • positionId: ラッパーインターフェース内では、そのラッパーのためにファクトリーによって記録されたポジションID。

ラッパーインターフェース

すべての準拠ラッパーは、ERC-20、ERC-1155レシーバーインターフェース、および以下のインターフェースを実装しなければなりません (MUST)。

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
 
interface ICTFWrapper /* is IERC20, IERC1155Receiver */ {
    event Wrapped(address indexed from, address indexed to, uint256 amount);
 
    event Unwrapped(address indexed from, address indexed to, uint256 amount);
 
    function factory() external view returns (address);
 
    function wrap(address to, uint256 amount) external;
 
    function unwrap(address to, uint256 amount) external;
}

ラッパーの不変条件

以下の条件は、すべての準拠ラッパーに対して満たされなければなりません (MUST)。

  • 単一ポジション。 ラッパーは正確に1つのポジションIDをラップしなければなりません (MUST)。そのIDはラッパーが公開される前に固定されなければならず (MUST)、変更されてはなりません (MUST NOT)。
  • 供給。 すべての状態変更呼び出しの後、totalSupply()IERC1155(CTF).balanceOf(address(this), positionId)と等しくなければなりません (MUST)。ラッパーは、そのERC-20供給によって1対1で表現されていないそのIDの残高を保持してはなりません (MUST NOT)。
  • 1対1、手数料なし。 wrapunwrapは、原資産ポジションの単位ごとに正確に1つのERC-20単位をミントおよびバーンしなければならず (MUST)、手数料を請求したり、スケーリングファクターを適用したりしてはなりません (MUST NOT)。

ラッパーのメタデータ

  • decimals()は表示のみであり、この仕様のいかなる量にも影響を与えません。また、COLLATERALdecimals()と等しくなければなりません (MUST)。ラッピングと分割はどちらも1対1であるため、ラッパーの1つのERC-20単位は決済時に1つのCOLLATERAL単位であり、不一致は引用されるすべての場所でラッパーの価格を誤らせることになります。
  • name()symbol()positionIdから決定論的に導出されるべきであり (SHOULD)、変更可能であってはなりません (SHOULD NOT)。

ラッパーのメソッド

factory

このラッパーを管理し、そのパラメータを保持するファクトリー

  • このラッパーが登録されているICTFWrapperFactoryに準拠するコントラクトを返さなければなりません (MUST)。すなわち、ICTFWrapperFactory(factory()).isWrapper(address(this))trueでなければなりません (MUST)。
  • 変更されてはなりません (MUST NOT)。
function factory() external view returns (address);
wrap

呼び出し元から原資産ポジションを受け取り、それに対してERC-20をミントします。

  • amountpositionIdmsg.senderからラッパーに転送しなければなりません (MUST)。
  • amountのERC-20をtoにミントしなければなりません (MUST)。
  • そのインバウンド転送によって引き起こされるラッパー自身のonERC1155Receivedの呼び出しは、ミントしてはなりません (MUST NOT)。
  • amountがゼロの場合、リバートしなければなりません (MUST)。
  • toがゼロアドレスの場合、リバートしなければなりません (MUST)。
  • 条件の決済状態によってゲートされてはなりません (MUST NOT)。
function wrap(address to, uint256 amount) external;
unwrap

呼び出し元からERC-20をバーンし、原資産ポジションを返します。

  • amountのERC-20をmsg.senderからバーンしなければなりません (MUST)。
  • amountpositionIdtoに転送しなければなりません (MUST)。
  • Unwrappedイベントを発行しなければなりません (MUST)。
  • amountがゼロの場合、リバートしなければなりません (MUST)。
  • toがゼロアドレスの場合、リバートしなければなりません (MUST)。
  • 条件の決済状態によってゲートされてはなりません (MUST NOT)。
function unwrap(address to, uint256 amount) external;
onERC1155Received

原資産ポジションのラッパーへの直接転送をwrapと同等にします。

  • msg.senderCTFでない場合、リバートしなければなりません (MUST)。
  • idpositionIdでない場合、リバートしなければなりません (MUST)。
  • 転送がこのラッパー自身のwrap(それ自体がミントを行う)に由来する場合、ミントしてはなりません (MUST NOT)。
function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes calldata data)
    external
    returns (bytes4);
onERC1155BatchReceived
  • リバートしなければなりません (MUST)。
function onERC1155BatchReceived(
    address operator,
    address from,
    uint256[] calldata ids,
    uint256[] calldata values,
    bytes calldata data
) external returns (bytes4);

ラッパーのイベント

Wrapped

wrapを通じて、またはonERC1155Receivedによって受け入れられた直接のERC-1155転送を通じて、ラップごとに正確に1回発行されなければなりません (MUST)。

  • from: 原資産ポジションが来たアカウント。
  • to: ERC-20がミントされたアカウント。
  • amount: ラップされた単位数。
event Wrapped(address indexed from, address indexed to, uint256 amount);
Unwrapped

ERC-20が原資産ポジションに戻ってアンラップされたときに発行されなければなりません (MUST)。

  • from: ERC-20がバーンされたアカウント。
  • to: 原資産ポジションが転送されたアカウント。
  • amount: アンラップされた単位数。
event Unwrapped(address indexed from, address indexed to, uint256 amount);

ファクトリーインターフェース

すべての準拠ファクトリーは、以下のインターフェースを実装しなければなりません (MUST)。

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
 
interface ICTFWrapperFactory {
    struct TokenParams {
        bytes32 conditionId;
        bytes32 parentCollectionId;
        uint256 indexSet;
        uint256 positionId;
    }
 
    event WrapperCreated(
        address indexed wrapper,
        uint256 indexed positionId,
        bytes32 indexed conditionId,
        bytes32 parentCollectionId,
        uint256 indexSet
    );
 
    event Split(
        address indexed account,
        address indexed to,
        bytes32 indexed conditionId,
        bytes32 parentCollectionId,
        uint256 amountIn,
        address tokenIn
    );
 
    event Merged(
        address indexed account,
        address indexed to,
        bytes32 indexed conditionId,
        bytes32 parentCollectionId,
        uint256 amountIn,
        address tokenOut,
        uint256 amountOut
    );
 
    function conditionalTokens() external view returns (address);
 
    function collateralToken() external view returns (address);
 
    function wrapperOf(uint256 positionId) external view returns (address);
 
    function paramsOf(address wrapper) external view returns (TokenParams memory);
 
    function isWrapper(address wrapper) external view returns (bool);
 
    function getAddress(uint256 positionId) external view returns (address);
 
    function siblingWrapperOf(address wrapper, uint256 siblingIndexSet)
        external
        view
        returns (address);
 
    function deploy(bytes32 conditionId, bytes32 parentCollectionId, uint256 indexSet)
        external
        returns (address wrapper);
 
    function deployMulti(bytes32 conditionId, bytes32 parentCollectionId, uint256[] calldata indexSets)
        external
        returns (address[] memory wrappers);
 
    function getSupportedTokens() external view returns (address[] memory);
 
    function split(
        bytes32 conditionId,
        bytes32 parentCollectionId,
        uint256 amount,
        address tokenIn,
        address to
    ) external;
 
    function merge(address[] calldata wrappers, uint256 amount, address tokenOut, address to)
        external
        returns (uint256 amountOut);
}

TokenParams

ラッパーの単一ポジションのパラメータ。

  • conditionId: ポジションが属するCTF条件。
  • parentCollectionId: ポジションがネストされている親コレクション。トップレベルポジションの場合はbytes32(0)
  • indexSet: ポジションがカバーするアウトカムスロットのビットマスク。
  • positionId: 上記3つのフィールドから定義に従って導出されたポジションID。
struct TokenParams {
    bytes32 conditionId;
    bytes32 parentCollectionId;
    uint256 indexSet;
    uint256 positionId;
}

ファクトリーのメソッド

conditionalTokens

このファクトリーのすべてのラッパーがラップするConditional Tokensコントラクト。

  • デプロイ時に固定され、変更されてはなりません (MUST NOT)。
function conditionalTokens() external view returns (address);
collateralToken

ポジションが額面表示される担保トークン

  • デプロイ時に固定され、変更されてはなりません (MUST NOT)。
function collateralToken() external view returns (address);
wrapperOf

ポジションIDのラッパー。

  • positionIdに対して登録されたラッパーを返さなければなりません (MUST)。デプロイされていない場合はaddress(0)を返します。
  • paramsOfの逆でなければなりません (MUST)。登録された任意のラッパーwについて、wrapperOf(paramsOf(w).positionId) == wが成り立ちます。
function wrapperOf(uint256 positionId) external view returns (address);
paramsOf

ラッパーのために記録されたパラメータ。

  • デプロイ時にwrapperのために記録されたパラメータを、その後のすべての呼び出しで変更せずに返さなければなりません (MUST)。
  • 不明なラッパーに対しては、ゼロ値のTokenParamsを返さなければなりません (MUST)。
function paramsOf(address wrapper) external view returns (TokenParams memory);
isWrapper

アドレスがこのファクトリーによってデプロイおよび登録されたかどうか。

  • wrapperがこのファクトリーによってデプロイおよび登録された場合にのみtrueを返さなければなりません (MUST)。
function isWrapper(address wrapper) external view returns (bool);
getAddress

ポジションIDのラッパーが持つ、または持つであろう決定論的なアドレス。

  • positionIdのラッパーが持つ、または持つであろうアドレスを返さなければなりません (MUST)。
function getAddress(uint256 positionId) external view returns (address);
siblingWrapperOf

登録されたラッパーと同じ条件および親コレクションの別のアウトカムのラッパー。

  • (paramsOf(wrapper).parentCollectionId, paramsOf(wrapper).conditionId, siblingIndexSet)のポジションIDに対する登録されたラッパーを返さなければなりません (MUST)。存在しない場合はaddress(0)を返します。
  • 不明なwrapperに対してはリバートしなければなりません (MUST)。wrapperOfとは異なり、この呼び出しは提供されたラッパーのパラメータからその答えを導出するため、ゼロを返すと「このファクトリーのラッパーではない」と「兄弟がまだデプロイされていない」が混同されてしまいます。
function siblingWrapperOf(address wrapper, uint256 siblingIndexSet)
    external
    view
    returns (address);
getSupportedTokens

tokenInとして受け入れられ、tokenOutとして配信されるトークンのセット。

  • ファクトリーtokenInとして受け入れ、tokenOutとして配信するトークンの正確なセットを返さなければなりません (MUST)。
  • collateralToken()を含まなければならず (MUST)、したがって空であってはなりません (MUST NOT)。
  • 重複を含んではなりません (MUST NOT)。
function getSupportedTokens() external view returns (address[] memory);
deploy

単一アウトカムポジションの既存のラッパーをデプロイするか、返します。

  • 引数から導出されたポジションIDのラッパーを、getAddress(positionId)にデプロイおよび登録しなければなりません (MUST)。
  • TokenParams(conditionId, parentCollectionId, indexSet, positionId)を記録しなければなりません (MUST)。
  • WrapperCreatedイベントを発行しなければなりません (MUST)。
  • ポジションIDごとに冪等でなければなりません (MUST)。同じIDに対する2回目の呼び出しは、既存のラッパーを返し、それ以上のイベントを発行してはなりません (MUST NOT)。
  • indexSetがゼロの場合、またはCTF.getOutcomeSlotCount(conditionId)以上のビットを設定する場合、リバートしなければなりません (MUST)。
function deploy(bytes32 conditionId, bytes32 parentCollectionId, uint256 indexSet)
    external
    returns (address wrapper);
deployMulti

1つの条件の複数のアウトカムポジションに対するラッパーをデプロイするか、既存のラッパーを返します。

  • indexSetsの各エントリに対して、順序通りにdeploy(conditionId, parentCollectionId, indexSet)を1回呼び出すことと同等でなければなりません (MUST)。
  • 結果のラッパーをindexSetsと同じ順序で返さなければなりません (MUST)。
  • indexSetsが空の場合、リバートしなければなりません (MUST)。
function deployMulti(bytes32 conditionId, bytes32 parentCollectionId, uint256[] calldata indexSets)
    external
    returns (address[] memory wrappers);
split

tokenInをラップされたアウトカムの完全セットに分割します。

  • amounttokenInmsg.senderから受け取らなければなりません (MUST)。
  • そのパーティションの各ラッパーのamounttoに配信しなければなりません (MUST)。まだ存在しないラッパーはデプロイします。
  • Splitイベントを発行しなければなりません (MUST)。
function split(
    bytes32 conditionId,
    bytes32 parentCollectionId,
    uint256 amount,
    address tokenIn,
    address to
) external;
merge

完全アウトカムセットをバーンし、担保トークンtokenOutとして解放します。

  • wrappers内の各ラッパーのamountmsg.senderから受け取らなければなりません (MUST)。
  • 正確にamounttokenOuttoに配信しなければなりません (MUST)。
  • Mergedイベントを発行しなければなりません (MUST)。
  • amountを返さなければなりません (MUST)。
  • wrappersのエントリがすべてこのファクトリーによって登録されており、1つのconditionIdと1つのparentCollectionIdを共有している場合にのみ、リバートしてはなりません (MUST NOT)。
  • amountがゼロの場合、toがゼロアドレスの場合、またはtokenOutがサポートされていない場合、リバートしなければなりません (MUST)。
function merge(address[] calldata wrappers, uint256 amount, address tokenOut, address to)
    external
    returns (uint256 amountOut);

ファクトリーのイベント

WrapperCreated

ラッパーがデプロイおよび登録されたときに発行されなければならず (MUST)、同じポジションIDに対して再度発行されてはなりません (MUST NOT)。このイベントは完全なTokenParamsを運び、ログのみを読み取るコンシューマーがpositionIdを再計算し、導出を検証できるようにします。

event WrapperCreated(
    address indexed wrapper,
    uint256 indexed positionId,
    bytes32 indexed conditionId,
    bytes32 parentCollectionId,
    uint256 indexSet
);
Split

すべてのsplitが成功したときに発行されなければなりません (MUST)。

event Split(
    address indexed account,
    address indexed to,
    bytes32 indexed conditionId,
    bytes32 parentCollectionId,
    uint256 amountIn,
    address tokenIn
);
Merged

すべてのmergeが成功したときに発行されなければなりません (MUST)。

event Merged(
    address indexed account,
    address indexed to,
    bytes32 indexed conditionId,
    bytes32 parentCollectionId,
    uint256 amountIn,
    address tokenOut,
    uint256 amountOut
);

理論的根拠

ファクトリーとトークンのインターフェースであり、トークン単独ではない理由

ビルダーが必要とする2つの機能、すなわちポジションのパラメータの読み取りと完全セット操作の実行は、単一のラッパーでは安価に実行できません。パラメータはすべてのアウトカムごとのデプロイメントに重複して格納されることになり、splitmergeは複数のポジションと担保トークンにまたがるため、それらを実行するラッパーはルーティングと兄弟に関する知識を必要とします。会場ごとに1つのファクトリーに両方を集中させることで、ラッパーはミニマルプロキシクローンとして十分ミニマルに保たれ、インテグレーターにはパラメータを読み取り、セットに参加または退出するための統一された場所が提供されます。

パラメータが各ラッパーではなくファクトリーに存在する理由

ラッパーは通常ミニマルプロキシクローンであり、インスタンスごとの不変値を保持できません。パラメータをファクトリーに保持することで、ラッパーはfactory()のみを公開し、そこからそれに関するすべての情報を読み取ることができます。

後方互換性

この提案はERC-20またはERC-1155のセマンティクスを変更するものではなく、デプロイ済みのConditional Tokensコントラクトに変更を要求するものでもありません。ファクトリーとそのラッパーは、既存のCTFCOLLATERALの上に完全に存在し、ラッパーはそれを転送するだけのすべてのコントラクトにとって通常のERC-20です。

1つの非互換性が受信側で導入されます。

ラッパーへのバッチ転送

ラッパーはERC-1155レシーバーインターフェースを実装していますが、単一の転送のみを受け入れます。そのonERC1155BatchReceivedは常にリバートします。リバートはERC-1155レシーバーにとって準拠した応答であるため、原資産が失われることはありません(リバートによって転送が取り消されます)。しかし、任意のレシーバーをバッチ対応とみなすコントラクトはラッパーに対して失敗し、ラッパーは呼び出し前にこれを検出する方法を提供しません。ERC165.supportsInterface(type(IERC1155Receiver).interfaceId)はラッパーに対してtrueを報告します。これはインターフェースが実装されているためであり、2つのエントリーポイントを区別しません。

この問題の深刻度は、資金の損失ではなくライブネスであり、影響を受ける呼び出し元は、エンドユーザーではなく、ユーザーに代わってERC-1155残高を移動するコントラクト(ルーター、ボールト、移行ヘルパーなど)です。このような呼び出し元は、wrapを通じて、またはsafeTransferFromを一度に1つのIDに対して実行することで、ポジションをラッパーにルーティングすべきであり、safeBatchTransferFromを通じてルーティングすべきではありません。

ラッパーはポジションIDと1対1であるため、単一のラッパー内でのバッチ処理は何も得られません。複数のポジションにまたがるバッチは、いずれにせよそれらのラッパー間で分割される必要があります。

セキュリティに関する考慮事項

パラメータの整合性

paramsOfは、ラッパーが何を表すかについての唯一のオンチェーンの真実の源です。誤ったconditionId担保トークンは、ラッパーが誤った市場に対して価格設定されることを許してしまいます。導出不変条件が防御策であり、それはファクトリーが提供されたポジションIDを決して受け入れず、登録されたラッパーのパラメータを決して変更しない場合にのみ成り立ちます。デプロイしていないラッパーを信頼するインテグレーターは、信頼するファクトリーの下でisWrapperを確認すべきであり (SHOULD)、WrapperCreatedのフィールドから不変条件を自分で再計算してもよいでしょう (MAY)。

レシートパスでの二重ミント

ラッパーには原資産を受け取る2つの方法があり、wrapはその両方を通過します。wrapが実行するsafeTransferFromは、ラッパー自身のonERC1155Receivedを呼び出します。両方でミントする実装は、1つのレシートに対して2回ミントすることになり、供給不変条件を恒久的に破り、呼び出し元が預け入れた量よりも多くアンラップすることを可能にします。これは、他のすべてのホルダーを裏付けるポジション残高を枯渇させることになります。

供給不変条件とバーン権限

供給不変条件は、ラッパーの供給が自身のwrapunwrapによってのみ変更される場合にのみ成り立ちます。ラッパーもファクトリーも、ホルダーのERC-20残高に対する特権的なバーンフックを導入してはなりません。ファクトリー完全セット操作は、どのインテグレーターでも同じアローワンスで行えることに限定されます。

リエントランシー

完全セット操作は、単一のトランザクション内でCTF、ラッパー、および変換アダプターを呼び出し、unwrapはERC-1155レシーバーフックを通じて任意のtoに制御を渡します。供給不変条件はすべての状態変更呼び出しに対して述べられており、リエントランシーパスは操作中にそれを侵害する可能性があるため、これらの操作はリエントランシーから保護されなければならず (MUST)、外部呼び出しの前に状態変更を順序付けなければなりません (MUST)。

著作権

著作権および関連する権利はCC0を通じて放棄されます。

1投稿 - 1参加者

トピック全文を読む