メインコンテンツへスキップ
Change page

スマートコントラクトの解剖学

最終編集者: @sekisanchi(opens in a new tab), 2024年6月14日

スマートコントラクトは、イーサリアム上のアドレスで実行されるプログラムです。 それらはトランザクションの受信時に実行できるデータと関数で構成されています。 ここでは、スマートコントラクトの構成要素の概要を説明します。

前提知識

最初に、スマートコントラクトを必ずお読みください。 このドキュメントは、JavaScriptやPythonなどのプログラミング言語に精通していることを前提としています。

データ

すべてのコントラクトのデータは、storageまたはmemoryのいずれかのロケーションに割り当てる必要があります。 スマートコントラクトのストレージの変更にはコストがかかりますので、データをどこに格納するかを考える必要があります。

ストレージ

永続データはストレージと呼ばれ、状態変数で表されます。 これらの値は、ブロックチェーンに永続的に保存されます。 コントラクトがコンパイル時に必要なブロックチェーンのストレージ容量を追跡できるように、型を宣言する必要があります。

1// Solidity example
2contract SimpleStorage {
3 uint storedData; // State variable
4 // ...
5}
コピー
1# Vyper example
2storedData: int128
コピー

オブジェクト指向言語でのプログラミングの経験がある場合は、ほとんどの型になじみがあるでしょう。 しかし、イーサリアムの開発が初めての場合、addressは目新しいかもしれません。

address型は、20バイトまたは160ビットに相当するイーサリアムアドレスを保持します。 先頭が0xの16進数を返します。

その他の型には次のものがあります。

  • ブール値
  • 整数
  • 固定小数点数
  • 固定サイズのバイト配列
  • 動的サイズのバイト配列
  • 有理数リテラルと整数リテラル
  • 文字列リテラル
  • 16進数リテラル
  • 列挙型

詳細については、以下のドキュメントをご覧ください。

メモリ

コントラクト関数の実行期間にのみ保存される値は、メモリ変数と呼ばれます。 これらはブロックチェーンに永続的に保存されることはないため、低コストで使用できます

EVMがデータ(ストレージ、メモリ、スタック)を格納する方法の詳細については、Solidityのドキュメント(opens in a new tab)をご覧ください。

環境変数

コントラクトで定義した変数に加え、特別なグローバル変数がいくつかあります。 これらは主にブロックチェーンや現在のトランザクションに関する情報を提供するために使用されます。

例:

プロパティ状態変数説明
block.timestampuint256現在のブロックエポックタイムスタンプ
msg.senderaddressメッセージの送信者(現在の呼び出し)

関数

簡単に言うと、関数は受信トランザクションに応じて情報を取得したり、情報を設定したりすることができます。

関数呼び出しには、以下の2種類があります。

  • internal - これらはEVM呼び出しを作成しません。
    • internal関数と状態変数は、内部(つまり、現在のコントラクト内またはそれから派生したコントラクト内)からのみアクセスできます。
  • external - これらはEVM呼び出しを作成します。
    • external関数はコントラクトインターフェイスの一部であり、他のコントラクトから呼び出したり、トランザクションを介して呼び出したりすることができます。 external関数fを内部で呼び出すことはできません(つまり、f()は動作しませんが、this.f()は動作します)。

publicまたはprivateにすることもできます。

  • public関数は、コントラクト内から内部で呼び出すことも、メッセージを介して外部から呼び出すこともできます。
  • private関数は、それらが定義されているコントラクトからのみ参照できます。派生したコントラクトからは参照できません。

関数と状態変数はどちらもpublicまたはprivateにすることができます。

コントラクトの状態変数を更新するための関数は次のとおりです。

1// Solidity example
2function update_name(string value) public {
3 dapp_name = value;
4}
コピー
  • string型のパラメータvalueupdate_name関数に渡されます。
  • publicと宣言されており、誰でもアクセスできます。
  • viewが宣言されていないため、コントラクトの状態を変更できます。

View関数

これらの関数によって、コントラクトのデータの状態を変更しないことを指定します。 一般的な例としては、「getter」関数があります。例えば、これを使用してユーザーの残高を受け取ることができます。

1// Solidity example
2function balanceOf(address _owner) public view returns (uint256 _balance) {
3 return ownerPizzaCount[_owner];
4}
コピー
1dappName: public(string)
2
3@view
4@public
5def readName() -> string:
6 return dappName
コピー

状態の変更と見なされるものは、以下のとおりです。

  1. 状態変数への書き込み。
  2. イベントの発行(opens in a new tab)
  3. 他のコントラクトの作成(opens in a new tab)
  4. selfdestructの使用。
  5. 呼び出しによるイーサ(ETH)の送信。
  6. viewpureが指定されていない関数の呼び出し。
  7. 低レベル呼び出しの使用。
  8. 特定のオペコードを含むインラインアセンブリの使用。

コンストラクタ関数

constructor関数は、コントラクトが最初にデプロイされたときに1回だけ実行されます。 多くのクラスベースのプログラミング言語のconstructorと同様に、これらの関数はしばしば、指定された値に状態変数を初期化します。

1// Solidity example
2// 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-properties
9 owner = msg.sender;
10}
すべて表示
コピー
1# Vyper example
2
3@external
4def __init__(_beneficiary: address, _bidding_time: uint256):
5 self.beneficiary = _beneficiary
6 self.auctionStart = block.timestamp
7 self.auctionEnd = self.auctionStart + _bidding_time
コピー

組み込み関数

コントラクトで定義した変数と関数に加え、特別な組み込み関数がいくつかあります。 最もわかりやすい例は、以下のとおりです。

  • address.send() – Solidity
  • send(address) – Vyper

これらの関数により、コントラクトは他のアカウントにETHを送信することができます。

関数を書く

関数には以下のものが必要です。

  • パラメータ変数と型(パラメータを受け取る場合)
  • internal/externalの宣言
  • pure/view/payableの宣言
  • 戻り値の型(値を返す場合)
1pragma solidity >=0.4.0 <=0.6.0;
2
3contract ExampleDapp {
4 string dapp_name; // state variable
5
6 // Called when the contract is deployed and initializes the value
7 constructor() public {
8 dapp_name = "My Example dapp";
9 }
10
11 // Get Function
12 function read_name() public view returns(string) {
13 return dapp_name;
14 }
15
16 // Set Function
17 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#pragma
3pragma solidity ^0.5.10;
4
5// 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.html
9contract HelloWorld {
10
11 // 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 contract
14 // and creates a function that other contracts or clients can call to access the value.
15 string public message;
16
17 // Similar to many class-based object-oriented languages, a constructor is
18 // 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#constructors
21 constructor(string memory initMessage) public {
22 // Accepts a string argument `initMessage` and sets the value
23 // into the contract's `message` storage variable).
24 message = initMessage;
25 }
26
27 // A public function that accepts a string argument
28 // and updates the `message` storage variable.
29 function update(string memory newMessage) public {
30 message = newMessage;
31 }
32}
すべて表示
コピー

トークン

1pragma solidity ^0.5.10;
2
3contract 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#address
7 address public owner;
8
9 // 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-types
12 mapping (address => uint) public balances;
13
14 // 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#events
17 event Transfer(address from, address to, uint amount);
18
19 // 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-properties
26 owner = msg.sender;
27 }
28
29 // 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-exceptions
35
36 // Only the contract owner can call this function
37 require(msg.sender == owner, "You are not the owner.");
38
39 // Enforces a maximum amount of tokens
40 require(amount < 1e60, "Maximum issuance exceeded");
41
42 // Increases the balance of `receiver` by `amount`
43 balances[receiver] += amount;
44 }
45
46 // 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 send
49 require(amount <= balances[msg.sender], "Insufficient balance.");
50
51 // Adjusts token balances of the two addresses
52 balances[msg.sender] -= amount;
53 balances[receiver] += amount;
54
55 // Emits the event defined earlier
56 emit Transfer(msg.sender, receiver, amount);
57 }
58}
すべて表示
コピー

固有のデジタル資産

1pragma solidity ^0.5.10;
2
3// 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-files
6
7import "../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";
11
12// 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#inheritance
15contract 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#SafeMath
18 using SafeMath for uint256;
19
20 // Constant state variables in Solidity are similar to other languages
21 // 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-variables
23 uint256 constant dnaDigits = 10;
24 uint256 constant dnaModulus = 10 ** dnaDigits;
25 bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;
26
27 // Struct types let you define your own type
28 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/types.html#structs
29 struct Pizza {
30 string name;
31 uint256 dna;
32 }
33
34 // Creates an empty array of Pizza structs
35 Pizza[] public pizzas;
36
37 // Mapping from pizza ID to its owner's address
38 mapping(uint256 => address) public pizzaToOwner;
39
40 // Mapping from owner's address to number of owned token
41 mapping(address => uint256) public ownerPizzaCount;
42
43 // Mapping from token ID to approved address
44 mapping(uint256 => address) pizzaApprovals;
45
46 // You can nest mappings, this example maps owner to operator approvals
47 mapping(address => mapping(address => bool)) private operatorApprovals;
48
49 // Internal function to create a random Pizza from string (name) and DNA
50 function _createPizza(string memory _name, uint256 _dna)
51 // The `internal` keyword means this function is only visible
52 // within this contract and contracts that derive this contract
53 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#visibility-and-getters
54 internal
55 // `isUnique` is a function modifier that checks if the pizza already exists
56 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html#function-modifiers
57 isUnique(_name, _dna)
58 {
59 // Adds Pizza to array of Pizzas and get id
60 uint256 id = SafeMath.sub(pizzas.push(Pizza(_name, _dna)), 1);
61
62 // Checks that Pizza owner is the same as current user
63 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions
64
65 // note that address(0) is the zero address,
66 // indicating that pizza[id] is not yet allocated to a particular user.
67
68 assert(pizzaToOwner[id] == address(0));
69
70 // Maps the Pizza to the owner
71 pizzaToOwner[id] = msg.sender;
72 ownerPizzaCount[msg.sender] = SafeMath.add(
73 ownerPizzaCount[msg.sender],
74 1
75 );
76 }
77
78 // 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 }
83
84 // Generates random DNA from string (name) and address of the owner (creator)
85 function generateRandomDna(string memory _str, address _owner)
86 public
87 // Functions marked as `pure` promise not to read from or modify the state
88 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#pure-functions
89 pure
90 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 }
98
99 // Returns array of Pizzas found by owner
100 function getPizzasByOwner(address _owner)
101 public
102 // Functions marked as `view` promise not to modify state
103 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#view-functions
104 view
105 returns (uint256[] memory)
106 {
107 // Uses the `memory` storage location to store values only for the
108 // 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-stack
110 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 }
120
121 // Transfers Pizza and ownership to other address
122 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.");
127
128 ownerPizzaCount[_to] = SafeMath.add(ownerPizzaCount[_to], 1);
129 ownerPizzaCount[_from] = SafeMath.sub(ownerPizzaCount[_from], 1);
130 pizzaToOwner[_pizzaId] = _to;
131
132 // Emits event defined in the imported IERC721 contract
133 emit Transfer(_from, _to, _pizzaId);
134 _clearApproval(_to, _pizzaId);
135 }
136
137 /**
138 * Safely transfers the ownership of a given token ID to another address
139 * If the target address is a contract, it must implement `onERC721Received`,
140 * which is called upon a safe transfer, and return the magic value
141 * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`;
142 * otherwise, the transfer is reverted.
143 */
144 function safeTransferFrom(address from, address to, uint256 pizzaId)
145 public
146 {
147 // solium-disable-next-line arg-overflow
148 this.safeTransferFrom(from, to, pizzaId, "");
149 }
150
151 /**
152 * Safely transfers the ownership of a given token ID to another address
153 * If the target address is a contract, it must implement `onERC721Received`,
154 * which is called upon a safe transfer, and return the magic value
155 * `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 _data
163 ) public {
164 this.transferFrom(from, to, pizzaId);
165 require(_checkOnERC721Received(from, to, pizzaId, _data), "Must implement onERC721Received.");
166 }
167
168 /**
169 * Internal function to invoke `onERC721Received` on a target address
170 * The call is not executed if the target address is not a contract
171 */
172 function _checkOnERC721Received(
173 address from,
174 address to,
175 uint256 pizzaId,
176 bytes memory _data
177 ) internal returns (bool) {
178 if (!isContract(to)) {
179 return true;
180 }
181
182 bytes4 retval = IERC721Receiver(to).onERC721Received(
183 msg.sender,
184 from,
185 pizzaId,
186 _data
187 );
188 return (retval == _ERC721_RECEIVED);
189 }
190
191 // Burns a Pizza - destroys Token completely
192 // The `external` function modifier means this function is
193 // part of the contract interface and other contracts can call it
194 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.");
198
199 ownerPizzaCount[msg.sender] = SafeMath.sub(
200 ownerPizzaCount[msg.sender],
201 1
202 );
203 pizzaToOwner[_pizzaId] = address(0);
204 }
205
206 // Returns count of Pizzas by address
207 function balanceOf(address _owner) public view returns (uint256 _balance) {
208 return ownerPizzaCount[_owner];
209 }
210
211 // Returns owner of the Pizza found by id
212 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 }
217
218 // Approves other address to transfer ownership of Pizza
219 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 }
224
225 // Returns approved address for specific Pizza
226 function getApproved(uint256 _pizzaId)
227 public
228 view
229 returns (address operator)
230 {
231 require(_exists(_pizzaId), "Pizza does not exist.");
232 return pizzaApprovals[_pizzaId];
233 }
234
235 /**
236 * Private function to clear current approval of a given token ID
237 * Reverts if the given address is not indeed the owner of the token
238 */
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 }
246
247 /*
248 * Sets or unsets the approval of a given operator
249 * An operator is allowed to transfer all tokens of the sender on their behalf
250 */
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 }
256
257 // Tells whether an operator is approved by a given owner
258 function isApprovedForAll(address owner, address operator)
259 public
260 view
261 returns (bool)
262 {
263 return operatorApprovals[owner][operator];
264 }
265
266 // Takes ownership of Pizza - only for approved users
267 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 }
272
273 // Checks if Pizza exists
274 function _exists(uint256 pizzaId) internal view returns (bool) {
275 address owner = pizzaToOwner[pizzaId];
276 return owner != address(0);
277 }
278
279 // Checks if address is owner or is approved to transfer Pizza
280 function _isApprovedOrOwner(address spender, uint256 pizzaId)
281 internal
282 view
283 returns (bool)
284 {
285 address owner = pizzaToOwner[pizzaId];
286 // Disable solium check because of
287 // https://github.com/duaraghav8/Solium/issues/175
288 // solium-disable-next-line operator-whitespace
289 return (spender == owner ||
290 this.getApproved(pizzaId) == spender ||
291 this.isApprovedForAll(owner, spender));
292 }
293
294 // Check if Pizza is unique and doesn't exist yet
295 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 == _dna
302 ) {
303 result = false;
304 }
305 }
306 require(result, "Pizza with such name already exists.");
307 _;
308 }
309
310 // Returns whether the target address is a contract
311 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 address
314 // than to check the size of the code at that address.
315 // See https://ethereum.stackexchange.com/a/14016/36603
316 // for more details about how this works.
317 // TODO Check this again before the Serenity release, because all addresses will be
318 // contracts then.
319 // solium-disable-next-line security/no-inline-assembly
320 assembly {
321 size := extcodesize(account)
322 }
323 return size > 0;
324 }
325}
すべて表示
コピー

参考文献

スマートコントラクトの全体的な概要については、SolidityとVyperのドキュメントをご確認ください。

  • スマートコントラクト
  • イーサリアム仮想マシン(EVM)
  • コントラクトのサイズ制限に対処するためのコントラクトのサイズ縮小 - スマートコントラクトのサイズを小さくするための実用的なヒント
  • イベントを使用してスマートコントラクトからデータをログに記録 - スマートコントラクトのイベントの紹介と、それを使ってデータをログに記録する方法
  • Solidityを使用した他のコントラクトとの連携 - 既存のコントラクトからスマートコントラクトをデプロイし、それを扱う方法

この記事は役に立ちましたか?