Ir al contenido

Contratos Inteligentes (TVM)

La Máquina Virtual de TRON (TVM) ofrece un entorno de alto rendimiento y compatible con la EVM para desplegar contratos inteligentes en Solidity. Aunque comparte gran parte del conjunto de herramientas de Ethereum, esta guía destaca comportamientos críticos específicos de la TVM, como el manejo de marcas de tiempo en milisegundos y la lógica de autodestrucción actualizada, para garantizar despliegues seguros y optimizados.


En Ethereum, block.timestamp devuelve el tiempo Unix en segundos. En la TVM, devuelve el tiempo Unix en milisegundos.

Timestamps.sol
// Tarea: Comparar marcas de tiempo en milisegundos según lo requiere la TVM.
// Esta comparación funciona en Ethereum pero se comporta incorrectamente en TRON:
require(block.timestamp > 1700000000); // espera segundos
// Correcto para la TVM — comparar contra milisegundos:
require(block.timestamp > 1700000000000); // milisegundos
// O: usar una comparación relativa para evitar el problema del valor absoluto:
uint256 private deployTime = block.timestamp;
require(block.timestamp > deployTime + 30 days * 1000); // 30 días en ms

Comportamiento de SELFDESTRUCT Actualizado (Propuesta 106)

Sección titulada «Comportamiento de SELFDESTRUCT Actualizado (Propuesta 106)»

Históricamente, el opcode selfdestruct estaba deshabilitado en TRON. Sin embargo, tras la aprobación de la Propuesta de Gobernanza de TRON N.° 106 (abril de 2026), el comportamiento ahora se alinea con Ethereum (EIP-6780) para mejorar la compatibilidad con la TVM:

  • Un contrato solo será eliminado si selfdestruct se ejecuta dentro de la misma transacción que creó el contrato.
  • Si se ejecuta en cualquier transacción posterior, solo ocurre la transferencia del activo TRX, y el código del contrato permanece en cadena.
  • El costo de Energía para llamar a selfdestruct se ha ajustado a 5000.

Almacenamiento Transitorio y Copia de Memoria

Sección titulada «Almacenamiento Transitorio y Copia de Memoria»

El soporte para TSTORE, TLOAD (EIP-1153) y MCOPY (EIP-5656) se añadió a la TVM como parte de la actualización GreatVoyage-v4.8.0 (Kant) (junio de 2025). Esto garantiza que los desarrolladores puedan migrar contratos que utilizan las actualizaciones Cancun/Dencun de Ethereum sin modificaciones.

Las direcciones de TRON tienen 21 bytes internamente (las direcciones de Ethereum tienen 20 bytes). La codificación ABI rellena a 32 bytes como en la EVM, por lo que las llamadas a contratos codificadas en ABI funcionan correctamente. Sin embargo, si estás haciendo manipulación de bytes sin procesar de direcciones (por ejemplo, en assembly), ten en cuenta el tamaño de 21 bytes.

En Ethereum, block.coinbase es la dirección del minero/validador. En la TVM, es la dirección del Super Representante que produjo el bloque actual. No uses block.coinbase como fuente de aleatoriedad — es predecible y manipulable por los SRs.

Sin Paridad de Dirección CREATE con Ethereum

Sección titulada «Sin Paridad de Dirección CREATE con Ethereum»

El opcode CREATE deriva una dirección de contrato de forma diferente en TRON que en Ethereum. Si estás desplegando contratos que dependen de direcciones CREATE deterministas calculadas fuera de cadena, recalcúlalas para el esquema de derivación de direcciones de TRON. CREATE2 con un salt explícito funciona correctamente para direcciones deterministas.

En Ethereum, address.transfer(amount) tiene un estipendio fijo de 2,300 de gas. En la TVM, esta llamada usa Energía. Los patrones de reentrada que dependían del límite de 2,300 de gas por seguridad pueden comportarse de forma diferente. Usa el patrón de verificaciones-efectos-interacciones independientemente.


TronBox es compatible con Solidity hasta 0.8.x. Usa pragma ^0.8.0 o fija una versión específica. Solidity 0.8 incluye comprobaciones de desbordamiento integradas, lo que elimina la necesidad de SafeMath.

version.sol
// Tarea: Especificar la versión del compilador de Solidity para la TVM.
pragma solidity ^0.8.18;

TRC-20 es funcionalmente idéntico a ERC-20. La referencia completa de firmas de funciones, definiciones de eventos y direcciones de contratos conocidos en la red principal están en Estándares de Tokens. A continuación se muestra un TRC-20 mínimo y desplegable completo con notas específicas de la TVM en línea.

SimpleTRC20.sol
// Tarea: Este código define un contrato de token TRC-20 mínimo.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
/**
* @title SimpleTRC20
* @dev Token TRC-20 mínimo y desplegable para TRON.
*/
contract SimpleTRC20 {
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
uint256 _initialSupply
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
totalSupply = _initialSupply * 10 ** uint256(_decimals);
_balances[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function balanceOf(address account) external view returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) external returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
function allowance(address owner, address spender) external view returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint256 amount) external returns (bool) {
_allowances[msg.sender][spender] = amount;
// Tarea: Emitir el evento Approval para cumplimiento del estándar.
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
uint256 current = _allowances[from][msg.sender];
require(current >= amount, "TRC20: insufficient allowance");
unchecked { _allowances[from][msg.sender] = current - amount; }
_transfer(from, to, amount);
return true;
}
// ── Interno ───────────────────────────────────────────────────────────
function _transfer(address from, address to, uint256 amount) internal {
require(from != address(0), "TRC20: transfer from zero address");
require(to != address(0), "TRC20: transfer to zero address");
uint256 bal = _balances[from];
require(bal >= amount, "TRC20: insufficient balance");
unchecked {
_balances[from] = bal - amount;
_balances[to] += amount;
}
emit Transfer(from, to, amount);
}
}

Desplegar con TronBox:

2_deploy_token.js
// Tarea: Usar el desplegador de TronBox para migrar el token TRC-20.
const SimpleTRC20 = artifacts.require("SimpleTRC20");
module.exports = function (deployer) {
deployer.deploy(
SimpleTRC20,
"My Token", // nombre
"MTK", // símbolo
6, // decimales (6 coincide con la convención de USDT en TRON)
1_000_000 // suministro inicial
);
};

Un ERC-20 estándar de OpenZeppelin también se despliega correctamente en TRON como TRC-20. Revisa solo dos cosas antes de desplegar: cada comparación de block.timestamp (multiplica los umbrales por 1000) y cómo manejas selfdestruct (dado que el comportamiento de eliminación ha sido actualizado).


Cada opcode consume Energía. Escribir Solidity eficiente en gas para Ethereum produce contratos eficientes en Energía en TRON también — los principios son los mismos. (Para desarrolladores de Web2: Optimizar el uso de gas es como optimizar consultas de base de datos costosas. En servidores tradicionales, las malas consultas solo ralentizan las cosas; en las blockchains, directamente le cuestan dinero real a tus usuarios).

Las lecturas y escrituras de almacenamiento son las operaciones más costosas. Minimiza las llamadas a SSTORE:

Inefficient.sol
// Tarea: Evitar múltiples llamadas SSTORE a diferentes slots.
// Ineficiente — tres escrituras de almacenamiento separadas
function update(uint256 a, uint256 b, uint256 c) external {
storedA = a;
storedB = b;
storedC = c;
}
Efficient.sol
// Tarea: Empaquetar valores en un único struct para ahorrar Energía.
struct Config {
uint128 a;
uint64 b;
uint64 c;
}
Config private config;
function update(uint128 a, uint64 b, uint64 c) external {
config = Config(a, b, c); // un único SSTORE en un solo slot
}

Usa funciones view y pure para las lecturas. Las llamadas view/pure no cuestan Energía cuando se llaman fuera de cadena (son gratuitas). Solo las llamadas que cambian de estado cuestan Energía.

Emite eventos en lugar de escribir en almacenamiento para datos históricos. Los eventos se almacenan en logs (económico) en lugar de en el almacenamiento del contrato (costoso).

Usa calldata en lugar de memory para parámetros de funciones cuando los datos solo se leen y no se modifican:

MemoryUsage.sol
// Tarea: Evitar copiar arrays grandes a memoria si solo se leen.
// Menos eficiente
function process(uint256[] memory data) external { ... }
CalldataUsage.sol
// Tarea: Usar calldata para parámetros de solo lectura para ahorrar Energía.
// Más eficiente — los datos no se copian a memoria
function process(uint256[] calldata data) external { ... }

Verificar el código fuente de tu contrato en TRONSCAN permite que los usuarios y otros desarrolladores lean tu lógica, inspeccionen el ABI e interactúen directamente a través del explorador.

Pasos de verificación

  1. Ve a la dirección de tu contrato en tronscan.org
  2. Haz clic en la pestaña Contract
  3. Haz clic en Verify and Publish
  4. Selecciona la versión del compilador usada por TronBox (debe coincidir exactamente)
  5. Pega el código fuente de Solidity
  6. Envía — TRONSCAN compila y verifica que el bytecode coincida con el que está en cadena

Para proyectos de múltiples archivos, usa el método Standard JSON Input, que acepta la entrada completa del compilador incluyendo todos los archivos importados. La salida de TronBox en build/contracts/*.json contiene la entrada del compilador necesaria.


Para un desglose completo de cómo funcionan la Energía y el Ancho de banda, el multiplicador del Modelo de Energía Dinámica y cómo implementar la delegación de comisiones para que tu DApp subsidie los costos de los usuarios, consulta Modelo de Comisiones y Delegación.

Cada transacción que llama a un contrato especifica un límite de comisión — el máximo de TRX que el remitente está dispuesto a quemar si se agota la Energía. Si la ejecución del contrato supera la Energía disponible y se alcanza el límite de comisión, la transacción revierte y el límite de comisión se consume parcialmente.

En tronweb, establece el límite de comisión por llamada:

sdk_call.js
// Tarea: Especificar un límite de comisión para protegerse de bucles infinitos o costos elevados.
await contract.someMethod().send({
feeLimit: 1_000_000_000, // máximo 1,000 TRX
shouldPollResponse: true,
});

Establece el límite de comisión generosamente durante el desarrollo y ajústalo después de conocer el costo real de Energía a través del historial de transacciones de TRONSCAN.


Discrepancia en unidades de marca de tiempo

Toda la lógica basada en tiempo debe usar milisegundos, no segundos. Audita cada comparación de block.timestamp en los contratos migrados.

Comportamiento de SELFDESTRUCT

Ten en cuenta el nuevo comportamiento de SELFDESTRUCT (Propuesta 106): los contratos ya no se eliminan a menos que sean destruidos en su transacción de creación. Ajusta tu lógica de actualización y limpieza en consecuencia.

Cálculo de direcciones fuera de cadena

Si tus scripts de despliegue calculan direcciones de contratos fuera de cadena (para fábricas CREATE), recalcúlalos usando el esquema de derivación de direcciones de TRON.

Subestimación de Energía

Siempre prueba el consumo de Energía en Nile antes de ir a la red principal. Subestimar los límites de comisiones provoca reversiones en el peor momento posible.