WEBアプリに抽選機能を実装しよう
🎲 0.0001ETH を送るユーザーをランダムに選ぶ
現在、コントラクトはすべてのユーザーに0.0001ETHを送るように設定されています。
しかし、それでは、コントラクトはすぐに資金を使い果たしてしまうでしょう。
これを防ぐために、これから下記の機能をWavePortal.sol
に実装していきます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "hardhat/console.sol";
contract WavePortal {
uint256 private _totalWaves;
/* 乱数生成のための基盤となるシード(種)を作成 */
uint256 private _seed;
event NewWave(address indexed from, uint256 timestamp, string message);
struct Wave {
address waver;
string message;
uint256 timestamp;
uint256 seed;
}
Wave[] private _waves;
constructor() payable {
console.log("We have been constructed!");
/*
* 初期シードを設定
*/
_seed = (block.timestamp + block.prevrandao) % 100;
}
function wave(string memory _message) public {
_totalWaves += 1;
console.log("%s has waved!", msg.sender);
/*
* ユーザーのために乱数を生成
*/
_seed = (block.prevrandao + block.timestamp + _seed) % 100;
_waves.push(Wave(msg.sender, _message, block.timestamp, _seed));
console.log("Random # generated: %d", _seed);
/*
* ユーザーがETHを獲得する確率を50%に設定
*/
if (_seed <= 50) {
console.log("%s won!", msg.sender);
/*
* ユーザーにETHを送るためのコードは以前と同じ
*/
uint256 prizeAmount = 0.0001 ether;
require(
prizeAmount <= address(this).balance,
"Trying to withdraw more money than the contract has."
);
(bool success, ) = (msg.sender).call{value: prizeAmount}("");
require(success, "Failed to withdraw money from contract.");
} else {
console.log("%s did not win.", msg.sender);
}
emit NewWave(msg.sender, block.timestamp, _message);
}
function getAllWaves() public view returns (Wave[] memory) {
return _waves;
}
function getTotalWaves() public view returns (uint256) {
return _totalWaves;
}
}
コードを見ていきましょう。
uint256 private _seed;
ここでは、乱数を生成するために使用する初期シード(乱数の種)を定義しています。
constructor() payable {
console.log("We have been constructed!");
/* 初期シードを設定 */
_seed = (block.timestamp + block.prevrandao) % 100;
}
ここでは、constructor
の中にユーザーのために生成された乱数を_seed
に格納しています。
block.prevrandao
とblock.timestamp
の2つは、Solidityから与えられた数値です。
-
block.prevrandao
は、Beacon Chain(proof-of-stake型ブロックチェーン)が提供する乱数です。 -
block.timestamp
は、ブロックが処理されている時のUNIXタイムスタンプです。
そして、%100
により、数値を0〜100の範囲に設定しています。
次に下記のコードを確認しましょう。
function wave(string memory _message) public {
_totalWaves += 1;
console.log("%s has waved!", msg.sender);
/*
* ユーザーのために乱数を生成
*/
_seed = (block.prevrandao + block.timestamp + _seed) % 100
_waves.push(Wave(msg.sender, _message, block.timestamp, _seed));
console.log("Random # generated: %d", _seed);
ここで、ユーザーがwave
を送信するたびに_seed
を更新しています。
これにより、ランダム性の担保を行っています。ランダム性を強化することにより、ハッカーからの攻撃を防げます。
最後に下記のコードを見ていきましょう。
if (_seed <= 50) {
console.log("%s won!", msg.sender);
:
ここでは、_seed
の値が、50以下であるかどうかを確認するために、if
ステートメントを実装しています。
_seed
の値が50以下の場合、ユーザーはETHを獲得できます。
✍️: 乱数が「ランダムであること」の重要性 「ユーザーに ETH がランダムで配布される」ようなゲーム性のあるサー ビスにおいて、ハッカーからの攻撃を防ぐことは大変重要です。
ブロックチェーン上にコードは公開されているので、信頼できる乱数生成のアルゴリズムは、手動で作る必要があります。
乱数の生成は、一見面倒ではありますが、何百万人ものユーザーがアクセスする dApp を構築する場合は、とても重要な作業となります。
☕️ 作成した機能の動作確認
下記のように、run.js
を更新して、ユーザーにランダムにETHを送れるか確認してみましょう。
const main = async () => {
const waveContractFactory = await hre.ethers.getContractFactory("WavePortal");
/*
* デプロイする際0.1ETHをコントラクトに提供する
*/
const waveContract = await waveContractFactory.deploy({
value: hre.ethers.utils.parseEther("0.1"),
});
await waveContract.deployed();
console.log("Contract deployed to: ", waveContract.address);
/*
* コントラクトの残高を取得(0.1ETH)であることを確認
*/
let contractBalance = await hre.ethers.provider.getBalance(
waveContract.address
);
console.log(
"Contract balance:",
hre.ethers.utils.formatEther(contractBalance)
);
/*
* 2回 waves を送るシミュレーションを行う
*/
const waveTxn = await waveContract.wave("This is wave #1");
await waveTxn.wait();
const waveTxn2 = await waveContract.wave("This is wave #2");
await waveTxn2.wait();
/*
* コントラクトの残高を取得し、Waveを取得した後の結果を出力
*/
contractBalance = await hre.ethers.provider.getBalance(waveContract.address);
/*
*コント ラクトの残高から0.0001ETH引かれていることを確認
*/
console.log(
"Contract balance:",
hre.ethers.utils.formatEther(contractBalance)
);
let allWaves = await waveContract.getAllWaves();
console.log(allWaves);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
それでは、ターミナル上で下記のコードを実行してみましょう。
yarn contract run:script
次のような結果が、ターミナルに出力されたでしょうか?
Compiling 1 file with 0.8.19
Solidity compilation finished successfully
We have been constructed!
Contract deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Contract balance: 0.1
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 has waved!
Random # generated: 89
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 did not win.
Contract balance: 0.1
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 has waved!
Random # generated: 31
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 won!
Contract balance: 0.0999
[
[
'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
'This is wave #1',
BigNumber { value: "1643887441" },
BigNumber { value: "89" },
waver: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
message: 'This is wave #1',
timestamp: BigNumber { value: "1643887441" }
seed: BigNumber { value: "89" }
],
[
'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
'This is wave #2',
BigNumber { value: "1643887442" },
BigNumber { value: "31" },
waver: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
message: 'This is wave #2',
timestamp: BigNumber { value: "1643887442" }
seed: BigNumber { value: "31" }
]
]
下記を見てみましょう。
一人目のユーザーは、乱数の結果89
という値を取得したので、ETHを獲得できませんでした。Contract balance
は0.1ETHのままです。
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 has waved!
Random # generated: 89
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 did not win.
Contract balance: 0.1
次に、二人目のユーザーの結果を見てみましょう。
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 has waved!
Random # generated: 31
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 won!
Contract balance: 0.0999
二人目のユーザーは、乱数の結果31
という値を取得したので、ETHを獲得しました。
Contract balance
が、0.0999ETHに更新されていることを確認してください。
🚔 スパムを防ぐためのクー ルダウンを実装する
最後に、スパムを防ぐためのクールダウン機能を実装していきます。
ここでいうスパムは、あなたのWebアプリケーションから連続してwave
を送って、ETHを稼ごうとする動作を意味します。
それでは、下記のようにWavePortal.sol
を更新しましょう。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "hardhat/console.sol";
contract WavePortal {
uint256 private _totalWaves;
uint256 private _seed;
event NewWave(address indexed from, uint256 timestamp, string message);
struct Wave {
address waver;
string message;
uint256 timestamp;
uint256 seed;
}
Wave[] private _waves;
/*
* "address => uint mapping"は、アドレスと数値を関連付ける
*/
mapping(address => uint256) public lastWavedAt;
constructor() payable {
console.log("We have been constructed!");
/*
* 初期シードの設定
*/
_seed = (block.timestamp + block.prevrandao) % 100;
}
function wave(string memory _message) public {
/*
* 現在ユーザーがwaveを送信している時刻と、前回waveを送信した時刻が15分以上離れていることを確認。
*/
require(
lastWavedAt[msg.sender] + 15 minutes < block.timestamp,
"Wait 15m"
);
/*
* ユーザーの現在のタイムスタンプを更新する
*/
lastWavedAt[msg.sender] = block.timestamp;
_totalWaves += 1;
console.log("%s has waved!", msg.sender);
/*
* ユーザーのために乱数を設定
*/
_seed = (block.prevrandao + block.timestamp + _seed) % 100;
_waves.push(Wave(msg.sender, _message, block.timestamp, _seed));
if (_seed <= 50) {
console.log("%s won!", msg.sender);
uint256 prizeAmount = 0.0001 ether;
require(
prizeAmount <= address(this).balance,
"Trying to withdraw more money than they contract has."
);
(bool success, ) = (msg.sender).call{value: prizeAmount}("");
require(success, "Failed to withdraw money from contract.");
}
emit NewWave(msg.sender, block.timestamp, _message);
}
function getAllWaves() public view returns (Wave[] memory) {
return _waves;
}
function getTotalWaves() public view returns (uint256) {
return _totalWaves;
}
}
新しく追加したコードを見ていきましょう。
mapping(address => uint256) public lastWavedAt;
ここでは、mapping
と呼ばれる特別なデータ構造を使用しています。
Solidityのmapping
は、ほかの言語におけるハッシュテーブルや辞書のような役割を果たします。
これらは、下記のように_Key
と_Value
のペアの形式でデータを格納するために使用されます。
mapping(_Key => _Value)public mappingName
今回は、ユーザーのアドレス(= _Key
= address
)をそのユーザーがwave
を送信した時刻(= _Value
= uint256
)に関連付けるためにmapping
を使用しました。
理解を深めるために、次のコードを見ていきましょう。
function wave(string memory _message) public {
/* 現在ユーザーがwaveを送信している時刻と、前回waveを送信した時刻が15分以上離れていることを確認。*/
require(
lastWavedAt[msg.sender] + 15 minutes < block.timestamp,
"Wait 15m"
);
ここでは、Webアプリケーション上で現在ユーザーがwave
を送ろうとしている時刻と、そのユーザーが前回wave
を送った時刻を比較して、15分以上経過しているか検証しています。
lastWavedAt[msg.sender]
の初期値は0
ですので、まだ一度もwave
を送ったことがないユーザーは、wave
を送信できます。
15分待たずにwave
を送ろうとしてくるユーザーには、"Wait 15min"
というアラートを返します。これにより、スパムを防止しています。
最後に、下記のコードを確認してください。
lastWavedAt[msg.sender] = block.timestamp;
ここで、ユーザーがwave
を送った時刻がタイムスタンプとして記録されます。
mapping(address => uint256) public lastWavedAt
でユーザーのアドレスとlastWavedAt
を紐づけているので、これで次に同じユーザーがwave
を送ってきた時に、15分経過しているか検証できます。