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

lesson-1_コントラクトに管理者を設けよう

🛫 コントラクトに機能を追加しましょう

このセクションではコントラクトに機能を追加し、フロントエンドに反映させます 🎉

以下の機能を追加していきます。

  • メッセージを保留できる数に上限値(numOfPendingLimits)を設けます。
  • numOfPendingLimitsに達した場合、その受取人にメッセージを送ることができません。
  • コントラクトに管理者機能を追加します。
  • コントラクトの管理者はnumOfPendingLimitsを変更することができます。

contractディレクトリへ移動しましょう。

📮 メッセージの保留数に上限を設けよう

それではcontractsディレクトリ内の Messenger.solを下記のように編集してください。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

import "hardhat/console.sol";

contract Messenger {
+ // ユーザが保留できるメッセージ数の上限を設定します。
+ uint256 public numOfPendingLimits;

// メッセージ情報を定義します。
struct Message {
address payable sender;
address payable receiver;
uint256 depositInWei;
uint256 timestamp;
string text;
bool isPending;
}

// メッセージの受取人アドレスをkeyにメッセージを保存します。
mapping(address => Message[]) private _messagesAtAddress;
+ // ユーザが保留中のメッセージの数を保存します。
+ mapping(address => uint256) private _numOfPendingAtAddress;

event NewMessage(
address sender,
address receiver,
uint256 depositInWei,
uint256 timestamp,
string text,
bool isPending
);

event MessageConfirmed(address receiver, uint256 index);

+ constructor(uint256 _numOfPendingLimits) payable {
+ numOfPendingLimits = _numOfPendingLimits;
+ }

// ユーザからメッセージを受け取り、状態変数に格納します。
function post(string memory _text, address payable _receiver)
public
payable
{
+ // メッセージ受取人の保留できるメッセージが上限に達しているかを確認します。
+ require(
+ _numOfPendingAtAddress[_receiver] < numOfPendingLimits,
+ "The receiver has reached the number of pending limits"
+ );
+
+ // 保留中のメッセージの数をインクリメントします。
+ _numOfPendingAtAddress[_receiver] += 1;

_messagesAtAddress[_receiver].push(
Message(
payable(msg.sender),
_receiver,
msg.value,
block.timestamp,
_text,
true
)
);

emit NewMessage(
msg.sender,
_receiver,
msg.value,
block.timestamp,
_text,
true
);
}

// ...
}

追加内容を見ていきます。

    // ユーザが保留できるメッセージ数の上限を設定します。
uint256 public numOfPendingLimits;
    // ユーザが保留中のメッセージの数を保存します。
mapping(address => uint256) private _numOfPendingAtAddress;

上記の2つはメッセージ保留数の上限値と、各アドレス宛のメッセージがどのくらい保留されているかを保持する状態変数です。

    constructor(uint256 _numOfPendingLimits) payable {
numOfPendingLimits = _numOfPendingLimits;
}

コンストラクタは引数により、デプロイ時に上限値を受け取れるようになっています。 そして状態変数にセットします。

    // ユーザからメッセージを受け取り、状態変数に格納します。
function post(string memory _text, address payable _receiver)
public
payable
{
// メッセージ受取人の保留できるメッセージが上限に達しているかを確認します。
require(
_numOfPendingAtAddress[_receiver] < numOfPendingLimits,
"The receiver has reached the number of pending limits"
);

// 保留中のメッセージの数をインクリメントします。
_numOfPendingAtAddress[_receiver] += 1;

// ...
}

post関数が呼び出された際、初めにメッセージの受取人のメッセージ保留数が上限に達しているかを確認しています。 上限に達していた場合、トランザクションをキャンセルします。

また、処理を続行する場合は保留数をインクリメントします。

🧪 テストを追加しましょう

機能を追加したのでテストしていきます。

testディレクトリ内のMessenger.tsファイルを以下のように編集してください。

変更点

  • deployContract関数を変更
  • Deploymentテストを追加
  • Postテスト内の最後にテストケースを追加
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { Overrides } from "ethers";
import hre, { ethers } from "hardhat";

describe("Messenger", function () {
async function deployContract() {
// 初めのアドレスはコントラクトのデプロイに使用されます。
const [owner, otherAccount] = await ethers.getSigners();

const numOfPendingLimits = 10;
const funds = 100;

const Messenger = await hre.ethers.getContractFactory("Messenger");
const messenger = await Messenger.deploy(numOfPendingLimits, {
value: funds,
} as Overrides);

return { messenger, numOfPendingLimits, funds, owner, otherAccount };
}

describe("Deployment", function () {
it("Should set the right number of pending message limits", async function () {
const { messenger, numOfPendingLimits } = await loadFixture(
deployContract
);

expect(await messenger.numOfPendingLimits()).to.equal(numOfPendingLimits);
});
});

describe("Post", function () {
// ...

it("Should revert with the right error if exceed number of pending limits", async function () {
const { messenger, otherAccount, numOfPendingLimits } = await loadFixture(
deployContract
);

// メッセージ保留数の上限まで otherAccount へメッセージを送信します。
for (let cnt = 1; cnt <= numOfPendingLimits; cnt++) {
await messenger.post("dummy", otherAccount.address);
}
// 次に送信するメッセージはキャンセルされます。
await expect(
messenger.post("exceed", otherAccount.address)
).to.be.revertedWith(
"The receiver has reached the number of pending limits"
);
});
});

describe("Accept", function () {
// ...
});

describe("Deny", function () {
// ...
});
});

deployContract関数では追加した要素であるnumOfPendingLimitsを用意して、デプロイ時に渡しています。

追加したDeploymentテストでは、デプロイ時に渡したnumOfPendingLimitsが正しくセットされているかを確認しています。

さらにPostテスト内で追加したテストケースitの中では、 for loopをしようして上限値までメッセージを送り続けることで、numOfPendingLimitsによる制限が働いているかを確認しています。

それではテストを実行しましょう!

ターミナル上でAVAX-Messenger/直下にいることを確認して、以下のコマンドを実行してください。

yarn test

以下のような表示がされたらテスト成功です!

💠 コントラクトに管理者機能を設けましょう

コントラクトに管理者を設けて、管理者のみnumOfPendingLimitsを変更してコントラクト内のルールを変更できるようにしましょう。

それではcontractsディレクトリ内の Messenger.solと同じ階層にOwnable.solというファイルを作成しましょう。

contracts
├── Messenger.sol
└── Ownable.sol

Ownable.sol内に以下のコードを記述してください。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

contract Ownable {
address public owner;

function ownable() internal {
owner = msg.sender;
}

modifier onlyOwner() {
require(msg.sender == owner, "You aren't the owner");
_;
}
}

このファイルはopenzeppelinライブラリのOwnable というコントラクトを簡単にしたものです。

コンストラクタ内ではコンストラクタを呼び出した(デプロイした)アドレスで状態変数のownerを初期化しています。

modifierはまだ出てきていない関数修飾子の1つで、その使用方法と共にどんなものなのかこの後理解します。 ここで見て頂きたいのは、requireを利用して、関数を実行する人がownerと等しいことを確認していること 次の行に_;を記述しているということです。

Messenger.solOwnableを継承させて、Ownableの関数を利用できるようにしましょう。

Messenger.solを以下のように編集してください。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

import "hardhat/console.sol";
+ import "./Ownable.sol";

+ contract Messenger is Ownable {
// ユーザが保留できるメッセージ数の上限を設定します。
uint256 public numOfPendingLimits;

// メッセージ情報を定義します。
struct Message {
address payable sender;
address payable receiver;
uint256 depositInWei;
uint256 timestamp;
string text;
bool isPending;
}

// メッセージの受取人アドレスをkeyにメッセージを保存します。
mapping(address => Message[]) private _messagesAtAddress;
// ユーザが保留中のメッセージの数を保存します。
mapping(address => uint256) private _numOfPendingAtAddress;

event NewMessage(
address sender,
address receiver,
uint256 depositInWei,
uint256 timestamp,
string text,
bool isPending
);

event MessageConfirmed(address receiver, uint256 index);
+ event NumOfPendingLimitsChanged(uint256 limits);

constructor(uint256 _numOfPendingLimits) payable {
+ ownable();

numOfPendingLimits = _numOfPendingLimits;
}

+ // ownerのみこの関数を実行できるように修飾子を利用します。
+ function changeNumOfPendingLimits(uint256 _limits) external onlyOwner {
+ numOfPendingLimits = _limits;
+ emit NumOfPendingLimitsChanged(numOfPendingLimits);
+ }

// ...
}

contract Messenger is Ownableのようにコントラクトを宣言することで、 Messenger.solOwnableを継承するという関係を作れます、するとOwnableの関数をMessenger.solも利用できるようになります。

コンストラクタ内でOwnableの関数ownableを実行しています。 ownableを実行すると、コンストラクタを呼び出した(=デプロイをした)アカウントのアドレスをコントラクトは管理者として記録します。

新しく追加したchangeNumOfPendingLimits関数に注目しましょう。 関数の実行に修飾子のonlyOwnerを指定しています。

ここで起こる処理の流れを整理します。

  • changeNumOfPendingLimitsが呼び出されると、修飾子onlyOwnerの内容が実行されます。
  • onlyOwnerの内では、関数を呼び出したアカウントが管理者であるかを確認しています。
  • 確認が取れたら、_;の記述された箇所から、今度はchangeNumOfPendingLimitsの処理が実行されます。
  • numOfPendingLimitsを変更しイベントをemitします。

このようにmodifierを利用した関数修飾子は、自分でカスタムした処理をある関数の実行前の処理として適用させることができます。 今回のように管理者権限の必要な関数にはonlyOwnerを修飾子としてつけるだけで決まった処理をしてくれるので便利です。

オーナーに特別な権限を与えることは、オーナーの利益のためにルールを変更できるという面で、濫用される恐れもあります。

以下、CryptoZombiesからの引用です。

DApp がイーサリアム上にあるというだけで、全てが分散型になっているというわけではないことを、常に念頭に入れておいてください。 気になる部分については、すべてのソースコードに目を通して、オーナーに特別な力がないことを確認する必要があります。 開発者としてバグを修正するように DApp をコントロールする権限が必要な一方で、オーナーの数を少なくしてユーザーのデータの安全性を確保できるようなプラットフォームを開発をすることも重要であり、両者のバランスに常に気をつける必要があります。

是非openzeppelinの Ownable コントラクトを見てみてください!

🧪 テストを追加しましょう

機能を追加したのでテストしていきます。

testディレクトリ内のMessenger.tsファイルを以下のように編集してください。

変更点

  • Deploymentテスト内の最後にテストケースを追加
  • ChangeLimitsテストを追加
// ...

describe("Messenger", function () {
async function deployContract() {
// ...
}

describe("Deployment", function () {
it("Should set the right number of pending message limits", async function () {
// ...
});

it("Should set the right owner", async function () {
const { messenger, owner } = await loadFixture(deployContract);

expect(await messenger.owner()).to.equal(owner.address);
});
});

describe("Change limits", function () {
it("Should revert with the right error if called by other account", async function () {
const { messenger, otherAccount } = await loadFixture(deployContract);

await expect(
messenger.connect(otherAccount).changeNumOfPendingLimits(5)
).to.be.revertedWith("You aren't the owner");
});

it("Should set the right number of pending limits after change", async function () {
const { messenger, numOfPendingLimits } = await loadFixture(
deployContract
);

const newLimits = numOfPendingLimits + 1;
await messenger.changeNumOfPendingLimits(newLimits);
expect(await messenger.numOfPendingLimits()).to.equal(newLimits);
});

it("Should emit an event on change limits", async function () {
const { messenger } = await loadFixture(deployContract);

await expect(messenger.changeNumOfPendingLimits(10)).to.emit(
messenger,
"NumOfPendingLimitsChanged"
);
});
});

// ...
});

Deploymentテスト内に追加したテストケースit内ではデプロイ後にownerが正しくセットされているかを確認しています。

ChangeLimitsテスト内では、

owner以外がnumOfPendingLimitsを変更しようとした場合にトランザクションがキャンセルされるか

ownernumOfPendingLimitsを正しく変更できているか

changeNumOfPendingLimits実行時に正しくイベントをemitできているか

をそれぞれ確認しています。

それではテストを実行しましょう! ターミナル上で以下のコマンドを実行してください。

yarn test

以下のような表示がされたらテスト成功です!

🛫 デプロイ スクリプトを変更する

コンストラクタを変更したので、デプロイ時のコードも変更する必要があります。

scripts/deploy.ts内を以下のコードに書き換えてください。 主にデプロイ時の引数を増やした部分が変わっています。

import { Overrides } from "ethers";
import { ethers } from "hardhat";

async function deploy() {
// コントラクトをデプロイするアカウントのアドレスを取得します。
const [deployer] = await ethers.getSigners();
console.log("Deploying contract with the account:", deployer.address);

const numOfPendingLimits = 10;
const funds = 100;

// コントラクトのインスタンスを作成します。
const Messenger = await ethers.getContractFactory("Messenger");

// The deployed instance of the contract
const messenger = await Messenger.deploy(numOfPendingLimits, {
value: funds,
} as Overrides);

await messenger.deployed();

console.log("Contract deployed at:", messenger.address);
console.log("Contract's owner is:", await messenger.owner());
console.log(
"Contract's number of pending message limits is:",
await messenger.numOfPendingLimits()
);
console.log(
"Contract's fund is:",
await messenger.provider.getBalance(messenger.address)
);
}

deploy()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

🛩 もう一度デプロイする

コントラクトを更新したので、下記を実行する必要があります。

1 . 再度コントラクトをデプロイする

ターミナル上でAVAX-Messenger/直下にいることを確認して、下記のコマンドを実行します。

yarn contract deploy

出力結果の例

yarn run v1.22.19
$ yarn workspace contract deploy
$ npx hardhat run scripts/deploy.ts --network fuji
Deploying contract with the account: 0xdf90d78042C8521073422a7107262D61243a21D0
Contract deployed at: 0xFCb785b459f0c701ca4019B23EFc66B5f481daA9
Contract's owner is: 0xdf90d78042C8521073422a7107262D61243a21D0
Contract's number of pending message limits is: BigNumber { value: "10" }
Contract's fund is: BigNumber { value: "100" }

出力結果のContract deployed at:の後に続くアドレスをコピーして client/hooks/useMessengerContract.tsの中のcontractAddress変数定義の部分に貼り付けます。

例:

const contractAddress = "0xFCb785b459f0c701ca4019B23EFc66B5f481daA9";

2 . フロントエンドの ABI ファイルを更新する

AVAX-Messenger直下からターミナルでコピーを行う場合、このようなコマンドになります。

$ cp ./packages/contract/artifacts/contracts/Messenger.sol/Messenger.json ./packages/client/utils/

3 . 型定義ファイルを更新する

AVAX-Messenger直下からターミナルでコピーを行う場合、このようなコマンドになります。

$ cp -r ./packages/contract/typechain-types ./packages/client/

コントラクトを更新するたび、これらの 3 つのステップを実行する必要があります。

✍️: 上記 3 つのステップが必要な理由 ずばり、スマートコントラクトは一度デプロイされると変更することができないからです。

コントラクトを更新するためには、再びデプロイする必要があります。

再びデプロイされたコントラクトは、完全に新しいコントラクトとして扱われるため、すべての変数はリセットされます。

つまり、コントラクトを一度更新してしまうと、すべての既存のメッセージデータが失われます。

🙋‍♂️ 質問する

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

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

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

コントラクトをアップデートしたら、次のレッスンに進みましょう 🎉