El selector de función es un identificador de 4 bytes que Solidity utiliza para identificar las funciones a nivel interno.
El selector de función es la forma en que un contrato de Solidity sabe qué función estás intentando llamar en la transacción.
Puedes ver el identificador de 4 bytes utilizando el método .selector:
pragma solidity 0.8.25;
contract SelectorTest{
function foo() public {}
function getSelectorOfFoo() external pure returns (bytes4) {
return this.foo.selector; // 0xc2985578
}
}
Si una función no recibe argumentos, puedes invocarla enviando esos 4 bytes (el selector) como datos al contrato con un call de bajo nivel.
En el siguiente ejemplo, el contrato CallFoo llama a la función foo definida en FooContract realizando un call a FooContract con el selector de función de cuatro bytes correspondiente
pragma solidity 0.8.25;
contract CallFoo {
function callFooLowLevel(address _contract) external {
bytes4 fooSelector = 0xc2985578;
(bool ok, ) = _contract.call(abi.encodePacked(fooSelector));
require(ok, "call failed");
}
}
contract FooContract {
uint256 public x;
function foo() public {
x = 1;
}
}
Si FooContract y CallFoo están desplegados, y la función callFooLowLevel() se ejecuta con la dirección de FooContract, entonces el valor de x dentro de FooContract se establecerá en 1. Esto indica una llamada exitosa a la función foo.
Identificando llamadas a funciones y el selector en Solidity con msg.sig
msg.sig es una variable global que devuelve los primeros cuatro bytes de los datos de la transacción, lo cual es la forma en que el contrato de Solidity sabe qué función invocar.
msg.sig se preserva durante toda la transacción, no cambia dependiendo de qué función esté activa en ese momento. Así que, incluso si una función public llama a otra función public durante una llamada, msg.sig será el selector de la función que fue llamada inicialmente.
En el código a continuación, foo llama a bar para obtener el msg.sig, pero el selector que se devuelve cuando se llama a foo es el selector de función de foo, no el de bar:
Puedes probar el código en Remix aquí:
//SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
contract SelectorTest {
// returns function selector of `bar()` 0xfebb0f7e
function bar() public pure returns (bytes4) {
return msg.sig;
}
// returns function selector of `foo()` 0xc2985578
function foo() public pure returns (bytes4) {
return bar();
}
function testSelectors() external pure returns (bool) {
assert(this.foo.selector == 0xc2985578);
assert(this.bar.selector == 0xfebb0f7e);
return true;
}
}
La firma de la función en Solidity
La firma de la función en Solidity es una cadena de texto que contiene el nombre de la función seguido de los tipos de los argumentos que acepta. Los nombres de las variables se eliminan de los argumentos.
En el siguiente fragmento, la función está a la izquierda y la firma de la función está a la derecha:
function setPoint(uint256 x, uint256 y) --> "setPoint(uint256,uint256)"
function setName(string memory name) --> "setName(string)"
function addValue(uint v) --> "addValue(uint256)"
No hay espacios en el selector de función. Todos los tipos uint deben incluir su tamaño explícitamente (uint256, uint40, uint8, etc.). Los tipos calldata y memory no se incluyen. Por ejemplo, getBalanceById(uint) es una firma inválida.
Cómo se calcula el selector de función a partir de la firma de la función
El selector de función consiste en los primeros cuatro bytes del hash keccak256 de la firma de la función.
function returnFunctionSelectorFromSignature(
string calldata functionName
) public pure returns(bytes4) {
bytes4 functionSelector = bytes4(keccak256(abi.encodePacked(functionName)));
return(functionSelector);
}
Introducir “foo()” en la función anterior devolverá 0xc2985578.
El código a continuación demuestra cómo calcular el selector de función, dada la firma de la función.
//SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
contract FunctionSignatureTest {
function foo() external {}
function point(uint256 x, uint256 y) external {}
function setName(string memory name) external {}
function testSignatures() external pure returns (bool) {
// NOTE: Casting to bytes4 takes the first 4 bytes
// and removes the rest
assert(bytes4(keccak256("foo()")) == this.foo.selector);
assert(bytes4(keccak256("point(uint256,uint256)")) == this.point.selector);
assert(bytes4(keccak256("setName(string)")) == this.setName.selector);
return true;
}
}
Las interfaces se codifican como direcciones. Por ejemplo f(IERC20 token) se codifica como f(address token). Hacer que una dirección sea payable no cambia la firma.
Las funciones internal no tienen selector de función
Las funciones public y external tienen un selector, pero las funciones internal y private no. El siguiente código no compila:
contract Foo {
function bar() internal {
}
// does not compile
function barSelector() external pure returns (bytes4) {
return this.bar.selector;
}
}
Si bar se cambia a public o external, el código compilará.
Las funciones internal no necesitan selectores de función porque los selectores de función están diseñados para ser utilizados por contratos externos. Es decir, el selector de función es la forma en que un usuario externo especifica qué función public o external está intentando llamar.
¿Por qué usar selectores de función en lugar del nombre de la función?
Los nombres de las funciones en Solidity pueden ser arbitrariamente largos, y si el nombre de la función es largo, aumentará el tamaño y el costo de la transacción. Las transacciones generalmente son de menor tamaño si se proporciona un selector de función en lugar del nombre.
¿Tiene la función fallback un selector de función?
La función fallback no tiene un selector de función.
Si se registra msg.sig dentro de la función fallback, simplemente registrará los primeros cuatro bytes de los datos de la transacción. Si no hay datos de transacción, entonces msg.sig devolverá cuatro bytes en cero. Esto no significa que el selector de función del fallback sea todo ceros, significa que msg.sig intentó leer datos que no estaban allí.
contract FunctionSignatureTest {
event LogSelector(bytes4);
fallback() external payable {
emit LogSelector(msg.sig);
}
}
Aquí tienes el código en Remix para probar el contrato anterior. A continuación hay un video que muestra cómo activar la función fallback en Remix:
Probabilidad de una colisión de selectores de función
Un selector de función puede contener hasta 2**32 - 1 (aproximadamente 4.200 millones) de valores posibles. Como tal, la probabilidad de que dos funciones tengan el mismo selector es pequeña pero realista.
contract WontCompile {
function collate_propagate_storage(bytes16 x) external {}
function burn(uint256 amount) external {}
}
El ejemplo anterior fue tomado de esta publicación del foro. Ambas funciones tienen un selector de función de 0x42966c68.
Recursos útiles sobre selectores de función
Calculadora de selectores de función. Asegúrate de añadir la palabra clave function. La calculadora espera un formato como function burn(uint256).
Base de datos de selectores de función. Introduce los 4 bytes en la búsqueda y aparecerán las funciones conocidas que coinciden.
Los selectores de función y la EVM — Ethereum Virtual Machine
Para los lectores familiarizados con la EVM, existe una fuente común de confusión. El selector de función de cuatro bytes ocurre a nivel de aplicación de Solidity, no a nivel de la EVM. Nada en la especificación de Ethereum indica que las funciones deban identificarse con 4 bytes. Esta es una convención fuerte, pero no es un requisito.
De hecho, una optimización común para aplicaciones de capa 2 es identificar la función con un selector de función de 1 byte. Es decir, todas las llamadas a funciones van a la función fallback, y luego la función fallback examina el primer byte de la transacción para determinar qué función invocar.
Aprende más con RareSkills
Consulta nuestro bootcamp de Solidity para temas más avanzados en el desarrollo de contratos inteligentes.
Publicado originalmente el 30 de marzo de 2024