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?

More than 1 year has passed since last update.

interface経由でAコントラクトからBコントラクトを呼び出すとmsg.senderの中身が変わる話

Posted at

はじめに

プログラミング初心者です。頑張って書きます。間違ってたらすみません。
interfaceについてはこちらをご覧ください。

環境

solidity: ^0.8.9
remix

本題

何が言いたいのか

interfaceを使用して、Aコントラクトの関数からBコントラクトの関数を実行します。この時、Aコントラクトの関数では、msg.sender = 関数の実行者となりますが、実際に呼び出されたBコントラクトの関数では、msg.sender = Aコントラクトのアドレスになります。

問題が生じる例

BコントラクトがOpenZeppleinのERC20.solを継承した、ERC20コントラクトだったとします。Bコントラクトでは、OpenZeppleinのERC20.solの_mint()を呼び出すためのmint()が定義されているとします。そして、Aコントラクトには、interfaceを経由してBコントラクトのmint()を実行するためのmintFromA()が定義されているとします。

@openzeppelin/contracts/token/ERC20/ERC20.sol
...
function _mint(address account, uint256 amount) internal virtual {...}
...

OpenZeppleinのERC20.solの_mint()は第一引数には、これから発行するトークンの持ち主を表すaccount(address型)が入ります。AコントラクトのmintFromA()からBコントラクトのmint()を呼び出し、そこから_mint()を実行する時、mint()において、_mint()の第一引数にmsg.senderを入れると、account(_mint()の第一引数) = msg.sender = Aコントラクトのアドレスとなり、トークンの持ち主がAコントラクトのアドレスになってしまいます。すなわち、tokenの持ち主が正しく設定されないと言うことです。もちろんbalanceOf()などで、自分のアカウントのトークン量を調べても、0が返ってきます。

以下に上で説明した具体的なコードと実行例を示します。

コードと実行例
A.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/access/Ownable.sol";
import "hardhat/console.sol";

interface BContract {
    function mint(uint256 amount) external;
}

contract AContract is Ownable {
    BContract private bContract;

    constructor(address BAddress) {
        bContract = BContract(BAddress);
    }

    function mintFromA(uint256 amount) external {
        console.log("msg.sender in A(mint function): %s", msg.sender);
        bContract.mint(amount);
    }
}
B.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "hardhat/console.sol";

contract BContract is ERC20, Ownable {

    constructor() ERC20("Test Token", "TTK") {}

    function mint(uint256 amount) external {
        console.log("msg.sender in B(mint function): %s", msg.sender);
        _mint(msg.sender, amount);
    }
}

remixにて、Bコントラクトをデプロイし、Bコントラクトのコントラクトアドレスを引数に入れAコントラクトをデプロイします。AコントラクトのminFromA()を実行します。引数amountは適当でいいです。mintされたことが確認できたら、今度はBコントラクトのbalanceOf()に自分のアドレスを入れて実行します。さっきmintしたにも関わらず0が返ってきます。totalSuply()を実行すると、さっきmintした分がしっかり反映されているのでmint自体はできていることが確認できます。

また、mintした際の、conosole.logによる出力をみると以下のようになっています。

msg.sender in A(mint function): 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
msg.sender in B(mint function): 0x1d142a62E2e98474093545D4A3A0f7DB9503B8BD

Aコントラクトのmsg.senderは関数実行者であることが確認できます。また、明らかにAコントラクトとBコントラクトでmsg.senderの値が異なっていることがわかります。そしてBコントラクトでのmsg.senderはAコントラクトのアドレスであるということもわかります。

解決法

解決法はさまざまあるかと思いますが、私は以下のようにしました。セキュリティ面で穴があるかもしれないのでそこは注意してください。また、この解決法では、Aコントラクトを経由せずBコントラクトから直接mintすることは想定していません。

A.sol

A.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

interface BContract {
    function mint(address sender, uint256 amount) external;
    function burn(address sender, uint256 amount) external;
}

contract AContract {
    BContract private bContract;

    constructor(address BAddress) {
        bContract = BContract(BAddress);
    }

    // 【編集】
    function mintFromA(uint256 amount) external {
        bContract.mint(msg.sender, amount);
    }
}

B.sol

B.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract BContract is ERC20, Ownable {
    // 【追加】 Aコントラクトのアドレスを保存する
    address private _aContractAddress;

    constructor() ERC20("Test Token", "TTK") {}

    // 【追加】 Aコントラクトからの実行しか認めないようにする修飾子を作成
    modifier isFromAContract() {
        require(msg.sender == _aContractAddress, "B.sol: msg.sender (in B.sol) should be A Contract address, but it is not A Contract address.");
        _;
    }

    //【追加】 Aコントラクトのアドレスを保存する関数を作成。オーナーはAコントラクトとBコントラクトをデプロイした直後、この関数を実行
    function setAContractAddress(address aContractAddress) external onlyOwner {
        _aContractAddress = aContractAddress;
    }

    //【追加】 Aコントラクトのアドレスを確認する関数を作成
    function checkAContractAddress() view external onlyOwner returns(address) {
        return aContractAddress;
    }

    //【編集】
    function mint(address sender, uint256 amount) external isFromAContract {
        _mint(sender, amount);
    }
}
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?