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 sido obsoleto desde Solidity v5, siendo reemplazado por DELEGATECALL. Estos opcodes tienen una implementación directa en Solidity y pueden ejecutarse como métodos de variables de tipo address.
Para tener una mejor comprensión de cómo funciona delegatecall, repasemos primero la funcionalidad del opcode CALL.
CALL
Para demostrar call considere 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()"));
}
}
Toda 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 que se ejecutarán en la transacción, es decir, un 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 de call se visualiza en el siguiente video:
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 hay.
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 se revierte. Si la transacción no es successful, success será false, y el programador necesita manejar esto en consecuencia.
Manejando fallos de 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 explícitamente, y la transacción se revertirá si success es false.
Debemos tener cuidado de rastrear si el call tuvo éxito o no, y volveremos a este problema en breve.
Lo que 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 ranuras de almacenamiento, lo que la función realmente hace es aumentar el valor en la primera ranura del almacenamiento, conocida como ranura 0. Esta operación ocurre dentro del almacenamiento del contrato Called.

Haber repasado cómo usar el método call nos ayudará a formarnos una idea sobre cómo usar delegatecall.
DELEGATECALL
Un contrato que hace un delegatecall a un contrato inteligente 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 contrato inteligente de destino y ejecuta ese código por sí mismo. Al contrato inteligente de destino se le suele llamar comúnmente el “contrato de implementación”.
Al igual que call, delegatecall también tiene 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 almacenamiento del contrato Caller será modificado, NO el almacenamiento 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 almacenamiento 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 ranuras de almacenamiento
El contrato que emite un delegatecall debe ser extremadamente cuidadoso al predecir cuáles de sus ranuras de almacenamiento serán modificadas. El ejemplo anterior funcionó perfectamente porque Caller no usó la variable de estado en la ranura 0. Un error 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 de la ranura 0 es la dirección del contrato Called, mientras que la variable myNumber ahora está almacenada en la ranura 1.
Si despliegas los contratos proporcionados y ejecutas la función callIncrement, la ranura 0 del almacenamiento de Caller se incrementará, pero la variable calledAddress se encuentra ahí, no la variable myNumber.
El siguiente video ilustra este error:
Ilustremos qué sucedió a continuación.

Por lo tanto, se debe tener precaución al usar delegatecall, ya que puede romper inadvertidamente nuestro contrato. En el ejemplo anterior, probablemente no sea la intención del programador 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 a la ranura 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 un nombre para la variable en Caller diferente al de Called para demostrar que el nombre de la variable no importa; lo fundamental es en qué ranura 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 por otro contrato y actualizar la referencia al contrato de implementación, sin tocar el almacenamiento. 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 se llamará, ya que hacerlo 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 a la cual está emitiendo el delegateCall, el 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 contratos inteligentes actualizables en Solidity.

En la imagen anterior, el contrato de la izquierda maneja tanto los datos como la lógica. En 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 hace un delegatecall al contrato lógico.
Manejo de los 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 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 comprobar si la función se ejecutó con éxito; de lo contrario, podríamos intentar procesar un retorno que no existe o terminar procesando una cadena con el motivo de reversión.
Cuándo call y delegatecall devuelven false
Un punto crucial a entender 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 a través de delegatecall (o call) encuentra cualquiera de estas condiciones, se revertirá, y el valor de retorno del delegatecall será false
Una pregunta que a menudo confunde a los desarrolladores es por qué un delegatecall hacia un contrato inexistente no se revierte y aun así reporta que la ejecución fue exitosa. Basado en 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 con variables de almacenamiento
Hagamos una ligera modificación al código anterior para dar otro ejemplo de errores relacionados con la distribución del almacenamiento.
El contrato Caller todavía invoca a un contrato de implementación a través de delegatecall, pero ahora el contrato Called lee un valor de una variable de estado. Esto podría parecer una alteración menor, pero en realidad conduce al desastre. ¿Puedes averiguar 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 que está en la ranura 0. Recuerda que en delegatecall, las funciones se ejecutan en el almacenamiento del contrato que llama. En otras palabras, puedes pensar que estás utilizando la variable discountRate del contrato Called para calcular el nuevo price, ¡pero en realidad estás utilizando la variable price del contrato Caller! Las variables de almacenamiento Called.discountRate y Called.price ocupan la ranura 0.
Recibirás un descuento del 200%, lo cual es bastante sustancial (y provocará que la función se revierta, ya que el nuevo precio calculado será negativo, lo cual no está permitido para una variable de tipo uint).
Variables Inmutables y Constantes en delegatecall: Una historia de un Bug
Otro problema complicado con delegatecall surge cuando hay variables inmutables o constantes involucradas. Examinemos un ejemplo que muchos programadores experimentados de 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 la ranura 0. - Dado que es un delegatecall, deberíamos examinar la ranura en el contrato que llama, 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 una ranura. Cuando declaramos variables inmutables, su valor está codificado rígidamente en el bytecode del contrato que se ejecuta durante el delegatecall. Por lo tanto, la función getValue devuelve el valor hardcoded de 2.
msg.sender, msg.value y address(this)
Si utilizamos msg.sender, msg.value y address(this) dentro del contrato Called, todos estos valores corresponderán a los valores msg.sender, msg.value y address(this) del contrato Caller. Recordemos cómo opera delegatecall: todo ocurre dentro del contexto del contrato 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 usando 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 ver en el lado izquierdo de la figura, en lugar de la dirección del contrato Called.

En Remix, mostramos los valores de registro 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 se está ejecutando 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 subcontexto 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, codificadas en ABI.
La función delegateMsgData, a su vez, hace delegatecall a la función returnMsgData. Para lograr esto, el calldata pasado al entorno de ejecución 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 delegatecall con la idea de que estamos tomando prestado el bytecode del contrato de implementación y ejecutándolo en el contrato que llama. Hay una excepción a esto, el opcode CODESIZE.
Supongamos que un contrato inteligente tiene CODESIZE en su bytecode, CODESIZE devuelve el tamaño de ese contrato. Codesize no devuelve el tamaño del código del llamador durante un delegatecall — devuelve el tamaño del código al que se le hizo delegatecall.
Para demostrar esta propiedad, hemos proporcionado el código a continuación. En Solidity, CODESIZE se puede ejecutar en ensamblador 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), con la intención de asegurar que los contratos tengan diferentes tamaños. 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() al hacer delegatecall a ContractA y ContractB serían los mismos. Es decir, serían el 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 a 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. - Esta función tiene la intención de emitir un evento que registra el
msg.sender. - Adicionalmente, el contrato
CalledFirst, aparte de crear este registro, también hace delegatecall al contratoCalledLast. - El contrato
CalledLasttambién emite un evento, el cual también registra elmsg.sender.
A continuación se presenta un diagrama que describe este flujo.

Recuerda, lo único que hace delegatecall es tomar prestado el bytecode del contrato al que se llama. Una forma de visualizar esto es que el bytecode es temporalmente “absorbido” por el contrato llamador. Cuando lo vemos de esa manera, vemos que msg.sender siempre es el msg.sender original ya que todo está sucediendo dentro de Caller. Mira 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 vernos inducidos a pensar que el msg.sender en CalledLast será la dirección de CalledFirst, ya que fue el que llamó a CalledLast, pero esto no respetaría nuestro modelo de que el bytecode del contrato llamado a través de 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 registros.

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 que suene como si CalledFirst estuviera haciendo el delegatecall — 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 necesita ser actualizado al 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 un 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 registros 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 hace call a CalledFirst y CalledFirst hace delegatecall a CalledLast, y cada contrato registra el msg.sender, ¿qué remitente de mensaje 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, por lo que resulta beneficioso examinar primero la definición del opcode DELEGATECALL.
DELEGATECALL toma 6 argumentos de la pila, en orden: gas, address, argsOffset, argsSize, retOffset y retSize, y devuelve un valor a la pila 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 se devuelve a este.
- address: la cuenta cuyo código se va a ejecutar.
- argsOffset: desplazamiento de bytes en la memoria en bytes, el calldata del subcontexto.
- argsSize: tamaño en bytes a copiar (tamaño del calldata).
- retOffset: desplazamiento de bytes en la memoria en bytes, donde almacenar los datos de retorno del subcontexto.
- retSize: tamaño en bytes a copiar (tamaño de los datos de retorno).
Enviar ether a un contrato utilizando delegatecall no está permitido (¡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 usando el método abi.encodeWithSignature.
Si no conocemos 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 usando delegatecall, cubriremos todos estos detalles.
EIP 150 y gas reenviado
Una nota sobre un problema relacionado con el gas reenviado: Utilizamos la función gas() como el primer parámetro de delegatecall, la cual devuelve el gas disponible. Esto debería indicar que tenemos la intención de 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 se reenvía 63/64 del mismo al nuevo subcontexto, mientras que se retiene 1/64.
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 llama. El contrato llamado, también conocido como el contrato de implementación, simplemente proporciona su bytecode, y nada dentro de él es modificado o recuperado de su almacenamiento.
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 utilizarse con gran cuidado, ya que pueden ocurrir cambios no intencionales en las variables de estado, lo que potencialmente puede hacer que el contrato que llama quede inutilizable.
Aprende Más con RareSkills
Para aquellos nuevos en Solidity, vean nuestro curso de Solidity gratuito. 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.
Publicado originalmente el 3 de mayo