Funciones call y delegatecall en Solidity

En esta nueva serie de tutoriales vamos a aprender qué son y cómo usar las funciones call y delegatecall de Solidity.
Por último estudiaremos cómo usar y leer el contenido de calldata.

Parte 1 call

Vamos a empezar con la función call ya que es la más fácil de comprender.
En Solidity, call es una función de bajo nivel la cual permite a un contrato invocar funciones de otro contrato. Esta función usará el contexto del contrato al que se llama.
Pongamos el siguiente ejemplo. Tenemos dos contratos, llamados A y B. El contrato B tiene una función que, mediante call, invoca a una función del contrato A.
El código de los contratos es el siguiente.

pragma solidity ^0.8;

contract A {
    uint public a;
    function llamandoPruebaContratoB(uint _a) public payable {
        a = _a;
    }
}

contract B {
    function prueba(address payable _direccionContratoB) public payable {
        (bool success,) = _direccionContratoB.call(abi.encodeWithSignature("llamandoPruebaContratoB(uint256)", 1));
        require(success, "Ha ocurrido un error");
    }
}

La función del contrato A es muy simple, tan solo hay una función que cambia el estado del contrato.
El contrato B es dónde está la parte interesante de este tutorial. La función prueba recibe como parámetro la dirección del contrato A.
La dirección del contrato A la conseguimos en el momento en que se despliegan los contratos. Nosotros vamos a usar Hardhat, pero podéis usar Remix, Foundry, etc.
Dentro de la función prueba es dónde está el código que nos interesa. Mediante la función de bajo nivel call, pasando como parámetro lo que la función encodeWithSignature genere.
Lo que encodeWithSignature hace es crear para nosotros la información necesaria para realizar la llamada (calldata) a la función en el contrato A.
El primer argumento es el nombre de la función y sus parámetros. Si la función tuviera más de un parámetro es importante no añadir espacios. Otro tema a tener en cuenta es que cualquier uint se debe indicar como uint256.
Después del “function signature” (llamandoPruebaContratoB(uint256) en nuestro ejemplo) añadimos el valor de los parámetros que queremos enviarle a al función, separados por comas.

Vamos a desplegar los contratos usando Hardhat.

Primero necesitamos copiar el contenido de los contratos en el fichero contracts/Call.sol

Después editamos el script encargado de desplegar los contratos, scripts/deploy.js

const hre = require("hardhat");

async function main() {
  const a = await A.deploy();
  await a.deployed();

  const b = await B.deploy();
  await b.deployed();

  console.log(
    `Desplegado el contrato A en: ${a.address} y el contrato B en; ${a.address}`
  );
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Instalamos el paquete de Hardat.

npm init
npm install --save-dev hardhat
npx hardhat

Abrimos un nuevo terminal en nuestro IDE (Visual Studio Code por ejemplo) y ejecutamos el nodo.

npx hardhat node

De nuevo abrimos un terminal nuevo y ejecutamos el siguiente comando.

npx hardhat run --network localhost scripts/deploy.js

Nos devolverá algo similar a: Desplegado el contrato A en: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 y el contrato B en; 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9

Una vez desplegados los contratos podemos hacer la prueba y ver como desde el contrato B llamamos a la función llamandoPruebaContratoB del contrato A y se modifica el estado de la variable a.

Accedemos al nodo para interactuar con los contratos recién desplegados.

npx hardhat console --network localhost
const A = await ethers.getContractFactory('A');
const a = await A.attach('0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0');

const B = await ethers.getContractFactory('B');
const b = await B.attach('0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9');

Miramos el valor actual de la variable a en el contrato A.

await a.a();
BigNumber { value: "0" }

Invocamos a la función prueba del contrato B, la cual mediante la función de bajo nivel call llamará a la función llamandoPruebaContratoB del contrato A y modificará el estado de la variable a 1.

await a.a();
BigNumber { value: "1" }

En la parte 2 veremos cómo usar delegatecall, el cual usa el contexto del contrato que invoca en vez del contrato al que se invoca.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *