На прошлой неделе была завершена видеозапись спецификаций кодирования Solidity, которая была загружена на Bilibili, Youtube и видеоаккаунт. Всего он разделен на 6 секций, а адрес сбора на станции B:
https://space.bilibili.com/60539794/channel/collectiondetail?sid=3780183
Чтобы облегчить некоторым студентам, которые привыкли читать статьи, за последние два дня я составил текстовую версию. Ниже приводится краткое изложение стандартов кодирования Solidity.
Мы знаем, что каждый язык кодирования будет иметь свои собственные спецификации кодирования. А как насчет зачем нужны стандарты кодирования? Я обобщил следующие причины:
В целом стандарты кодирования играют важную роль в обеспечении качества кода, повышении эффективности разработки, содействии командной работе, снижении затрат на обслуживание и т. д. Таким образом, каждый язык программирования требует стандартов кодирования, и Solidity, как язык разработки смарт-контрактов, также не является исключением. . Спецификации кодирования языка Solidity будут официально представлены ниже.
Наш первый шаг — создать исходный файл,Итак, первый шаг, с которого я хочу начать, — это именование файлов. И оименование файлов этой части спецификации кодирования,Я могу свести это к четырем пунктам:
В официальной документации Solidity перечислено несколько различных стилей именования:
b
(single lowercase letter)B
(single uppercase letter)lowercase
UPPERCASE
UPPER_CASE_WITH_UNDERSCORES
CapitalizedWords
(or CapWords)mixedCase
(differs from CapitalizedWords by initial lowercase character!)Прежде всего, так называемая номенклатура верблюжьих дел на самом деле CapitalizedWords
Этот стиль номенклатуры представляет собой комбинацию слов с заглавной буквой. и mixedCase
Также известна как номенклатура CamelCase.
Компонент контракта на самом деле interface
、library
、contract
。Требуется имя файла Соблюдайте соответствие имени компонента контракта., что означает, что он должен соответствовать определению в файле. interface/library/contract
Имя то же самое. Например, имя файла UniswapV3Pool.sol
,определено в файлеиз contract
Имя UniswapV3Pool
。
Определяйте только один компонент контракта в каждом файле, то есть не определяйте в файле несколько компонентов контракта одновременно. Например, ядро Uniswap v3 определяет несколько интерфейсов классов обратного вызова:
IUniswapV3FlashCallback
IUniswapV3MintCallback
IUniswapV3SwapCallback
Из этих трех интерфейсов каждый определяет только одну функцию. Грамматически говоря, можно использовать только один файл для хранения этих трех интерфейсов, например, использовать UniswapV3Callback.sol
,Документ примерно следующий:
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0
interface IUniswapV3FlashCallback {
...
}
interface IUniswapV3MintCallback {
...
}
interface IUniswapV3SwapCallback {
...
}
С грамматической точки зрения это не является ошибкой. Однако Uniswap делит его на три файла следующим образом:
IUniswapV3FlashCallback.sol
IUniswapV3MintCallback.sol
IUniswapV3SwapCallback.sol
Это означает, что в каждом файле определен только один компонент контракта.
Кроме того, в приведенном выше примере также используется последнее соглашение об именах, то есть interface Дополнительная капитализация имен I как префикс。тыможно увидеть Эти interface
Все большими буквами I в качестве префикса, в то время как library
и contract
Такой префикса не будет. Следуя этой спецификации, вы можете напрямую скачать с именования. файлы различают какие из них interface
.
существовать в проекте,Обычно существует не только несколько файлов,Это много файлов,Естественно, нам нужно хорошо поработать файл.Что о Макете файлаэтот кусокизспецификация,Мое предложение,Несколько файлов контрактов разных типов можно распределить по разным папкам.
Например, существует несколько interface
, это могут быть interface
Файлы в совокупности подразделяются на interfaces
каталог. И несколько library
Документы можно разделить на libraries
каталог. Абстрактные контракты могут быть размещены в base
или utils
каталог. Большинство основных контрактов, которые необходимо развернуть, размещаются непосредственно в contracts
или src
в домашнем каталоге.
кроме того,Если файлов много, каталог можно разделить еще раз. например,Uniswap/v3-core
из interfaces
Он далее разделен ниже callback
и pool
два подкаталога.
Мне нравится OpenZeppelin Этот тип библиотеки отличается,Это скорее классификация каталогов, основанная на функциональных модулях.,Различные файлы контрактов разделены на разные функциональные каталоги. существуют Эти функции каталога,Большинство из них не разделены на каталоги по типам компонентов контракта. например,access
Поместите его прямо в каталог IAccessControl.sol
,Как только я приду,не так уж и много interface
,Во-вторых,Расположение каталогов больше основано на функциональных настройках. Но у него все еще есть interfaces
В каталоге хранятся все файлы интерфейса.
Следующий,Приступим к вводу спецификации внутреннего кодирования документа. Этот раздел.,Давайте сначала подведем итог части «Заявление о контракте».
Во-первых, в первой строке мы должны объявить SPDX-License-Identifier
,Укажите, что будет использоваться лицензия. Хотя я этого не заявляю,Компиляция не сообщит об ошибке,Но будут предупреждения,Чтобы устранить предупреждение,так Нам лучше составить это заявление。
Во второй строке используйте pragma
Заявление в поддержку из solidity Версия. Что касается этого, я полагаю, что это зависит от ситуации. в случае Рекомендуется объявить интерфейс, библиотеку и абстрактные контракты как совместимые версии, например: ^0.8.0
。Если необходимо развернутьиз Рекомендуется объявить основной контракт как фиксированную версию.。об этом,Uniswap/v4-core
Это хороший пример. его основной контракт PoolManager
из pragma
Заявление заключается в следующем:
pragma solidity 0.8.26
Как видите, она заявлена как фиксированная версия. 0.8.26
。
И все Интерфейс, библиотека и абстрактные контракты объявлены как совместимые версии, да. ^0.8.0
,да ^0.8.24
。объявлен как ^0.8.24
из-за использования этой версии обычно начинает поддерживать некоторые функции.
И о import Частично есть два основных предложения: одно — указать имя для импорта, другое — указать; import из Когда сторонний контракт хранит несколько версий,Рекомендуется указать номер версии。
Просто посмотрите на следующие примеры:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20} from "@openzeppelin/contracts@0.5.2/token/ERC20/ERC20.sol";
Вышеупомянутые три import Из метода записи первый из них наиболее часто используется многими людьми (включая меня) и заключается в прямом импорте всего файла. Второй тип добавляет конкретное имя импортируемого контракта. Это то, что я говорил ранее. Рекомендуется указать имя для импорта. Третий тип существует contracts
добавлено позже @0.5.2
из, чтобы указать конкретный номер версии из, которую нужно импортировать.
Этот третий способ записи — наш наиболее рекомендуемый метод импорта. Потому что OpenZeppelin выпустил несколько разных версий библиотек.
но,Если вы хотите импортировать сторонний контракт и существует несколько его версий,Тогда нет необходимости добавлять объявление версии. Например.,@uniswap/v3-core
Толькоиметьверсияиз Библиотека,Тогда нет необходимости указывать версию при импорте контракта библиотеки.,Следующее:
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
interface Это контрактное взаимодействие, которое также используется во внешнем и внутреннем взаимодействии. abi из исходного кода, поэтому, когда мы пишем настоящий код, мы обычно сначала определяем его интерфейс. В этом разделе пойдет речь interface Каковы стандарты кодирования при кодировании.
Прежде всего, из структуры кода рекомендуется расположить его в следующем порядке:
Errors
Events
Enums
Structs
Functions
Сначала определите Errors
。error
Тип находится в 0.8.4
только начал поддерживать из, так что если interface
там есть определение error
,Предлагаемое заявление pragma
если указано как минимум ^0.8.4
。И заявление error При именовании рекомендуется использовать метод именования в большом верблюжьем регистре. Если есть параметры, для параметров используется метод именования в маленьком верблюжьем регистре. Пример:
error ZeroAddress();
error InvalidAddress(address addr);
Далее определите Events
。определенныйизсобытиеимя Используйте номенклатуру Camel Case,Параметры событий также используют номенклатуру верблюжьего регистра. кроме того,Если параметров много,,Рекомендуется каждый параметр располагать на отдельной строке. Пример:
event Transfer(address indexed from, address indexed to, uint value);
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] values
);
Фактически, в официальной документации также указано, что рекомендуется, чтобы каждая строка кода не превышала 120 символов, поэтому, если он слишком длинный, его необходимо расположить с помощью разрывов строк. существовать В Solidity отдельная строка для каждого параметра — очень распространенный формат.
Далее, если есть из, его можно определить Enums
,То есть тип перечисления. Название также используется в номенклатуре Camel Case.,Пример:
enum OrderStatus {
Pending,
Shipped,
Delivered,
Canceled
}
После этого вы можете определить Structs
,Такова структура. Структура из Название Используйте номенклатуру Camel Case,В поле «из» используется небольшая номенклатура верблюжьего регистра.,Пример:
struct PopulatedTick {
int24 tick;
int128 liquidityNet;
uint128 liquidityGross;
}
Затем вы можете определить Functions
.Но функцию можно подразделить и объявить в следующем порядке:
External functions with payable
External functions
External functions with view
External functions with pure
То есть мы interface
При определении нескольких функций рекомендуется располагать этифункции в следующем порядке. Вот несколько примеров:
// external function with payable
function burn(uint256 tokenId) external payable;
function createAndInitializePoolIfNecessary(
address token0,
address token1,
uint24 fee,
uint160 sqrtPriceX96
) external payable returns (address pool);
// external function
function mint(
address recipient,
int24 tickLower,
int24 tickUpper,
uint128 amount,
bytes calldata data
) external returns (uint256 amount0, uint256 amount1);
// external function with view
function getPopulatedTicksInWord(address pool, int16 tickBitmapIndex)
external
view
returns (PolulatedTick[] memory populatedTicks);
function ticks(int24 tick)
external
view
returns (
uint128 liquidityGross,
int128 liquidityNet,
uint256 feeGrowthOutside0X128,
uint256 feeGrowthOutside1X128,
int56 tickCumulativeOutside,
uint160 secondsPerLiquidityOutsideX128,
uint32 secondsOutside,
bool initialized
);
// external function with pure
function mulDiv(uint a, uint b, uint c) external pure returns (uint d);
Помимо заказа, есть некоторые детали, о которых нужно поговорить.
Прежде всего, вы можете видеть, что имена функций и параметров используют верблюжий регистр.
Во-вторых,возвращатьсяможно увидетьиметьнет то же самоеизформат новой строки。createAndInitializePoolIfNecessary
и mint
эти двоефункция Вседа Параметры по одной строке каждый。getPopulatedTicksInWord
затем становится модификатором функции returns
Заявления по одной строке каждое,Это также новая строка метода записи. наконец,ticks
Функция: Список возвращаемых параметров разбит на отдельную строку для каждого возвращаемого параметра. Это также распространенный метод записи, когда имеется много возвращаемых параметров.
Далее поговорим о library
изиспользование.
library
Переменные состояния здесь не могут быть определены, поэтому library
Фактически, это лицо без гражданства.
Синтаксически говоря, библиотека Могут быть определены различные функции видимости, но в практических приложениях они обычно определяются только internal
и private
изфункция, редко определяемая, внешне видимая из external
и public
функция. если library Определено, что внешняя видимая функция, при ее развертывании, также будет и только внутренней функцией. library
нет то же самое. Конечно, это другая тема.
library
Также можно определить постоянные errors
。иметь些当纯Только定义了постоянныйиз library
,например Uniswap/v4-periphery
Есть один в library
следующее:
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;
/// @title Action Constants
/// @notice Common constants used in actions
/// @dev Constants are gas efficient alternatives to their literal values
library ActionConstants {
/// @notice used to signal that an action should use the input value of the open delta on the pool manager
/// or of the balance that the contract holds
uint128 internal constant OPEN_DELTA = 0;
/// @notice used to signal that an action should use the contract's entire balance of a currency
/// This value is equivalent to 1<<255, i.e. a singular 1 in the most significant bit.
uint256 internal constant CONTRACT_BALANCE = 0x8000000000000000000000000000000000000000000000000000000000000000;
/// @notice used to signal that the recipient of an action should be the msgSender
address internal constant MSG_SENDER = address(1);
/// @notice used to signal that the recipient of an action should be the address(this)
address internal constant ADDRESS_THIS = address(2);
}
Как видите, это ActionConstants
Это просто определение 4 Просто константа.
library
Большинство сценариев использованияизна самом деледаконкретномутипиз Инкапсулируйте расширенные функции。UniswapV3Pool
В начале тела кода есть куча объявлений. using for
,Как показано ниже:
contract UniswapV3Pool is IUniswapV3Pool, NoDelegateCall {
using LowGasSafeMath for uint256;
using LowGasSafeMath for int256;
using SafeCast for uint256;
using SafeCast for int256;
using Tick for mapping(int24 => Tick.Info);
using TickBitmap for mapping(int16 => uint256);
using Position for mapping(bytes32 => Position.Info);
using Position for Position.Info;
using Oracle for Oracle.Observation[65535];
...
}
Эти using
обратно из libraries
Функция инкапсуляции в основном правильная for
типиз Расширение функции。например,LowGasSafeMath
Можно понять, что это правильно uint256
Тип расширения функции, первый параметр, определенный в нем, — это функция. uint256
типиз。LowGasSafeMath
Существует определенный add
функция,следующее:
function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x + y) >= x);
}
существовать UniswapV3Pool
привыкший LowGasSafeMath
из add
На самом деле существует множество функциональных мест, например следующие:
uint256 balance0Before = balance0();
require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA');
в,balance0Before.add(uint256(amount0))
На самом деле это называется LowGasSafeMath
из add
Функция. и,Вызывается через этот синтаксический сахар из оболочки,Не значит ли это, что оно сталоuint256
Сам этот тип расширяется функцией, поэтому говорят library
Его можно понимать как функциональное расширение определенного типа.
Наконец, хотя library
Нет прямого доступа к переменным состояния,нопеременные Состояние может быть передано косвенно через метод функции. library
Чтобы получить доступ к функции, просто объявите параметр функции как storage
Вот и все. например library Position
,заявил get
и update
Функция, первый параметр обеих функций берется storage
Модификация, ее основной код следующий:
function get(
mapping(bytes32 => Info) storage self,
address owner,
int24 tickLower,
int24 tickUpper
) internal view returns (Position.Info storage position) {
position = self[keccak256(abi.encodePacked(owner, tickLower, tickUpper))];
}
function update(
Info storage self,
int128 liquidityDelta,
uint256 feeGrowthInside0X128,
uint256 feeGrowthInside1X128
) internal {
...
// update the position
if (liquidityDelta != 0) self.liquidity = liquidityNext;
self.feeGrowthInside0LastX128 = feeGrowthInside0X128;
self.feeGrowthInside1LastX128 = feeGrowthInside1X128;
if (tokensOwed0 > 0 || tokensOwed1 > 0) {
// overflow is acceptable, have to withdraw before you hit type(uint128).max fees
self.tokensOwed0 += tokensOwed0;
self.tokensOwed1 += tokensOwed1;
}
}
get
функцияиз self
Параметры storage
из, а тип данных mapping
из, означает, что параметр, переданный в из, является переменным состояние, и возвращаемое значение также storage
из, так что вернись из position
Фактически, это тоже переменная состояния.
update
функцияиз self
Параметры тоже такие же storage
из, также переменные состояния,существовать код внутри self
Модификации также будут напрямую отражены в переменных состояниесуществовать в сети из-за изменения статуса.
поговорим об этом ниже contract аспектизкодированиеспецификация,В основном говорим о структуре кода,Обычно рекомендуется располагать в следующем порядке:
using for
постоянный
переменные состояния
Events
Errors
Modifiers
Functions
Во-первых, если вам нужно использовать библиотеку, используйте ее в первую очередь. using for
Объявите используемую библиотеку.
Далее определитепостоянный。постоянныйвозвращатьсяразделен на public
постоянныйи private
Константы, обычно рекомендуется сначала их объявить. public
из Пересчет private
из。кроме того,постоянный именуется заглавными буквами с подчеркиванием и з,так:UPPER_CASE_WITH_UNDERSCORES
。
После объявления констант вы можете объявить переменные состояния. Переменные состояния также объявляются первыми. public
из, то объяви private
из。жизньимя则通常采用小驼峰式жизньимя Закон。кроме того,private
изпеременные Состояние обычно также добавляет начальное подчеркивание к и public
изотдельные переменные。Пример:
// public state variable
uint256 public totalSupply;
// private state variable
address private _owner;
mapping(address => uint256) private _balanceOf;
Впоследствии, если есть событие, которое необходимо объявить, объявите это событие. Впоследствии, если есть ошибка, которую необходимо объявить, объявите об ошибке.
событиеиошибка,обычносуществовать interface
Сделать заявление в , так существовать напрямую contract
Деклараций здесь относительно немного.
Далее, есть заявление Настроитьиз. modifiers
.кроме того,modifier
в именовании используется метод именования верблюжьего регистра. Ниже приведен пример:
modifier onlyOwner() {
if(msg.sender != _owner) revert OwnableUnauthorizedAccount(msg.sender);
_;
}
наконец,Это просто набор определений функций, потому что их много.,такнасвозвращаться Необходимо реорганизовать,Его можно расположить в следующем порядке:
структурафункция
receive функция
fallback функция
external функция
external payable
external
external view
external pure
public функция
public payable
public
public view
public pure
internal функция
internal
internal view
internal pure
private функция
private
private view
private pure
Видно, что сначала определяется конструктор, а затем receive
и fallback
Функция обратного вызова, если не требуется, то она и не нужна.
После этого отсортируйте по видимости, порядок такой external、public、internal、private
。
Различные функции невидимости,А затем отсортировать по изменчивости,к external
Если взять в качестве примера функции класса, то порядок следующий: payable функция,внешняя Писатьфункция, внешняя view функция, внешняя pure функция
。Другая видимостьтипфункциятакжеда Та же причина。
Кроме того, относительно internal
и private
функцияизжизньимя,предположениеияиметьпеременные Как и в случае с состоянием, добавьте начальное подчеркивание как префикс,кивнешнийфункцияразличать。Пример:
// internal functions
function _transfer(address from, address to, uint256 value) internal {}
function _getBalance(address account) internal view returns (uint256) {}
function _tryAdd(uint256 a, uint256 b) internal pure returns (uint256) {}
// private functions
function _turn() private {}
function _isParsed() private view returns (bool) {}
function _canAdd(uint256 a, uint256 b) private pure returns (uint256) {}
наконец,Несколько модификаторов функций в каждой функции также необходимо отсортировать.,Обычно рекомендуется сортировать в следующем порядке:
Видимость (внешняя/публичная/внутренняя/частная)
Вариативность (платная/просмотр/чистая)
Virtual
Override
Пользовательские модификаторы
Просто посмотрите на следующий пример:
function mint() external payable virtual override onlyOwner {}
Выше речь идет о contract
Частичное изкодирование нормативных рекомендаций.
наконецчат Комментарий。
первый,Что-то, что стоит запомнить:Хорошо из именования, т.е. Комментарий。
то есть контракт、событие、ошибка、переменные состояния、функция、Параметры и т. д.,Все имена сами по себе должны уметь хорошо объяснять значение того, что они представляют.,Это само по себе наиболее интуитивно понятно из Комментарий. Дополнительное добавление из Комментария фактически используется для дополнительных пояснений.
Solidity поддерживает теги из Комментарий, включая следующие:
@title
@author
@notice
@dev
@param
@return
@inheritdoc
@custom:...
Описание этического тега «существовать» также объясняется в официальном документе, как показано ниже:
Официальная спецификация кодирования рекомендует для всех публичных интерфейсов добавлять полный из Комментарий.,То есть,нас可к Давать interface
Добавить полный Комментарий. Унисвап Как видите, это очень хорошая практика. Uniswap все interface
Все очень полные из Комментариев.
кроме того,Добавив контент из Комментарий, вы также можете увидеть контент Комментарий в браузере существующих блоков. например,Просматриваем COMP Токен-контракт:
существуют интерактивные страницы, при нажатии кнопки «Одобрить» следующее:
Проверьте исходный код еще раз. approve
функцияиз Комментарий:
/**
* @notice Approve `spender` to transfer up to `amount` from `src`
* @dev This will overwrite the approval amount for `spender`
* and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve)
* @param spender The address of the account which may transfer tokens
* @param rawAmount The number of tokens that are approved (2^256-1 means infinite)
* @return Whether or not the approval succeeded
*/
function approve(address spender, uint rawAmount) external returns (bool) {
...
}
Это можно увидеть,@notice
、@dev
и @param
из контента отображается блок браузера, интерактивная страница существует.