Recentemente, tenho revisitado o Solidity para consolidar alguns detalhes e estou escrevendo uma "Introdução Simples ao Solidity" para ajudar os iniciantes (os especialistas em programação podem procurar outros tutoriais). A atualização será feita semanalmente com 1-3 aulas.
Twitter: @0xAA_Science
Comunidade: Discord | Grupo do WeChat | Website wtf.academy
Todo o código e tutoriais estão disponíveis no GitHub: github.com/AmazingAng/WTFSolidity
Os tipos de referência (Reference Type) no Solidity incluem arrays (matrizes) e structs (estruturas). Como esses tipos de variáveis são mais complexos e ocupam mais espaço de armazenamento, é necessário especificar a localização de armazenamento dos dados ao usá-los.
No Solidity, existem três tipos de localização de dados: storage
, memory
e calldata
. Cada tipo de armazenamento possui um custo de gas
diferente. Os dados do tipo storage
são armazenados on-chain, semelhante a um disco rígido em um computador, e consomem mais gas
. Os dados do tipo memory
e calldata
são temporariamente armazenados na memória, com um consumo de gas
menor. Em linhas gerais:
-
storage
: As variáveis de estado do contrato são por padrão do tipostorage
e são armazenadas na cadeia. -
memory
: Parâmetros de função e variáveis temporárias dentro de funções normalmente utilizam o tipomemory
e são armazenadas na memória, fora da cadeia. Especialmente quando o tipo de retorno da função é variável (de comprimento variável), é necessário utilizar o modificadormemory
, por exemplo: string, bytes, array e estruturas personalizadas. -
calldata
: Semelhante aomemory
, os dados são armazenados na memória e fora da cadeia. A diferença em relação aomemory
é que as variáveis emcalldata
são somente leitura (imutáveis) e são normalmente utilizadas para parâmetros de função. Exemplo:
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
// O parâmetro é um array calldata, não pode ser modificado
// _x[0] = 0 // Esta modificação causará um erro
return(_x);
}
Exemplo:
Ao atribuir entre diferentes tipos de armazenamento, às vezes é criada uma cópia independente (modificar a nova variável não afeta a variável original), e outras vezes é criada uma referência (modificar a nova variável afeta a original). As regras são as seguintes:
-
Atribuir essencialmente cria uma referência ao corpo, portanto, a alteração do corpo ou da referência pode ser sincronizada:
-
Atribuir de
storage
(variáveis de estado do contrato) parastorage
local (dentro de funções) cria uma referência, e alterar a nova variável afetará a original. Exemplo:uint[] x = [1,2,3]; // Variável de estado: array x function fStorage() public{ // Declaração de uma variável storage xStorage, que aponta para x. Alterar xStorage afetará x uint[] storage xStorage = x; xStorage[0] = 100; }
Exemplo:
-
memory
atribuído amemory
cria uma referência, e alterar a nova variável afetará a original.
-
-
Em outras situações, a atribuição cria uma cópia do corpo, ou seja, a modificação em um não será refletida no outro.
As variáveis no Solidity são divididas em três tipos de escopo: variáveis de estado (state variable), variáveis locais (local variable) e variáveis globais (global variable).
As variáveis de estado são aquelas cujos dados são armazenados na cadeia e podem ser acessadas por todas as funções dentro do contrato. Essas variáveis são declaradas dentro do contrato, fora das funções:
contract Variaveis {
uint public x = 1;
uint public y;
string public z;
}
Podemos alterar o valor das variáveis de estado dentro das funções:
function foo() external{
// Podemos alterar o valor das variáveis de estado dentro da função
x = 5;
y = 2;
z = "0xAA";
}
As variáveis locais são aquelas válidas apenas durante a execução de uma função e se tornam inválidas após a saída da função. Os dados das variáveis locais são armazenados na memória, fora da cadeia, e consomem menos gas
. As variáveis locais são declaradas dentro de funções:
function bar() external pure returns(uint){
uint xx = 1;
uint yy = 3;
uint zz = xx + yy;
return(zz);
}
As variáveis globais possuem um escopo global de funcionamento e são palavras-chave reservadas do Solidity. Elas podem ser utilizadas diretamente dentro das funções sem precisar serem declaradas:
function global() external view returns(address, uint, bytes memory){
address remetente = msg.sender;
uint numeroBloco = block.number;
bytes memory dados = msg.data;
return(remetente, numeroBloco, dados);
}
No exemplo acima, utilizamos três variáveis globais comuns: msg.sender
, block.number
e msg.data
, que representam o endereço do remetente da mensagem, a altura do bloco atual e os dados da mensagem, respectivamente. Abaixo seguem algumas variáveis globais comuns. Para uma lista completa, consulte este link:
blockhash(uint blockNumber)
: (bytes32
) Hash de um bloco específico - apenas válido para aproximadamente os últimos 256 blocos, excluindo o bloco atual.block.coinbase
: (address payable
) Endereço do minerador do bloco atual.block.gaslimit
: (uint
) Limite de gas do bloco atual.block.number
: (uint
) Número do bloco atual.block.timestamp
: (uint
) Timestamp do bloco atual, em segundos desde a "unix epoch".gasleft()
: (uint256
) Gas restante.msg.data
: (bytes calldata
) Dados completos da chamada.msg.sender
: (address payable
) Remetente da mensagem (caller atual).msg.sig
: (bytes4
) Primeiros quatro bytes dos dados da chamada (identificador da função).msg.value
: (uint
) Valor em wei enviado na transação atual.
Exemplo:
Não existem pontos flutuantes no Solidity, e o ponto é representado como 0
, para garantir a precisão das transações e evitar perdas de precisão. O uso das unidades de Ether ajuda a evitar erros de cálculo e facilita o manuseio de transações de criptomoedas no contrato.
wei
: 1gwei
: 1e9 = 1000000000ether
: 1e18 = 1000000000000000000
function weiUnit() external pure returns(uint) {
assert(1 wei == 1e0);
assert(1 wei == 1);
return 1 wei;
}
function gweiUnit() external pure returns(uint) {
assert(1 gwei == 1e9);
assert(1 gwei == 1000000000);
return 1 gwei;
}
function etherUnit() external pure returns(uint) {
assert(1 ether == 1e18);
assert(1 ether == 1000000000000000000);
return 1 ether;
}
Exemplo:
É comum definir uma operação que deve ser concluída em uma semana dentro de um contrato, ou que um evento ocorrerá um mês após. Isso garante a precisão da execução do contrato, independentemente de possíveis erros técnicos. Portanto, as unidades de tempo são um conceito importante no Solidity, melhorando a legibilidade e manutenção do contrato.
seconds
: 1minutes
: 60 segundos = 60hours
: 60 minutos = 3600days
: 24 horas = 86400weeks
: 7 dias = 604800
function secondsUnit() external pure returns(uint) {
assert(1 seconds == 1);
return 1 seconds;
}
function minutesUnit() external pure returns(uint) {
assert(1 minutes == 60);
assert(1 minutes == 60 seconds);
return 1 minutes;
}
function hoursUnit() external pure returns(uint) {
assert(1 hours == 3600);
assert(1 hours == 60 minutes);
return 1 hours;
}
function daysUnit() external pure returns(uint) {
assert(1 days == 86400);
assert(1 days == 24 hours);
return 1 days;
}
function weeksUnit() external pure returns(uint) {
assert(1 weeks == 604800);
assert(1 weeks == 7 days);
return 1 weeks;
}
Exemplo:
Nesta aula, apresentamos os tipos de referência, a localização dos dados e o escopo das variáveis no Solidity. O foco principal foi nos termos storage
, memory
e calldata
e suas aplicações. Esses termos foram introduzidos para economizar espaço de armazenamento limitado on-chain e reduzir o consumo de gas
. Na próxima aula, abordaremos arrays nos tipos de referência.