lesson-2_コントラクトを作成しよう
👩💻 実装する内容の確認
本プロジェクトで作成するdappの内容を整理します。
銀行の行っている手形取引をスマートコントラクトで管理するアプリを作成します。
手形取引とは以下のようなものです。
手形の発行者と受取人、仲介人としての銀行がいる状態で以下のような取引が行われます。
- ある事業者の間で商品の取引が行われます。ここで商品の買い手がその場で代金を用意できない場合に手形が利用されます。商品の買い手を手形発行者、売り手が受取人とします。
- 発行者はすぐには代金を支払えないために、期限が来たら現金と交換することができる手形を受取人に対して発行します。
- 受取人は手形を銀行に持っていきます。
- 銀行は手形を現金と交換し受取人へ渡します。この時、手形の期限に達していない場合は本来の金額から割り引いた金額を受取人に渡すことになります。
- 銀行は期限に達した手形に対して発行者に請求します。
- 発行者は手形の金額と手数料を銀行に支払います。
- 期限までに手形の支払いを行わなかった発行者は不渡りを起こした事業者となり取引ができなくなります。
以上の仕組みをスマートコントラクトに実装します。
発行者と受取人がユーザとして存在し、スマートコントラクトが銀行の役目を果たします。
代金にはネイティブトークンを使用します。
🥦 既存金融と Subnet
ブロックチェーンは透明性や改ざん耐性から、銀行などの既存金融にとってメリットのあるデータベースと言えます。
またスマートコントラクトなどによりデータ管理業務を自動化できる点もメリットです。
しかし企業の運営には不正な活動をする事業者を排除する責任や利用者への説明責任などが伴うため、ブロックチェーンを使う場合はある程度のコントロールがネットワークに対してできる状態であることも求められます。
Subnetでは,「許可されたユーザーのみがコントラクトの展開やトランザクションを行うことができる」と要求することができます。 許可リストは管理者によってのみ更新され、許可リスト自体はPreCompileコントラクト内に実装されるため、コンプライアンスに関する事項についてはより透明で監査が可能です。
🥮 Bank
コントラクトを作成する
section1
のこれから先の作業は、AVAX-Subnet/packages/contract
ディレクトリをルートディレクトリとして話を進めます。 🙌
contracts
ディレクトリの下にBank.sol
という名前のファイルを作成します。
Hardhatを使用する場合ファイル構造は非常に重要ですので、注意する必要があります。 ファイル構造が下記のようになっていれば大丈夫です 😊
contract
└── contracts
└── Bank.sol
次に、コードエディタでプロジェクトのコードを開きます。
Bank.sol
の中に以下のコードを貼り付けてください。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Bank {
// 手形の属性です。
struct Bill {
uint256 id;
uint256 amount;
uint256 timestamp;
address issuer;
address recipient;
BillStatus status;
}
// 手形の状態を表します。
enum BillStatus {
Issued, // 発行された
Paid, // 支払われた
Cashed, // 現金化された
Completed, // 正常に処理された
Dishonored // 不渡りとなった
}
// 全ての手形を保存します。
Bill[] public allBills;
// 不渡りを起こしたアドレスを保存します。
address[] public dishonoredAddresses;
// 各アドレスがコントラクトにロックしたトークンの数を保有します。
mapping(address => uint256) private _balance;
// 手形の期間を定数で用意します。
uint256 public constant TERM = 1 days * 60;
// 割引金利
uint256 public constant DISCOUNT_RATE = 10;
// 手形手数料
uint256 public constant INTEREST_RATE = 10;
constructor() payable {}
}
もし,hardhat.config.ts
の中に記載されているSolidityのバージョンが0.8.17
でなかった場合は,Bank.sol
の中身をhardhat.config.ts
に記載されているバージョンに変更しましょう。
コントラクト冒頭ではBill(手形)の属性をあらわすstructや状態を表すenumが定義されています。
その下に状態変数を定義しています。
_balance
: 発行者が手形の金額をスマートコントラクトに支払った際にその金額を記録するためのものです。
DISCOUNT_RATE
: 期限に達していない手形を受取人が現金化する際に、10%割引で現金化します。
INTEREST_RATE
: 発行者が手形の支払いをする際は、手形の代金の10% を手数料としてスマートコントラクトに支払う必要があります。
Bank
のデプロイ時にはある程度のトークンを渡したいため、constructorにはpayable
を指定しています。
次にBank
の最後の行に以下のコードを貼り付けてください。
function _sendToken(address payable _to, uint256 _amount) private {
(bool success, ) = (_to).call{value: _amount}("");
require(success, "Failed to send token");
}
function getNumberOfBills() public view returns (uint256) {
return allBills.length;
}
function getNumberOfDishonoredAddresses() public view returns (uint256) {
return dishonoredAddresses.length;
}
function getBalance() public view returns (uint256) {
return _balance[msg.sender];
}
function beforeDueDate(uint256 _id) public view returns (bool) {
Bill memory bill = allBills[_id];
if (block.timestamp <= bill.timestamp + TERM) {
return true;
} else {
return false;
}
}
function getAmountToCashBill(uint256 _id) public view returns (uint256) {
Bill memory bill = allBills[_id];
if (beforeDueDate(_id)) {
return (bill.amount * (100 - DISCOUNT_RATE)) / 100;
}
return bill.amount;
}
function getAmountToPayBill(uint256 _id) public view returns (uint256) {
Bill memory bill = allBills[_id];
return (bill.amount * (100 + DISCOUNT_RATE)) / 100;
}
_sendToken
はネイティブトークンを_toへ_amount分送信する関数です。
beforeDueDate
は現在のタイムスタンプと手形の期限を比較して、期限に達しているかどうかを返却します。
📓
block.timestamp
の使用について スマートコントラクトで時間の参照方法はいくつかあります。block.timestamp
はブロックチェーンにブロックが書き込まれる際に、バリデータによって操作ができるという懸念点がありますが、操作のできる範囲は 30 秒ほどです。 つまり 30 秒の範囲で実際とは差のある時間をコントラクト内のロジックに使用しても良いのならblock.timestamp
を使用できます。 今回は簡易的な実装なのでこちらを使います。 Ethereum のコントラクトでは、block.number
を使用した方法(参考)などもありますが、Avalanche では定期的にブロックが生成されるという仕組みではないためこちらは使用できなそうです。 正確な情報を取得するためにはオラクルを使用する必要があります。
getAmountToCashBill
は、受取人が手形を現金化する際に実際に現金化される金額を返却するものです。
手形の期限前である場合は割引した金額を返却します。
getAmountToPayBill
は、発行者が手形の支払いを行う際に実際に払う金額を返却します。
手数料の分割増した金額を返却します。
次にBank
の最後の行に以下のコードを貼り付けてください。
function issueBill(uint256 _amount, address _recipient) public {
Bill memory bill = Bill(
allBills.length,
_amount,
block.timestamp, // block.timestampは正確な値ではありません。
msg.sender,
_recipient,
BillStatus.Issued
);
allBills.push(bill);
}
function cashBill(uint256 _id) public payable {
Bill storage bill = allBills[_id];
require(
bill.status == BillStatus.Issued || bill.status == BillStatus.Paid,
"Status is not Isued or Paid"
);
require(bill.recipient == msg.sender, "Your are not recipient");
bill.status = BillStatus.Cashed;
uint256 amount = getAmountToCashBill(_id);
_sendToken(payable(msg.sender), amount);
}
function lockToken(uint256 _id) public payable {
Bill storage bill = allBills[_id];
uint256 amount = getAmountToPayBill(_id);
require(msg.value == amount, "Amount is not correct");
bill.status = BillStatus.Paid;
_balance[msg.sender] += msg.value;
}
function completeBill(uint256 _id) public payable {
Bill storage bill = allBills[_id];
require(
bill.status == BillStatus.Issued ||
bill.status == BillStatus.Cashed ||
bill.status == BillStatus.Paid,
"Bill is already completed"
);
require(!beforeDueDate(_id), "Before due date");
uint256 amount = getAmountToPayBill(_id);
if (amount <= _balance[bill.issuer]) {
_balance[bill.issuer] -= amount;
bill.status = BillStatus.Completed;
} else {
bill.status = BillStatus.Dishonored;
dishonoredAddresses.push(bill.issuer);
}
}
issueBill
: 発行者が使用します。_recipient(受取人)に対して_amount(トークン量)分の手形の発行を行います。
cashBill
: 受取人が使用します。指定したidのBillを現金化します。
lockToken
: 発行者が手形の支払いに使用します。送付されたトークン量を_balance
に記録します。
completeBill
: 銀行の管理者が利用します。期限に達したBillを処理します。発行者が手形の金額分を納めていない場合は手形の状態を不渡りとし、発行者のアドレスをdishonoredAddresses
に追加します。