The NFT proxy contract must inherit from IERC721Receiver and implement the required onERC721Received function to correctly receive ERC‑721 tokens.
// SPDX-License-Identifier: MITpragma solidity ^0.8.28;import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";import { TacProxyV1 } from "@tonappchain/evm-ccl/contracts/proxies/TacProxyV1.sol";import { TacHeaderV1, TokenAmount, NFTAmount, OutMessageV1 } from "@tonappchain/evm-ccl/contracts/core/Structs.sol";contract TestNFTProxy is TacProxyV1, IERC721Receiver { constructor(address crossChainLayer) TacProxyV1(crossChainLayer) {} /** * @dev Handles the receipt of an ERC-721 token. * * Returns its Solidity selector to confirm the token transfer. */ function onERC721Received( address, address, uint256, bytes calldata ) external pure override(IERC721Receiver) returns (bytes4) { return this.onERC721Received.selector; } /** * @dev Receives NFTs bridged from TON. */ function receiveNFT(bytes calldata tacHeader, bytes calldata arguments) external _onlyCrossChainLayer { // this arguments just for example, you can define your own NFTAmount[] memory nfts = abi.decode(arguments, (NFTAmount[])); for (uint i = 0; i < nfts.length; i++) { IERC721(nfts[i].evmAddress).approve(_getCrossChainLayerAddress(), nfts[i].tokenId); } TacHeaderV1 memory header = _decodeTacHeader(tacHeader); // Bridge NFT back by creating an OutMessageV1 OutMessageV1 memory outMessage = OutMessageV1( header.shardsKey, header.tvmCaller, "", 0, // roundTripMessages don't require tvmProtocolFee as it's already paid on TON 0, // roundTripMessages don't require tvmExecutorFee as it's already paid on TON new string[](0), // no need to specify validExecutors as it's already specified in initial tx on TON new TokenAmount[](0), // No ERC20 tokens bridged nfts // NFTs to bridge (the 'amount' field is ignored for ERC721) ); _sendMessageV1(outMessage, 0); // 0 TACs to send back }}
OutMessageV1 memory outMessage = OutMessageV1( header.shardsKey, // Use from incoming header header.tvmCaller, // Send back to caller "", // Must be empty 0, // roundTrip - already paid on TON 0, // roundTrip - already paid on TON new string[](0), // roundTrip - already defined on TON new TokenAmount[](0), // No ERC20 tokens nfts // NFTs to bridge);
You can name the function whatever you want (e.g., processNFT, handleNFT, etc.) as long as it follows the function <name>(bytes calldata, bytes calldata) external pattern.