今回作成するもの
ガチャンと壊すタイプのETH貯金箱を作成する。
完成品
https://github.com/kotaroo0/eth-piggy-box
仕様
- 目標額をこえるまでは出金できず、出金すると貯金箱は壊れる。
- 1つのアドレスを1ユーザとし、異なるアドレスを持つ人は異なるユーザとみなす。
- 目標額をいれて、貯金箱を作成する。
- 1ユーザは複数の貯金箱を作成できる 。
- 自分の貯金箱に額を指定して貯金ができる 。
- 出金は目標額はこえた場合おこなえて、全て引き出される 。
- 出金がおわった貯金箱には入金できない 。
- 他人の貯金箱には入金できない 。
開発環境
$ truffle version
Truffle v5.0.0 (core: 5.0.0)\
Solidity - ^0.4.25 (solc-js)\
Node v8.9.4
開発工程
- Truffleでコントラクトの実装
- Web3jsでフロントエンドの実装
- Javascriptでテストの記述
以上の三段階である。テストは余力があればで良い。
1. Truffleでコントラクトの実装
実装方針として、2通りを思いついた。
- ひとつの貯金箱で1コントラクト
- 全ての貯金箱で1コントラクト
結論から言うと、私は最初愚直に1で実装していた。
しかし、これではデプロイ時にかかるgasが大きい。
そのため、1つのコントラクトで完結するように変更する。
1の実装
pragma solidity ^0.4.25;
import 'openzeppelin-solidity/contracts/ownership/Ownable.sol';
// ETH貯金箱
contract PiggyBank is Ownable {
uint256 public goalAmount; // 貯金目標額(wei)
event Deposit(address userAddress, address contractAddress, uint amount);
event Destroy(address userAddress, address contractAddress, uint amount);
// コントラクトの初期化
constructor(uint _goalAmount) {
goalAmount = _goalAmount;
}
// ETHを貯金する
function deposit() onlyOwner payable {
emit Deposit(msg.sender, address(this), msg.value);
}
// 貯金箱を壊してお金を取り出す
function destroy() onlyOwner {
address contractAddress = address(this);
uint amount = contractAddress.balance;
require(amount >= goalAmount, "Insufficient Savings");
// emit Destroy(msg.sender, contractAddress, amount);
selfdestruct(owner());
}
}
2の実装
pragma solidity ^0.4.25;
// ETH貯金箱
contract PiggyBank {
// 貯金箱
struct PiggyBank {
uint id;
address owner; // 所有者
uint savingAmount; // 貯金額
uint goalAmount; // 目標額
bool isActive; // 貯金箱が破壊されたかどうか
}
PiggyBank[] public piggyBanks;
event Create(address userAddress, uint id, uint goalAmount);
event Deposit(address userAddress, uint id, uint amount, uint savingAmount, uint goalAmount);
event Destroy(address userAddress, uint id, uint amount);
function create(uint goalAmount) public {
uint id = piggyBanks.length;
piggyBanks.push(PiggyBank({
id: id,
owner: msg.sender,
savingAmount: 0,
goalAmount: goalAmount,
isActive: true
}));
emit Create(msg.sender, id, goalAmount);
}
// ETHを貯金する
function deposit(uint id) payable public {
bool isExist = false;
uint index = 0;
for (uint i = 0; i < piggyBanks.length; i++) {
if (piggyBanks[i].id == id ) {
index = i;
isExist = piggyBanks[index].isActive;
break;
}
}
require(isExist, "Not Exist");
require(piggyBanks[index].owner == msg.sender, "Incorrect User");
piggyBanks[index].savingAmount += msg.value;
emit Deposit(msg.sender, id, msg.value, piggyBanks[index].savingAmount, piggyBanks[index].goalAmount);
}
// 貯金箱を壊してお金を取り出す
function destroy(uint id) public {
bool isExist = false;
uint index = 0;
for (uint i = 0; i < piggyBanks.length; i++) {
if (piggyBanks[i].id == id) {
index = i;
isExist = piggyBanks[index].isActive;
break;
}
}
require(isExist, "Not Exist");
require(piggyBanks[index].owner == msg.sender, "Incorrect User");
require(piggyBanks[index].savingAmount >= piggyBanks[index].goalAmount, "Insufficient Saving Amount");
piggyBanks[index].savingAmount = 0;
piggyBanks[index].isActive = false;
msg.sender.transfer(piggyBanks[index].savingAmount);
emit Destroy(msg.sender, id, piggyBanks[index].savingAmount);
}
}
この実装では、リストで貯金箱を保持している。
そのため、deposit
やdestroy
する際にいちいちループを回して貯金箱を探索している。
これはハッシュ構造にして保持した方が賢いと思われる。
フロントエンドからハッシュを扱うのがややこしいと思ったりした関係で、リストで実装した。
結果的に、ハッシュでよかったと思う。
また、貯金箱を破壊する場合はリエントラントに注意して実装する必要がある。
*** 個人的まとめ ****
リエントラントとはコントラクトへの攻撃の一種であり、The DAO事件でも用いられた。
被攻撃コントラクトと攻撃コントラクト間で再帰的に処理が行われる。
具体的にはetherを引き出すような処理を行うとき、その処理が終わる前に再帰的にその処理が呼ばれることによって無限にetherが引き出される。
対策としては、ロックフラグのようなものを用いることが考えられる。
非攻撃コントラクトの処理を呼び出す時、ロックフラグによってその処理を再帰的に呼び出せないようにすることでリエントラントを防ぐことができる。
この場合では、
// 正しい
piggyBanks[index].savingAmount = 0;
piggyBanks[index].isActive = false;
msg.sender.transfer(piggyBanks[index].savingAmount);
// 攻撃されうる
msg.sender.transfer(piggyBanks[index].savingAmount);
piggyBanks[index].savingAmount = 0;
piggyBanks[index].isActive = false;
残高やisActiveを0にする前に、etherを送金してしまうと再帰的に呼び出されコントラクトのetherをむしりとられてしまう可能性がある。
https://medium.com/@yuyasugano/%E3%83%AA%E3%82%A8%E3%83%B3%E3%83%88%E3%83%A9%E3%83%B3%E3%83%88-%E5%86%8D%E5%85%A5%E5%8F%AF%E8%83%BD-%E5%86%8D%E8%80%83-a194c80f131f
この記事などわかりやすく解説してある。
2. Web3jsでフロントエンドの実装
次の記事へ続く。