共通パターン
コントラクトからの出金
エフェクト後の送金方法としては、出金パターンの使用が推奨されます。
エフェクトの結果としてEtherを送信する最も直感的な方法はダイレクトな transfer
コールですが、これは潜在的なセキュリティリスクがあるため推奨されません。
これについては、 セキュリティへの配慮 のページで詳しく説明しています。
King of the Ether をヒントに、「一番のお金持ち」になるために何らかの対価、例えば、Etherを一番多く送ることを目的としたコントラクトにおいて、実際に行われている出金パターンの例を以下に示します。
次のコントラクトでは、自分が一番お金持ちでなくなった場合、新しく一番お金持ちになった人の資金を受け取ります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract WithdrawalContract {
address public richest;
uint public mostSent;
mapping(address => uint) pendingWithdrawals;
/// Etherの送信量が現在最も多い量より多くなかった
error NotEnoughEther();
constructor() payable {
richest = msg.sender;
mostSent = msg.value;
}
function becomeRichest() public payable {
if (msg.value <= mostSent) revert NotEnoughEther();
pendingWithdrawals[richest] += msg.value;
richest = msg.sender;
mostSent = msg.value;
}
function withdraw() public {
uint amount = pendingWithdrawals[msg.sender];
// Reentrancy攻撃を防ぐため、送金前にpendingしている返金の額をゼロにすることを忘れないでください
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
これは、より直感的な送信パターンとは対照的です。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SendContract {
address payable public richest;
uint public mostSent;
/// Etherの送信量が現在最も多い量より多くなかった
error NotEnoughEther();
constructor() payable {
richest = payable(msg.sender);
mostSent = msg.value;
}
function becomeRichest() public payable {
if (msg.value <= mostSent) revert NotEnoughEther();
// この行は問題を引き起こす可能性があります(以下で説明します)。
richest.transfer(msg.value);
richest = payable(msg.sender);
mostSent = msg.value;
}
}
この例では、攻撃者は、失敗する受信関数やフォールバック関数を持つコントラクトのアドレスを richest
にすることで、コントラクトを使用不能な状態に陥れることができることに注意してください(例えば、 revert()
を使用したり、送金された2300ガス制限を超えて消費したりすることなど)。
そうすれば、「毒された」コントラクトに資金を届けるために transfer
が呼び出されるたびに、それは失敗し、したがって becomeRichest
も失敗して、コントラクトは永遠に動けなくなります。
一方、最初の例の出金パターンを使用した場合、攻撃者は自分の出金が失敗するだけで、コントラクトの残りの部分の働きを引き起こすことはできません。
アクセス制限
アクセスを制限することはコントラクトの一般的なパターンです。 トランザクションの内容やコントラクトの状態を人間やコンピュータに読まれないように制限できないことに注意してください。 暗号化することで多少難しくできますが、あなたのコントラクトがデータを読めることになっていれば、他の人も読めてしまいます。
コントラクトの状態を 他のコントラクト が読み取るアクセスを制限できます。
状態変数を public
で宣言しない限り、これはデフォルトの動作です。
さらに、コントラクトの状態を変更したり、コントラクトの関数を呼び出すことができる人を制限できます。 これがこのセクションの目的です。
関数モディファイア を使用することで、これらの制限が非常に読みやすくなります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract AccessRestriction {
// これらはコンストラクション段階で代入され、`msg.sender`はこのコントラクトを作成するアカウントです
address public owner = msg.sender;
uint public creationTime = block.timestamp;
// 次に、このコントラクトで発生しうるエラーの一覧とテキストによる説明を特殊なコメントで示します
/// この操作を実行する権限が送信者にありません
error Unauthorized();
/// 関数呼び出しが早すぎます
error TooEarly();
/// 関数呼び出しで送信されるEtherが不足しています
error NotEnoughEther();
// モディファイアは、関数のボディを変更するために使用できます
// このモディファイアを使用すると、特定のアドレスから関数が呼び出された場合にのみ実行されるチェックが前置されます
modifier onlyBy(address account)
{
if (msg.sender != account)
revert Unauthorized();
// "_;" を忘れないでください!
// モディファイアが使用されると、実際の関数ボディに置き換えられます
_;
}
/// `newOwner` をこのコントラクトの新しいオーナーにします
function changeOwner(address newOwner)
public
onlyBy(owner)
{
owner = newOwner;
}
modifier onlyAfter(uint time) {
if (block.timestamp < time)
revert TooEarly();
_;
}
/// 所有者情報を消去します
/// コントラクトが作成されてから6週間後にのみ呼び出すことができます
function disown()
public
onlyBy(owner)
onlyAfter(creationTime + 6 weeks)
{
delete owner;
}
// このモディファイアは、関数呼び出しに関連する一定の料金を要求します
// 呼び出し側が過剰に送金した場合、払い戻されますが、関数ボディの後にのみ払い戻されます
// これは Solidity バージョン 0.4.0 以前では危険で、`_;` の後の部分をスキップすることが可能でした
modifier costs(uint amount) {
if (msg.value < amount)
revert NotEnoughEther();
_;
if (msg.value > amount)
payable(msg.sender).transfer(msg.value - amount);
}
function forceOwnerChange(address newOwner)
public
payable
costs(200 ether)
{
owner = newOwner;
// これは条件の一例です
if (uint160(owner) & 0 == 1)
// バージョン0.4.0以前のSolidityでは、返金されませんでした
return;
// 過払い金を返還します
}
}
関数呼び出しへのアクセスを制限する、より特殊な方法については、次の例で説明します。
ステートマシン
コントラクトはしばしばステートマシンとして動作します。 つまり、異なる動作をする特定の ステージ を持っていたり、異なる関数を呼び出すことができるということです。 関数呼び出しはしばしばステージを終了し、コントラクトを次のステージに移行させます(特にコントラクトが インタラクション をモデルとしている場合)。 また、 ある時点 で自動的に到達するステージもあるのが一般的です。
例えば、ブラインドオークションのコントラクトでは、「ブラインド入札を受け付ける」というステージから始まり、「入札を公開する」に移行し、「オークションの結果を決定する」で終了します。
このような場合、関数モディファイアを使って状態をモデル化し、コントラクトの間違った使い方を防ぐことができます。
例
次の例では、モディファイア atStage
によって、あるステージでしかその関数を呼び出すことができないようにしています。
時限式の自動トランジションはモディファイア timedTransitions
で処理されます。
注釈
モディファイアの順序に関して: atStageがtimedTransitionsと組み合わされている場合は、新しいステージが考慮されるように、後者の後に言及するようにしてください。
最後に、モディファイア transitionNext
を使うと、関数が終了したときに自動的に次のステージに進むことができます。
注釈
モディファイアは省略可能: これは、バージョン0.4.0以前のSolidityにのみ適用されます。 モディファイアは、関数呼び出しを使用せず、単にコードを置き換えることで適用されるため、関数自体がreturnを使用している場合、transitionNextモディファイアのコードをスキップできます。 その場合は、それらの関数から手動でnextStageを呼び出すようにしてください。 バージョン0.4.0からは、モディファイアのコードは、関数が明示的にreturnしても実行されます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract StateMachine {
enum Stages {
AcceptingBlindedBids,
RevealBids,
AnotherStage,
AreWeDoneYet,
Finished
}
/// 現時点では関数を呼び出せません
error FunctionInvalidAtThisStage();
// これが現在のステージです
Stages public stage = Stages.AcceptingBlindedBids;
uint public creationTime = block.timestamp;
modifier atStage(Stages stage_) {
if (stage != stage_)
revert FunctionInvalidAtThisStage();
_;
}
function nextStage() internal {
stage = Stages(uint(stage) + 1);
}
// 時間指定でトランジションを行います
// 必ずこのモディファイアを最初に指定してください、そうしないとガードは新しいステージを考慮しません
modifier timedTransitions() {
if (stage == Stages.AcceptingBlindedBids &&
block.timestamp >= creationTime + 10 days)
nextStage();
if (stage == Stages.RevealBids &&
block.timestamp >= creationTime + 12 days)
nextStage();
// トランザクションによる他のステージへの推移
_;
}
// モディファイアの順序が重要です!
function bid()
public
payable
timedTransitions
atStage(Stages.AcceptingBlindedBids)
{
// 実装は省略します
}
function reveal()
public
timedTransitions
atStage(Stages.RevealBids)
{
}
// このモディファイアは、関数が終わった後、次のステージに移行します
modifier transitionNext()
{
_;
nextStage();
}
function g()
public
timedTransitions
atStage(Stages.AnotherStage)
transitionNext
{
}
function h()
public
timedTransitions
atStage(Stages.AreWeDoneYet)
transitionNext
{
}
function i()
public
timedTransitions
atStage(Stages.Finished)
{
}
}