👌

ERC1155でownerOf関数を実装する2つの方法

2025/02/18に公開

ERC1155について

おそらく多くの方が以下のコード・記事を見ていると思います。ですので、以下のコードで今回はownerOf関数を実装できないか考えます。
https://eips.ethereum.org/EIPS/eip-1155
https://docs.openzeppelin.com/contracts/3.x/api/token/erc1155

問題点

ERC1155では、balanceOf関数が用意されています。これは引数にtokenIdの持ち主のaddressとtokenIdを渡してあげると、そのaddressがいくらそのtokenIdを持っているのかを返してくれる関数です。

contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI {
   // Mapping from token ID to account balances
    mapping(uint256 => mapping(address => uint256)) private _balances;

    function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
      require(account != address(0), "ERC1155: address zero is not a valid owner");
      return _balances[id][account];
  }
}

上記のようにERC1155ではaddressとtokenIdを渡すと保有トークン量を返してくれるのですが、ERC721などで用意されているownerOf関数はありません。これはERC1155が特定のトークンidを複数人のaddressが持つ設計で作られているため、1つのtokenIdで1つのaddressが決まる設計ではないからです。

ownerOf関数を実装する2つのパターン

ですが、実際はownerOf関数のようなあるtokenIdを引数で渡したらaddressが返ってくる関数をアプリ側で用意したいことがあると思います。

筆者が思うに2つのパターンが可能です。

  1. privateなownersといった変数をコントラクトに用意してownerOf関数を作る
  2. TransferSingleもしくはTransferBatchイベントの履歴から最新のownerを探す

ownersという変数を用意し、ownerOf関数を作る

// tokenIdから複数addressへマッピング
mapping(uint256 => address[]) private owners;

function onwnerOf(uint256 tokenId) public view virtual returns(address[]) {
  address[] ownersArr = owners[tokenId]; 
  return ownersArr;
}

上記のように自前でowners変数を設けてあげることで、ownerOf関数を実装できます。ただし注意点として、mintやburn、transferが実行され、tokenIdの持ち主のaddressに変更があるときはownersを書き換えてあげる処理が必要になります。ERC1155.solで定義されている_beforeTokenTransferフックスなどをoverrideしてあげるとスムーズです。

TransferSingleもしくはTransferBatchイベントから探す

あんまり自前で変数を用意したくない場合は、EFC1155.solで定義されているTransferSingleかTransferBatchイベントを検索して、最新のownerアドレスを探す方法があります。

やりたいこととしては以下のようなことです。

function ownerOf(uint256 tokenId) public view returns (address) {
    
    // TransferSingleイベントをフィルタリング
    event TransferSingle(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 id,
        uint256 value
    );
    
    // 最新のTransferSingleイベントを取得
    TransferSingle[] memory transfers = this.TransferSingle({
        id: tokenId
    });
    
    // 最新の転送先アドレスがオーナー
    if (transfers.length > 0) {
        address lastTo = transfers[transfers.length - 1].to;
        // 残高チェックで確認
        if (balanceOf(lastTo, tokenId) == 1) {
            return lastTo;
        }
    }
    
    revert("tokenIdのオーナーが見つかりません");
}

ただし、上記のようにsolidity内でevent情報をクエリすることはできません。そこでether.jsやThe Graphを使ってイベント履歴を検索することになります。

以下はサンプルです。

const filter = contract.filters.TransferSingle(null, null, null, tokenId);
const events = await contract.queryFilter(filter);
const lastEvent = events[events.length - 1];
const currentOwner = lastEvent.args.to;

The graphを使う場合は以下のようなクエリが考えられます。

{
  transferSingles(
    where: { id: $tokenId }
    orderBy: blockNumber
    orderDirection: desc
    first: 1
  ) {
    to
  }
}

Discussion