Anatomía de los contratos inteligentes
Última edición: @506utyutyuty(opens in a new tab), 14 de junio de 2024
Un contrato inteligente es un programa que se ejecuta en una dirección en Ethereum. Están formados por datos y funciones, que se pueden ejecutar al recibir una transacción. A continuación encontrarás una visión general de lo que compone un contrato inteligente.
Requisitos previos
Asegúrate de haber leído primero la documentación sobre los contratos inteligentes. Este documento asume que ya estás familiarizado con lenguajes de programación como JavaScript o Python.
Datos
Cualquier dato del contrato debe asignarse a una ubicación: ya sea a almacenamiento
o memoria
. Es costoso modificar el almacenamiento en un contrato inteligente, por lo que debes considerar dónde deben ubicarse sus datos.
Almacenamiento
Los datos persistentes se denominan almacenamiento y se representan por variables de estado. Estos valores se almacenan permanentemente en la blockchain. Necesitas declarar el tipo de dato para que el contrato pueda llevar un seguimiento de la cantidad de almacenamiento en la blockchain que se necesitará cuando compile.
1// ejemplo de Solidity2contract SimpleStorage {3 uint storedData; // variable de estado4 // ...5}Copiar
1# ejemplo de Vyper2storedData: int128Copiar
Si ya has utilizado lenguajes de programación orientados a objetos, probablemente estarás familiarizado con la mayoría de tipos de datos. Sin embargo, la dirección
debe ser nueva para ti si eres nuevo en el desarrollo de Ethereum.
Una variable de tipo dirección
puede contener una dirección de Ethereum que equivale a 20 bytes o 160 bits. Devuelve en notación hexadecimal con un 0x al inicio.
Otros tipos de variables incluyen:
- booleano
- entero
- números de punto fijo
- matrices de bytes de punto fijo
- matrices de bytes de tamaño dinámico
- Literales de tipo real, racional o integradores
- Literales de cadenas de caracteres
- Literales en base hexadecimal
- Enumeraciones
Para más explicación, echa un vistazo a la documentación:
Memoria
Los valores que sólo se almacenan durante la vida útil de la ejecución de una función de contrato se llaman variables de memoria. Dado que estos no se almacenan permanentemente en la blockchain, son mucho más baratos de usar.
Obtén más información sobre cómo la EVM almacena datos (almacenamiento, memoria y pila) en la documentación de Solidity(opens in a new tab).
Variables de entorno
Además de las variables que se definen en su contrato, hay algunas variables globales especiales. Se utilizan principalmente para proporcionar información acerca de la cadena de bloques o la transacción actual.
Ejemplos:
Propiedad | Variable de estado | Descripción |
---|---|---|
block.timestamp | uint256 | Marca de tiempo del bloque actual |
msg.sender | dirección | Remitente del mensaje (llamada actual) |
Funciones
De una forma simplista, las funciones pueden obtener información o establecer información en respuesta a las transacciones entrantes.
Existen dos tipos de llamadas de funciones:
Internas
: Estas no crean una llamada a la EVM.- Solo se puede acceder a las funciones internas y a las variables de estado internamente (es decir, desde el contrato actual o los contratos que derivan de él)
Externas
: Estas crean una llamada a la EVM.- Las funciones externas forman parte de la interfaz del contrato, lo que significa que se pueden llamar desde otros contratos y a través de transacciones. Una función externa
f
no puede llamarse internamente (es decir,f()
no funciona, perothis.f()
funciona).
- Las funciones externas forman parte de la interfaz del contrato, lo que significa que se pueden llamar desde otros contratos y a través de transacciones. Una función externa
También pueden ser públicas
o privadas
.
- las funciones
públicas
pueden llamarse internamente desde dentro del contrato o externamente a través de mensajes - las funciones
privadas
solo son visibles para el contrato en el que están definidas y no en contratos derivados
Tanto las funciones como las variables de estado pueden hacerse públicas o privadas.
Aquí se ejemplifica una función para actualizar una variable de estado en un contrato:
1// ejemplo de Solidity2function update_name(string value) public {3 dapp_name = value;4}Copiar
- El parámetro
valor
del tipostring
se transfiere a la función:update_name
- Se declara
pública
, lo que significa que cualquiera puede acceder a ella. - No está declarada
view
para solo lectura, por lo que puede modificar el estado del contrato.
Funciones de visualización
Estas funciones no modifican el estado de los datos del contrato. Ejemplos comunes son las funciones "getter", que se pueden utilizar para recibir el saldo o balance de un usuario, por ejemplo.
1// Ejemplo de Solidity2function balanceOf(address _owner) public view returns (uint256 _balance) {3 return ownerPizzaCount[_owner];4}Copiar
1dappName: public(string)23@view4@public5def readName() -> string:6 return dappNameCopiar
Qué se considera modificar un estado:
- Escribir a variables de estado.
- Emisión de eventos(opens in a new tab).
- Creando otros contratos(opens in a new tab).
- Usar la variable
selfdestruct
. - Enviae ethers mediante llamadas.
- Llamar a cualquier función no marcada como sólo lectura
view
opure
. - Usar llamadas de bajo nivel.
- Utilizando un ensamblaje en línea que contiene ciertos códigos de operador.
Funciones de constructor
Las funciones constructor
solo se ejecutan una vez cuando el contrato es implementado por primera vez. Al igual que ocurre con constructor
en muchos otros lenguajes de programación basados en clases, estas funciones a menudo inicializan variables de estado a sus valores especificados.
1// Ejemplo de Solidity2// Inicializa los datos del contrato, estableciendo el `propietario`3// Establece la dirección del creador del contrato4constructor() public {5 // Todos los contratos inteligentes dependen de transacciones externas para activar sus funciones.6 // `msg` es una variable global que incluye datos relevantes en la transacción dada,7 // tales como la dirección del remitente y el valor ETH incluido en la transacción.8 // Más información: https://solidity.readthedocs.io/en/v0.5.10/units-and-global-variables.html#block-and-transaction-properties9 owner = msg.sender;10}Mostrar todoCopiar
1# Ejemplo en Vyper23@external4def __init__(_beneficiary: address, _bidding_time: uint256):5 self.beneficiary = _beneficiary6 self.auctionStart = block.timestamp7 self.auctionEnd = self.auctionStart + _bidding_timeCopiar
Funciones integradas
Además de las variables y funciones que define en su contrato, hay algunas funciones especiales integradas. El ejemplo más obvio es:
address.send()
: Soliditysend(address)
– Vyper
Esto permite que los contratos envíen ETH a otras cuentas.
Funciones de escritura
Su función necesita:
- parámetro de la variable y tipo de variable (si acepta parámetros)
- declaraciónde variable interna/externa
- declaración de variable de tipo pure/view/payable
- devuelve el tipo (valor, en caso de devolución)
1pragma solidity >=0.4.0 <=0.6.0;23contract ExampleDapp {4 string dapp_name; // state variable56 // Called when the contract is deployed and initializes the value7 constructor() public {8 dapp_name = "My Example dapp";9 }1011 // Get Function12 function read_name() public view returns(string) {13 return dapp_name;14 }1516 // Set Function17 function update_name(string value) public {18 dapp_name = value;19 }20}Mostrar todoCopiar
Un contrato completo podría verse así. Aquí la función constructor
proporciona un valor inicial para la variable dapp_name
.
Eventos y registros
Los eventos le permiten comunicarse con su contrato inteligente desde su front-end u otras aplicaciones de suscripción. Cuando se mina una transacción, los contratos inteligentes pueden emitir eventos y escribir registros en la cadena de bloques que el front-end pueda procesar.
Ejemplos anotados
Estos son algunos ejemplos escritos en Solidity. Si quiere experimentar con el código, puede interactuar con ellos en Remix(opens in a new tab).
Hello World
1// Especifica la versión de Solidity, utilizando la versión semántica.2// Más información: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragmma3pragma solidity ^0.5.10;45// Define un contrato llamado `HelloWorld`.6// Un contrato es una colección de funciones y datos (su estado).7// Una vez desplegado, un contrato reside en una dirección específica en la blockchain de Ethereum.8// Más información: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html9contract HelloWorld {1011 // Declara una variable de estado `message` del tipo `string`.12 // Las variables de estado son variables cuyos valores se almacenan permanentemente en el almacenamiento del contrato.13 // La palabra clave `public` hace que las variables sean accesibles desde fuera de un contrato14 // y crea una función que otros contratos o clientes pueden llamar para acceder al valor.15 string public message;1617 // Similar a muchos idiomas orientados a objetos basados en clases, un constructor es18 // una función especial que sólo se ejecuta cuando se crea un contrato.19 // Los constructores se utilizan para inicializar los datos del contrato.20 // Más información: https://solidity.readthedocs.io/es/v0.5.10/contracts. tml#constructors21 constructor(string memory initMessage) public {22 // Acepta un argumento de cadena `initMessage` y establece el valor23 // en la variable de almacenamiento `message` del contrato).24 message = initMessage;25 }2627 // Una función pública que acepta un argumento de cadena28 // y actualiza la variable de almacenamiento `message`.29 function update(string memory newMessage) public {30 message = newMessage;31 }32}Mostrar todoCopiar
Token
1pragma solidity ^0.5.10;23contract Token {4 // Una `dirección` es comparable a una dirección de correo electrónico - se usa para identificar una cuenta en Ethereum.5 // Direcciones pueden representar un contrato inteligente o una cuenta externa (de usuario).6 // Más información: https://solidity.readthedocs.io/en/v0.5.10/types.html#address7 address public owner;89 // Un `mapping` es esencialmente una estructura de datos de tabla hash.10 // Este `mapping` asigna un entero sin signo (el saldo del token) a una dirección (el titular del token).11 // Más información: https://solidity.readthedocs.io/en/v0.5.10/types.html#mapping-types12 mapping (address => uint) public balances;1314 // Los eventos permiten registrar la actividad en la blockchain.15 // Los clientes de Ethereum pueden escuchar eventos para reaccionar a los cambios de estado del contrato.16 // Más información: https://solidity.readthedocs.io/es/v0.5.10/contracts. tml#events17 event Transfer(address from, address to, uint amount);1819 // Inicializa los datos del contrato establecer el `dueño`20 // a la dirección del creador del contrato.21 constructor() public {22 // Todos los contratos inteligentes dependen de transacciones externas para activar sus funciones.23 // `msg` es una variable global que incluye datos relevantes en la transacción dada,24 // tales como la dirección del remitente y el valor ETH incluido en la transacción.25 // Más información: https://solidity.readthedocs.io/en/v0.5.10/units-and-global-variables.html#block-and-transaction-properties26 owner = msg.sender;27 }2829 // Crea una cantidad de nuevos tokens y los envía a una dirección.30 function mint(address receiver, uint amount) public {31 // `requiere` es una estructura de control utilizada para hacer cumplir ciertas condiciones.32 // Si una instrucción `require` evalúa a `falso`, se activa una excepción,33 // la cual revierte todos los cambios realizados en el estado durante la llamada actual.34 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions3536 // Only the contract owner can call this function37 require(msg.sender == owner, "You are not the owner.");3839 // Enforces a maximum amount of tokens40 require(amount < 1e60, "Maximum issuance exceeded");4142 // Increases the balance of `receiver` by `amount`43 balances[receiver] += amount;44 }4546 // Sends an amount of existing tokens from any caller to an address.47 function transfer(address receiver, uint amount) public {48 // El remitente debe tener suficientes tokens para enviar49 require(amount <= balances[msg.sender], "Insufficient balance.");5051 // Ajusta el saldo del token de las dos direcciones52 balances[msg.sender] -= amount;53 balances[receiver] += amount;5455 // Emite el evento definido anteriormente56 emit Transfer(msg.sender, receiver, amount);57 }58}Mostrar todoCopiar
Activo digital único
1pragma solidity ^0.5.10;23// Importa símbolos de otros archivos al contrato actual.4// En este caso, una serie de contratos de ayuda de OpenZeppelin.5// Learn more: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#importing-other-source-files67import "../node_modules/@openzeppelin/contracts/token/ERC721/IERC721.sol";8import "../node_modules/@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";9import "../node_modules/@openzeppelin/contracts/introspection/ERC165.sol";10import "../node_modules/@openzeppelin/contracts/math/SafeMath.sol";1112// The `is` keyword is used to inherit functions and keywords from external contracts.13// En este caso, `CryptoPizza` hereda de los contratos `IERC721` y `ERC165`.14// Más información: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#inheritance15contract CryptoPizza is IERC721, ERC165 {16 // Utiliza la librería SafeMath de OpenZeppelin para realizar operaciones aritméticas de forma segura.17 // Más información:18https://docs.openzeppelin.com/contracts/3. /api/math#SafeMath19 using SafeMath for uint256;2021 // Las variables de estado constantes en Solidity son similares a otros idiomas22 // pero debe asignar desde una expresión que es constante en el momento de compilar.23 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constant-state-variables24 uint256 constant dnaDigits = 10;25 uint256 constant dnaModulus = 10 ** dnaDigits;26 bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;2728 // Struct types let you define your own type29 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/types.html#structs30 struct Pizza {31 string name;32 uint256 dna;33 }3435 // Creates an empty array of Pizza structs36 Pizza[] public pizzas;3738 // Mapping from pizza ID to its owner's address39 mapping(uint256 => address) public pizzaToOwner;4041 // Mapping from owner's address to number of owned token42 mapping(address => uint256) public ownerPizzaCount;4344 // Mapping from token ID to approved address45 mapping(uint256 => address) pizzaApprovals;4647 // You can nest mappings, this example maps owner to operator approvals48 mapping(address => mapping(address => bool)) private operatorApprovals;4950 // Internal function to create a random Pizza from string (name) and DNA51 function _createPizza(string memory _name, uint256 _dna)52 // The `internal` keyword means this function is only visible53 // within this contract and contracts that derive this contract54 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#visibility-and-getters55 internal56 // `isUnique` is a function modifier that checks if the pizza already exists57 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html#function-modifiers58 isUnique(_name, _dna)59 {60 // Adds Pizza to array of Pizzas and get id61 uint256 id = SafeMath.sub(pizzas.push(Pizza(_name, _dna)), 1);6263 // Checks that Pizza owner is the same as current user64 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions6566 // note that address(0) is the zero address,67 // indicating that pizza[id] is not yet allocated to a particular user.6869 assert(pizzaToOwner[id] == address(0));7071 // Maps the Pizza to the owner72 pizzaToOwner[id] = msg.sender;73 ownerPizzaCount[msg.sender] = SafeMath.add(74 ownerPizzaCount[msg.sender],75 176 );77 }7879 // Creates a random Pizza from string (name)80 function createRandomPizza(string memory _name) public {81 uint256 randDna = generateRandomDna(_name, msg.sender);82 _createPizza(_name, randDna);83 }8485 // Generates random DNA from string (name) and address of the owner (creator)86 function generateRandomDna(string memory _str, address _owner)87 public88 // Functions marked as `pure` promise not to read from or modify the state89 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#pure-functions90 pure91 returns (uint256)92 {93 // Generates random uint from string (name) + address (owner)94 uint256 rand = uint256(keccak256(abi.encodePacked(_str))) +95 uint256(_owner);96 rand = rand % dnaModulus;97 return rand;98 }99100 // Returns array of Pizzas found by owner101 function getPizzasByOwner(address _owner)102 public103 // Functions marked as `view` promise not to modify state104 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#view-functions105 view106 returns (uint256[] memory)107 {108 // Uses the `memory` storage location to store values only for the109 // lifecycle of this function call.110 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/introduction-to-smart-contracts.html#storage-memory-and-the-stack111 uint256[] memory result = new uint256[](ownerPizzaCount[_owner]);112 uint256 counter = 0;113 for (uint256 i = 0; i < pizzas.length; i++) {114 if (pizzaToOwner[i] == _owner) {115 result[counter] = i;116 counter++;117 }118 }119 return result;120 }121122 // Transfers Pizza and ownership to other address123 function transferFrom(address _from, address _to, uint256 _pizzaId) public {124 require(_from != address(0) && _to != address(0), "Invalid address.");125 require(_exists(_pizzaId), "Pizza does not exist.");126 require(_from != _to, "Cannot transfer to the same address.");127 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");128129 ownerPizzaCount[_to] = SafeMath.add(ownerPizzaCount[_to], 1);130 ownerPizzaCount[_from] = SafeMath.sub(ownerPizzaCount[_from], 1);131 pizzaToOwner[_pizzaId] = _to;132133 // Emits event defined in the imported IERC721 contract134 emit Transfer(_from, _to, _pizzaId);135 _clearApproval(_to, _pizzaId);136 }137138 /**139 * Safely transfers the ownership of a given token ID to another address140 * If the target address is a contract, it must implement `onERC721Received`,141 * which is called upon a safe transfer, and return the magic value142 * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`;143 * otherwise, the transfer is reverted.144 */145 function safeTransferFrom(address from, address to, uint256 pizzaId)146 public147 {148 // solium-disable-next-line arg-overflow149 this.safeTransferFrom(from, to, pizzaId, "");150 }151152 /**153 * Safely transfers the ownership of a given token ID to another address154 * If the target address is a contract, it must implement `onERC721Received`,155 * which is called upon a safe transfer, and return the magic value156 * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`;157 * otherwise, the transfer is reverted.158 */159 function safeTransferFrom(160 address from,161 address to,162 uint256 pizzaId,163 bytes memory _data164 ) public {165 this.transferFrom(from, to, pizzaId);166 require(_checkOnERC721Received(from, to, pizzaId, _data), "Must implement onERC721Received.");167 }168169 /**170 * Internal function to invoke `onERC721Received` on a target address171 * The call is not executed if the target address is not a contract172 */173 function _checkOnERC721Received(174 address from,175 address to,176 uint256 pizzaId,177 bytes memory _data178 ) internal returns (bool) {179 if (!isContract(to)) {180 return true;181 }182183 bytes4 retval = IERC721Receiver(to).onERC721Received(184 msg.sender,185 from,186 pizzaId,187 _data188 );189 return (retval == _ERC721_RECEIVED);190 }191192 // Burns a Pizza - destroys Token completely193 // The `external` function modifier means this function is194 // part of the contract interface and other contracts can call it195 function burn(uint256 _pizzaId) external {196 require(msg.sender != address(0), "Invalid address.");197 require(_exists(_pizzaId), "Pizza does not exist.");198 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");199200 ownerPizzaCount[msg.sender] = SafeMath.sub(201 ownerPizzaCount[msg.sender],202 1203 );204 pizzaToOwner[_pizzaId] = address(0);205 }206207 // Returns count of Pizzas by address208 function balanceOf(address _owner) public view returns (uint256 _balance) {209 return ownerPizzaCount[_owner];210 }211212 // Returns owner of the Pizza found by id213 function ownerOf(uint256 _pizzaId) public view returns (address _owner) {214 address owner = pizzaToOwner[_pizzaId];215 require(owner != address(0), "Invalid Pizza ID.");216 return owner;217 }218219 // Approves other address to transfer ownership of Pizza220 function approve(address _to, uint256 _pizzaId) public {221 require(msg.sender == pizzaToOwner[_pizzaId], "Must be the Pizza owner.");222 pizzaApprovals[_pizzaId] = _to;223 emit Approval(msg.sender, _to, _pizzaId);224 }225226 // Returns approved address for specific Pizza227 function getApproved(uint256 _pizzaId)228 public229 view230 returns (address operator)231 {232 require(_exists(_pizzaId), "Pizza does not exist.");233 return pizzaApprovals[_pizzaId];234 }235236 /**237 * Private function to clear current approval of a given token ID238 * Reverts if the given address is not indeed the owner of the token239 */240 function _clearApproval(address owner, uint256 _pizzaId) private {241 require(pizzaToOwner[_pizzaId] == owner, "Must be pizza owner.");242 require(_exists(_pizzaId), "Pizza does not exist.");243 if (pizzaApprovals[_pizzaId] != address(0)) {244 pizzaApprovals[_pizzaId] = address(0);245 }246 }247248 /*249 * Sets or unsets the approval of a given operator250 * An operator is allowed to transfer all tokens of the sender on their behalf251 */252 function setApprovalForAll(address to, bool approved) public {253 require(to != msg.sender, "Cannot approve own address");254 operatorApprovals[msg.sender][to] = approved;255 emit ApprovalForAll(msg.sender, to, approved);256 }257258 // Tells whether an operator is approved by a given owner259 function isApprovedForAll(address owner, address operator)260 public261 view262 returns (bool)263 {264 return operatorApprovals[owner][operator];265 }266267 // Takes ownership of Pizza - only for approved users268 function takeOwnership(uint256 _pizzaId) public {269 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");270 address owner = this.ownerOf(_pizzaId);271 this.transferFrom(owner, msg.sender, _pizzaId);272 }273274 // Checks if Pizza exists275 function _exists(uint256 pizzaId) internal view returns (bool) {276 address owner = pizzaToOwner[pizzaId];277 return owner != address(0);278 }279280 // Checks if address is owner or is approved to transfer Pizza281 function _isApprovedOrOwner(address spender, uint256 pizzaId)282 internal283 view284 returns (bool)285 {286 address owner = pizzaToOwner[pizzaId];287 // Disable solium check because of288 // https://github.com/duaraghav8/Solium/issues/175289 // solium-disable-next-line operator-whitespace290 return (spender == owner ||291 this.getApproved(pizzaId) == spender ||292 this.isApprovedForAll(owner, spender));293 }294295 // Check if Pizza is unique and doesn't exist yet296 modifier isUnique(string memory _name, uint256 _dna) {297 bool result = true;298 for (uint256 i = 0; i < pizzas.length; i++) {299 if (300 keccak256(abi.encodePacked(pizzas[i].name)) ==301 keccak256(abi.encodePacked(_name)) &&302 pizzas[i].dna == _dna303 ) {304 result = false;305 }306 }307 require(result, "Pizza with such name already exists.");308 _;309 }310311 // Returns whether the target address is a contract312 function isContract(address account) internal view returns (bool) {313 uint256 size;314 // Currently there is no better way to check if there is a contract in an address315 // than to check the size of the code at that address.316 // See https://ethereum.stackexchange.com/a/14016/36603317 // for more details about how this works.318 // TODO Check this again before the Serenity release, because all addresses will be319 // contracts then.320 // solium-disable-next-line security/no-inline-assembly321 assembly {322 size := extcodesize(account)323 }324 return size > 0;325 }326}Mostrar todoCopiar
Más información
Revise la documentación de Solidity y Vyper para ver una descripción más completa de los contratos inteligentes:
Temas relacionados
Tutoriales relacionados
- Reducir el tamaño de los contratos para luchar contra el límite de tamaño del contrato : Algunos consejos prácticos para reducir el tamaño de tu contrato inteligente.
- Registro de datos de contratos inteligentes con eventos : Una introducción a los eventos de contratos inteligentes y cómo puede utilizarlos para registrar datos.
- Interactuar con otros contratos de Solidity: Cómo implementar un contrato inteligente de un contrato existente e interactuar con él.