Este é um guia para os engenheiros da Coinbase que desenvolvem contratos inteligentes baseados em EVM. Utilizamos Solidity para desenvolver tais contratos, portanto, chamamos isso de "Guia de Estilo Solidity." Este guia também abrange práticas de desenvolvimento e teste. Estamos compartilhando isso publicamente caso seja útil para outros.
Devemos ser extremamente específicos e minuciosos ao definir nosso estilo, testes e práticas de desenvolvimento. O tempo que economizamos ao não precisar debater essas questões em pull requests é tempo produtivo que pode ser investido em outras discussões e revisões. Seguir o guia de estilo é uma prova de cuidado.
A. A menos que uma exceção ou adição seja especificamente notada, seguimos o Guia de Estilo Solidity.
O guia de estilo afirma:
Prefixo de sublinhado para funções e variáveis não externas
Uma das motivações para esta regra é que ela é uma pista visual útil.
Sublinhados iniciais permitem que você reconheça imediatamente a intenção de tais funções...
Concordamos que um sublinhado inicial é uma pista visual útil, e é por isso que nos opomos ao uso deles para funções internas de biblioteca que podem ser chamadas de outros contratos. Visualmente, parece errado.
Library._function()
ou
using Library for bytes
bytes._function()
Observe, não podemos remediar isso insistindo no uso de funções públicas. Se as funções de uma biblioteca são internas ou externas tem implicações importantes. Na [documentação do Solidity] (https://docs.soliditylang.org/en/latest/contracts.html#libraries):
... o código das funções internas de uma biblioteca que são chamadas de um contrato e todas as funções chamadas a partir daí serão incluídas no contrato que faz a chamada no momento da compilação, e uma chamada JUMP regular será usada em vez de uma DELEGATECALL.
Os desenvolvedores podem preferir funções internas porque são mais eficientes em termos de gás para chamadas.
Se uma função nunca deve ser chamada de outro contrato, ela deve ser marcada como privada e seu nome deve ter um sublinhado inicial.
Erros personalizados são, em alguns casos, mais eficientes em termos de gás e permitem a passagem de informações úteis.
Por exemplo, InsufficientBalance
.
Por exemplo, UpdatedOwner
não UpdateOwner
.
Eventos devem rastrear coisas que aconteceram e, portanto, devem ser no passado. Usar o passado também ajuda a evitar colisões de nomes com estruturas ou funções.
Estamos cientes de que isso não segue o precedente dos primeiros ERCs, como o ERC-20. No entanto, isso se alinha com algumas implementações mais recentes de Solidity de alto perfil, exemplos: 1,2,3.
O código assembly é difícil de ler e auditar. Devemos evitá-lo, a menos que as economias de gás sejam muito significativas, por exemplo, > 25%.
Em funções curtas, argumentos de retorno nomeados são desnecessários.
NÃO:
function add(uint a, uint b) public returns (uint result) {
result = a + b;
}
Argumentos de retorno nomeados podem ser úteis em funções com múltiplos valores retornados.
function validate(UserOperation calldata userOp) external returns (bytes memory context, uint256 validationData)
No entanto, é importante ser explícito ao retornar mais cedo.
NÃO:
function validate(UserOperation calldata userOp) external returns (bytes memory context, uint256 validationData) {
context = "";
validationData = 1;
if (condition) {
return;
}
}
SIM:
function validate(UserOperation calldata userOp) external returns (bytes memory context, uint256 validationData) {
context = "";
validationData = 1;
if (condition) {
return (context, validationData);
}
}
Se uma função ou conjunto de funções poderia razoavelmente ser definido como seu próprio contrato ou como parte de um contrato maior, prefira definir como parte de um contrato maior. Isso torna o código mais fácil de entender e auditar.
Observe que isso não significa que devemos evitar a herança, em geral. A herança é útil às vezes, especialmente quando se constrói em contratos existentes e confiáveis. Por exemplo, não reimplemente a funcionalidade Ownable
para evitar a herança. Herde Ownable
de um fornecedor confiável, como OpenZeppelin ou Solady.
Interfaces separam NatSpec da lógica do contrato, exigindo que os leitores façam mais trabalho para entender o código. Por essa razão, elas devem ser evitadas.
Embora os contratos principais que implantamos devem especificar uma única versão do Solidity, todos os contratos de suporte e bibliotecas devem ter um Pragma tão aberto quanto possível. Uma boa regra é até a próxima versão principal. Por exemplo:
pragma solidity ^0.8.0;
B. Se um struct ou erro é usado em muitos arquivos, sem nenhuma interface, contrato ou biblioteca sendo razoavelmente o "dono," então defina-os em seu próprio arquivo. Vários structs e erros podem ser definidos juntos em um arquivo.
Importações nomeadas ajudam os leitores a entender o que exatamente está sendo usado e onde é originalmente declarado.
NÃO:
import "./contract.sol"
SIM:
import {Contract} from "./contract.sol"
Para conveniência, importações nomeadas não precisam ser usadas em arquivos de teste.
NÃO:
import {B} from './B.sol'
import {A} from './A.sol'
SIM:
import {A} from './A.sol'
import {B} from './B.sol'
Por exemplo:
import {Math} from '/solady/Math.sol'
import {MyHelper} from './MyHelper.sol'
Em arquivos de teste, importações de /test
devem ser seu próprio grupo também.
import {Math} from '/solady/Math.sol'
import {MyHelper} from '../src/MyHelper.sol'
import {Mock} from './mocks/Mock.sol'
Às vezes, autores e leitores acham útil comentar divisórias entre grupos de funções. Isso é permitido, no entanto, garanta que o guia de estilo ordenação de funções ainda seja seguido.
Por exemplo:
/// External Functions ///
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* VALIDATION OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Arte ASCII é permitida no espaço entre o fim do Pragma e o início das importações.
Passar argumentos para funções, eventos e erros com nomeação explícita ajuda na clareza
, especialmente quando o nome da variável passada não corresponde ao nome do parâmetro.
NÃO:
pow(x, y, v)
SIM:
pow({base: x, exponent: y, scalar: v})
A. Uso do Forge para teste e gerenciamento de dependências.
1. Nomes de arquivos de teste devem seguir as convenções do Guia de Estilo Solidity para nomes de arquivos e também ter .t
antes de .sol
.
Por exemplo, ERC20.t.sol
2. Nomes de contratos de teste devem incluir o nome do contrato ou função sendo testado, seguido por "Test".
Por exemplo,
ERC20Test
TransferFromTest
Por exemplo
test_transferFrom_debitsFromAccountBalance
test_transferFrom_debitsFromAccountBalance_whenCalledViaPermit
test_transferFrom_reverts_whenAmountExceedsBalance
Se o contrato é nomeado após uma função, então o nome da função pode ser omitido.
contract TransferFromTest {
function test_debitsFromAccountBalance() ...
}
Isso é geralmente uma boa prática, mas especialmente porque Forge não fornece números de linha em falhas de afirmação. Isso dificulta rastrear o que, exatamente, falhou se um teste tem muitas afirmações.
NÃO:
function test_transferFrom_works() {
// debita corretamente
// credita corretamente
// emite corretamente
// reverte corretamente
}
SIM:
function test_transferFrom_debitsFrom() {
...
}
function test_transferFrom_creditsTo() {
...
}
function test_transferFrom_emitsCorrectly() {
...
}
function test_transferFrom_reverts_whenAmountExceedsBalance() {
...
}
Observe, isso não significa que um teste deve ter apenas uma afirmação. Às vezes, ter várias afirmações é útil para certeza sobre o que está sendo testado.
function test_transferFrom_creditsTo() {
assertEq(balanceOf(to), 0);
...
assertEq(balanceOf(to), amount);
}
NÃO:
function test_transferFrom_creditsTo() {
assertEq(balanceOf(to), 0);
transferFrom(from, to, 10);
assertEq(balanceOf(to), 10);
}
SIM:
function test_transferFrom_creditsTo() {
assertEq(balanceOf(to), 0);
uint amount = 10;
transferFrom(from, to, amount);
assertEq(balanceOf(to), amount);
}
Tudo o mais sendo igual, prefira testes de fuzz.
NÃO:
function test_transferFrom_creditsTo() {
assertEq(balanceOf(to), 0);
uint amount = 10;
transferFrom(from, to, amount);
assertEq(balanceOf(to), amount);
}
SIM:
function test_transferFrom_creditsTo(uint amount) {
assertEq(balanceOf(to), 0);
transferFrom(from, to, amount);
assertEq(balanceOf(to), amount);
}
Remeapeamentos ajudam Forge a encontrar dependências com base em instruções de importação. Forge deduzirá automaticamente alguns remapeamentos, por exemplo
forge-std/=lib/forge-std/src/
solmate/=lib/solmate/src/
Devemos evitar adicionar a estes ou definir quaisquer remapeamentos explicitamente, pois isso torna nosso projeto mais difícil para outros usarem como uma dependência. Por exemplo, se nosso projeto depende de Solmate e o deles também, queremos evitar que nosso projeto tenha algum nome de importação irregular, resolvido com um remapeamento personalizado, que entrará em conflito com o nome de importação deles.
1. Prefira a convenção "Namespaced Storage Layout" do ERC-7201 para evitar colisões de armazenamento.
2. Campos de carimbo de data/hora em uma struct devem ter pelo menos uint32 e idealmente ser uint40.
uint32
dará ao contrato cerca de 82 anos de validade (2^32 / (60*60*24*365)) - (2024 - 1970)
. Se o espaço permitir, uint40 é o tamanho preferido.
A. A menos que uma exceção ou adição seja especificamente notada, siga Solidity NatSpec.
Minimamente incluindo um @notice
. @param
e @return
devem estar presentes se houver parâmetros ou valores de retorno.
Structs podem ser documentados com um @notice
acima e, se desejado, @dev
para cada campo.
/// @notice Uma struct que descreve a posição de uma conta
struct Position {
/// @dev O carimbo de data/hora unix (segundos) do bloco quando a posição foi criada.
uint created;
/// @dev A quantidade de ETH na posição
uint amount;
}
Para facilitar a leitura, adicione uma nova linha entre os tipos de tags, quando várias estão presentes e há três ou mais linhas.
NÃO:
/// @notice ...
/// @dev ...
/// @dev ...
/// @param ...
/// @param ...
/// @return
SIM:
/// @notice ...
///
/// @dev ...
/// @dev ...
///
/// @param ...
/// @param ...
///
/// @return
Se você estiver usando a tag @author
, ela deve ser
/// @author Coinbase
Seguido opcionalmente por um link para o repositório público no Github.