メインコンテンツまでスキップ

lesson-1_資金の引き出しを実装しよう

🛠 資金を引き出す機能を実装する

前回まででスマートコントラクトをデプロイし、ユーザーがドメインを作成できるようにするReactベースのWebアプリケーションを作成してきました。

これでかの有名なBored Apeを購入し、ミームを1日中Twitterに投稿することもできます。

しかしまだできないことがあります。

まだ資金を引き出す方法を作成できていないからです。

では、人々がドメインに支払ってくれたトークンにどのようにアクセスできるでしょうか?

👻 関数修飾子と撤回関数

その機能を追加するためにコントラクト(つまりバックエンド側です)を修正していきましょう。

Domains.solに以下を追加します。

modifier onlyOwner() {
require(isOwner(), "You aren't the owner");
_;
}

function isOwner() public view returns (bool) {
return msg.sender == owner;
}

function withdraw() public onlyOwner {
uint amount = address(this).balance;

(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Failed to withdraw Matic");
}

現在、エラーが発生している可能性があります。

でも心配しないでください。予想通りです。

先に進んで、これを少し分解しましょう:

最初に作成するのは関数modifierです。 これにより、関数の動作を変更できます。

requireステートメントを実装するための便利な手法です。

この修飾子が行うのは、 isOwner()関数がtrueを返すことだけです。 trueでない場合、この修飾子で宣言された関数は実行できません。

初見で不思議に思える部分は最後の_;でしょう。modifier修飾子を使用する関数は、requireの後に実行する必要があります。_;がrequireの前にある場合、withdraw関数が最初に呼び出され、次にrequireが呼び出されることになります。これではrequireが役に立ちません。

_;を記載することで、その後の処理を続行するという意味になります。

withdraw()関数に関しては、コントラクトの残高を取得し、それをリクエスター(関数を実行するためには所有者である必要があります)に送信することだけです。 これは資金を引き出すのに簡単な手法です。 msg.sender.call {value:amount}(" ")は、送金するための書き方です。 構文は少し奇妙ですがamountを渡す方法に注目してください。 require(successは、トランザクションが成功したことが想定されるところです。 成功しない場合は、トランザクションをエラーとして認識し、"Failed to withdraw Matic"と表示します。

🤠 コントラクトオーナーの設定

厄介なownerエラーを修正するには、コントラクトの先頭にグローバルなowner変数を作成し、次のようにコンストラクターに設定するだけです。

address payable public owner;

constructor(string memory _tld) ERC721 ("Ninja Name Service", "NNS") payable {
owner = payable(msg.sender);
tld = _tld;
console.log("%s name service deployed", _tld);
}

重要な点は、payableタイプとして設定したことです。 これは、所有者のaddressが支払いを受け取ることができることを意味し、明示的に宣言する必要があります。 詳細については、こちらをご参照ください。

これでコントラクトにある資金を引き出すことができます。

🏦 テストしてみましょう

run.jsスクリプトを設定します。

const main = async () => {
const [owner, superCoder] = await hre.ethers.getSigners();
const domainContractFactory = await hre.ethers.getContractFactory("Domains");
const domainContract = await domainContractFactory.deploy("ninja");
await domainContract.deployed();

console.log("Contract owner:", owner.address);

// 今回は多額を設定しています。
let txn = await domainContract.register("a16z", {
value: hre.ethers.utils.parseEther("1234"),
});
await txn.wait();

// コントラクトにいくらあるかを確認しています。
const balance = await hre.ethers.provider.getBalance(domainContract.address);
console.log("Contract balance:", hre.ethers.utils.formatEther(balance));

// スーパーコーダーとしてコントラクトから資金を奪おうとします。
try {
txn = await domainContract.connect(superCoder).withdraw();
await txn.wait();
} catch (error) {
console.log("Could not rob contract");
}

// 引き出し前のウォレットの残高を確認します。あとで比較します。
let ownerBalance = await hre.ethers.provider.getBalance(owner.address);
console.log(
"Balance of owner before withdrawal:",
hre.ethers.utils.formatEther(ownerBalance)
);

// オーナーなら引き出せるでしょう。
txn = await domainContract.connect(owner).withdraw();
await txn.wait();

// contract と owner の残高を確認します。
const contractBalance = await hre.ethers.provider.getBalance(
domainContract.address
);
ownerBalance = await hre.ethers.provider.getBalance(owner.address);

console.log(
"Contract balance after withdrawal:",
hre.ethers.utils.formatEther(contractBalance)
);
console.log(
"Balance of owner after withdrawal:",
hre.ethers.utils.formatEther(ownerBalance)
);
};

const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};

runMain();

このスクリプトをyarn contract run:scriptで実行すると、盗み取ろうとしたことがブロックされてcatch errorが作用したことがわかると思います。

Compiled 1 Solidity file successfully
ninja name service deployed
Contract owner: 0x---------
Registering a16z.ninja on the contract with tokenID 0

Contract balance: 1234.0
Could not rob contract
Balance of owner before withdrawal: 8765.98283853524900992
Contract balance after withdrawal: 0.0
Balance of owner after withdrawal: 9999.982788363651247088

ここで何が起こっているのかというと、 withdraw()関数をランダムな人物superCoderとして呼び出すと、modifierは私たちが所有者ではないことを確認し、トランザクションを元に戻します。

しかし、私たちがownerとして行うと支払いがされます。

このように必要に応じて、try catchを使ってコントラクトの内容を確認できます。

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#polygonで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨

1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット

お疲れ様でした!! 一休みしてからでも次のレッスンに進みましょう 🚀