Skip to main content
Smart accounts (account abstraction) enable programmable wallet logic on TAC, allowing developers to create accounts with custom execution rules, gas payment mechanisms, and transaction batching capabilities. TAC provides a shared factory that all developers can use, creating one smart account per user per proxy for seamless cross-chain interactions.
Contract Addresses: Find the latest TacSAFactory and TacSmartAccount addresses for both testnet and mainnet on the Contract Addresses page.

Core Concept

TAC uses a shared factory approach where:
  • There is one factory instance deployed on the chain that everyone can use
  • Each user gets one smart account per proxy contract
  • Smart accounts support advanced features like multicall, NFT handling, and hook-based execution
  • All accounts are upgradeable through the shared beacon pattern
  • TacSmartAccount Contract
  • TacSAFactory Contract
Advanced account logic with comprehensive capabilities:
  • Execute arbitrary transactions with custom validation
  • Multicall support for batch operations
  • One-time ticket system for secure proxy interactions
  • NFT receiving capabilities (IERC721Receiver)
  • Safe token operations with SafeERC20
  • Execute and executeUnsafe methods
  • Delegatecall support for advanced patterns

TacSmartAccount Implementation

The latest smart account implementation provides comprehensive account abstraction functionality:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

contract TacSmartAccount is Initializable, IERC721Receiver {

    using SafeERC20 for IERC20;

    address public owner;

    event Executed(address indexed target, uint256 value, bytes data);

    mapping(address caller => bool ticket) public oneTimeTickets;

    error ExecutionFailed(address target, uint256 value, bytes data, bytes returnData);
    error AccessDenied(address caller);

    modifier onlyOwnerOrTicket() {
        if (oneTimeTickets[msg.sender]) {
            oneTimeTickets[msg.sender] = false;
        } else {
            require(msg.sender == owner, AccessDenied(msg.sender));
        }
        _;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, AccessDenied(msg.sender));
        _;
    }

    constructor() {
        _disableInitializers();
    }

    function initialize(address _owner) public initializer {
        owner = _owner;
    }

    function execute(address target, uint256 value, bytes calldata data) external payable onlyOwnerOrTicket returns(bytes memory) {
        (bool success, bytes memory returnData) = target.call{value: value}(data);
        require(success, ExecutionFailed(target, value, data, returnData));
        emit Executed(target, value, data);
        return returnData;
    }

    function executeUnsafe(address target, uint256 value, bytes calldata data) external payable onlyOwnerOrTicket returns(bool success, bytes memory returnData)  {
        (success, returnData) = target.call{value: value}(data);
        emit Executed(target, value, data);
    }

    function delegatecall(address target, bytes calldata data) external onlyOwner returns(bool success, bytes memory returnData) {
        (success, returnData) = target.delegatecall(data);
        require(success, ExecutionFailed(target, 0, data, returnData));
        emit Executed(target, 0, data);
    }

    function createOneTimeTicket(address caller) external onlyOwner {
        oneTimeTickets[caller] = true;
    }

    function revokeOneTimeTicket(address caller) external onlyOwner {
        oneTimeTickets[caller] = false;
    }

    function approve(IERC20 token, address to, uint256 amount) external onlyOwnerOrTicket{
        token.forceApprove(to, amount);
    }

    function multicall(address[] calldata targets, uint256[] calldata values, bytes[] calldata data) external payable onlyOwnerOrTicket returns(bytes[] memory) {
        bytes[] memory results = new bytes[](targets.length);
        for (uint256 i = 0; i < targets.length; i++) {
            (bool success, bytes memory returnData) = targets[i].call{value: values[i]}(data[i]);
            require(success, ExecutionFailed(targets[i], values[i], data[i], returnData));
            results[i] = returnData;
        }
        return results;
    }

    receive() external payable {}

    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external pure override returns (bytes4) {
        return this.onERC721Received.selector;
    }
}

Key Features

Multiple execution methods provide flexibility for different use cases:
  • execute(): Safe execution that reverts on failure
  • executeUnsafe(): Returns success/failure without reverting
  • delegatecall(): Advanced pattern for library calls
  • multicall(): Batch multiple operations in one transaction
  • Return Data: All methods capture and return execution results
  • Event Logging: Track all executed transactions
Secure proxy interaction system for cross-chain operations:
  • Proxy Authorization: Proxy contracts can create tickets for users
  • Single Use: Each ticket can only be used once for security
  • Owner Control: Only the owner can create/revoke tickets
  • Automatic Cleanup: Tickets are automatically consumed after use
Comprehensive asset management capabilities:
  • SafeERC20: Uses OpenZeppelin’s safe token operations
  • forceApprove(): Handles tokens with approval edge cases
  • IERC721Receiver: Can receive NFTs directly
  • Batch Operations: Combine token operations with other calls
Flexible access control supporting both owner and proxy operations:
  • onlyOwner: Functions restricted to the account owner
  • onlyOwnerOrTicket: Functions accessible via one-time tickets
  • Custom Errors: Clear error messages for access violations
  • Initialization Security: Secure setup during deployment

TacSAFactory - Shared Factory Contract

The shared factory contract that all developers can use for smart account deployment:
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.28;

import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { TacSmartAccount } from "./TacSmartAccount.sol";
import { ISAFactory } from "./interfaces/ISAFactory.sol";
import { TacInitializable } from "../core/TacInitializable.sol";

contract TacSAFactory is TacInitializable, Ownable2StepUpgradeable, UUPSUpgradeable, ISAFactory {
    UpgradeableBeacon public beacon;
    mapping(address application => mapping(bytes32 id => address smartAccount)) public smartAccounts;

    event SmartAccountCreated(address indexed smartAccountAddress, address indexed application, string tvmWallet);

    function initialize(
        address _initBlueprint
    ) external initializer {
        __Ownable2Step_init();
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
        beacon = new UpgradeableBeacon(_initBlueprint, address(this));
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        override
        onlyOwner
    {}

    function getOrCreateSmartAccount(
        string memory tvmWallet
    ) external returns (address, bool isNewAccount) {
        bytes32 id = keccak256(abi.encodePacked(tvmWallet));
        if (smartAccounts[msg.sender][id] != address(0)) {
            return (smartAccounts[msg.sender][id], false);
        }
        address account = _createSmartAccount(tvmWallet, msg.sender);
        smartAccounts[msg.sender][id] = account;
        return (account, true);
    }

    function getSmartAccountForApplication(
        string memory tvmWallet,
        address application
    ) external view returns (address) {
        bytes32 id = keccak256(abi.encodePacked(tvmWallet));
        if (smartAccounts[application][id] == address(0)) {
            return predictSmartAccountAddress(tvmWallet, application);
        }
        return smartAccounts[application][id];
    }

    function predictSmartAccountAddress(
        string memory tvmWallet,
        address application
    ) public view returns (address) {
        bytes32 id = keccak256(abi.encodePacked(tvmWallet));
        if (smartAccounts[application][id] != address(0)) {
            return smartAccounts[application][id];
        }

        // Predict the address using the same logic as _createSmartAccount
        bytes memory bytecode = abi.encodePacked(
            type(BeaconProxy).creationCode,
            abi.encode(
                address(beacon),
                abi.encodeWithSelector(
                    TacSmartAccount.initialize.selector,
                    application
                )
            )
        );

        bytes32 salt = keccak256(abi.encodePacked(application, id));
        return address(uint160(uint256(keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            keccak256(bytecode)
        )))));
    }

    function _createSmartAccount(string memory tvmWallet, address application) internal returns (address) {
        bytes32 id = keccak256(abi.encodePacked(tvmWallet));
        bytes32 salt = keccak256(abi.encodePacked(application, id));
        BeaconProxy proxy = new BeaconProxy{salt: salt}(
            address(beacon),
            abi.encodeWithSelector(
                TacSmartAccount.initialize.selector,
                application
            )
        );
        emit SmartAccountCreated(address(proxy), application, tvmWallet);
        return address(proxy);
    }

    function updateBlueprint(address _newBlueprint) external onlyOwner {
        beacon.upgradeTo(_newBlueprint);
    }
}

Shared Factory Benefits

TAC provides a single factory instance that all developers can use. This approach ensures consistency and reduces deployment costs.
  • One Factory for All: Single deployed instance serves all proxy contracts
  • Per-Application Isolation: Each proxy gets its own mapping of user accounts
  • Deterministic Addresses: Predict smart account addresses before deployment
  • Atomic Upgrades: All accounts upgrade simultaneously when the beacon is updated
  • Version Consistency: Ensures all accounts have the same feature set

Using the Shared Factory in Proxy Contracts

Proxy contracts can leverage the shared TacSAFactory to create and manage smart accounts for users:

Factory Integration Pattern

// 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";
import { ISAFactory } from "@tonappchain/evm-ccl/contracts/interfaces/ISAFactory.sol";
import { ITacSmartAccount } from "@tonappchain/evm-ccl/contracts/interfaces/ITacSmartAccount.sol";

contract MyProxy is TacProxyV1 {
    ISAFactory public immutable saFactory;

    constructor(address _crossChainLayer, address _saFactory)
        TacProxyV1(_crossChainLayer)
    {
        saFactory = ISAFactory(_saFactory);
    }

    function executeWithSmartAccount(bytes calldata tacHeader, bytes calldata arguments)
        external
        _onlyCrossChainLayer
    {
        TacHeaderV1 memory header = _decodeTacHeader(tacHeader);

        // Get or create smart account for this user and proxy combination
        (address smartAccount, bool isNewAccount) = saFactory.getOrCreateSmartAccount(header.tvmCaller);

        // Decode operation parameters
        (address target, bytes memory data, uint256 value) = abi.decode(arguments, (address, bytes, uint256));

        // Create one-time ticket for the smart account to execute
        ITacSmartAccount(smartAccount).createOneTimeTicket(address(this));

        // Execute through smart account
        ITacSmartAccount(smartAccount).execute(target, value, data);
    }
}

Off-Chain Address Calculation

Calculate smart account addresses before transactions for encoding in arguments:
// Get the smart account address for a specific user and proxy
const smartAccountAddress = await tacSAFactory.getSmartAccountForApplication(
  tvmWalletCaller, // TON wallet address in EQ format (e.g., "EQAbc123...")
  proxyAddress // Address of your proxy contract
);

// Use this address in your transaction arguments
const encodedArguments = ethers.AbiCoder.defaultAbiCoder().encode(
  ["address", "bytes", "uint256"],
  [smartAccountAddress, callData, value]
);

SaHelper Library for Advanced Hooks

TAC provides a powerful hooks system for complex smart account operations:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import { IHooks } from "../interfaces/IHooks.sol";
import { ITacSmartAccount } from "../interfaces/ITacSmartAccount.sol";
import { TokenAmount, NFTAmount } from "../../core/Structs.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

library SaHelper {
    function executePreHooks(address sa, IHooks.SaHooks memory hooks) internal returns(bytes[] memory) {
        IHooks.PreHook[] memory preHooks = hooks.preHooks;
        bytes[] memory results = new bytes[](preHooks.length);
        for (uint256 i = 0; i < preHooks.length; i++) {
            if (preHooks[i].isFromSAPerspective) {
                results[i] = ITacSmartAccount(sa).execute(preHooks[i].contractAddress, preHooks[i].value, preHooks[i].data);
            } else {
                results[i] = _selfCall(preHooks[i].contractAddress, preHooks[i].value, preHooks[i].data);
            }
        }
        return results;
    }

    function executePostHooks(address sa, IHooks.SaHooks memory hooks) internal returns(bytes[] memory) {
        IHooks.PostHook[] memory postHooks = hooks.postHooks;
        bytes[] memory results = new bytes[](postHooks.length);
        for (uint256 i = 0; i < postHooks.length; i++) {
            if (postHooks[i].isFromSAPerspective) {
                results[i] = ITacSmartAccount(sa).execute(postHooks[i].contractAddress, postHooks[i].value, postHooks[i].data);
            } else {
                results[i] = _selfCall(postHooks[i].contractAddress, postHooks[i].value, postHooks[i].data);
            }
        }
        return results;
    }

    function executeMainCall(address sa, IHooks.SaHooks memory hooks) internal returns(bytes memory) {
        IHooks.MainCallHook memory mainCallHook = hooks.mainCallHook;
        if (mainCallHook.isFromSAPerspective) {
            return ITacSmartAccount(sa).execute(mainCallHook.contractAddress, mainCallHook.value, mainCallHook.data);
        } else {
            return _selfCall(mainCallHook.contractAddress, mainCallHook.value, mainCallHook.data);
        }
    }

    function _selfCall(address to, uint256 value, bytes memory data) internal returns(bytes memory) {
        (bool success, bytes memory returnData) = to.call{value: value}(data);
        require(success, "Self call failed");
        return returnData;
    }
}

Hook System Usage

The hooks system allows you to define:
  • Pre-hooks: Operations before main execution
  • Main call: Primary operation
  • Post-hooks: Operations after main execution
  • Perspective: Whether calls are from smart account or proxy contract

TypeScript SDK for Hooks

Use the SaHooksBuilder for easy hook creation:
import { SaHooksBuilder } from "@tonappchain/sdk";

// Create hooks for a complex DeFi operation
const hooksBuilder = new SaHooksBuilder()
  .addContractInterface(tokenAddress, tokenABI)
  .addContractInterface(dexAddress, dexABI)
  // Pre-hook: Approve tokens from smart account
  .addPreHookCallFromSA(tokenAddress, "approve", [dexAddress, amount])
  // Main call: Execute swap from proxy perspective
  .setMainCallHookCallFromSelf(dexAddress, "swapTokens", [
    tokenIn,
    tokenOut,
    amount,
    minAmountOut,
  ])
  // Post-hook: Check balance from smart account
  .addPostHookCallFromSA(tokenAddress, "balanceOf", [smartAccountAddress]);

const encodedHooks = hooksBuilder.encode();
System Control: The smart account abstraction system is currently under the control of a multisig account managed by the TAC team. This ensures coordinated upgrades and security oversight during the protocol’s early phases.
I