TAC Proxy contracts are Solidity contracts that can receive function calls from TON users. This guide walks you through creating your first basic proxy contract, explaining each concept as we build.
Why Proxy Contracts?
Regular smart contracts can only be called by users on the same blockchain. TAC Proxy contracts solve a different problem: they let TON users call your EVM functions directly.
Without proxies:
- TON user bridges tokens to TAC
- User switches to MetaMask
- User interacts with your dApp
- User bridges tokens back to TON
With proxies:
- TON user calls your function directly from TON wallet
- ✨ Everything else happens automatically
Think of proxy contracts as API endpoints that TON users can call with blockchain transactions instead of HTTP requests.
Your First Proxy Contract
Let’s start with the absolute minimum proxy contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import { TacProxyV1 } from "@tonappchain/evm-ccl/contracts/proxies/TacProxyV1.sol";
import { TacHeaderV1 } from "@tonappchain/evm-ccl/contracts/core/Structs.sol";
contract HelloProxy is TacProxyV1 {
event HelloFromTON(string indexed tonUser, string message);
constructor(address crossChainLayer) TacProxyV1(crossChainLayer) {}
function sayHello(bytes calldata tacHeader, bytes calldata arguments)
external
_onlyCrossChainLayer
{
// 1. Decode who's calling from TON
TacHeaderV1 memory header = _decodeTacHeader(tacHeader);
// 2. Decode their message
string memory message = abi.decode(arguments, (string));
// 3. Do something with it
emit HelloFromTON(header.tvmCaller, message);
}
}
That’s it! This contract can receive messages from any TON user.
Understanding the Structure
Required Imports
import { TacProxyV1 } from "@tonappchain/evm-ccl/contracts/proxies/TacProxyV1.sol";
import { TacHeaderV1 } from "@tonappchain/evm-ccl/contracts/core/Structs.sol";
TacProxyV1
: The base contract that handles all cross-chain communication
TacHeaderV1
: Data structure containing information about the TON user
Inheritance
contract HelloProxy is TacProxyV1 {
Your contract must inherit from TacProxyV1
to receive cross-chain calls.
Constructor Parameter
constructor(address crossChainLayer) TacProxyV1(crossChainLayer) {}
The crossChainLayer
address is TAC’s infrastructure contract that will call your functions. You get this address from TAC documentation or contract addresses page.
Function Signature (Critical!)
function sayHello(bytes calldata tacHeader, bytes calldata arguments)
external
_onlyCrossChainLayer
{
Every cross-chain function must follow this exact pattern:
bytes calldata tacHeader
- Encoded TacHeaderV1 containing TON user information
bytes calldata arguments
- Your custom ABI-encoded parameters
external
- Must be externally callable
_onlyCrossChainLayer
- Security modifier that ensures only CrossChainLayer can call this function
Step-by-Step Walkthrough
TacHeaderV1 memory header = _decodeTacHeader(tacHeader);
The header tells you:
header.tvmCaller
- TON user’s address. !!! Important !!! It is always base64 mainnet bounceable format and starts with “EQ” (like “EQAbc123…”)
header.operationId
- Unique ID for this operation
header.timestamp
- When the TON transaction happened
Step 2: Decode Your Parameters
string memory message = abi.decode(arguments, (string));
The arguments
contain whatever data the TON user sent. You decide what format this should be - it could be:
- A single string:
abi.decode(arguments, (string))
- Multiple values:
abi.decode(arguments, (uint256, address, bool))
- A struct:
abi.decode(arguments, (MyCustomStruct))
Step 3: Execute Your Logic
emit HelloFromTON(header.tvmCaller, message);
This is where you do whatever your contract is supposed to do. In this example, we just emit an event, but you could:
- Store data in state variables
- Call other contracts
- Perform calculations
- Transfer tokens
Sending Responses Back to TON
Sometimes you want to send tokens back to the TON user. Use the OutMessageV1
structure:
import { TokenAmount, OutMessageV1 } from "@tonappchain/evm-ccl/contracts/core/Structs.sol";
// Send back to TON user (no fees for RoundTrip messages)
OutMessageV1 memory outMsg = OutMessageV1({
shardsKey: header.shardsKey, // Link to original operation
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: tokensToSend, // Tokens to send
toBridgeNFT: new NFTAmount[](0) // No NFTs
});
_sendMessageV1(outMsg, 0);
Deployment
Deploy your basic proxy like any other contract:
// scripts/deploy-basic-proxy.js
const { ethers } = require("hardhat");
async function main() {
// Get CrossChainLayer address from TAC documentation or contract addresses page
const crossChainLayerAddress = process.env.CROSS_CHAIN_LAYER_ADDRESS;
const HelloProxy = await ethers.getContractFactory("HelloProxy");
const proxy = await HelloProxy.deploy(crossChainLayerAddress);
await proxy.deployed();
console.log("HelloProxy deployed to:", proxy.address);
}
main().catch(console.error);
What’s Next?
You now understand the basics of TAC Proxy contracts. Ready for more complex patterns?
Start simple: Build and test basic proxy contracts before moving to
upgradeable patterns. Most use cases don’t actually need upgradeability.