スマートコントラクトの解剖学
最終編集者: @sekisanchi(opens in a new tab), 2024年6月14日
スマートコントラクトは、イーサリアム上のアドレスで実行されるプログラムです。 それらはトランザクションの受信時に実行できるデータと関数で構成されています。 ここでは、スマートコントラクトの構成要素の概要を説明します。
前提知識
最初に、スマートコントラクトを必ずお読みください。 このドキュメントは、JavaScriptやPythonなどのプログラミング言語に精通していることを前提としています。
データ
すべてのコントラクトのデータは、storage
またはmemory
のいずれかのロケーションに割り当てる必要があります。 スマートコントラクトのストレージの変更にはコストがかかりますので、データをどこに格納するかを考える必要があります。
ストレージ
永続データはストレージと呼ばれ、状態変数で表されます。 これらの値は、ブロックチェーンに永続的に保存されます。 コントラクトがコンパイル時に必要なブロックチェーンのストレージ容量を追跡できるように、型を宣言する必要があります。
1// Solidity example2contract SimpleStorage {3 uint storedData; // State variable4 // ...5}コピー
1# Vyper example2storedData: int128コピー
オブジェクト指向言語でのプログラミングの経験がある場合は、ほとんどの型になじみがあるでしょう。 しかし、イーサリアムの開発が初めての場合、address
は目新しいかもしれません。
address
型は、20バイトまたは160ビットに相当するイーサリアムアドレスを保持します。 先頭が0xの16進数を返します。
その他の型には次のものがあります。
- ブール値
- 整数
- 固定小数点数
- 固定サイズのバイト配列
- 動的サイズのバイト配列
- 有理数リテラルと整数リテラル
- 文字列リテラル
- 16進数リテラル
- 列挙型
詳細については、以下のドキュメントをご覧ください。
メモリ
コントラクト関数の実行期間にのみ保存される値は、メモリ変数と呼ばれます。 これらはブロックチェーンに永続的に保存されることはないため、低コストで使用できます
EVMがデータ(ストレージ、メモリ、スタック)を格納する方法の詳細については、Solidityのドキュメント(opens in a new tab)をご覧ください。
環境変数
コントラクトで定義した変数に加え、特別なグローバル変数がいくつかあります。 これらは主にブロックチェーンや現在のトランザクションに関する情報を提供するために使用されます。
例:
プロパティ | 状態変数 | 説明 |
---|---|---|
block.timestamp | uint256 | 現在のブロックエポックタイムスタンプ |
msg.sender | address | メッセージの送信者(現在の呼び出し) |
関数
簡単に言うと、関数は受信トランザクションに応じて情報を取得したり、情報を設定したりすることができます。
関数呼び出しには、以下の2種類があります。
internal
- これらはEVM呼び出しを作成しません。- internal関数と状態変数は、内部(つまり、現在のコントラクト内またはそれから派生したコントラクト内)からのみアクセスできます。
external
- これらはEVM呼び出しを作成します。- external関数はコントラクトインターフェイスの一部であり、他のコントラクトから呼び出したり、トランザクションを介して呼び出したりすることができます。 external関数
f
を内部で呼び出すことはできません(つまり、f()
は動作しませんが、this.f()
は動作します)。
- external関数はコントラクトインターフェイスの一部であり、他のコントラクトから呼び出したり、トランザクションを介して呼び出したりすることができます。 external関数
public
またはprivate
にすることもできます。
public
関数は、コントラクト内から内部で呼び出すことも、メッセージを介して外部から呼び出すこともできます。private
関数は、それらが定義されているコントラクトからのみ参照できます。派生したコントラクトからは参照できません。
関数と状態変数はどちらもpublicまたはprivateにすることができます。
コントラクトの状態変数を更新するための関数は次のとおりです。
1// Solidity example2function update_name(string value) public {3 dapp_name = value;4}コピー
string
型のパラメータvalue
がupdate_name
関数に渡されます。public
と宣言されており、誰でもアクセスできます。view
が宣言されていないため、コントラクトの状態を変更できます。
View関数
これらの関数によって、コントラクトのデータの状態を変更しないことを指定します。 一般的な例としては、「getter」関数があります。例えば、これを使用してユーザーの残高を受け取ることができます。
1// Solidity example2function balanceOf(address _owner) public view returns (uint256 _balance) {3 return ownerPizzaCount[_owner];4}コピー
1dappName: public(string)23@view4@public5def readName() -> string:6 return dappNameコピー
状態の変更と見なされるものは、以下のとおりです。
- 状態変数への書き込み。
- イベントの発行(opens in a new tab)。
- 他のコントラクトの作成(opens in a new tab)。
selfdestruct
の使用。- 呼び出しによるイーサ(ETH)の送信。
view
やpure
が指定されていない関数の呼び出し。- 低レベル呼び出しの使用。
- 特定のオペコードを含むインラインアセンブリの使用。
コンストラクタ関数
constructor
関数は、コントラクトが最初にデプロイされたときに1回だけ実行されます。 多くのクラスベースのプログラミング言語のconstructor
と同様に、これらの関数はしばしば、指定された値に状態変数を初期化します。
1// Solidity example2// Initializes the contract's data, setting the `owner`3// to the address of the contract creator.4constructor() public {5 // All smart contracts rely on external transactions to trigger its functions.6 // `msg` is a global variable that includes relevant data on the given transaction,7 // such as the address of the sender and the ETH value included in the transaction.8 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/units-and-global-variables.html#block-and-transaction-properties9 owner = msg.sender;10}すべて表示コピー
1# Vyper example23@external4def __init__(_beneficiary: address, _bidding_time: uint256):5 self.beneficiary = _beneficiary6 self.auctionStart = block.timestamp7 self.auctionEnd = self.auctionStart + _bidding_timeコピー
組み込み関数
コントラクトで定義した変数と関数に加え、特別な組み込み関数がいくつかあります。 最もわかりやすい例は、以下のとおりです。
address.send()
– Soliditysend(address)
– Vyper
これらの関数により、コントラクトは他のアカウントにETHを送信することができます。
関数を書く
関数には以下のものが必要です。
- パラメータ変数と型(パラメータを受け取る場合)
- internal/externalの宣言
- pure/view/payableの宣言
- 戻り値の型(値を返す場合)
1pragma solidity >=0.4.0 <=0.6.0;23contract ExampleDapp {4 string dapp_name; // state variable56 // Called when the contract is deployed and initializes the value7 constructor() public {8 dapp_name = "My Example dapp";9 }1011 // Get Function12 function read_name() public view returns(string) {13 return dapp_name;14 }1516 // Set Function17 function update_name(string value) public {18 dapp_name = value;19 }20}すべて表示コピー
完全なコントラクトはこのようになります。 ここで、constructor
関数は、dapp_name
変数の初期値を提供します。
イベントとログ
イベント(event)を使用すると、フロントエンドやその他のサブスクライブアプリケーションからスマートコントラクトと通信できます。 トランザクションがマイニングされると、スマートコントラクトはイベントを発行し、フロントエンドが処理できるログをブロックチェーンに書き込みます。
注釈付きの例
Solidityで書かれた例を以下に示します。 コードを実行したい場合は、Remix(opens in a new tab)で操作できます。
Hello World
1// Specifies the version of Solidity, using semantic versioning.2// Learn more: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma3pragma solidity ^0.5.10;45// Defines a contract named `HelloWorld`.6// A contract is a collection of functions and data (its state).7// Once deployed, a contract resides at a specific address on the Ethereum blockchain.8// Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html9contract HelloWorld {1011 // Declares a state variable `message` of type `string`.12 // State variables are variables whose values are permanently stored in contract storage.13 // The keyword `public` makes variables accessible from outside a contract14 // and creates a function that other contracts or clients can call to access the value.15 string public message;1617 // Similar to many class-based object-oriented languages, a constructor is18 // a special function that is only executed upon contract creation.19 // Constructors are used to initialize the contract's data.20 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors21 constructor(string memory initMessage) public {22 // Accepts a string argument `initMessage` and sets the value23 // into the contract's `message` storage variable).24 message = initMessage;25 }2627 // A public function that accepts a string argument28 // and updates the `message` storage variable.29 function update(string memory newMessage) public {30 message = newMessage;31 }32}すべて表示コピー
トークン
1pragma solidity ^0.5.10;23contract Token {4 // An `address` is comparable to an email address - it's used to identify an account on Ethereum.5 // Addresses can represent a smart contract or an external (user) accounts.6 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/types.html#address7 address public owner;89 // A `mapping` is essentially a hash table data structure.10 // This `mapping` assigns an unsigned integer (the token balance) to an address (the token holder).11 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/types.html#mapping-types12 mapping (address => uint) public balances;1314 // Events allow for logging of activity on the blockchain.15 // Ethereum clients can listen for events in order to react to contract state changes.16 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#events17 event Transfer(address from, address to, uint amount);1819 // Initializes the contract's data, setting the `owner`20 // to the address of the contract creator.21 constructor() public {22 // All smart contracts rely on external transactions to trigger its functions.23 // `msg` is a global variable that includes relevant data on the given transaction,24 // such as the address of the sender and the ETH value included in the transaction.25 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/units-and-global-variables.html#block-and-transaction-properties26 owner = msg.sender;27 }2829 // Creates an amount of new tokens and sends them to an address.30 function mint(address receiver, uint amount) public {31 // `require` is a control structure used to enforce certain conditions.32 // If a `require` statement evaluates to `false`, an exception is triggered,33 // which reverts all changes made to the state during the current call.34 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions3536 // Only the contract owner can call this function37 require(msg.sender == owner, "You are not the owner.");3839 // Enforces a maximum amount of tokens40 require(amount < 1e60, "Maximum issuance exceeded");4142 // Increases the balance of `receiver` by `amount`43 balances[receiver] += amount;44 }4546 // Sends an amount of existing tokens from any caller to an address.47 function transfer(address receiver, uint amount) public {48 // The sender must have enough tokens to send49 require(amount <= balances[msg.sender], "Insufficient balance.");5051 // Adjusts token balances of the two addresses52 balances[msg.sender] -= amount;53 balances[receiver] += amount;5455 // Emits the event defined earlier56 emit Transfer(msg.sender, receiver, amount);57 }58}すべて表示コピー
固有のデジタル資産
1pragma solidity ^0.5.10;23// Imports symbols from other files into the current contract.4// In this case, a series of helper contracts from OpenZeppelin.5// Learn more: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#importing-other-source-files67import "../node_modules/@openzeppelin/contracts/token/ERC721/IERC721.sol";8import "../node_modules/@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";9import "../node_modules/@openzeppelin/contracts/introspection/ERC165.sol";10import "../node_modules/@openzeppelin/contracts/math/SafeMath.sol";1112// The `is` keyword is used to inherit functions and keywords from external contracts.13// In this case, `CryptoPizza` inherits from the `IERC721` and `ERC165` contracts.14// Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#inheritance15contract CryptoPizza is IERC721, ERC165 {16 // Uses OpenZeppelin's SafeMath library to perform arithmetic operations safely.17 // Learn more: https://docs.openzeppelin.com/contracts/2.x/api/math#SafeMath18 using SafeMath for uint256;1920 // Constant state variables in Solidity are similar to other languages21 // but you must assign from an expression which is constant at compile time.22 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constant-state-variables23 uint256 constant dnaDigits = 10;24 uint256 constant dnaModulus = 10 ** dnaDigits;25 bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;2627 // Struct types let you define your own type28 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/types.html#structs29 struct Pizza {30 string name;31 uint256 dna;32 }3334 // Creates an empty array of Pizza structs35 Pizza[] public pizzas;3637 // Mapping from pizza ID to its owner's address38 mapping(uint256 => address) public pizzaToOwner;3940 // Mapping from owner's address to number of owned token41 mapping(address => uint256) public ownerPizzaCount;4243 // Mapping from token ID to approved address44 mapping(uint256 => address) pizzaApprovals;4546 // You can nest mappings, this example maps owner to operator approvals47 mapping(address => mapping(address => bool)) private operatorApprovals;4849 // Internal function to create a random Pizza from string (name) and DNA50 function _createPizza(string memory _name, uint256 _dna)51 // The `internal` keyword means this function is only visible52 // within this contract and contracts that derive this contract53 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#visibility-and-getters54 internal55 // `isUnique` is a function modifier that checks if the pizza already exists56 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html#function-modifiers57 isUnique(_name, _dna)58 {59 // Adds Pizza to array of Pizzas and get id60 uint256 id = SafeMath.sub(pizzas.push(Pizza(_name, _dna)), 1);6162 // Checks that Pizza owner is the same as current user63 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions6465 // note that address(0) is the zero address,66 // indicating that pizza[id] is not yet allocated to a particular user.6768 assert(pizzaToOwner[id] == address(0));6970 // Maps the Pizza to the owner71 pizzaToOwner[id] = msg.sender;72 ownerPizzaCount[msg.sender] = SafeMath.add(73 ownerPizzaCount[msg.sender],74 175 );76 }7778 // Creates a random Pizza from string (name)79 function createRandomPizza(string memory _name) public {80 uint256 randDna = generateRandomDna(_name, msg.sender);81 _createPizza(_name, randDna);82 }8384 // Generates random DNA from string (name) and address of the owner (creator)85 function generateRandomDna(string memory _str, address _owner)86 public87 // Functions marked as `pure` promise not to read from or modify the state88 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#pure-functions89 pure90 returns (uint256)91 {92 // Generates random uint from string (name) + address (owner)93 uint256 rand = uint256(keccak256(abi.encodePacked(_str))) +94 uint256(_owner);95 rand = rand % dnaModulus;96 return rand;97 }9899 // Returns array of Pizzas found by owner100 function getPizzasByOwner(address _owner)101 public102 // Functions marked as `view` promise not to modify state103 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#view-functions104 view105 returns (uint256[] memory)106 {107 // Uses the `memory` storage location to store values only for the108 // lifecycle of this function call.109 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/introduction-to-smart-contracts.html#storage-memory-and-the-stack110 uint256[] memory result = new uint256[](ownerPizzaCount[_owner]);111 uint256 counter = 0;112 for (uint256 i = 0; i < pizzas.length; i++) {113 if (pizzaToOwner[i] == _owner) {114 result[counter] = i;115 counter++;116 }117 }118 return result;119 }120121 // Transfers Pizza and ownership to other address122 function transferFrom(address _from, address _to, uint256 _pizzaId) public {123 require(_from != address(0) && _to != address(0), "Invalid address.");124 require(_exists(_pizzaId), "Pizza does not exist.");125 require(_from != _to, "Cannot transfer to the same address.");126 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");127128 ownerPizzaCount[_to] = SafeMath.add(ownerPizzaCount[_to], 1);129 ownerPizzaCount[_from] = SafeMath.sub(ownerPizzaCount[_from], 1);130 pizzaToOwner[_pizzaId] = _to;131132 // Emits event defined in the imported IERC721 contract133 emit Transfer(_from, _to, _pizzaId);134 _clearApproval(_to, _pizzaId);135 }136137 /**138 * Safely transfers the ownership of a given token ID to another address139 * If the target address is a contract, it must implement `onERC721Received`,140 * which is called upon a safe transfer, and return the magic value141 * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`;142 * otherwise, the transfer is reverted.143 */144 function safeTransferFrom(address from, address to, uint256 pizzaId)145 public146 {147 // solium-disable-next-line arg-overflow148 this.safeTransferFrom(from, to, pizzaId, "");149 }150151 /**152 * Safely transfers the ownership of a given token ID to another address153 * If the target address is a contract, it must implement `onERC721Received`,154 * which is called upon a safe transfer, and return the magic value155 * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`;156 * otherwise, the transfer is reverted.157 */158 function safeTransferFrom(159 address from,160 address to,161 uint256 pizzaId,162 bytes memory _data163 ) public {164 this.transferFrom(from, to, pizzaId);165 require(_checkOnERC721Received(from, to, pizzaId, _data), "Must implement onERC721Received.");166 }167168 /**169 * Internal function to invoke `onERC721Received` on a target address170 * The call is not executed if the target address is not a contract171 */172 function _checkOnERC721Received(173 address from,174 address to,175 uint256 pizzaId,176 bytes memory _data177 ) internal returns (bool) {178 if (!isContract(to)) {179 return true;180 }181182 bytes4 retval = IERC721Receiver(to).onERC721Received(183 msg.sender,184 from,185 pizzaId,186 _data187 );188 return (retval == _ERC721_RECEIVED);189 }190191 // Burns a Pizza - destroys Token completely192 // The `external` function modifier means this function is193 // part of the contract interface and other contracts can call it194 function burn(uint256 _pizzaId) external {195 require(msg.sender != address(0), "Invalid address.");196 require(_exists(_pizzaId), "Pizza does not exist.");197 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");198199 ownerPizzaCount[msg.sender] = SafeMath.sub(200 ownerPizzaCount[msg.sender],201 1202 );203 pizzaToOwner[_pizzaId] = address(0);204 }205206 // Returns count of Pizzas by address207 function balanceOf(address _owner) public view returns (uint256 _balance) {208 return ownerPizzaCount[_owner];209 }210211 // Returns owner of the Pizza found by id212 function ownerOf(uint256 _pizzaId) public view returns (address _owner) {213 address owner = pizzaToOwner[_pizzaId];214 require(owner != address(0), "Invalid Pizza ID.");215 return owner;216 }217218 // Approves other address to transfer ownership of Pizza219 function approve(address _to, uint256 _pizzaId) public {220 require(msg.sender == pizzaToOwner[_pizzaId], "Must be the Pizza owner.");221 pizzaApprovals[_pizzaId] = _to;222 emit Approval(msg.sender, _to, _pizzaId);223 }224225 // Returns approved address for specific Pizza226 function getApproved(uint256 _pizzaId)227 public228 view229 returns (address operator)230 {231 require(_exists(_pizzaId), "Pizza does not exist.");232 return pizzaApprovals[_pizzaId];233 }234235 /**236 * Private function to clear current approval of a given token ID237 * Reverts if the given address is not indeed the owner of the token238 */239 function _clearApproval(address owner, uint256 _pizzaId) private {240 require(pizzaToOwner[_pizzaId] == owner, "Must be pizza owner.");241 require(_exists(_pizzaId), "Pizza does not exist.");242 if (pizzaApprovals[_pizzaId] != address(0)) {243 pizzaApprovals[_pizzaId] = address(0);244 }245 }246247 /*248 * Sets or unsets the approval of a given operator249 * An operator is allowed to transfer all tokens of the sender on their behalf250 */251 function setApprovalForAll(address to, bool approved) public {252 require(to != msg.sender, "Cannot approve own address");253 operatorApprovals[msg.sender][to] = approved;254 emit ApprovalForAll(msg.sender, to, approved);255 }256257 // Tells whether an operator is approved by a given owner258 function isApprovedForAll(address owner, address operator)259 public260 view261 returns (bool)262 {263 return operatorApprovals[owner][operator];264 }265266 // Takes ownership of Pizza - only for approved users267 function takeOwnership(uint256 _pizzaId) public {268 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");269 address owner = this.ownerOf(_pizzaId);270 this.transferFrom(owner, msg.sender, _pizzaId);271 }272273 // Checks if Pizza exists274 function _exists(uint256 pizzaId) internal view returns (bool) {275 address owner = pizzaToOwner[pizzaId];276 return owner != address(0);277 }278279 // Checks if address is owner or is approved to transfer Pizza280 function _isApprovedOrOwner(address spender, uint256 pizzaId)281 internal282 view283 returns (bool)284 {285 address owner = pizzaToOwner[pizzaId];286 // Disable solium check because of287 // https://github.com/duaraghav8/Solium/issues/175288 // solium-disable-next-line operator-whitespace289 return (spender == owner ||290 this.getApproved(pizzaId) == spender ||291 this.isApprovedForAll(owner, spender));292 }293294 // Check if Pizza is unique and doesn't exist yet295 modifier isUnique(string memory _name, uint256 _dna) {296 bool result = true;297 for (uint256 i = 0; i < pizzas.length; i++) {298 if (299 keccak256(abi.encodePacked(pizzas[i].name)) ==300 keccak256(abi.encodePacked(_name)) &&301 pizzas[i].dna == _dna302 ) {303 result = false;304 }305 }306 require(result, "Pizza with such name already exists.");307 _;308 }309310 // Returns whether the target address is a contract311 function isContract(address account) internal view returns (bool) {312 uint256 size;313 // Currently there is no better way to check if there is a contract in an address314 // than to check the size of the code at that address.315 // See https://ethereum.stackexchange.com/a/14016/36603316 // for more details about how this works.317 // TODO Check this again before the Serenity release, because all addresses will be318 // contracts then.319 // solium-disable-next-line security/no-inline-assembly320 assembly {321 size := extcodesize(account)322 }323 return size > 0;324 }325}すべて表示コピー
参考文献
スマートコントラクトの全体的な概要については、SolidityとVyperのドキュメントをご確認ください。
関連トピック
関連チュートリアル
- コントラクトのサイズ制限に対処するためのコントラクトのサイズ縮小 - スマートコントラクトのサイズを小さくするための実用的なヒント
- イベントを使用してスマートコントラクトからデータをログに記録 - スマートコントラクトのイベントの紹介と、それを使ってデータをログに記録する方法
- Solidityを使用した他のコントラクトとの連携 - 既存のコントラクトからスマートコントラクトをデプロイし、それを扱う方法