Este artículo explica cómo funciona delegatecall en detalle. La Ethereum Virtual Machine (EVM) ofrece cuatro opcodes para realizar llamadas entre contratos:
CALL (F1)CALLCODE (F2)STATICCALL (FA)- y
DELEGATECALL (F4).
Notablemente, el opcode CALLCODE ha quedado obsoleto desde la versión v5 de Solidity, siendo reemplazado por DELEGATECALL. Estos opcodes tienen una implementación directa en Solidity y pueden ser ejecutados como métodos de variables de tipo address.
Para comprender mejor cómo funciona delegatecall, revisemos primero la funcionalidad del opcode CALL.
CALL
Para demostrar call, considera el siguiente contrato:
contract Called {
uint public number;
function increment() public {
number++;
}
}
La forma más directa de ejecutar la función increment() desde otro contrato es utilizando la interfaz del contrato Called. En esta receta, podemos ejecutar la función con una declaración tan simple como called.increment(), donde called es la dirección de Called. Pero llamar a increment() también se puede lograr utilizando un call de bajo nivel, como se muestra en el contrato a continuación:
contract Caller {
address constant public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138; // Called's address
function callIncrement() public {
calledAddress.call(abi.encodeWithSignature("increment()"));
}
}
Cada variable de tipo address, como la variable calledAddress, tiene un método llamado call. Este método espera como parámetro los datos de entrada a ser ejecutados en la transacción, es decir, el calldata codificado en ABI. En el caso mencionado anteriormente, los datos de entrada deben corresponder a la firma de la función increment(), con el selector de función 0xd09de08a. Empleamos el método abi.encodeWithSignature para generar esta firma a partir de la definición de la función.
Si ejecutas la función callIncrement en el contrato Caller, observarás que la variable de estado number en Called se incrementará en 1. El método call no verifica si la dirección de destino corresponde realmente a un contrato existente, ni si contiene la función especificada.
La transacción call se visualiza en el video a continuación:
Call devuelve una tupla
El método call devuelve una tupla con dos valores. El primer valor es un booleano que indica el éxito o fracaso de la transacción. El segundo valor, de tipo bytes, contiene el valor de retorno de la función ejecutada por el call, codificado en ABI, si lo hubiera.
Para recuperar el retorno del call, podemos modificar la función callIncrement de la siguiente manera:
function callIncrement() public {
(bool success, bytes memory data) = called.call(
abi.encodeWithSignature("increment()")
);
}
El método call nunca revierte. Si la transacción no es exitosa, success será false, y el programador debe manejar esto en consecuencia.
Manejo de fallos en Call
Modifiquemos el contrato anterior para incluir otra llamada a una función inexistente, como se muestra a continuación.
contract Caller {
address public constant calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;
function callIncrement() public {
(bool success, bytes memory data) = called.call(
abi.encodeWithSignature("increment()")
);
if (!success) {
revert("Something went wrong");
}
}
// calls a non-existent function
function callWrong() public {
(bool success, bytes memory data) = called.call(
abi.encodeWithSignature("thisFunctionDoesNotExist()")
);
if (!success) {
revert("Something went wrong");
}
}
}
Creé intencionalmente dos funciones: una con la firma correcta de la función increment y otra con una firma inválida. La primera función devolverá true para success, mientras que la segunda devolverá false. El booleano de retorno se maneja de forma explícita, y la transacción se revertirá si success es false.
Debemos tener cuidado de rastrear si la llamada tuvo éxito o no, y volveremos a este problema en breve.
Qué hace la EVM bajo el capó
El propósito de la función increment es incrementar la variable de estado llamada number. Dado que la EVM no tiene conocimiento de las variables de estado sino que opera en los slots de almacenamiento (storage slots), lo que realmente hace la función es aumentar el valor en el primer slot del storage, conocido como slot 0. Esta operación ocurre dentro del storage del contrato Called.

Haber revisado cómo utilizar el método call nos ayudará a formarnos una idea sobre cómo usar delegatecall.
DELEGATECALL
Un contrato que realiza un delegatecall a un smart contract de destino ejecuta la lógica del contrato de destino dentro de su propio entorno.
Un modelo mental es que copia el código del smart contract de destino y ejecuta ese código por sí mismo. El smart contract de destino se conoce comúnmente como el “contrato de implementación” (implementation contract).
Al igual que call, delegatecall también recibe como parámetro los datos de entrada que ejecutará el contrato de destino.
Aquí está el código del contrato Called, correspondiente a la animación anterior, que se ejecuta en el entorno de Caller:
contract Called {
uint public number;
function increment() public {
number++;
}
}
Y el código para Caller
contract Caller {
uint public number;
function callIncrement(address _calledAddress) public {
_calledAddress.delegatecall(
abi.encodeWithSignature("increment()")
);
}
}
Este delegatecall ejecutará la función increment; sin embargo, la ejecución ocurrirá con una diferencia crucial. El storage del contrato Caller será modificado, NO el storage de Called. Es como si el contrato Caller tomara prestado el código de Called para ejecutarlo en su propio contexto.
El siguiente diagrama ilustra aún más cómo delegatecall modifica el storage de Caller en lugar del de Called.

La imagen a continuación ilustra la distinción entre ejecutar la función increment usando call y delegatecall.

Colisión de slots de almacenamiento
El contrato que emite un delegatecall debe ser extremadamente cuidadoso para predecir cuáles de sus slots de almacenamiento se modificarán. El ejemplo anterior funcionó a la perfección porque Caller no utilizó la variable de estado en el slot 0. Un bug común al usar delegatecall es olvidar este hecho. Veamos un ejemplo de esto.
contract Called {
uint public number;
function increment() public {
number++;
}
}
contract Caller {
// there is a new storage variable here
address public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;
uint public myNumber;
function callIncrement() public {
calledAddress.delegatecall(
abi.encodeWithSignature("increment()")
);
}
}
Nota que en el contrato actualizado arriba, el contenido del slot 0 es la dirección del contrato Called, mientras que la variable myNumber ahora se almacena en el slot 1.
Si despliegas los contratos proporcionados y ejecutas la función callIncrement, el slot 0 del storage de Caller se incrementará, pero la variable calledAddress es la que está allí, no la variable myNumber.
El siguiente video ilustra este bug:
Ilustremos lo que sucedió a continuación.

Por lo tanto, se debe tener precaución al usar delegatecall, ya que inadvertidamente puede romper nuestro contrato. En el ejemplo anterior, es probable que la intención del programador no fuera alterar la variable calledAddress a través de la función callIncrement.
Hagamos un pequeño cambio a Caller moviendo la variable de estado myNumber al slot 0.
contract Caller {
uint public myNumber;
address public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;
function callIncrement() public {
called.delegatecall(
abi.encodeWithSignature("increment()")
);
}
}
Ahora, al ejecutar la función callIncrement, la variable myNumber se incrementará, ya que este es el propósito de la función increment. Elegí a propósito el nombre de la variable en Caller de forma diferente al de Called para demostrar que el nombre de la variable no importa; lo fundamental es en qué slot se encuentra. Alinear las variables de estado de ambos contratos es crucial para el funcionamiento adecuado de delegatecall.
Desacoplar la implementación de los datos
Uno de los usos más importantes de delegatecall es desacoplar el contrato donde se almacenan los datos, como Caller en este caso, del contrato donde reside la lógica de ejecución, como Called. Por lo tanto, si uno desea alterar la lógica de ejecución, simplemente puede reemplazar Called con otro contrato y actualizar la referencia al contrato de implementación, sin tocar el storage. Caller ya no está limitado por las funciones que tiene, puede hacer delegatecall a las funciones que necesita de otros contratos.
En caso de que exista la necesidad de cambiar la lógica de ejecución, por ejemplo, restando el valor de myNumber en 1 unidad en lugar de sumarlo, puedes crear un nuevo contrato de implementación, como se muestra a continuación.
contract NewCalled {
uint public number;
function increment() public {
number = number - 1;
}
}
Desafortunadamente, no es posible cambiar el nombre de la función que será llamada, ya que hacer esto alteraría su firma.
Después de crear el nuevo contrato de implementación, NewCalled, uno simplemente puede desplegar este nuevo contrato y cambiar la variable de estado calledAddress en Caller. Por supuesto, Caller necesitaría tener un mecanismo para cambiar la dirección hacia la cual emite el delegateCall, lo cual no incluimos para mantener el código conciso.
Hemos modificado con éxito la lógica de negocio utilizada por el contrato Caller. Separar los datos de la lógica de ejecución nos permite crear smart contracts actualizables (upgradable) en Solidity.

En la imagen de arriba, el contrato de la izquierda maneja tanto los datos como la lógica. A la derecha, el contrato superior contiene los datos, pero el mecanismo para actualizar los datos se encuentra en el contrato lógico. Para actualizar los datos, se realiza un delegatecall al contrato lógico.
Manejo de retornos de delegatecall
Al igual que call, delegatecall también devuelve una tupla que contiene dos valores: un booleano que indica el éxito de la ejecución y el retorno de la función ejecutada a través de delegatecall, en bytes. Para ver cómo manejar este retorno, escribamos un nuevo ejemplo.
contract Called {
function calculateDiscountPrice(
uint256 amount,
uint256 discountRate
) public pure returns (uint) {
return amount - (amount * _discountRate)/100;
}
}
contract Caller {
uint public price = 200;
uint public discountRate = 10;
address public called;
function setCalled(address _called) public {
called = _called;
}
function setDiscountPrice() public {
(bool success, bytes memory data) = called.delegatecall(
abi.encodeWithSignature(
"calculateDiscountPrice(uint256,uint256)",
price,
discountRate)
);
if (success) {
uint newPrice = abi.decode(data, (uint256));
price = newPrice;
}
}
}
El contrato Called contiene la lógica para calcular un precio con descuento. Utilizamos esta lógica ejecutando la función calculateDiscountPrice a través de delegatecall. Esta función devuelve un valor, el cual debemos decodificar utilizando abi.decode. Antes de tomar cualquier decisión basada en este valor de retorno, es crucial verificar si la función se ejecutó de manera exitosa, de lo contrario podríamos intentar parsear un retorno que no está allí o terminar parseando un string de motivo de revert.
Cuándo call y delegatecall devuelven false
Un punto crucial a comprender es cuándo el valor de success será true o false. Esencialmente, depende de si la función que se está ejecutando se revertirá o no. Hay tres formas en que una ejecución puede revertirse:
- si encuentra un opcode REVERT,
- si se queda sin gas,
- si intenta algo prohibido, como dividir por cero.
Si la función que se está ejecutando mediante delegatecall (o call) encuentra alguna de estas condiciones, se revertirá, y el valor de retorno de delegatecall será false
Una pregunta que a menudo confunde a los desarrolladores es por qué un delegatecall a un contrato inexistente no se revierte y aún reporta que la ejecución fue exitosa. Según lo que hemos dicho, una dirección vacía nunca cumplirá con una de las tres condiciones para revertirse, por lo que nunca se revertirá.
Otro ejemplo de trampas (gotchas) en variables de almacenamiento
Hagamos una ligera modificación al código anterior para dar otro ejemplo de bugs relacionados con la disposición del almacenamiento (storage layout).
El contrato Caller todavía invoca un contrato de implementación a través de delegatecall, pero ahora el contrato Called lee un valor de una variable de estado. Esto puede parecer una alteración menor, pero en realidad conduce al desastre. ¿Puedes adivinar por qué?
contract Called {
uint public discountRate = 20;
function calculateDiscountPrice(uint256 amount) public pure returns (uint) {
return amount - (amount * discountRate)/100;
}
}
contract Caller {
uint public price = 200;
address public called;
function setCalled(address _called) public {
called = _called;
}
function setDiscount() public {
(bool success, bytes memory data) =called.delegatecall(
abi.encodeWithSignature(
"calculateDiscountPrice(uint256)",
price
)
);
if (success) {
uint newPrice = abi.decode(data, (uint256));
price = newPrice;
}
}
}
El problema surge porque calculateDiscountPrice está leyendo una variable de estado, específicamente la del slot 0. Recuerda que en delegatecall, las funciones se ejecutan en el storage del contrato que realiza la llamada. En otras palabras, ¡puedes pensar que estás usando la variable discountRate del contrato Called para calcular el nuevo price, pero en realidad estás usando la variable price del contrato Caller! Las variables de almacenamiento Called.discountRate y Called.price ocupan el slot 0.
Recibirás un descuento del 200%, lo cual es bastante sustancial (y causará que la función se revierta, ya que el nuevo precio calculado se volverá negativo, lo cual no está permitido para una variable de tipo uint).
Variables Inmutables y Constantes en delegatecall: La Historia de un Bug
Otro problema complicado con delegatecall surge cuando están involucradas variables inmutables o constantes. Examinemos un ejemplo que muchos programadores experimentados en Solidity malinterpretan:
contract Caller {
uint256 private immutable a = 3;
function getValueDelegate(address called) public pure returns (uint256) {
(bool success, bytes memory data) = called.delegatecall(
abi.encodeWithSignature("getValue()"));
return abi.decode(data, (uint256)); // is this 3 or 2?
}
}
contract Called {
uint256 private immutable a = 2;
function getValue() public pure returns (uint256) {
return a;
}
}
La pregunta es: al ejecutar getValueDelegate, ¿el retorno será 2 o 3? Razonemos al respecto.
- La función
getValueDelegateejecuta la funcióngetValue, que supuestamente devuelve el valor correspondiente a la variable de estado en el slot 0. - Como es delegatecall, debemos examinar el slot en el contrato que hace la llamada, no en el contrato llamado.
- El valor de la variable
aenCalleres 3, por lo que la respuesta debe ser 3. Acertado.
Sorprendentemente, la respuesta correcta es 2. ¡¿POR QUÉ?!
Las variables de estado inmutables o constantes no son verdaderas variables de estado: no ocupan un slot. Cuando declaramos variables inmutables, su valor está embebido (hardcoded) en el bytecode del contrato que se ejecuta durante el delegatecall. Por lo tanto, la función getValue devuelve el valor hardcodeado 2.
msg.sender, msg.value y address(this)
Si usamos msg.sender, msg.value y address(this) dentro del contrato Called, todos estos valores corresponderán a los valores de msg.sender, msg.value y address(this) del contrato Caller. Recordemos cómo opera delegatecall: todo ocurre dentro del contexto del contrato caller (el que llama). El contrato de implementación simplemente proporciona el bytecode a ser ejecutado, nada más.

Apliquemos este concepto en un ejemplo. Consideremos el siguiente código:
contract Called {
function getInfo() public payable returns (address, uint, address) {
return (msg.sender, msg.value, address(this));
}
}
contract Caller {
function getDelegatedInfo(
address _called
) public payable returns (address, uint, address) {
(bool success, bytes memory data) = _called.delegatecall(
abi.encodeWithSignature("getInfo()")
);
return abi.decode(data, (address, uint, address));
}
}
En el contrato Called, estoy utilizando msg.sender, msg.value y address(this), y devolviendo estos valores en la función getInfo. En la figura a continuación, se representa una ejecución de getDelegateInfo utilizando Remix, mostrando los valores devueltos.
msg.sendercorresponde a la cuenta que ejecutó la transacción, específicamente la primera cuenta predeterminada de Remix, que es0x5B38Da6a701c568545dCfcB03FcB875f56beddC4.msg.valuerefleja el valor de 1 ether, el cual fue enviado en la transacción original.address(this)es la dirección del contrato Caller, como se puede observar en el lado izquierdo de la figura, en lugar de la dirección del contrato Called.

En Remix, mostramos los valores del log para msg.sender (0), msg.value (1) y address(this) (2).
msg.data y datos de entrada en delegatecall
La propiedad msg.data devuelve el calldata del contexto que se está ejecutando. Cuando msg.data se llama en una función que está siendo ejecutada directamente a través de una transacción por una EOA, msg.data representa los datos de entrada de la transacción.
Cuando ejecutamos un call o un delegatecall, especificamos como argumento los datos de entrada que se ejecutarán en el contrato de implementación. Por lo tanto, el calldata original difiere del calldata dentro del sub-contexto creado por delegatecall, y en consecuencia msg.data diferirá.

El código a continuación se utilizará para demostrar esto.
contract Called {
function returnMsgData() public pure returns (bytes memory) {
return msg.data;
}
}
contract Caller {
function delegateMsgData(
address _called
) public returns (bytes memory data) {
(, data) = _called.delegatecall(
abi.encodeWithSignature("returnMsgData()"));
}
}
La transacción original ejecuta la función delegateMsgData, la cual requiere un parámetro de tipo address. Como resultado, los datos de entrada consistirán en la firma de la función junto con una dirección, codificados en ABI.
La función delegateMsgData, a su vez, hace un delegatecall a la función returnMsgData. Para lograr esto, el calldata pasado al runtime debe contener la firma de returnMsgData. En consecuencia, el valor de msg.data dentro de returnMsgData es su propia firma, dada por 0x0b1c837f.
En la imagen a continuación, podemos observar que el retorno de returnMsgData es su propia firma, codificada en ABI.

La salida decodificada es la firma de la función returnMsgData, codificada en ABI como bytes.
Codesize como contraejemplo
Mencionamos que podemos concebir a delegatecall con la idea de que estamos tomando prestado el bytecode del contrato de implementación y ejecutándolo en el contrato que hace la llamada. Existe una excepción a esto, el opcode CODESIZE.
Supongamos que un smart contract tiene CODESIZE en su bytecode, CODESIZE devuelve el tamaño de ese contrato. Codesize no devuelve el tamaño del código del caller durante un delegatecall — devuelve el tamaño del código al que se le hizo el delegatecall.
Para demostrar esta propiedad, hemos proporcionado el código a continuación. En Solidity, CODESIZE se puede ejecutar en assembly mediante la función codesize(). Tenemos dos contratos de implementación, CalledA y CalledB, que difieren solo por una variable local (unused en ContractB — esa variable está ausente en ContractA), destinada a garantizar que los contratos tengan tamaños diferentes. Estos contratos son llamados a través de delegatecall por la función getSizes del contrato Caller.
// codesize 1103
contract Caller {
function getSizes(
address _calledA,
address _calledB
) public returns (uint sizeA, uint sizeB) {
(, bytes memory dataA) = _calledA.delegatecall(
abi.encodeWithSignature("getCodeSize()")
);
(, bytes memory dataB) = _calledB.delegatecall(
abi.encodeWithSignature("getCodeSize()")
);
sizeA = abi.decode(dataA, (uint256));
sizeB = abi.decode(dataB, (uint256));
}
}
// codesize 174
contract CalledA {
function getCodeSize() public pure returns (uint size) {
assembly {
size := codesize()
}
}
}
// codesize 180
contract CalledB {
function getCodeSize() public pure returns (uint size) {
uint unused = 100;
assembly {
size := codesize()
}
}
}
// You can use this contract to check the size of contracts
contract MeasureContractSize {
function measureConctract(address c) external view returns (uint256 size){
size = c.code.length;
}
}
Si la función codesize devolviera el tamaño del contrato Caller, los valores devueltos por getSizes() mediante la llamada de delegate a ContractA y ContractB serían los mismos. Es decir, serían del tamaño de Caller, que es 1103. Sin embargo, como podemos ver en la figura a continuación, los valores son diferentes, indicando explícitamente que estos son los tamaños de CalledA y CalledB.

Delegatecall de un delegatecall
Uno podría preguntarse: ¿Qué sucede si un contrato emite un delegatecall a un segundo contrato que emite un delegatecall a un tercer contrato? En tal caso, el contexto persistirá como el del contrato que inició el primer delegatecall, en lugar del contrato intermedio.
Funciona de la siguiente manera:
- El contrato
Callerhace un delegatecall a la funciónlogSender()en el contratoCalledFirst. - El propósito de esta función es emitir un evento que registre (log)
msg.sender. - Además, el contrato
CalledFirst, aparte de crear este log, también hace un delegatecall al contratoCalledLast. - El contrato
CalledLasttambién emite un evento, que de igual forma registra elmsg.sender.
A continuación se presenta un diagrama que representa este flujo.

Recuerda, todo lo que hace delegatecall es tomar prestado el bytecode del contrato llamado. Una forma de visualizar esto es que el bytecode es “absorbido” temporalmente en el contrato que hace la llamada. Al verlo de esa manera, vemos que msg.sender es siempre el msg.sender original, ya que todo está sucediendo dentro de Caller. Ve la animación a continuación:
A continuación, proporcionamos algo de código fuente para probar el concepto de un delegatecall a un delegatecall:
contract Caller {
address calledFirst = 0xF27374C91BF602603AC5C9DaCC19BE431E3501cb;
function delegateCallToFirst() public {
calledFirst.delegatecall(
abi.encodeWithSignature("logSender()")
);
}
}
contract CalledFirst {
event SenderAtCalledFirst(address sender);
address constant calledLast = 0x1d142a62E2e98474093545D4A3A0f7DB9503B8BD;
function logSender() public {
emit SenderAtCalledFirst(msg.sender);
calledLast.delegatecall(
abi.encodeWithSignature("logSender()")
);
}
}
contract CalledLast {
event SenderAtCalledLast(address sender);
function logSender() public {
emit SenderAtCalledLast(msg.sender);
}
}
Podríamos llegar a pensar que el msg.sender en CalledLast será la dirección de CalledFirst, ya que fue este el que llamó a CalledLast, pero esto no respetaría nuestro modelo de que el bytecode del contrato llamado mediante delegatecall simplemente se toma prestado, y que el contexto es siempre del contrato que ejecutó el delegatecall.
El resultado final es que ambos valores de msg.sender corresponden a la cuenta que inició la transacción con Caller.delegateCallToFirst(). Esto se puede observar en la figura a continuación, donde ejecutamos este proceso en Remix y capturamos los logs.

msg.sender es el mismo en CalledFirst y CalledLast
Una fuente de confusión es que algunos podrían describir esta operación como “Caller hace delegatecall a CalledFirst y CalledFirst hace delegatecall a CalledLast”. Pero esto hace sonar como si CalledFirst estuviera realizando el delegatecall — y ese no es el caso. CalledFirst está proporcionando el bytecode a Called — y ese bytecode está haciendo un delegatecall a CalledLast — desde Called.
Call desde un delegatecall
Introduzcamos un giro argumental y modifiquemos el contrato CalledFirst. Ahora, CalledFirst invocará a CalledLast utilizando call, no delegatecall.

En otras palabras, el contrato CalledFirst debe actualizarse con el siguiente código:
contract CalledFirst {
event SenderAtCalledFirst(address sender);
address constant calledLast = ...;
function logSender() public {
emit SenderAtCalledFirst(msg.sender);
calledLast.call(
abi.encodeWithSignature("logSender()")
); // this is new
}
}
Surge la pregunta: ¿Cuál será el msg.sender registrado en el evento SenderAtCalledLast? La siguiente animación ilustra lo que sucede:

Cuando Caller llama a una función en CalledFirst a través de delegatecall, esa función se ejecuta dentro del contexto de Caller. Recuerda, CalledFirst simplemente “presta” su bytecode para ser ejecutado por Caller. En este punto, es como si ejecutáramos msg.sender en el contrato Caller, lo que significa que msg.sender es la dirección que inició la transacción.

Ahora, CalledFirst llama a CalledLast, pero CalledFirst se está utilizando en el contexto de Caller, por lo que es como si Caller hiciera una llamada (call) a CalledLast. En este caso, el msg.sender en CalledLast será la dirección de Caller.
En la figura a continuación, observamos los logs en Remix. Nota que esta vez los valores de msg.sender son diferentes.

msg.sender en CalledLast es la dirección de Caller
Ejercicio: Si Caller llama (calls) a CalledFirst y CalledFirst hace delegatecall a CalledLast, y cada contrato registra msg.sender, ¿qué message sender registrará cada contrato?
Delegatecall de bajo nivel
En esta sección, utilizaremos delegatecall en YUL para explorar su funcionalidad a un nivel más profundo. Las funciones en YUL se asemejan mucho a la sintaxis de los opcodes, lo que hace beneficioso examinar primero la definición del opcode DELEGATECALL.
DELEGATECALL toma 6 argumentos de la pila (stack), en orden: gas, address, argsOffset, argsSize, retOffset y retSize, y devuelve un valor al stack indicando si la operación se llevó a cabo con éxito (1) o no (0).
La explicación de cada argumento es la siguiente (tomada de evm.codes):
- gas: cantidad de gas a enviar al subcontexto para ejecutar. El gas que no es utilizado por el subcontexto es devuelto a este.
- address: la cuenta cuyo código se va a ejecutar.
- argsOffset: desplazamiento (offset) en bytes en la memoria, el calldata del subcontexto.
- argsSize: tamaño en bytes a copiar (tamaño del calldata).
- retOffset: desplazamiento (offset) en bytes en la memoria, donde almacenar los datos de retorno del subcontexto.
- retSize: tamaño en bytes a copiar (tamaño de los datos de retorno).
No está permitido enviar ether a un contrato usando delegatecall (¡imagina los posibles exploits si lo estuviera!). El opcode CALL, por otro lado, permite la transferencia de ether e incluye un parámetro adicional para indicar cuánto ether se debe enviar.
En YUL, la función delegatecall refleja el opcode DELEGATECALL e incluye los mismos 6 argumentos mencionados anteriormente. Su sintaxis es:
delegatecall(g, a, in, insize, out, outsize).
A continuación, presentamos un contrato con dos funciones que realizan la misma acción, ejecutando un delegatecall. Una está escrita en Solidity puro, y la otra incorpora YUL.
contract DelegateYUL {
function delegateInSolidity(
address _address
) public returns (bytes memory data) {
(, data) = _address.delegatecall(
abi.encodeWithSignature("sayOne()")
);
}
function delegateInYUL(
address _address
) public returns (uint data) {
assembly {
mstore(0x00, 0x34ee2172) // Load the calldata I intend to send into memory at 0x00. The first slot will become 0x0000000000000000000000000000000000000000000000000000000034ee2172
let result := delegatecall(gas(), _address, 0x1c, 4, 0, 0x20) // The third parameter indicates the starting position in memory where the calldata is located, the fourth parameter specifies its size in bytes, and the fifth parameter specifies where the returned calldata, if any, should be stored in memory
data := mload(0) // Read delegatecall return from memory
}
}
}
contract Called {
function sayOne() public pure returns (uint) {
return 1;
}
}
En la función delegateInSolidity, utilizo el método delegatecall en Solidity, pasando como parámetro la firma de la función sayOne, calculada utilizando el método abi.encodeWithSignature.
Si no sabemos el tamaño del retorno de antemano, no te preocupes, podemos usar la función returndatacopy más adelante para manejar esto. En otro artículo, cuando profundicemos en la escritura de contratos actualizables (upgradables) usando delegatecall, cubriremos todos estos detalles.
EIP 150 y gas reenviado
Una nota sobre un problema relacionado con el gas reenviado (forwarded): Utilizamos la función gas() como el primer parámetro de delegatecall, que devuelve el gas disponible. Esto debería indicar que pretendemos reenviar todo el gas disponible. Sin embargo, desde el Tangerine Whistle fork, ha habido un límite de 63/64 del gas total posible para reenviar a través de delegatecall (y otros opcodes). En otras palabras, aunque la función gas() devuelve todo el gas disponible, solo 63/64 del mismo se reenvía al nuevo subcontexto, mientras que 1/64 se retiene.
Conclusión
Para concluir este artículo, resumamos lo que hemos aprendido. Delegatecall permite la ejecución de funciones definidas en otros contratos dentro del contexto del contrato que realiza la llamada. El contrato llamado, también conocido como contrato de implementación, simplemente proporciona su bytecode, y nada dentro de él es modificado o recuperado de su storage.
Delegatecall se emplea para separar el contrato donde se almacenan los datos del contrato donde se aloja la lógica de negocio o la implementación de funciones. Esto forma la base del patrón más utilizado de actualizabilidad de contratos en Solidity. Sin embargo, como hemos observado, delegatecall debe ser utilizado con mucho cuidado, ya que pueden ocurrir cambios no intencionales en las variables de estado, lo que potencialmente inutilizaría el contrato que hace la llamada.
Aprende más con RareSkills
Para aquellos nuevos en Solidity, consulten nuestro curso de Solidity gratuito. Los desarrolladores intermedios de Solidity por favor vean nuestro Solidity Bootcamp.
Autoría
Este artículo fue escrito por João Paulo Morais en colaboración con RareSkills.
Originalmente publicado el 3 de mayo