Skip to main content

๐Ÿ’ป Solidity

Introductionโ€‹

This section is dedicated to outlining the best Solidity practices and style guidelines that we adhere to. By following these guidelines, we ensure that each contribution not only enhances the capabilities of our library, but also maintains a high standard of code clarity. This uniform approach to coding is essential to prevent vulnerabilities, streamline collaboration, and create a robust foundation for both new and experienced developers.

Guidelinesโ€‹

1 Formattingโ€‹

At the foundational level, we align closely with the official Solidity's Style Guide. To assist in maintaining these standards, we've integrated a Prettier Solidity Plugin. You can format Solidity code manually like this.

$ npm run lint-sol-fix

This command is automatically executed before each commit via the Husky pre-commit hook, so you don't have to worry about running it yourself.

2 Namingโ€‹

2.1 Contract functionsโ€‹

Internal and private contract functions have an underscore before the name, while public and external ones have no underscore.

contract GoodContractFunctions {
function externalFunction() external {}

function publicFunction() public {}

function _internalFunction() internal {}

function _privateFunction() private {}
}

2.2 Library functionsโ€‹

Internal functions in internal libs (without external and public functions) are named without an underscore before the name.

library GoodInternalLibFunctions {
function internalLibFunction() internal {}

function _privateFunction() private {}
}

library GoodExternalLibFunctions {
function externalFunction() external {}

function publicFunction() public {}

function _internalLibFunction() internal {}

function _privateFunction() private {}
}

2.3 Storage variablesโ€‹

Internal and private storage variables are named with an underscore before the name, while public ones have no underscore. If you want to declare a variable of type storage, the name of such a variable will have the same format as the storage variable. Storage parameters have no underscores.

contract GoodStorageVariables {
struct Point {
uint256 x;
uint256 y;
}

Point[] internal _points;

uint256 public storageVariable1;
uint256 internal _storageVariable2;
uint256 private _storageVariable3;

function someFunction() external {
uint256 pointsLength_ = points.length;

for (uint256 i = 0; i < pointsLength_; ++i) {
Point storage _point = points[i];

_processPoint(_point);
}
}

function _processPoint(Point storage point) private {}
}

2.4 Local variablesโ€‹

Local variables, both memory and calldata, including function arguments, are named with an underscore at the end. Exception: loop iterator variable.

contract GoodLocalVariables {
function someFunction(
uint256 localVariable1_,
string memory localVariable2_,
string calldata localVariable3_
) external {
uint256 localVariable4_ = 1;
string memory localVariable5_ = "hello";

for (uint256 i = 0; i < 10; ++i) {}
}
}

2.5 Event parametersโ€‹

Event parameters are named without underscores. The name of the event must refer to an action that has already happened.

interface IGoodEventParameterNames {
event GoodEvent(uint256 firstParam, string secondParam);
}

2.6 Constant and immutable variablesโ€‹

Constant and immutable variables are named with SCREAMING_SNAKE_CASE. Internal and private constant and immutable variables are named with an underscore before the name, while public ones have no underscore.

contract GoodConstantNames {
uint256 public constant PUBLIC_CONSTANT = 1;
uint256 internal constant _INTERNAL_CONSTANT = 1;
uint256 private constant _PRIVATE_CONSTANT = 1;

uint256 public immutable PUBLIC_IMMUTABLE = 1;
uint256 internal immutable _INTERNAL_IMMUTABLE = 1;
uint256 private immutable _PRIVATE_IMMUTABLE = 1;
}

3 NatSpec and Commentsโ€‹

3.1 All contracts must be documented using NatSpecโ€‹

All NatSpec should be written in multi-line comments. Start with a @notice tag to describe the contract. This comment should include the module to which the contract belongs. For functions, use @notice to explain what they do, @param for each parameter, and @return for the return value. The @dev tag is also available for additional developer comments, providing deeper insights or explanations. Ideally, NatSpec should be written in the contract's interface. In cases where a contract doesn't have an interface, the documentation should be included in the contract implementation itself.

/**
* @notice The Hasher module
*/
interface IHasher {
/**
* @notice The function to calculate the keccak256 of two values
* @param lhs_ the left-hand side value
* @param rhs_ the right-hand side value
* @return result_ the keccak256 of two values
*/
function hash2(
bytes32 lhs_,
bytes32 rhs_
) external pure returns (bytes32 result_);
}

3.2 Comments in the implementation can be not written at allโ€‹

If additional explanations are needed, write them as a single line comment.

// Concatenates values and computes keccak256
assembly {
mstore(0, lhs_)
mstore(32, rhs_)

result_ := keccak256(0, 64)
}

4 Errorsโ€‹

4.1 Error handlingโ€‹

For error handling, primarily use the require statement. The revert message statement may be used for more complex conditions but it is less common. Avoid using custom errors.

contract GoodErrorStatement {
function someFunction() external {
(bool ok_, ) = address(0x...).call{value: 1 ether}("");
require(ok_, "GoodErrorHandling: transfer failed");

if (/* Some complex condition */) {
revert("GoodErrorHandling: condition failed");
}
}
}

4.2 Error messagesโ€‹

Error messages should begin with the contract name or its abbreviation, formatted as "SomeContractName: ..." or "SCN: ...", respectively. Aim to keep these messages as short as possible to optimize gas usage. Avoid errors with no message.

contract GoodErrorMessages {
function someFunction() external {
(bool ok_, ) = address(0x..).call{value: 1 ether}("");
require(ok_, "GoodErrorMessages: transfer failed");

(ok_, ) = address(0x..).call{value: 1 ether}("");
require(ok_, "GEM: transfer failed");
}
}

5 Optimizationsโ€‹

While we prioritize gas optimization in our code, readability remains equally important. As such, we don't engage in extreme optimization practices that could obscure code understanding or maintainability. To guide our approach, below are examples illustrating when optimization is necessary and when it's not.

5.1 Uncheckedโ€‹

Use unchecked statements only to migrate code from older Solidity versions. Avoid using it for loop iterator variables as it reduces readability.

for (uint256 i = 0; i < 10; ++i) {}

5.2 Storage usageโ€‹

Optimize gas by not reusing the same storage repeatedly. Always save storage array lengths before loops. Declare variables inside functions to reduce repeated calls to storage.

contract GoodStorageUsage {
struct Point {
uint256 x;
uint256 y;
}

Point[] public points;

function someFunction() external {
uint256 pointsLength_ = points.length;

for (uint256 i = 0; i < pointsLength_; ++i) {
Point memory point_ = points[i];

_processPoint(point_);
_processPoint2(point_);
}
}

function _processPoint(Point memory point_) private {}

function _processPoint2(Point memory point_) private {}
}

6 Other agreementsโ€‹

6.1 Licenseโ€‹

All our contracts are under the MIT License. This ensures that they can be freely used, modified, and distributed.

// SPDX-License-Identifier: MIT

6.2 Solidity versionโ€‹

Use Solidity version 0.8.21. Exceptions for specific integrations are possible but must be discussed on a case-by-case basis.

pragma solidity ^0.8.21;

6.3 Importsโ€‹

Use named imports.

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

6.4 Internal storage variablesโ€‹

Explicitly specify internal keyword for internal storage variables.

contract GoodInternalStorageVariables {
uint256 internal _internalVariable;
}

6.5 Type cutsโ€‹

Use uint256 and int256 types instead of uint and int cuts.

contract GoodTypeCuts {
uint256 public variable1;
mapping(int256 => uint256) public variable2;

function someFunction(uint256 varaible3_, int256 variable4_) external {}
}

6.6 Transfer of native currencyโ€‹

Use low-level calls for transferring native currency instead of transfer and send.

contract GoodTransfer {
function someFunction() external {
(bool ok_, ) = address(0x..).call{value: 1 ether}("");
require(ok_, "GoodTransfer: transfer failed");
}
}