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.

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.

interface IOneSwapERC20 { ... }
interface IOneSwapPool { ... }
interface IOneSwapPair { ... }
abstract contract OneSwapERC20 is IOneSwapERC20 { ... }
abstract contract OneSwapPool is OneSwapERC20, IOneSwapPool { ... }
contract OneSwapPair is OneSwapPool, IOneSwapPair { ... }
+---------------+
| 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.

pragma solidity =0.6.12;

contract PayableDemo {
function f1() external {}
function f2() payable external {}
}
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.

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.

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);
}
}
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
}

}
0x6080604052348015600f57600080fd5b506004361060285760003560e01c806329e99f0714602d575b600080fd5b605660048036036020811015604157600080fd5b8101908080359060200190929190505050606c565b6040518082815260200191505060405180910390f35b600073__MyLib_________________________________63412dab59836040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b15801560bc57600080fd5b505af415801560cf573d6000803e3d6000fd5b505050506040513d602081101560e457600080fd5b8101908080519060200190929190505050905091905056fea26469706673582212202f6ad810ce4f7299f7a53cc1cd3ecec4bcf4f366145d0bf4b5db4c537c153a9d64736f6c634300060c0033

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

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