Understanding how messages flow between TON and TAC EVM is crucial for building effective proxy contracts. This guide explains the data structures and message patterns.
RoundTrip Messages
If a TON → TAC call is reverted on the EVM side after tokens have been
bridged, a rollback transaction is created to send the bridged assets back to
the originating TON wallet.To guarantee this behavior, the call must be
treated (and paid) as a RoundTrip. The SDK sets messages to RoundTrip by
default, so on revert (e.g., slippage), funds will be returned automatically.
RoundTrip messages follow this flow:
- TON user sends transaction with assets and function call
- TAC proxy receives the call and processes it
- TAC proxy sends result assets back to the same TON user
With RoundTrip messages, the user pays all fees upfront on TON, so your proxy sets fees to 0 when responding.
If you expect the TAC-side transaction may revert and assets need to be bridged back to TON, you must pay fees as for a RoundTrip message.
By default, the SDK treats all messages as RoundTrip, so even if a revert occurs (e.g., due to slippage), the funds will be returned to the original TON user because the RoundTrip fee was already paid.
Message Processing
When a TON user interacts with your EVM contract:
- User submits a transaction on TON with target contract, function name, and arguments
- Assets (if any) are bridged to TAC and transferred to your proxy contract
- CrossChainLayer calls your proxy function with TAC header and arguments
- Your contract can optionally send assets back to TON using
_sendMessageV1()
Every cross-chain call includes a TAC header with verified information about the original TON transaction:
struct TacHeaderV1 {
uint64 shardsKey; // Developer/operation identifier
uint256 timestamp; // Block timestamp from TON
bytes32 operationId; // Unique operation ID
string tvmCaller; // TON user's address (base64, starts with EQ)
bytes extraData; // Additional data (currently unused)
}
shardsKey: Links related cross-chain operations together. Use this in your response messages to maintain the connection.
timestamp: The block timestamp from the original TON transaction. Useful for time-based logic or debugging.
operationId: Unique identifier for this specific cross-chain operation. Use for logging and tracking.
tvmCaller: The TON user’s wallet address. !!! Important !!! It is always base64 mainnet bounceable format and starts with “EQ”. This is your authenticated user identity - treat it like msg.sender in regular Ethereum contracts.
extraData: For now it’s always a zero-bytes array and not used.
function processMessage(bytes calldata tacHeader, bytes calldata arguments)
external
_onlyCrossChainLayer
{
// Decode header using inherited function
TacHeaderV1 memory header = _decodeTacHeader(tacHeader);
// Access user information
string memory tonUser = header.tvmCaller;
uint256 operationTime = header.timestamp;
bytes32 opId = header.operationId;
// Use header data in your logic
require(block.timestamp - operationTime < 3600, "Operation too old");
emit MessageProcessed(tonUser, opId, block.timestamp);
}
Asset Handling in Messages
Token Assets
Tokens are automatically transferred to your contract before your function is called:
function handleTokens(bytes calldata tacHeader, bytes calldata arguments)
external
_onlyCrossChainLayer
{
// Decode the tokens you're expecting
TokenAmount[] memory expectedTokens = abi.decode(arguments, (TokenAmount[]));
// Tokens are already in your contract balance
for (uint i = 0; i < expectedTokens.length; i++) {
uint256 balance = IERC20(expectedTokens[i].evmAddress).balanceOf(address(this));
require(balance >= expectedTokens[i].amount, "Expected tokens not received");
// Process each token
processToken(expectedTokens[i].evmAddress, expectedTokens[i].amount);
}
}
What’s Next?
Now that you understand the message flow, learn how to implement the core proxy function logic: