Contract-to-contract calls
Three patterns for calling another contract from inside Solidity on TRON: via interface, via raw address.call, and via TRX transfer to a payable contract.
Prerequisites
A contract can call another contract to reuse logic, fetch data, or compose protocols. On TRON, three patterns cover the practical cases. Pick the one that matches what you know about the target contract.
| Pattern | Use when |
|---|---|
| Calling via interface | The target contract's ABI is known at compile time |
Calling via address.call | The interface is unknown, or you need dynamic dispatch |
| Calling via TRX transfer | You only need to send value to the target's receive/fallback |
Pattern 1 — Calling via interface (recommended)
Define a Solidity interface for the target contract and instantiate it at the target's address. This pattern gives you compile-time type safety and the cleanest code. It is the recommended default.
// IContractB.sol
pragma solidity 0.8.0;
interface IContractB {
function increment(uint256 value) external returns (uint256);
}// ContractA.sol
pragma solidity 0.8.0;
import "./IContractB.sol";
contract ContractA {
address public contractBAddress;
constructor(address _contractBAddress) {
contractBAddress = _contractBAddress;
}
function callIncrement(uint256 value) public returns (uint256) {
IContractB contractB = IContractB(contractBAddress);
uint256 newValue = contractB.increment(value);
return newValue;
}
}Pattern 2 — Calling via address.call
address.callWhen you do not have the interface — for example, when calling a contract loaded from an on-chain registry, or when you need dynamic dispatch — call the target through the low-level address.call. This pattern requires manual encoding of the function selector and arguments, plus manual decoding of the return value.
pragma solidity 0.8.0;
contract ContractA {
address public contractBAddress;
constructor(address _contractBAddress) {
contractBAddress = _contractBAddress;
}
function callIncrement(uint256 value) public returns (uint256) {
(bool success, bytes memory result) = contractBAddress.call(
abi.encodeWithSignature("increment(uint256)", value)
);
require(success, "Call failed");
return abi.decode(result, (uint256));
}
}address.call always returns a (bool, bytes) tuple — the bool flag indicates whether the call reverted, and bytes contains the raw return data. Always check success before using result — a silent failure is a common bug.
For more details on encoding the function selector and arguments, see Parameter encoding and decoding.
Pattern 3 — Sending TRX to a contract address
Transferring TRX directly to a contract address triggers the target's receive or fallback function. Use this when the only purpose of the call is to deposit funds.
pragma solidity 0.8.0;
contract ContractA {
address public contractBAddress;
constructor(address _contractBAddress) {
contractBAddress = _contractBAddress;
}
function directTransfer() external payable {
payable(contractBAddress).transfer(msg.value);
}
}
TRON-specific behaviorOn TRON, plain
TransferContracttransactions (system contracts) bypass the contract'sfallbackfunction entirely. Thetransfer()call shown here is aTriggerSmartContractinvocation withcallValueset, which does invokereceive/fallback. This is different from Ethereum, where any value transfer to a contract runsfallback. See TVM vs EVM for the full list of TRX transfer paths.
Considerations for contract-to-contract calls
- Energy consumption — every cross-contract call incurs extra Energy on top of the base call. Set
fee_limitaccordingly. See FeeLimit & Energy cost for calibration. - Call depth limit — TRON limits the call stack depth (currently 64). Excessively deep recursive calls fail with a stack overflow. See VM exception handling for details.
- Failure handling — for
address.call, always check thesuccessflag and revert if appropriate. For interface calls, exceptions in the callee bubble up to your contract. - Reentrancy — calling another contract surrenders execution control. The callee can re-enter your contract. Apply checks-effects-interactions and consider OpenZeppelin's
ReentrancyGuardfor sensitive paths. See Best practices.
Example: a counter chained between two contracts
ContractB implements a counter:
// ContractB.sol
pragma solidity 0.8.0;
contract ContractB {
uint256 public counter;
function increment(uint256 value) external returns (uint256) {
counter += value;
return counter;
}
}ContractA (Pattern 1 above) calls ContractB.increment. Each call increments ContractB.counter by value and returns the new total.
Deployment
- Deploy
ContractBfirst; record its address. - Deploy
ContractA, passingContractB's address as a constructor argument. - Call
ContractA.callIncrement(value)—ContractB.counterincreases byvalueandContractA.callIncrementreturns the new value.
Related resources
- Parameter encoding and decoding —
abi.encodeWithSignaturedetails for Pattern 2 - VM exception handling — call depth, exception types, and revert behavior
- TVM vs EVM — TRX transfer paths (Pattern 3 caveat)
- FeeLimit & Energy cost — calibrate
fee_limitfor cross-contract calls - Best practices — reentrancy and other security considerations
Updated 20 days ago