はじめに
プログラミング初心者です。頑張って書きます。間違ってたらすみません。
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()が定義されているとします。
...
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が返ってきます。
以下に上で説明した具体的なコードと実行例を示します。
コードと実行例
//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);
}
}
//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
//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
//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);
}
}