0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[ERC7246] 所有権を手放さずに権利だけ渡す仕組みを理解しよう!

Posted at

はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、所有権を失わずに別のアカウントへ移動権を保証できる仕組みを提案しているERC7246についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ERC20に「エンカンバー」という機能を追加し、アカウントが自分の保有残高の一部について、特定のアカウントに対して「排他的に移動させる権利」を与えられるようにする内容です
ここでいう排他的な権利とは、他の誰でもなく指定した相手だけが、その範囲内のトークンを動かせる状態を指します。

ERC20については以下の記事を参考にしてください。

一般的に使われているERC20の承認(allowance)は、指定した量のトークンを移動できる許可を与える仕組みですが、実際に移動されるまでトークンが確保されている保証はありません。
残高が減ってしまえば、承認された側は移動できなくなります。

エンカンバーは、承認と同様に移動権限を与える点は同じですが、指定したトークンが「必要なときに確実に存在する」ことを保証します。
つまり、承認よりも強い仕組みであり、残高が確保されたまま移動権だけを付与できるようにします。

この仕組みはERC20に限らず、NFTであるERC721などにもそのまま近い形で適用できる拡張性があります。

ERC721については以下の記事を参考にしてください。

動機

ERC20に柔軟性を持たせ、トークンを手放さずにロックする必要がある場面に対応できるようにすることが目的です。
トークン保有者は、特定の条件を満たしたときに返却される前提で、トークンをスマートコントラクトへ送ることがよくあります。
ただし、実際にはスマートコントラクトがトークンを保管する必要がなく、「必要な場合に確実に利用できる状態」が確保できれば十分なケースがあります。

現在の承認では、その保証が不十分なため、スマートコントラクトにトークンを移すしか方法がありません。
エンカンバーがあれば、トークンを移動せずにロックでき、所有者が変わらないことが明確になります。
これにより、エアドロップなど、所有者に対して行われる恩恵が正しく届きます。

さらに、安全性の観点でも利点があります。
大量のトークンが保管されているアドレスから、一度の操作で全て流出してしまうリスクがありますが、エンカンバーされたトークンはアカウントごとに確保されており、一括で移動することが難しくなります。
そのため、攻撃に対してより強い抑止力が働きます。

仕様

IERC7246.sol
/**
 * @dev Interface of the ERC-7246 standard.
 */
interface IERC7246{
    /**
     * @dev Emitted when `amount` tokens are encumbered from `owner` to `taker`.
     */
    event Encumber(address indexed owner, address indexed taker, uint amount);

    /**
     * @dev Emitted when the encumbrance of a `taker` to an `owner` is reduced by `amount`.
     */
    event Release(address indexed owner, address indexed taker, uint amount);

    /**
     * @dev Returns the total amount of tokens owned by `owner` that are currently encumbered.
     * MUST never exceed `balanceOf(owner)`
     *
     * Any function which would reduce balanceOf(owner) below encumberedBalanceOf(owner) MUST revert
     */
    function encumberedBalanceOf(address owner) external returns (uint);

    /**
     * @dev Returns the number of tokens that `owner` has encumbered to `taker`.
     *
     * This value increases when {encumber} or {encumberFrom} are called by the `owner` or by another permitted account.
     * This value decreases when {release} and {transferFrom} are called by `taker`.
     */
    function encumbrances(address owner, address taker) external returns (uint);

    /**
     * @dev Increases the amount of tokens that the caller has encumbered to `taker` by `amount`.
     * Grants to `taker` a guaranteed right to transfer `amount` from the caller's balance by using `transferFrom`.
     *
     * MUST revert if caller does not have `amount` tokens available
     * (e.g. if `balanceOf(caller) - encumbrances(caller) < amount`).
     *
     * Emits an {Encumber} event.
     */
    function encumber(address taker, uint amount) external;

    /**
     * @dev Increases the amount of tokens that `owner` has encumbered to `taker` by `amount`.
     * Grants to `taker` a guaranteed right to transfer `amount` from `owner` using transferFrom
     *
     * The function SHOULD revert unless the owner account has deliberately authorized the sender of the message via some mechanism.
     *
     * MUST revert if `owner` does not have `amount` tokens available
     * (e.g. if `balanceOf(owner) - encumbrances(owner) < amount`).
     *
     * Emits an {Encumber} event.
     */
    function encumberFrom(address owner, address taker, uint amount) external;

    /**
     * @dev Reduces amount of tokens encumbered from `owner` to caller by `amount`
     *
     * Emits a {Release} event.
     */
    function release(address owner, uint amount) external;


    /**
     * @dev Convenience function for reading the unencumbered balance of an address.
     * Trivially implemented as `balanceOf(owner) - encumberedBalanceOf(owner)`
     */
    function availableBalanceOf(address owner) public view returns (uint);
}

イベント

Encumber

event Encumber(address indexed owner, address indexed taker, uint amount);

owner から taker に対して指定量のトークンがエンカンバーされた時に発行されるイベント。
owner が保有している残高のうち、taker が排他的に移動できる量が新しく確保された時に通知されます。
エンカンバーされた量は保証付きであり、後から別の操作によって消費されるまでは保持されます。
indexed が付いているため、特定のアドレスを基準にログ検索しやすくなっています。

パラメータ

  • owner
    • トークンを保有しているアドレス。
  • taker
    • 移動権を与えられたアドレス。
  • amount
    • エンカンバーされたトークン量。

Release

event Release(address indexed owner, address indexed taker, uint amount);

taker に対して設定されたエンカンバー量が減少した時に発行されるイベント。
taker が保持していた移動権が部分的または全量解放された時に通知されます。
これはtakertransferFrom によってエンカンバー分を実際に移動した場合や、release 関数により明示的に解放された場合に起こります。
indexed によりアドレスベースで追跡が可能です。

パラメータ

  • owner
    • トークンを保有しているアドレス。
  • taker
    • エンカンバー解除の対象アドレス。
  • amount
    • 減少したトークン量。

関数

encumberedBalanceOf

function encumberedBalanceOf(address owner) external returns (uint);

owner が現在エンカンバーしている合計トークン量を返す関数。
owner に紐づく全ての taker へのエンカンバー量を合算して返します。
この値は balanceOf(owner) を超えてはならず、残高をこれより下回らせる操作は必ず失敗します。
エンカンバーされたトークンはロック状態となり、自由に移動できません。

引数

  • owner
    • 対象となるアドレス。

戻り値

  • uint
    • エンカンバー済みの合計トークン量。

encumbrances

function encumbrances(address owner, address taker) external returns (uint);

owner から taker に設定されているエンカンバー量を返す関数。
特定の taker に割り当てられた保証付き移動権の量を取得します。
この値は encumber または encumberFrom によって増加し、releasetransferFrom によって減少します。
単一の関係のみ確認したい場合に使用します。

引数

  • owner
    • トークンを保有しているアドレス。
  • taker
    • 移動権を与えられているアドレス。

戻り値

  • uint
    • エンカンバーされているトークン量。

encumber

function encumber(address taker, uint amount) external;

呼び出し元が taker に対してエンカンバー量を増加させる関数。
呼び出し元が持つ未使用の残高から amount をエンカンバーし、taker に保証付きの移動権を与えます。
残高が不足している場合や、既に他のエンカンバーによって利用可能残高が足りない場合は処理が失敗します。
成功時には Encumber イベントが発行されます。

引数

  • taker
    • 移動権を付与されるアドレス。
  • amount
    • 新しくエンカンバーするトークン量。

encumberFrom

function encumberFrom(address owner, address taker, uint amount) external;

owner の代わりに taker へのエンカンバー量を増加させる関数。
メッセージ送信者が正当な手段で owner から許可されている場合に使用されます。
owner の未使用残高が amount 未満であれば処理は必ず失敗します。
成功すると Encumber イベントが発行されます。
承認の仕組みは規定されておらず、外部の認可ロジックに委ねられます。

引数

  • owner
    • トークンを保有しているアドレス。
  • taker
    • 移動権を付与されるアドレス。
  • amount
    • 新しくエンカンバーするトークン量。

release

function release(address owner, uint amount) external;

owner から呼び出し元に設定されているエンカンバー量を減少させる関数。
taker が不要になったエンカンバー分を解放する際に使用します。
利用可能な範囲内で amount を減らし、結果としてエンカンバー済み残高が更新されます。
成功すると Release イベントが発行されます。

引数

  • owner
    • トークンを保有しているアドレス。
  • amount
    • 解放するトークン量。

availableBalanceOf

function availableBalanceOf(address owner) public view returns (uint);

owner の自由に使える残高を取得する関数。
balanceOf(owner) から encumberedBalanceOf(owner) を差し引いた値を返します。
エンカンバーによってロックされた量を除いた実際に移動可能な残高を確認できます。

引数

  • owner
    • 対象となるアドレス。

戻り値

  • uint
    • エンカンバーされていない利用可能な残高。

補足

ERC20とそろえた設計方針

ERC7246は、既存のERC20の仕様と「補い合い」、「似た形にする」ことを意識して設計されています。
目的は、すでにERC20に慣れている開発者が、新しいインターフェイスも自然に理解して導入できるようにすることです。

そのため、完全に別物の仕組みとしてではなく、あくまでERC20の拡張として扱えるように意図されています。

最小限の必須条件と設計のスタンス

ERC7246の仕様は、どのようにERC20と結びつけるかについて、あえて細かくルールを決めすぎないようにされています。
設計として「最小限だけ縛る」というスタンスです。

唯一はっきり求められている必須条件は、「オーナーがエンカンバーされているトークンを transfer できてはならない」という点です。
つまり、あるアカウントのトークンが他者に対してエンカンバーされている場合、その分についてはオーナー自身が自由に transfer などで動かしてはいけない、ということだけが強い制約として課されています。

そのうえで、ERC20との具体的な結合方法については、実装者が自由に設計できる余地を残しています。

approveencumberFrom の関係

ERC7246では、「サンプル実装」の中で encumberFrom をどのように実現しているかについても触れています。
サンプル実装では、encumberFrom を動かすためにERC20approve に依存していると説明されています。

つまり、誰かが他人のトークンをエンカンバーするとき、その元となる権限管理に approve を使っている、という前提になっています。

ただし ERC7246自体は、必ずしも approve に依存しなければならないとはしていません。
実装者が望むなら、approveEncumber のような、エンカンバー専用の承認関数を用意してもよいとされています。
この専用関数は、ERC20allowance と同じような振る舞いをするイメージで書かれています。

ここで重要なのは、仕様として「approve を必須とはしていない」という点です。
encumberFrom の権限をどう設計するかは、実装側で選べるようになっています。

transferFromencumbrances / allowance の結び付け方

transferFrom(src, dst, amount) がエンカンバーとどう連携するかについても、サンプル実装の方針と、別の設計案の両方が説明されています。

分かりやすく比較すると、以下のようになります。

観点 サンプル実装の方針 代替案
transferFrom(src, dst, amount) の順序 まず encumbrances(src, amount) を減らし、そのあとに allowance(src, msg.sender) を消費する。 encumbrances を消費するのと同時に allowance も消費する実装にする。
approve に必要なチェック 特に追加の説明なし。 approve が、承認後の残高がエンカンバーで必要とされる量を下回らないことを確認する必要があるとされている。
encumber 呼び出しの前提 特に前提条件には触れていない。 encumber を呼ぶ前に、先に approve を行っておくことが前提になると説明されている。

代替案の説明では、transferFrom を「エンカンバー分」と「allowance」の両方から同時に消費するように実装する可能性が述べられています。

その場合、以下のようになります。

  • approve は、承認した後の残高が、すでに存在しているエンカンバー分の要求量を下回らないことをチェックしなければならない。
  • そして encumber を呼ぶ前に、あらかじめ approve によって承認を作成しておくことが前提になる。

ERC721への拡張可能性

ERC7246で定義されている Encumberインターフェイスは、もともとの主な用途としてERC20を想定していますが、説明ではERC721にも「引き伸ばして」適用できると述べられています。

ERC20では amount パラメータがトークン量を表す uint 型ですが、ERC721では各トークンが tokenId という uint で識別されます。
この共通点を利用して、「amount の代わりに tokenId を使う」という形でERC721にもこのインターフェイスをあてはめることが可能とされています。

ただし、インターフェイスとしては、もっとも典型的な利用ケースであるERC20を前提とした分かりやすい形を優先して選んでいる、と明記されています。
つまり、他のフォーマットでも理論上使えるが、見た目や意味が一番自然になるERC20向けの表現を採用している、という整理です。

互換性

ERC7246は、既存のERC20標準との後方互換性があるとされています。
つまり、ERC20の基本仕様を壊さず、その上にEncumberの機能を追加する位置づけです。

そのうえで、実装において明確に求められていることは、以下の一点です。

  • あるアカウントのトークンが別のアカウントに対してエンカンバーされている場合、そのエンカンバーされているトークンは transfer できないようにしなければならない。

これにより、エンカンバーによって「保証されたトークン」が、意図せず動かされてしまうことを防ぐ必要がある、と説明されています。

参考実装

EncumberableERC20.sol
// An erc-20 token that implements the encumber interface by blocking transfers.

pragma solidity ^0.8.0;
import {ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import { IERC7246 } from "./IERC7246.sol";

contract EncumberableERC20 is ERC20, IERC7246 {
    // Owner -> Taker -> Amount that can be taken
    mapping (address => mapping (address => uint)) public encumbrances;

    // The encumbered balance of the token owner. encumberedBalance must not exceed balanceOf for a user
    // Note this means rebasing tokens pose a risk of diminishing and violating this prototocol
    mapping (address => uint) public encumberedBalanceOf;

    address public minter;

    constructor(string memory name, string memory symbol) ERC20(name, symbol) {
        minter = msg.sender;
    }

    function mint(address recipient, uint amount) public {
        require(msg.sender == minter, "only minter");
        _mint(recipient, amount);
    }

    function encumber(address taker, uint amount) external {
        _encumber(msg.sender, taker, amount);
    }

    function encumberFrom(address owner, address taker, uint amount) external {
        require(allowance(owner, msg.sender) >= amount);
       _encumber(owner, taker, amount);
    }

    function release(address owner, uint amount) external {
        _release(owner, msg.sender, amount);
    }

    // If bringing balance and encumbrances closer to equal, must check
    function availableBalanceOf(address a) public view returns (uint) {
        return (balanceOf(a) - encumberedBalanceOf[a]);
    }

    function _encumber(address owner, address taker, uint amount) private {
        require(availableBalanceOf(owner) >= amount, "insufficient balance");
        encumbrances[owner][taker] += amount;
        encumberedBalanceOf[owner] += amount;
        emit Encumber(owner, taker, amount);
    }

    function _release(address owner, address taker, uint amount) private {
        if (encumbrances[owner][taker] < amount) {
          amount = encumbrances[owner][taker];
        }
        encumbrances[owner][taker] -= amount;
        encumberedBalanceOf[owner] -= amount;
        emit Release(owner, taker, amount);
    }

    function transfer(address dst, uint amount) public override returns (bool) {
        // check but dont spend encumbrance
        require(availableBalanceOf(msg.sender) >= amount, "insufficient balance");
        _transfer(msg.sender, dst, amount);
        return true;
    }

    function transferFrom(address src, address dst, uint amount) public override returns (bool) {
        uint encumberedToTaker = encumbrances[src][msg.sender];
        bool exceedsEncumbrance = amount > encumberedToTaker;
        if (exceedsEncumbrance)  {
            uint excessAmount = amount - encumberedToTaker;

            // check that enough enencumbered tokens exist to spend from allowance
           require(availableBalanceOf(src) >= excessAmount, "insufficient balance");

           // Exceeds Encumbrance , so spend all of it
            _spendEncumbrance(src, msg.sender, encumberedToTaker);

            _spendAllowance(src, dst, excessAmount);
        } else {
            _spendEncumbrance(src, msg.sender, amount);
        }

        _transfer(src, dst, amount);
        return true;
    }

    function _spendEncumbrance(address owner, address taker, uint256 amount) internal virtual {
        uint256 currentEncumbrance = encumbrances[owner][taker];
        require(currentEncumbrance >= amount, "insufficient encumbrance");
        uint newEncumbrance = currentEncumbrance - amount;
        encumbrances[owner][taker] = newEncumbrance;
        encumberedBalanceOf[owner] -= amount;
    }
}

引用

Coburn Berry (@coburncoburn), Mykel Pereira (@mykelp), Scott Silver (@scott-silver), "ERC-7246: Encumber - Splitting Ownership & Guarantees [DRAFT]," Ethereum Improvement Proposals, no. 7246, June 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7246.

最後に

今回は「所有権を失わずに別のアカウントへ移動権を保証できる仕組みを提案しているERC7246」についてまとめてきました!
いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

Twitter @cardene777

他の媒体でも情報発信しているのでぜひ他も見ていってください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?