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.

PatternUse when
Calling via interfaceThe target contract's ABI is known at compile time
Calling via address.callThe interface is unknown, or you need dynamic dispatch
Calling via TRX transferYou 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

When 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 behavior

On TRON, plain TransferContract transactions (system contracts) bypass the contract's fallback function entirely. The transfer() call shown here is a TriggerSmartContract invocation with callValue set, which does invoke receive/fallback. This is different from Ethereum, where any value transfer to a contract runs fallback. 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_limit accordingly. 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 the success flag 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 ReentrancyGuard for 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

  1. Deploy ContractB first; record its address.
  2. Deploy ContractA, passing ContractB's address as a constructor argument.
  3. Call ContractA.callIncrement(value)ContractB.counter increases by value and ContractA.callIncrement returns the new value.

Related resources