OneSwap Series 5 - How to Organize the Code

This article will introduce how to organize the Solidity source code with OneSwap as an example. We will go deep into the various “Object-Oriented” features supported by the Solidity language and the usage of libraries, and introduce various function modifiers in detail.

Standard Directory Structure

The OneSwap project uses Truffle as a development and testing tool, so the overall directory structure also follows the Truffle convention: Solidity source code is in the contracts subdirectory, deployment scripts in the migrations subdirectory, external scripts in the scripts subdirectory, and unit tests in the test subdirectory. Among them, the contracts subdirectory has two subdirectories in it: all Interfaces are in the interfaces subdirectory, and all Libraries are in the libraries subdirectory. The following is the overall directory structure of OneSwap (only some directories and files are shown):

oneswap/
├── contracts/
│ ├── interfaces/
│ │ ├── IERC20.sol
│ │ ├── IOneSwapFactory.sol
│ │ ├── IOneSwapPair.sol
│ │ └── ...
│ ├── libraries/
│ │ ├── OneSwapPair.sol
│ │ └── ...
│ ├── OneSwapFactory.sol
│ ├── OneSwapPair.sol
│ └── ...
├── migrations/
├── scripts/
├── test/
│ ├── OneSwapFactory.js
│ ├── OneSwapPair.js
│ └── ...
└── truffle-config.js

“Object Oriented” Programming

Although Solidity is not strictly an Object Oriented Programming (OOP) language, it has drawn on the concepts and syntax of traditional OOP languages ​​in many aspects. This section will introduce some concepts of the traditional OOP language as well as the usage and implementation of these concepts in the Solidity language.

We all know that Class is the core abstract unit in traditional OOP languages. A series of state variables (usually called Fields) and Methods to manipulate these states can be defined inside the class. A class is just a template, and it must be instantiated as an Object before it is used, and the initial state of the object is determined by the Constructor. To reuse existing logic and avoid duplication of code, classes can be organized into a complex inheritance hierarchy, and subclasses can inherit the state and methods of the superclass. To improve the readability and maintainability of the code, the class usually hides (encapsulates) the state and only exposes the methods with good behavior.

The Solidity language borrows these OOP concepts well. For example, we use the Solidity language to define a Contract, which is roughly equivalent to a class in OOP. In the contract, we can define state variables and functions for operating state variables, and limit the visibility of state variables and functions. Like classes, contracts can also form an inheritance hierarchy. Also, in Solidity we can define Interfaces and Abstract Contracts. Besides the conceptual similarity, the Solidity language also directly uses many grammars of traditional OOP languages. As a result, programmers familiar with other OOP languages ​​(such as C++, Java, JS, etc.) can easily learn the Solidity language and use it to write contracts smoothly.

This article will not explain the OOP concepts in depth as most of them are well known. Now we will introduce the usage and implementation principle of these concepts in Solidity with the source code of the OneSwap project as examples.

Inheritance

Besides single inheritance, Solidity also supports interfaces, abstract contracts, and multiple inheritance. If contract D inherits contract B, then we call contract B the Base Contract and contract D the Derived Contract. The derived contract will inherit all the state variables and functions of the base contract, but only the non-private state and functions will be visible in it. We will explain the visibility modifiers of functions in detail when we introduce encapsulation. We all know that multiple inheritance comes with some problems, such as the notorious Diamond Problem. Similar to Python, Solidity also uses C3 linearization algorithm to determine the order of inheritance, so the specified order of the underlying contracts when declaring derived contracts is very important. In short, we should avoid multiple inheritance if possible. For more details, please refer to Solidity Documentation.

OneSwap does not use multiple inheritance, but use interfaces, abstract contracts, and single inheritance in many places. Take the OneSwapPair contract as an example. The contract mainly provides the limit order trading logic. It maintains two order books for buying and selling internally, and the state and logic related to automatic market making (AMM) are inherited from the OneSwapPool abstract contract which inherits the state and logic related to ERC20 tokens from the OneSwapERC20 abstract contract. These three contracts all implement the interfaces corresponding to their respective functions. The following is the definition code of the three contracts (the specific implementation is omitted):

interface IOneSwapERC20 { ... }
interface IOneSwapPool { ... }
interface IOneSwapPair { ... }
abstract contract OneSwapERC20 is IOneSwapERC20 { ... }
abstract contract OneSwapPool is OneSwapERC20, IOneSwapPool { ... }
contract OneSwapPair is OneSwapPool, IOneSwapPair { ... }

The following UML class diagram shows the inheritance hierarchy of the entire OneSwapPaircontract:

+---------------+
| IOneSwapERC20 |
+---------------+

|
+------------------+
|
+--------------+ +--------------+
| OneSwapERC20 | | IOneSwapPool |
+--------------+ +--------------+
△ △
| |
+------------------+
|
+--------------+ +--------------+
| OneSwapPool | | IOneSwapPair |
+--------------+ +--------------+
△ △
| |
+------------------+
|
+--------------+
| OneSwapPair |
+--------------+

Encapsulation

Like the traditional OOP language, the Solidity language also provides Visibility Modifiers to limit the visibility of state variables and functions. Because of the special features of smart contracts, Solidity also provides mutability modifiers to restrict the function’s reading and writing of state variables and whether it can receive Ether payments.

There are four visibility modifiers, which can be used both on state variables (except external) and functions. Here are some main points. First, the Solidity compiler will automatically generate a Getter function for the public state variable, which will result in a larger size of the compiled bytecode. Second, the external modifier can only be used in functions and called by sending a message (i.e. the call data). For details, please refer to Solidity Documentation. The following table summarizes the four visibility modifiers:

Image for post

There are 3 mutability modifiers in total and they can only be used on functions. By default, functions have the authority to read and write state variables. But purefunctioncannot read or write any state variables, and viewfunctioncannot write state variables. The Solidity compiler will check the functions' behavior the make sure they are really pureand view. If a pureor viewfunction is external, it will be protected by the STATICCALLinstruction at the EVM level. See the Solidity documentation for details. Only the payablefunction can receive Ether transfers, and the compiler will insert special check logic for non-payable functions. The following table is a summary of the mutability modifiers:

Image for post

That’s all for the pureand viewfunctions. Now we use a simple contract to observe the code inserted by the compiler for non-payablefunctions:

pragma solidity =0.6.12;

contract PayableDemo {
function f1() external {}
function f2() payable external {}
}

Below is the disassembledresult of the runtime bytecode produced by Solidity compiler. We can see that the compiler checks the value of msg.valuebefore entering the non-payable function, and, therefore, keep them from receiving Ether.

contract disassembler {

function f2() public return () { return(); }
function f1() public return () { return(); }

function main() public return () {
mstore(0x40,0x80);
if ((msg.data.length < 0x4)) {
label_00000026:
revert(0x0,0x0);
} else {
var0 = SHR(0xE0, msg.data(0x0));
if ((0x9942EC6F == SHR(0xE0,msg.data(0x0)))) { //ISSUE:COMMENT: Function f2()
f2();
stop();
} else if ((0xC27FC305 == var0)) { //ISSUE:COMMENT: Function f1()
require(!msg.value);
f1();
stop();
} else {
goto label_00000026;
}
}
}
}

Polymorphism

The Solidiy language supports function overriding and overloading, respectively corresponding to dynamic and static polymorphism. There are two function modifiers related to polymorphism: virtual and override. The rules are also very simple: only virtual functions can be overridden in derived contracts, and only override functions can override functions in base contracts. These are relatively easy to understand. It is worth noting that Solidity specifically provides a syntactic sugar that allows the public state variable to use the override modifier. At this time, the override modifier will be applied to the getter function generated by the compiler.

The OneSwap project uses OOP features such as inheritance, encapsulation, and polymorphism in many places, and features like interface implementation and method overriding can be seen everywhere. So we won’t take examples one by one in this article. In the part below, we take the OneSwapFactory contract as an example to show the use of the override modifier on state variables:

contract OneSwapFactory is IOneSwapFactory {
struct TokensInPair {
address stock;
address money;
}

address public override feeTo; // Note here!
address public override feeToSetter; // Note here!
address public immutable gov;
address public immutable ones;
uint32 public override feeBPS = 50; // Note here!
address public override pairLogic; // Note here!
mapping(address => TokensInPair) private _pairWithToken;
mapping(bytes32 => address) private _tokensToPair;
address[] public allPairs;

... // Functions omitted
}

Use of libraries

In addition to inheritance, the Solidity language also allows us to reuse code through a Library. A library is a special contract, defined with the library keyword. In the library, we cannot define state variables, implement interfaces, payable and fallback functions or inherit other contracts. In addition, when calling the library's public or external functions, the compiler will generate DELEGATECALL instructions.

Since the library only implements logic and does not have its own internal states, so generally it only needs to be deployed once, and other contracts can be linked to the deployed library. However, only public and external functions need to be linked, and the compiler will perform inline optimization for internal functions. Here is an example to demonstrate the use of the library:

pragma solidity =0.6.12;

library MyLib {
function x1234(uint256 n) internal pure returns (uint256) {
return n * 0x1234;
}
function x5678(uint256 n) public pure returns (uint256) {
return n * 0x5678;
}
}

contract LibDemo1 {
function test(uint256 n) public pure returns (uint256) {
return MyLib.x1234(n);
}
}
contract LibDemo2 {
function test(uint256 n) public pure returns (uint256) {
return MyLib.x5678(n);
}
}

We defined a library MyLiband two contracts LibDemo1and LibDemo2. LibDemo1called the internalfunction of MyLib, and LibDemo2called the publicfunction of MyLib. Compile libraries and contracts, and observe the runtime bytecode generated by the compiler. As you can see, there is nothing special about the runtime bytecode of LibDemo1. After Disassembly, it is shown as below:

contract disassembler {

function test( uint256 arg0) public return (var0) {
var4 = func_0000007C(arg0);
return(var4);
}

function func_0000007C( uint256 arg0) private return (var0) {
return((arg0 * 0x1234));
}

function main() public return () {
... // Code omitted
}

}

It can be seen that the compiler does optimize the internalfunction of the library and compile it into the contract. But the runtime bytecode of LibDemo2is a bit strange as it contains __MyLib_________________________________:

0x6080604052348015600f57600080fd5b506004361060285760003560e01c806329e99f0714602d575b600080fd5b605660048036036020811015604157600080fd5b8101908080359060200190929190505050606c565b6040518082815260200191505060405180910390f35b600073__MyLib_________________________________63412dab59836040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b15801560bc57600080fd5b505af415801560cf573d6000803e3d6000fd5b505050506040513d602081101560e457600080fd5b8101908080519060200190929190505050905091905056fea26469706673582212202f6ad810ce4f7299f7a53cc1cd3ecec4bcf4f366145d0bf4b5db4c537c153a9d64736f6c634300060c0033

__MyLib_________________________________is an illegal opcode, and thus the whole bytecode cannot be disassembled. However, we can easily guess that this is a placeholder generated by the compiler and represents the actual deployment address of MyLib. The Solidity compiler (solc) provides the --linkoption to enter link mode. In this mode, you can use the --librariesoption to specify the actual deployed address of the library (for example, --libraries MyLib:0xADD...), and replace the placeholders. We will not discuss more details here.

A total of four libraries are defined in the OneSwap project. Among them, the SafeMath256library is used for safe mathematical calculations, the Mathlibrary defines functions such as min, the DecFloat32library defines 32-bit floating-point numbers used by the Pair contract, and the ProxyDatalibrary encapsulates the logic for proxy data used in the "immutable forwarding" mode. The functions they provide are all internal. For more details of the code of these libraries, you can refer to their source code.

Summary

This article introduces the source code organization form of the Solidity project, the OOP features supported by Solidity, types of contracts (interfaces, abstract contracts, ordinary contracts, and libraries), modifiers (visibility modifiers, mutability modifiers, and Polymorphic modifiers), and the use of libraries. Properly organizing source code files and using the OOP features introduced above can greatly improve the code readability and reusability of smart contract projects.

Main References

Written by

A fully decentralized exchange protocol on Smart Contract, with permission-free token listing and automated market making.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store