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

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.