Solidity
Solidity tutorial - HSCS workshop. Learn how to enable custom logic & processing on Hedera through smart contracts.
Last updated
Solidity tutorial - HSCS workshop. Learn how to enable custom logic & processing on Hedera through smart contracts.
Last updated
The term "smart contracts" is kind of a poor choice for a name, and is the source of a lot of confusion: Smart contracts are neither smart, and neither are they contracts.
They are, at their core, simply computer programs that are executable. So what is the big deal about them then? What makes them different from "regular" computer programs? The biggest one, that you probably know already, is that they are executed on blockchains/ DLTs. But there are a few others to be aware of:
They are typically executed within a Virtual Machine (VM)
Any state changes need to be agreed upon through network consensus
Any state queries return values agreed upon by network consensus
While their state is mutable, their code is not
Combine the above with the decentralized nature of blockchains/ DLTs, and you get a special breed of computer programs like no other: Deterministic, p2p execution, that is censorship resistant and interruption resistant.
You can use this powerful technology within the Hedera network too, via the Hedera Smart Contract Service. This workshop will show you how!
To start, open and edit intro/trogdor.sol
.
In Solidity, comment syntax is similar to what you might be familiar with from Javascript. Single line comments use //
, and extend till the rest of the line. Multi-line comments use /*
to begin, and */
to end.
SPDX defines a list of software licenses, and allows you to reference one using a standard short identifier. The solidity compiler will explicitly check for this as a comment in the first line of any Solidity file. If it is missing, it will output a warning.
Near the top of the file, you should see the following comment:
Note that this type of comment will be present throughout the tutorial repo that accompanies this written tutorial. Each numbered step of a section heading here corresponds to the the same number in a comment there. In the subsequent steps of this tutorial, you will follow the same pattern as above. However, this tutorial does not repeat the comments marking the steps for the remainder of the tutorial and instead only include the new/changed lines of code.
The pragmas simply defines the which version of the Solidity compiler, solc
, it is intended to be compiled with.
Here we simply specify that this file should be compiled with version 0.8.17
only. You may specify more complex rules, similar to semver
used by npm.
A smart contract is a grouping of state variables, functions, and other things. The solidity code needs to group them, and does so by surrounding them with a pair of squiggly brackets ({
and }
). It also needs a name - and we'll name this one Trogdor
.
A smart contract persists its state on the virtual machine, and may only be modified during a successful transaction on the network. Solidity supports many different primitive types, here let's use uint256
as we'll be representing an unsigned integer value.
The above would work, but in this case, we know that we will not be modifying its value, so instead of a state variable, let's use a state constant instead. This is achieved by adding the constant
keyword to it.
Smart contracts can also persist more complex types of data in its state, and this is accomplished using dynamic state variables. A mapping
is used to represent key-value pairs, and is analogous to a Hashmap in other programming languages.
This mapping stores key-value pairs where the keys are of type address
, and the values are of type uint256
.
Note that both MIN_FEE
and amounts
have a visibility modifier of public
. In other cases you might want internal
or private
,
Functions are the main part of the smart contract where things actually happen: Code is executed, and perhaps state is accessed or updated. The syntax of a function is somewhat similar to Javascript, with the main differences being the addition of types, and of modifiers.
In the above example:
Name: doSomething
Modifiers: public
and pure
Parameter name: param1
Parameter type: uint256
Return type: uint256
The burninate
function modifies the state of the smart contract, and also accepts payment (in HBAR), so let's go with public
, payable
for its modifiers.
The totalBurnt
function does not modify the state of the smart contract, but does access the state. It does not accept any payment. It is intended to only be called by an Externally Owned Account (EOA) or another smart contract. For this let's go with external
, view
for its modifiers.
Just like state variables, functions may have the have visibility modifiers public
, private
, and internal
; however external
is a new one, and may apply only to functions.
The totalBurnt
function performs a query of the smart contract's state. Therefore it should reply with this information. This is done through the returns
keyword, which specifies the type of the returned value.
In this case, the function returns a single value of type uint256
. which specifies one or more return values. Note that Solidity allows functions to return multiple return values, for example returns(uint256, address)
would mean that it returns both a uint256
and an address
.
When a smart contract function is invoked, it has access to values that are passed in as parameters. It also has access to the state variables persisted by the smart contract.
Additionally, there are also several special values that are specific to the current block (group of transactions) or specific to the current transaction that are also accessible. Two of these are msg.sender
and msg.value
.
The require
function is used to check for specific conditions within a function invocation. If these conditions are not met, it throws an exception. This causes the function invocation to be reverted, meaning the state of the smart contract would remain as it was before, as if the function invocation was never made.
Within the burninate
function, we use a require
to ensure that the transaction is seemingly not from the null address, also known as the zero address. This essentially disallows any transactions sent from that particular address
Technically it should not be possible for a transaction to be sent by the zero address. This is done here purely for illustrative purposes.
We have another require
in this function to ensure that the amount paid (in HBAR) is at least above a certain threshold (the MIN_FEE
constant).
This function is payable
, meaning that any value (of HBAR) sent along with the function gets deducted from the balance of the sender's account, and gets added to the balance of the smart contract's account. In other words: The transaction sender pays into the smart contract via this function.
The numeric value of msg.value
is not denominated in HBAR, but rather tinybar, when a smart contract is deployed on a Hedera network. This is consistent with msg.value
being denominated not in Ether, but rather in wei, when a smart contract is deployed on an Ethereum network.
There is a key difference though:
1 HBAR = 10^8 tinybar (10 million)
1 Ether = 10^18 wei (1 billion billion)
In functions which are not payable
, msg.value
is guaranteed to be zero. Whereas in functions which are payable
, msg.value
could be zero or more. In this case, the intent is for the function to reject any function invocations which do not pay enough.
After the checks have been completed successfully, by the require
statements, we're ready to update the persisted state of this smart contract. In this case, we are keeping track, as a running tally, of the total amount paid by each different address that this function has been invoked with.
This statement increments the current value by the amount paid into the function, keyed on the address that invoked this function.
The EVM outputs logs, which essentially is information that is persisted on the network, but not accessible by smart contracts. Instead they are intended to be accessed by client applications (such as DApps), which typically search for specific events, or listen for specific events.
The canonical use case for events within a smart contract is to create a "history" of actions performed by that smart contract. In this case, let's commemorate each time the burninate
function is successfully invoked.
This event
is named Burnination
, and whenever it is produced, it is added to the EVM logs, and will contain an address
and a uint256
.
Once the event
has been defined, the smart contract should specify exactly when it should be added to the EVM logs. This is done using emit
.
Thus, based on where this emit
statement is located within the function, this event
is added to the logs upon each time the burninate
function is invoked, only if both of the require
statements are satisfied. When it gets added the transaction sender's address and the amount that they paid into the function are logged.
Note that this particular smart contract does not include any means to take out the HBAR balance that accrues within it over time each time burninate
is invoked. This effectively means that the HBAR sent into it is stuck there forever, and hence is effectively lost.
Trogdor would be proud ;)