Pular para o conteúdo
0%

Como funcionam as chamadas de contrato em Yul?

Mark Jonathas headshot

Escrito por Mark Jonathas

Publicado em 1 de agosto de 20237 min de leitura

Yul é uma linguagem de programação intermediária que pode ser usada para escrever uma forma de linguagem assembly dentro de smart contracts. Agora que você entende a sintaxe, o storage e como a memória funciona em Yul, é hora de chamar contratos!

Como funcionam as chamadas de contrato em yul?

Na seção final desta série veremos como funcionam as chamadas de contrato em Yul. Antes de mergulharmos em alguns exemplos, precisamos aprender mais algumas Operações Yul primeiro. Vamos dar uma olhada.‍

Instruction
Explanation

gas()

A quantidade de gas que ainda está disponível para execução

gasPrice()

O gas price da transação

address()

O endereço do contrato atual

balance(a)

O saldo de ether, em wei, do endereço a

selfbalance()

O mesmo que chamar balance(address()), mas ligeiramente mais barato

caller()

O endereço que chamou o contrato (msg.sender)

origin()

O endereço que originou a transação (tx.origin)

callvalue()

A quantidade de ether, em wei, que foi enviada na chamada do contrato (msg.value)

calldataload(p)

O calldata começando na posição p (apenas 32 bytes de dados)

calldatacopy(t, f, s)

O calldata é copiado para a posição de memória t. Começando a partir da posição f no calldata. Copia s bytes de dados.

extcodesize(a)

O tamanho do código no endereço a

returndatasize()

O tamanho do último returndata

returndatacopy(t, f, s)

Copia s bytes do return data na posição f para a memória na posição t

timestamp()

O timestamp atual do bloco em segundos

number()

O número do bloco atual

call(g, a, v, in, insize, out, outsize)

Chama um contrato no endereço a, com gas g, passando v wei como msg.value, passando tx.data a partir da posição de memória in - insize, e armazenando o return data na posição de memória out - outsize. Retorna 1 se a chamada foi bem-sucedida, caso contrário retorna 0.

delegatecall(g, a, in, insize, out, outsize)

Similar a call(). A principal diferença é que delegatecall() atualiza as variáveis de estado no contrato que faz a chamada. Frequentemente usado para contratos proxy. É importante notar que o storage layout deve ser idêntico ao do contrato chamado, para evitar sobrescrever variáveis de storage não intencionais. Note que o parâmetro v está ausente. Não é possível enviar ether com delegatecall().

staticcall(g, a, in, insize, out, outsize)

Similar a call(). Exceto que não pode ser usado para chamar contratos que alteram o estado da blockchain (ou seja, funções marcadas como pure e view). Note que o parâmetro v está ausente. Não é possível enviar ether com staticcall().

Certo, agora vamos olhar para alguns novos contratos para esses exemplos. Primeiro, vamos ver o contrato que iremos chamar.

Este contrato tem duas variáveis de storage var1 e var2, armazenadas nos storage slots 1 e 2 respectivamente.

A função a() exige que o usuário envie pelo menos 1 ether ao contrato, caso contrário ela reverte.

Em seguida, a função a() atualiza var1 e var2 e as retorna.

A função b() simplesmente lê var1 e var2 e as retorna.

Antes de passarmos ao nosso contrato que chama o contrato CallMe, precisamos de um momento para entender os function selectors.

Vamos olhar para o seguinte calldata de uma transação 0x773d45e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002.

Os primeiros 4 bytes do calldata são o que chamamos de function selector (0x773d45e0). É assim que a EVM sabe qual função você quer chamar. Derivamos o function selector obtendo os primeiros 4 bytes do hash de uma string da assinatura da função.

Então a assinatura da função a() seria a(uint256,uint256).

Calculando o hash dessa string obtemos: 0x773d45e097aa76a22159880d254a5f1db8365bc2d0f0987a82bda7dfd3b9c8aa.

Olhando para os primeiros 4 bytes, vemos que é igual a 0x773d45e0.

Note a ausência de espaços na assinatura. Isso é importante porque adicionar espaços resultaria em um hash completamente diferente. Você não precisa se preocupar em obter os selectors para nossos exemplos de código, eu os fornecerei.

Vamos começar olhando para o storage layout.

Note como var1 & var2 têm o mesmo layout do contrato CallMe. Você deve se lembrar de eu ter dito que o layout precisa ser o mesmo do nosso outro contrato para que delegatecall() funcione corretamente.

Atendemos a essas necessidades e conseguimos ter outras variáveis (selectorA & selectorB) desde que nossas novas variáveis sejam adicionadas ao final. Isso evita colisões de storage.

Agora estamos prontos para fazer nossa primeira chamada de contrato.

Como usar staticcall() em yul

Vamos começar com algo simples, staticcall(). Aqui está nossa função:

Aqui está o que está acontecendo:

  • Recuperar o function selector de b() do storage carregando o slot 2 (ambos os selectors estão empacotados em um único slot)
  • Deslocar à direita 4 bytes (32 bits) para isolar selectorB
  • Armazenar o function selector no scratch space da memória
  • Fazer nossa static call
  • Passar gas() (você também pode especificar a quantidade de gas)
  • Passar o parâmetro _callMe como endereço do contrato
  • Verificar se a chamada de função foi bem-sucedida, caso contrário retornamos sem dados.
  • Retornar nossos dados da memória e ver os valores 1 e 2

Observação: 0x1c e 0x20 indicam que queremos passar os últimos 4 bytes do que armazenamos no scratch space. Os dois últimos parâmetros de staticcall() especificam que queremos armazenar o return data nas posições de memória 0x80 - 0xc0.

Como usar call() em yul

A seguir vamos olhar para call(). Vamos chamar a função a() de CallMe. Lembre-se de enviar pelo menos 1 ether ao contrato! Vou passar 3 e 4 como _var1 & _var2 neste exemplo.

Aqui está o código:

Ok, então de forma semelhante ao nosso último exemplo, precisamos carregar o slot2. Desta vez, no entanto, vamos mascarar selectorB para isolar selectorA.

Agora vamos armazenar o selector em 0x80.

Como precisamos de parâmetros do calldata, vamos usar calldatacopy(). Estamos dizendo a calldatacopy() para:

  • armazenar nosso calldata na posição de memória 0xa0
  • pular os primeiros 36 bytes
  • armazenar o tamanho do calldata menos 36 bytes

Observação: os primeiros 4 bytes que pulamos são o function selector de callA() e os próximos 32 bytes são o endereço de callMe.

Agora estamos prontos para fazer nossa chamada de contrato!

Assim como da última vez, passamos gas() e _callMe. No entanto, desta vez passamos nosso calldata a partir de 0x9c (últimos 4 bytes da série de memória 0x80) - 0xe0, e armazenamos nossos dados na posição de memória 0x100 - 0x120.

Novamente, verificamos se a chamada foi bem-sucedida e retornamos nossa saída. Se verificarmos o contrato CallMe, veremos que os valores foram atualizados com sucesso para 3 e 4.

Para maior clareza sobre o que está acontecendo, aqui está o layout de memória logo antes de retornarmos:

Memory Location
Value Stored

0x00

Scratch Space (Vazio)

0x20

Scratch Space (Vazio)

0x40

0x80 (Nunca atualizamos o Free Memory Pointer, já que todas as operações são feitas manualmente em Yul.)

0x60

Vazio

0x80

0x0000000000000000000000000000000000000000000000000000000000773d45e0 (Function Selector)

0xa0

0x0000000000000000000000000000000000000000000000000000000000000003 (_var1)

0xc0

0x0000000000000000000000000000000000000000000000000000000000000004 (_var2)

0xe0

Vazio

0x100

0x0000000000000000000000000000000000000000000000000000000000000003 (Primeiro valor retornado)

0x120

0x0000000000000000000000000000000000000000000000000000000000000004 (Segundo valor retornado)

0x140

Free Memory Pointer (Vazio)

Como usar delegate call em yul

Em nossa última seção, veremos delegatecall(). O código será quase idêntico ao de call(), com apenas uma mudança.

A única mudança que fizemos foi trocar call() por delegatecall() e remover callvalue().

Não precisamos de um callvalue(), porque delegate call executa o código de CallMe dentro de seu próprio estado. Portanto, o statement require() em a() está verificando se ether foi enviado ao nosso contrato Caller. Se verificarmos var1 e var2 em CallMe, não veremos mudanças. No entanto, var1 e var2 no nosso contrato Caller foram atualizadas com sucesso.

Isso encerra nossa seção sobre chamadas de contrato! A seguir, aprenda como Memory, Storage e Smart Contract Calls funcionam em Yul.

Background gradient

Construa magia blockchain

A Alchemy combina os produtos e ferramentas de desenvolvimento Web3 mais poderosos com recursos, comunidade e suporte lendário.