Security
TVM-specific security considerations: development process, common attack patterns (reentrancy, overflow, send rejection), and audit tools that work on TVM bytecode.
Prerequisites
Smart contracts are extremely flexible — they can hold large amounts of value and run immutable logic indefinitely. That flexibility creates a productive ecosystem of trustless, composable applications. It also creates an environment where attackers profit by exploiting bugs and unexpected behavior. Smart contract code usually cannot be patched after deployment, so assets stolen from contracts are often unrecoverable.
Before deploying anything to Mainnet, take precautions proportionate to the value the contract will hold. This page covers the development process and common attack patterns to think about. For a pre-deployment checklist, see Best practices; for specific deployment-time mistakes, see Smart contract errors.
A solid development process
Most contract incidents trace back to development-process gaps rather than novel attack patterns. At minimum:
- All code lives in version control (git).
- All changes go through pull requests.
- Every pull request has at least one reviewer.
- A TRON-aware build tool (TronBox or equivalent) compiles, deploys, and runs a full test suite with a single command.
- Static analysis tools (Mythril, Slither — see caveat below) run on every pull request, and warnings are reviewed before merging.
- Solidity emits zero compiler warnings.
- The code is documented well enough for someone other than the original author to audit.
EVM audit tools on TVM bytecodeMythril and Slither were built for the Ethereum EVM. Most opcodes are shared with the TVM, so analysis is mostly accurate — but TRON-specific opcodes (
CALLTOKEN,FREEZEBALANCEV2, etc.) are unknown to these tools and can produce false positives or be silently skipped. Treat their output as a useful first pass, not a complete audit. For high-value contracts, supplement with a TVM-aware audit by a third-party firm.
Common attack patterns
Reentrancy
Reentrancy is one of the most consequential classes of contract vulnerabilities. The TVM cannot run two contracts in parallel, but when a contract calls another contract, the caller's execution and memory state are paused until the call returns. The pause-and-resume creates the opening: if the callee calls back into the caller before the caller finishes, the caller's state may not yet reflect the actions it intended to take.
A vulnerable example:
// THIS CONTRACT HAS AN INTENTIONAL VULNERABILITY, DO NOT COPY
pragma solidity 0.8.6;
contract Victim {
mapping (address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // <-- balance reset AFTER the call
}
}withdraw() does three things in order: read the caller's balance, send the TRX, then reset the balance to 0. The bug is the order: between sending TRX and resetting the balance, control returns to the caller's fallback/receive. If the caller is a malicious contract, that fallback can call withdraw() again with the balance still showing the original amount.
A malicious caller:
pragma solidity 0.8.6;
interface IVictim {
function deposit() external payable;
function withdraw() external;
}
contract Attacker {
IVictim victim;
uint256 count;
constructor(address victimAddress) {
victim = IVictim(victimAddress);
}
function beginAttack() external payable {
count = 5;
victim.deposit{value: 1 trx}();
victim.withdraw();
}
receive() external payable {
if (count > 0) {
count -= 1;
victim.withdraw();
}
}
}Calling Attacker.beginAttack() with 1 TRX kicks off a chain like this:
Attacker.beginAttack()deposits 1 TRX intoVictim.AttackercallsVictim.withdraw()—Victimreads balance (1 TRX) and sends it back.Victim's send triggersAttacker.receive, which callsVictim.withdraw()again — balance still reads 1 TRX (the previous reset has not yet executed), soVictimsends another 1 TRX.- Repeats until
countruns out or the call stack overflows.
Victim ends up paying out far more than Attacker deposited — the missing TRX comes from other users' balances.
Fix: checks-effects-interactions
Reorder the function so state is updated before the external call:
pragma solidity 0.8.6;
contract NoLongerVulnerable {
mapping (address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0; // 1. effects (state update)
(bool success, ) = msg.sender.call{value: amount}(""); // 2. interactions
require(success);
}
}The pattern is checks → effects → interactions: validate inputs, update state, then make external calls. By the time the callee receives control, the caller's state already reflects the intended changes, so reentrant calls operate on already-updated state.
For sensitive paths, also consider OpenZeppelin's ReentrancyGuard modifier as a defense-in-depth measure. By design, your contract should never need both — ReentrancyGuard catches mistakes in the checks-effects-interactions pattern, but it is not a substitute for the pattern itself.
Sending TRX to a contract that rejects it
A contract can intentionally or unintentionally reject incoming TRX (for example, by reverting in receive or fallback, or by exhausting the limited Energy stipend forwarded by transfer() — the Solidity compiler emits a small fixed stipend for these calls, following the EVM convention). If your contract assumes a TRX send will always succeed, an attacker can deploy a TRX-rejecting contract and use it as the destination, blocking your logic.
Mitigations:
- Use
call{value: amount}("")instead oftransfer(amount)—callforwards all available Energy, not the small fixed stipend thattransfer()emits. - Use the withdraw pattern: instead of pushing TRX to recipients, let recipients pull it themselves.
- If you must push, handle the failure case explicitly rather than
require-reverting.
Integer overflow / underflow
For Solidity 0.8.0 and later, arithmetic operations revert automatically on overflow or underflow. For older versions, you must use SafeMath (or equivalent) explicitly. Confirm the compiler version in your pragma statement and your tronbox-config.js — the protections only apply for 0.8+.
// 0.8.0+ — overflow check is automatic
pragma solidity 0.8.6;
uint256 public total;
function add(uint256 amount) external {
total += amount; // reverts if total + amount > 2^256 - 1
}If you need wrapping arithmetic for performance reasons, use unchecked { ... } blocks explicitly — the compiler then assumes you have validated the math yourself.
Related resources
- Best practices — pre-deployment checklist
- Smart contract errors — frequent deployment-time mistakes
- VM exception handling — exception types and call depth
- Contract-to-contract calls — call patterns and reentrancy considerations
- Upgrading smart contracts — proxy pattern security considerations
Updated 25 days ago