Solidity: Tipos Avanzados y Patrones
En esta lección profundizaremos en las estructuras de datos avanzadas de Solidity, la gestión de memoria y los patrones de diseño fundamentales para escribir smart contracts robustos.
Data Locations: Storage, Memory, Calldata
En Solidity, las variables de tipo referencia deben especificar dónde se almacenan:
contract DataLocations {
// storage: persistente en la blockchain (caro)
uint[] public storageArray;
function ejemplos(uint[] calldata _input) external {
// calldata: solo lectura, datos del input de la función
// Más barato que memory porque no se copia
uint primerElemento = _input[0];
// memory: temporal, solo durante la ejecución
uint[] memory tempArray = new uint[](3);
tempArray[0] = 1;
tempArray[1] = 2;
tempArray[2] = 3;
// storage: referencia al estado persistente
storageArray.push(primerElemento);
}
}
Comparación de costos
Location Persistencia Costo Uso típico
──────────────────────────────────────────────────────
storage Permanente 20,000 gas Estado del contrato
memory Temporal 3 gas Variables locales
calldata Temporal Mínimo Parámetros external
Mappings Avanzados
Mapping anidado
contract MappingsAvanzados {
// Mapping simple
mapping(address => uint) public balances;
// Mapping anidado: allowances para ERC-20
mapping(address => mapping(address => uint)) public allowances;
// Mapping a struct
struct Producto {
string nombre;
uint precio;
bool existe;
}
mapping(uint => Producto) public productos;
// Mapping con array
mapping(address => uint[]) public historialCompras;
function agregarProducto(uint _id, string memory _nombre, uint _precio) public {
productos[_id] = Producto(_nombre, _precio, true);
}
}
Iterable Mapping Pattern
Los mappings no son iterables por defecto. Este patrón lo soluciona:
contract IterableMapping {
mapping(address => uint) public balances;
address[] public keys;
mapping(address => bool) public inserted;
function set(address _key, uint _val) public {
balances[_key] = _val;
if (!inserted[_key]) {
inserted[_key] = true;
keys.push(_key);
}
}
function getAll() public view returns (address[] memory, uint[] memory) {
uint[] memory vals = new uint[](keys.length);
for (uint i = 0; i < keys.length; i++) {
vals[i] = balances[keys[i]];
}
return (keys, vals);
}
}
Herencia
Solidity soporta herencia múltiple con linearización C3:
contract Animal {
string public especie;
constructor(string memory _especie) {
especie = _especie;
}
function sonido() public pure virtual returns (string memory) {
return "...";
}
}
contract Mascota is Animal {
string public nombre;
constructor(string memory _nombre, string memory _especie)
Animal(_especie)
{
nombre = _nombre;
}
}
contract Perro is Mascota {
constructor(string memory _nombre)
Mascota(_nombre, "Canino")
{}
function sonido() public pure override returns (string memory) {
return "Guau!";
}
}
Interfaces y Abstract Contracts
// Interface: define el "qué" sin implementación
interface IERC20 {
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address to, uint amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint value);
}
// Abstract: puede tener funciones implementadas y sin implementar
abstract contract Pausable {
bool public paused;
modifier whenNotPaused() {
require(!paused, "Contrato pausado");
_;
}
function _pause() internal {
paused = true;
}
// Función abstracta (sin implementación)
function emergencyAction() public virtual;
}
Bibliotecas (Libraries)
library SafeMath {
function add(uint a, uint b) internal pure returns (uint) {
uint c = a + b;
require(c >= a, "Overflow");
return c;
}
}
library ArrayUtils {
function contains(uint[] memory arr, uint val) internal pure returns (bool) {
for (uint i = 0; i < arr.length; i++) {
if (arr[i] == val) return true;
}
return false;
}
}
contract UsaLibreria {
using SafeMath for uint;
using ArrayUtils for uint[];
function ejemplo() public pure returns (uint) {
uint a = 100;
return a.add(50); // SafeMath.add(100, 50)
}
}
Receive y Fallback
Funciones especiales para recibir ETH:
contract Receptor {
event RecibidoETH(address sender, uint amount);
// Se ejecuta cuando se envía ETH sin data
receive() external payable {
emit RecibidoETH(msg.sender, msg.value);
}
// Se ejecuta cuando se llama una función que no existe
// o cuando se envía ETH con data y no hay receive
fallback() external payable {
emit RecibidoETH(msg.sender, msg.value);
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
Flujo de decisión
ETH enviado al contrato
│
¿msg.data vacío?
/ \
Sí No
│ │
¿Existe receive()? ¿Existe fallback()?
/ \ / \
Sí No Sí No
│ │ │ │
receive() fallback() fallback() REVERT
Envío de ETH
Tres formas de enviar ETH, con diferentes implicaciones:
contract EnviarETH {
// transfer: 2300 gas, revierte en error (RECOMENDADO para simple)
function viaTransfer(address payable _to) public payable {
_to.transfer(msg.value);
}
// send: 2300 gas, retorna bool (NO RECOMENDADO)
function viaSend(address payable _to) public payable {
bool sent = _to.send(msg.value);
require(sent, "Fallo al enviar");
}
// call: gas personalizable, retorna (bool, data)
// RECOMENDADO para contratos complejos
function viaCall(address payable _to) public payable {
(bool sent, ) = _to.call{value: msg.value}("");
require(sent, "Fallo al enviar");
}
}
ABI Encoding
contract ABIEncoding {
// abi.encode: encoded con padding (estándar)
function encode(uint x, string memory s) public pure returns (bytes memory) {
return abi.encode(x, s);
}
// abi.encodePacked: sin padding (más compacto, riesgo de colisión)
function encodePacked(uint x, string memory s) public pure returns (bytes memory) {
return abi.encodePacked(x, s);
}
// abi.encodeWithSignature: para llamadas a contratos
function encodeCall() public pure returns (bytes memory) {
return abi.encodeWithSignature("transfer(address,uint256)",
0x742d35Cc6634C0532925a3b844Bc9e7595f2bD09,
100
);
}
}
Resumen
Solidity maneja datos en tres ubicaciones: storage (persistente y costoso), memory (temporal), y calldata (solo lectura para inputs). Los mappings y structs permiten estructuras complejas, la herencia y las interfaces organizan el código, y las libraries reutilizan lógica. Comprender receive/fallback y las formas de enviar ETH es crucial para contratos que manejan fondos.