Use this file to discover all available pages before exploring further.
Proxy functions are the heart of your TAC contracts - they receive cross-chain calls from TON users and execute your application logic. This guide covers how to implement robust proxy functions that handle different use cases and error conditions.
Every proxy function that handles cross-chain calls must follow this exact pattern:
function <function_name>(bytes calldata, bytes calldata) external _onlyCrossChainLayer;
The function above also can be payable.You can name the function as you wish (e.g. myProxyFunction, invokeWithCallback, swap, etc.), but it must accept two bytes arguments:
function yourFunctionName(bytes calldata tacHeader, bytes calldata arguments) external _onlyCrossChainLayer{ // Your implementation}
Required Components:
bytes calldata tacHeader - Contains verified TON user information
bytes calldata arguments - Your custom encoded parameters
external visibility - Functions must be externally callable
Important: Only the Cross-Chain Layer (CCL) contract can call these functions. When a user on TON sends a message, the CCL automatically transfers any bridged tokens to your proxy contract before calling your function.
Let’s build a complete proxy function step-by-step,
showing external dApp integration - the most common real-world pattern implementing TON->TAC->TON transaction type:
// SPDX-License-Identifier: MITpragma solidity ^0.8.28;import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";import { TacProxyV1 } from "@tonappchain/evm-ccl/contracts/proxies/TacProxyV1.sol";import { TokenAmount, OutMessageV1, TacHeaderV1, NFTAmount } from "@tonappchain/evm-ccl/contracts/core/Structs.sol";interface IDappContract { function doSomething(address tokenA, address tokenB, uint256 amount) external returns (uint256);}contract MyProxy is TacProxyV1 { IDappContract public dappContract; struct MyProxyFunctionArguments { address tokenA; address tokenB; uint256 amount; } constructor(address _dappContract, address _crossChainLayer) TacProxyV1(_crossChainLayer) { dappContract = IDappContract(_dappContract); } function myProxyFunction(bytes calldata tacHeader, bytes calldata arguments) external _onlyCrossChainLayer { // 1. Decode the custom arguments MyProxyFunctionArguments memory args = abi.decode(arguments, (MyProxyFunctionArguments)); // 2. Approve tokens to your Dapp contract for some action IERC20(args.tokenA).approve(address(dappContract), args.amount); // 3. Call the Dapp contract uint256 tokenBAmount = dappContract.doSomething( args.tokenA, args.tokenB, args.amount ); // 4. Prepare tokens to send back to TON TokenAmount[] memory tokensToBridge = new TokenAmount[](1); tokensToBridge[0] = TokenAmount(args.tokenB, tokenBAmount); // 5. Approve the CrossChainLayer to pull them IERC20(tokensToBridge[0].evmAddress).approve( _getCrossChainLayerAddress(), tokensToBridge[0].amount ); // 6. Decode the TAC header TacHeaderV1 memory header = _decodeTacHeader(tacHeader); // 7. Form an OutMessage OutMessageV1 memory outMsg = OutMessageV1({ shardsKey: header.shardsKey, // Use same key for RoundTrip tvmTarget: header.tvmCaller, // Send back to caller tvmPayload: "", // Must be empty - not supported tvmProtocolFee: 0, // 0 for RoundTrip - already paid on TON tvmExecutorFee: 0, // 0 for RoundTrip - already paid on TON tvmValidExecutors: new string[](0), // Empty for RoundTrip - already defined on TON toBridge: tokensToBridge, // Result tokens toBridgeNFT: new NFTAmount[](0) // No NFTs }); // 8. Send message back through CrossChainLayer with zero native _sendMessageV1(outMsg, 0); // 0 TACs to send back }}