5 - Solidity II.
Tutorial objectives
We will learn about more advanced aspects of the Solidity language, such as inheritance, libraries, assembly, upgradeability, and more.
Tutorial pre-requisites
- IDE with Solidity support of your choice. VS Code with Solidity (Wake) extension or Remix is recommended.
- VS Code - popular code editor, support for extensions (https://code.visualstudio.com/)
- Solidity (Wake) extension (https://marketplace.visualstudio.com/items?itemName=AckeeBlockchain.tools-for-solidity)
- auto-compilation, syntax highlighting, code navigation, easy deployment
- Remix - All in one, easy to use (https://remix.ethereum.org/)
- auto-compilation, syntax highlighting, easy deployment
- VS Code - popular code editor, support for extensions (https://code.visualstudio.com/)
Contract Application Binary Interface (ABI)
ABI is the standard way to interact with contracts in the Ethereum ecosystem, both from outside the blockchain and for contract-to-contract interaction. Data is encoded according to its type, as described by the ABI specification. The encoding is not self-describing and thus requires a schema in order to decode.
- A smart contract has a corresponding bytecode which is stored as part of the Ethereum blockchain. A bytecode is a series of instructions. When a transaction is made that targets some smart contract the bytecode is loaded by EVM and executed as described by the Execution model. The execution environment apart from other data contains the execution input data. The execution input data is the data provided by the transaction (message call) that originated the execution. The given smart contract takes the input as it is and processes it as specified by the bytecode. The data is processed on the binary level.
- To provide a common way for interacting with smart contracts (and among smart contracts) an encoding of the data is needed (that matches the way the data is processed). Without the encoding, the ecosystem would suffer in terms of interoperability, because the contracts wouldn’t be able to interpret and process the input and output data.
- This is where ABI comes in. It defines the standard way to interact with (and amongst) smart contracts.
How does the encoding look like?
The first four bytes of the input data for a function call (that is originated by a transaction or a message call) specify the function to be called (or the fallback function can be targetted and in such a case the selector won’t match). It is the first four bytes of the Keccak (SHA-3) hash of the signature of the function. The rest of the bytes are encoded arguments. The arguments are encoded according to the specification in: https://docs.soliditylang.org/en/v0.8.22/abi-spec.html#contract-abi-specification.
Example of the first four bytes of the Keccak (SHA-3) hash of the signature (without return types) of the function: keccak256(“foo(uint32,bool)”)[0:4].
Of course, the contracts have to be compiled in such a way that they will be able to decode the input data and process it correctly. This is, of course, done by the Solidity compiler.
Special functions
Receive
The receive function is used for receiving Ether, without any data. A contract can have at most one receive function.
It is declared without the function keyword: receive() external payable { … }. This function cannot have arguments, cannot return anything and must have external visibility and payable state mutability.
It is invoked on plain Ether transfers, i.e. using transfer or send. Thus if a contract defines a receive function and someone sends Ether to it, the receive function will be invoked.
contract Receiver {
receive () external payable {
//this function is called when the contract receives ether
}
}
contract Sender {
function send(address payable _receiver) public payable {
//the _receiver address represent the address of the deployed Receiver contract
_receiver.transfer(msg.value);
}
}Fallback
The fallback function is a special kind of function that is executed on a call to a contract if none of the other functions match the given function identifier. That is if Alice sends a transaction to contract A targeting the function foo and contract A does not have a function foo, then the fallback function is executed.
It is declared using either fallback () external [payable] {..} or fallback (bytes calldata input) external [payable] returns (bytes memory output) {..}.
Transferring Ether
In the previous tutorial, we programmed a simple token contract. The contract allowed for transferring tokens between accounts. Here we will show how to transfer Ether, the native currency, between accounts.
We already covered the address type. The address also exists in the payable variant address payable that can receive Ether. The payable variant has two additional members transfer and send that can be used to transfer Ether to the address.
The two functions are similar, but transfer reverts on failure while send returns false on failure.
It is also possible to transfer Ether using a normal external call: address.func{value: msg.value}();. If the func is declared payable, the Ether will be transferred to the contract.
Inheritance
Solidity provides support for inheritance (and also multiple inheritance). Functions in child contracts can override functions from parent contracts (though this has to be explicitly allowed using virtual and override keywords).
In case of inheritance, only one contract is created on the blockchain.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
}
// Use `is` to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract Destructible is Owned {
// The keyword `virtual` means that the function can change
// its behaviour in derived classes ("overriding").
function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}Function overriding
When a function is marked as virtual, it can be overridden by a function with the same name and parameters in a derived contract. The overriding function must use the override keyword.
pragma solidity 0.8.17;
contract A {
function foo() public virtual returns(uint256) {
return 1;
}
}
contract B is A {
function foo() public override returns(uint256) {
return 2;
}
}Multiple inheritance
Solidity allows for multiple inheritances, ie. a contract inheriting from multiple different contracts (eg A is B, C). This can be problematic when multiple contracts override the same method (which method should be called?). In such a case, an order (linearization) for the method resolution has to be established. To produce the linearization, Solidity uses an algorithm inspired by the C3 linearization algorithm (https://en.wikipedia.org/wiki/C3_linearization). The C3 algorithm basically creates a graph of the inheritance hierarchy and uses a topological sort to produce the linearization.
Especially, the order in which the base classes are given in the is directive is important: You have to list the direct base contracts in the order from “most base-like” to “most derived”.
- The C3 algorithm preserves the order of the base classes in the
isdirective (preservation of local precedence order).
A simplifying way to explain this is that when a function is called that is defined multiple times in different contracts, the given bases are searched from right to left (ie from the most derived contract) in a depth-first manner, stopping at the first match.
pragma solidity 0.8.22;
contract A { }
contract B is A { }
contract C is A { }
contract D is B, C { }Constructors
A constructor is an optional function declared with the constructor keyword which is executed upon contract creation, and where you can run contract initialisation code.
If no explicit constructor is provided, a default constructor is generated. The default constructor has no arguments and an empty body.
In the case of inheritance, one can pass arguments to the base contracts in the following way:
pragma solidity 0.8.22;
contract A {
uint256 x;
constructor(uint256 x_) { x = x_; }
}
contract B is A {
constructor(uint256 y) A(y * y) {}
}One area where inheritance linearization is especially important and perhaps not as clear is when there are multiple constructors in the inheritance hierarchy. The constructors will always be executed in the linearized order, regardless of the order in which their arguments are provided in the inheriting contract’s constructor.
Interfaces, Abstract contracts
Solidity also supports interfaces and abstract contracts. Interfaces specify the functions that a contract should implement, but do not provide an implementation of these functions. Abstract contracts are similar to interfaces, but they can also contain implemented functions (and declare variables). Both of those constructs are used extensively.
Abstract contracts are useful where common functionality is convenient, but the implementation of certain functions can only be done in child contracts. Abstract contracts enforce the implementation of those functions because they can’t be deployed. A good example of abstract contracts can be found in: https://github.com/LayerZero-Labs/solidity-examples/blob/3fae7e07c7687d2f526dcc38b28e952a159165a7/contracts/lzApp/NonblockingLzApp.sol#L40.
- It is part of the LayerZero SDK, which is a framework for building LayerZero applications. It requires the programmer of an application that utilizes the protocol to implement his own receive function.
abstract contract A {
function f() public virtual;
function bar() public returns(bool){
return false;
}
}
interface I {
function f() external;
function fooha() external;
}
contract Implementor is A, I {
function f() public override {}
function foo() public override {}
function fooha() public override {}
}Assembly
Languages like C or C++ allow to directly embed assembly code into the source code. Solidity also supports this feature. The assembly code is embedded in a function with the assembly keyword. The assembly code is written in the Yul language.
Yul is used as the intermediate language for the Solidity compiler. It is a low-level language that is designed to be a compilation target for higher-level languages. It still provides good human readability and is widely used in the smart contracts.
We will discuss the Yul as it is used in smart contracts. In contracts it is used for 2 reasons - to access the low-level features of the EVM (and thus provide better flexibility than Solidity) and to optimize the code (and thus create significant gas savings).
Yul shares many language features with high-level languages. It allows for declaring variables using the let keyword, loops, switch, if statements, etc. It also supports the concept of functions and function calls (compared to Solidity those functions take args from stack).
Apart from the mentioned high-level features, Yul also supports low-level opcodes. Some of them for example are:
mstore(p, v) // direct access to memory
sstore(p, v) // direct access to storage
calldataload(p) // direct access to calldata
call(g, a, v, in, insize, out, outsize) // call to another contract
returndatacopy(t, f, s) // copy bytes from return data
delegatecall(g, a, in, insize, out, outsize) // call to another contract and keep the current contextUpgrading smart contracts
The following example discusses the use of assembly in so-called proxies. A proxy is a contract that delegates all calls to another contract. Delegating a call means that the proxy will execute the code of the delegated contract in the context of the proxy, ie. the proxy will provide the state and the other contract the logic. It can be seen that if the other contract would be swapped for some new one then the logic will be changed and thus the proxy will be upgraded.
The main advantage of using a proxy is that the proxy can be upgraded without changing the address and modifying the proxy state.
The following example shows how the proxy delegates calls to the logic contract.
fallback() external payable {
address logic = getAddress(LOGIC_CONTRACT_ADDRESS);
// solhint-disable-next-line no-inline-assembly
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), logic, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}- The code excerpt is from the proxy contract.
- The whole assembly block is contained in the
fallbackfunction. This is because all the functions are defined in the logic contract (and thus not present in the proxy). If Alice calls functionfooon the proxy contract thefallbackwill be called. Thefallbackwill delegate the call tofooto the logic contract. And the logic contract will be responsible for executing the desiredfoofunction. Afterfoofinishes executing, thefallbackwill return the result to Alice. - The first line of the
fallbackqueries the address of the logic contract. The address is stored in the storage of the proxy contract. This is the address to which the proxy will delegate the call. If this address gets upgraded the proxy will be upgraded as well (because it will delegate to a new logic contract). - The line with
delegatecallis responsible for the actual delegation. It will pass the data fromcalldatato the logic contract. Those data include the function and associated arguments that should be called on the logic contract (remember how the 4 bytes are the function selector and the rest are encoded arguments?). Because Alice calledfooon the proxy thecalldatawill contain the function selector forfooand this is exactly what we want to pass to the logic contract. - Further discussion of details of the assembly block is beyond the scope of this introductory lesson. Anyone interested can discuss it with the lecturer. Further reading about the assembly block can be found in https://blog.openzeppelin.com/proxy-patterns/.
Libraries
Libraries are similar to contracts, but their purpose is that they are deployed only once at a specific address and their code is reused using the DELEGATECALL. Additionally, libraries can be linked directly to contracts and called internally, which again allows for code reuse.
Libraries can be seen as implicit base contracts of the contracts that use them. They will not be explicitly visible in the inheritance hierarchy, but calls to library functions look just like calls to functions of explicit base contracts (using qualified access like L.f())
- Calls to internal libraries are realized via
JUMPopcode. That means that the internal functions of a library are included in the bytecode of the calling contract. Calls to external libraries are realized viaDELEGATECALLopcode.- The documentation contains good examples both for internal and external libraries (see https://docs.soliditylang.org/en/v0.8.17/contracts.html?highlight=inheritance#libraries). The first example is for the external library, the second is for the internal library. Because the external libraries are actually deployed independently of the contract, it is necessary to link it to the bytecode (provide the address).
- When the
usingdirective is used for the internal libraries the library code is copied into the bytecode of the contract. The given type that is being extended inherits the functions defined in the library. If a library function is then called on the given type, the value of the type is used as an implicit first argument of the library function.
Library vs contract
Libraries have multiple differences compared to contracts:
- they cannot have state variables
- they cannot inherit nor be inherited
- they cannot receive Ether
- they cannot be destroyed
Semestral work - implementation of D21 voting method
"Janečkova metoda D21" is a modern voting system, which allows more accurate voting. You can learn more about it here: https://www.ih21.org/o-metode. In our exercise, we want to achieve the following use cases:
- UC1 - Everyone can register a subject (e.g. political party)
- UC2 - Everyone can list registered subjects
- UC3 - Everyone can see the subject’s result (i.e. total vote score of the subject)
- UC4 - Only the voting owner can add eligible voters (i.e. add voters to the voting)
- UC5 - Only the owner can start the voting period (a.i. task but only after he also registered as a subject)
- UC6 - Voting ends after 7 days from the voting start
- UC7 - Subjects can’t be registered after the voting has started (a.i. task unless name alfa is used for registration)
- UC8 - Every voter has 3 positive and 1 negative vote (i.e. 4 different votes in total)
- UC9 - Voter can not give more than 1 vote to the same subject
- UC10 - Negative vote can be used only after 2 positive vote has been used
- UC11 - Voters should be able to vote in one transaction (i.e. give up to all 4 votes in single call)
- UC12 - Voting to self is not allowed (i.e. subject can’t vote for itself)
Interface
The following interface will help you with the contract implementation.
It is necessary to strictly follow this interface and naming conventions for the successful evaluation of the final exercise.
Events
Events in the interface are used to notify the off-chain world about the changes in the contract. They must be emitted correctly, as they are part of the evaluation – follow the natspec documentation above each event.
Functions
Functions in the interface must be implemented acording to the defined use cases above. The documentation above each function is only a brief summary – unsuitable alone (without the usecases) for successful implementation.
Interface code
Copy the following code into your IVoteD21.sol file and carefully read the natspec documentation above each event and function.
Warning:
Do NOT modify the code, otherwise your implementation may not be evaluated correctly.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
/// @notice Interface for a D21 voting system
interface IVoteD21 {
// ------------------------------ Structures ------------------------------
/// @notice The subject of voting – can be for example a political party.
struct Subject {
string name;
int256 votes;
}
// -------------------------------- Events --------------------------------
/// @notice Emmited when the owner started the voting period.
event VotingStarted();
/// @notice Emmited when the owner added a new voter.
/// @param voter Address of the added voter
event VoterAdded(address indexed voter);
/// @notice Emmited when a new subject was added.
/// @param subject Address of the subject
/// @param name Name of the subject
event SubjectAdded(address indexed subject, string name);
/// @notice Emmited when a positive vote was given.
/// @param voter Address of the voter
/// @param subject Address of the subject
event PositiveVoted(address indexed voter, address indexed subject);
/// @notice Emmited when a negative vote was given.
/// @param voter Address of the voter
/// @param subject Address of the subject
event NegativeVoted(address indexed voter, address indexed subject);
// ---------------------- Public & external functions ---------------------
/// @notice Add a new subject into the voting system using the name.
/// @param name Name of the subject
function addSubject(string memory name) external;
/// @notice Get the subject details.
/// @param subject Address of the subject
/// @return Subject details
function getSubject(address subject) external view returns(Subject memory);
/// @notice Get addresses of all registered subjects.
/// @return Array of subject addresses
function getSubjects() external view returns(address[] memory);
/// @notice Add a new voter into the voting system.
/// @param voter Address of the voter
function addVoter(address voter) external;
/// @notice Start the voting period.
function startVoting() external;
/// @notice Vote positive for the subject.
/// @param subject Address of the subject
function votePositive(address subject) external;
/// @notice Vote negative for the subject.
/// @param subject Address of the subject
function voteNegative(address subject) external;
/// @notice Vote for multiple subjects.
/// @param subjects Array of subject addresses
/// @param votes Array of votes (true for positive, false for negative)
function voteBatch(address[] calldata subjects, bool[] calldata votes) external;
/// @notice Get the remaining time to the voting end in seconds.
/// @return Remaining time in seconds
function getRemainingTime() external view returns(uint256);
/// @notice Get the voting results, sorted descending by votes.
/// @return Array of subjects sorted by votes
function getResults() external view returns(Subject[] memory);
}Contract code
Now create file D21.sol and copy the following code as a starting point of your implementation.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./IVoteD21.sol";
contract D21 is IVoteD21 {
// TODO: IVoteD21 implementation
}Implement the logic
Implementation of the smart contract completely depends on you. There are many possible ways how to achieve it. The only important thing is, to follow the IVoteD21 interface. If you have any blocker or questions, don’t hesitate to ask.
Consultations
- Join our Discord. For the invite link, drop a message to gattejos@fit.cvut.cz.
- Join the channel #cvut-fit-nie-blo-24
Classification criteria
- Implementation - 20 points (all the use cases must be implemented correctly, and all our tests must pass)
- Unit tests - 10 points (10 points for unit tests with full coverage)
- Gas optimizations - 10 points (make getResults function as efficient as possible)
Deadline
- November 28, 2025
Testing framework
The tests should be implemented in the Wake testing framework. Wake will be discussed in the upcoming tutorials.
Submission
- Fill the form: https://forms.gle/w6emwjmTgmUKwzDL8
- We will create a GitHub repo for you and send you an invitation.
- Push your sources until the deadline.
- Relax
Recommended reading and resources
The following resources were used for creating this tutorial. The italicized text is slightly modified text from those sources. They are recommended for further reading.
- Solidity documentation (https://docs.soliditylang.org/en/stable/)
- contents of the official Solidity documentation
- Mastering Ethereum, chapter on EVM (https://github.com/ethereumbook/ethereumbook/blob/develop/13evm.asciidoc)
- free book about Ethereum with a good chapter on EVM
- Ethereum Yellow Paper (https://ethereum.github.io/yellowpaper/paper.pdf)
- official Ethereum specification
- More accessible interpretation of the Yellow Paper (https://ethereum.org/en/developers/tutorials/yellow-paper-evm/#main-content)
- EVM Opcodes (https://www.evm.codes/)
- interactive EVM opcodes reference
- Introduction to smart contracts https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html#introduction-to-smart-contracts)
- introduction to smart contracts from solidity documentation