SPL Token Transfer & Approval on Neon EVM: A Comprehensive Guide to Cross-Chain Token Operations

Introduction

The blockchain landscape has shifted to a multi-chain paradigm, where Solana offers lightning-fast transactions and low costs, while Ethereum excels with its rich DeFi ecosystem. Neon EVM enables developers to harness the strengths of both networks. SPL tokens, Solana's native token standard, are now accessible to Ethereum-compatible applications via Neon EVM, opening doors to innovative cross-chain DeFi, gaming, and financial solutions.

This guide, put together from my research and personal knowledge acquired through testing and implementations, walks you through building SPL token transfer and approval functionality on Neon EVM.


The Cross-Chain Challenge

The Problem Space

Blockchain silos force users to pick between:

  • Solana: High performance, low fees, but a limited DeFi ecosystem.

  • Ethereum: Extensive DeFi, yet plagued by high gas costs and congestion.

Traditional cross-chain bridges introduce:

  • Security risks: Centralized points of failure.

  • Complexity: Multi-step transactions.

  • Cost: Additional bridge fees.

  • Liquidity fragmentation: Assets locked in bridge contracts.

Neon EVM's Solution

Neon EVM tackles these issues with:

  • Native Solana Integration: Direct access to Solana programs.

  • Ethereum Compatibility: Leverage EVM tools and interfaces.

  • Unified Experience: Develop for both ecosystems with one codebase.

  • Simplified Operations: Streamlined cross-chain workflows.


Neon EVM Architecture Overview

Core Components

Neon EVM translates Ethereum transactions into Solana program calls, featuring:

1. Neon Proxy

  • Purpose: Routes transactions across chains.

  • Functionality: Manages scheduling, execution, gas estimation.

  • Key Features: Nonce management and transaction routing.

2. Solana Native SDK

  • Purpose: Offers JavaScript/TypeScript interfaces for Solana.

  • Components: NeonProxyRpcApi, ScheduledTransaction, and balance account management.

3. Composability Libraries

  • Purpose: Modular libraries for Solana program interactions.

  • Structure: Separate modules for SPL tokens, system programs, etc.

Transaction Flow: Using Solana Native SDK

  1. User action via JavaScript/TypeScript.

  2. Solana wallet signing.

  3. Scheduled transaction creation.

  4. Solana transaction execution.

  5. Neon EVM execution.

  6. State synchronization.


SPL Token Integration

ERC20ForSPL: ERC 20 Interface Contract for SPL Tokens

ERC20ForSPL contracts link SPL tokens to ERC20 interfaces, offering:

  • Dual Interface Support: Compatible with both standards.

  • Cross-Chain State Management: Syncs token states.

  • Event Emission: Standard ERC20 events.

Contract Structure

contract ERC20ForSPL {
    bytes32 public tokenMint;  // Solana token mint address
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    function transfer(address to, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferSolana(bytes32 to, uint64 amount) external returns (bool);
    function approveSolana(bytes32 spender, uint64 amount) external returns (bool);
}

Associated Token Accounts (ATAs)

SPL tokens use ATAs for balance management:

  • Deterministic Addresses: Derived from owner and token mint.

  • Single Token Type: One token per ATA.

  • Ownership Model: Tied to the owner's Solana account.

ATA Creation

const ataAddress = await getAssociatedTokenAddress(
    tokenMint,      // SPL token mint
    ownerPublicKey, // Owner's Solana public key
    true           // Allow off-curve owner
);

Implementation Deep Dive

Project Structure

transfer-approve-spl/
├── composability/           # Cross-chain libraries
│   ├── CallSPLTokenProgram.sol
│   ├── CallAssociatedTokenProgram.sol
│   └── libraries/
├── token/                  # ERC20ForSPL contracts
├── precompiles/           # Neon EVM precompile interfaces
├── utils/                # Utility libraries
├── token-approval-solana-signer-sdk.js
└── token-transfer-solana-signer-sdk.js

Core Dependencies

{
  "@neonevm/solana-sign": "^0.2.1",
  "@solana/spl-token": "^0.4.13",
  "@solana/web3.js": "^1.98.2",
  "ethers": "^6.12.1"
}

Initialization

const connection = new web3.Connection('https://api.devnet.solana.com', 'confirmed');
const proxyApi = new NeonProxyRpcApi('https://devnet.neonevm.org/sol');

const solanaPrivateKey = bs58.decode(process.env.PRIVATE_KEY_SOLANA);
const keypair = web3.Keypair.fromSecretKey(solanaPrivateKey);
const {chainId, solanaUser} = await proxyApi.init(keypair);

if (await connection.getBalance(solanaUser.publicKey) == 0) {
    console.error('Add SOLs to', solanaUser.publicKey.toBase58());
    process.exit();
}

Token Approval Logic

Understanding SPL Approvals

Unlike ERC20, SPL approvals use a delegate model:

  • ERC20: State stored in contract.

  • SPL: State stored in token account.

Implementation

Approval Flow

const currentApproval = await USDC.allowance(solanaUser.neonWallet, solanaUser.neonWallet);

const transactionData = {
    from: solanaUser.neonWallet,
    to: USDC_ADDRESS,
    data: USDC.interface.encodeFunctionData("approve", [
        solanaUser.neonWallet, 
        Math.floor(Date.now() / 1000)
    ])
};

const transactionGas = await proxyApi.estimateScheduledTransactionGas({
    solanaPayer: solanaUser.publicKey,
    transactions: [transactionData]
});

Key Insights

  • Self-Approval: Same address for owner and spender.

  • Time-Based: Uses timestamps for approval windows.

  • Cross-Chain State: Maintained on Neon EVM, initiated from Solana.

Security Tips

  • Use time-limited approvals.

  • Approve only necessary amounts.

  • Regularly review and revoke approvals.


Token Transfer Logic

Cross-Chain Architecture

  • Solana Layer: ATA management, balance validation, delegation setup.

  • Neon EVM Layer: Contract execution, state updates, event emission.

Implementation

ATA Management

const solanaUserUSDC_ATA = await getAssociatedTokenAddress(
    new web3.PublicKey(ethers.encodeBase58(usdcTokenMint)),
    solanaUser.publicKey,
    true
);

try {
    ataInfo = await getAccount(connection, solanaUserUSDC_ATA);
} catch (e) {
    if (e instanceof TokenAccountNotFoundError) {
        const ataIx = createAssociatedTokenAccountInstruction(
            keypair.publicKey,
            solanaUserUSDC_ATA,
            keypair.publicKey,
            new web3.PublicKey(ethers.encodeBase58(usdcTokenMint))
        );
    }
}

Delegation Setup

if (ataInfo.delegate == null || ataInfo.delegate.toBase58() != usdcContractPDA.toBase58()) {
    scheduledTransaction.instructions.unshift(
        createApproveInstruction(
            solanaUserUSDC_ATA,
            usdcContractPDA,
            solanaUser.publicKey,
            '18446744073709551615' // max uint64
        )
    );
}

Transfer Execution

const transactionData = {
    from: solanaUser.neonWallet,
    to: USDC_ADDRESS,
    data: USDC.interface.encodeFunctionData("transfer", [
        receiver.address, 
        1 * 10 ** 6  // 1 USDC
    ])
};

const { scheduledTransaction } = await proxyApi.createScheduledTransaction({
    transactionGas,
    transactionData,
    nonce
});

Security Measures

  • Validate balances and amounts.

  • Verify delegates and recipients.


Scheduled Transaction Mechanism

Lifecycle

  1. Initialization with Solana wallet.

  2. Gas estimation.

  3. Transaction creation.

  4. Balance account setup.

  5. Signing and execution.

Implementation

const nonce = Number(await proxyApi.getTransactionCount(solanaUser.neonWallet));

const { scheduledTransaction } = await proxyApi.createScheduledTransaction({
    transactionGas,
    transactionData,
    nonce
});

const account = await connection.getAccountInfo(solanaUser.balanceAddress);
if (account === null) {
    scheduledTransaction.instructions.unshift(
        createBalanceAccountInstruction(
            neonEvmProgram,
            solanaUser.publicKey,
            solanaUser.neonWallet,
            chainId
        )
    );
}

const { blockhash } = await connection.getLatestBlockhash();
scheduledTransaction.recentBlockhash = blockhash;
scheduledTransaction.sign({ publicKey: solanaUser.publicKey, secretKey: solanaUser.keypair.secretKey });

const signature = await connection.sendRawTransaction(scheduledTransaction.serialize());

Nonce Management

  • Unique per account.

  • Sequential ordering.

  • Cross-chain managed.

Nonce Code

const nonce = Number(await proxyApi.getTransactionCount(solanaUser.neonWallet));
const scheduledTransactionInstance = new ScheduledTransaction({
    nonce: toBeHex(nonce),
    payer: solanaUser.neonWallet,
    target: contractAddress,
    callData: transactionData,
    maxFeePerGas: toBeHex(0x77359400),
    chainId: toBeHex(NeonChainId.testnetSol)
});

Security Considerations

Authentication

  • Secure Solana keypair storage.

  • Proper Neon EVM address derivation.

Authorization

  • PDA-based delegation.

  • Time-limited, amount-capped approvals.

Challenges

  • State Sync: Handle race conditions and consistency.

  • Attacks: Prevent replay and front-running with nonces.

Best Practices

modifier nonReentrant() {
    require(!locked, "Reentrant call");
    locked = true;
    _;
    locked = false;
}
  • Regular audits and monitoring.

Performance Optimization

Gas Strategies

Batch Operations

const instructions = [
    createBalanceAccountInstruction(...),
    createApproveInstruction(...),
    createTransferInstruction(...)
];
scheduledTransaction.instructions.push(...instructions);

Caching

  • Cache account info, balances, and transaction status.

Network Optimization

  • Use connection pooling and fallback RPCs.

  • Adjust priority fees dynamically.


Real-World Applications

DeFi

  • Cross-chain DEX and yield farming.

Gaming

  • Portable NFTs and cross-chain tournaments.

Enterprise

  • Supply chain tracking and cross-chain payments.

Best Practices

Code Organization

class TokenManager {
    constructor(provider, signer) {
        this.provider = provider;
        this.signer = signer;
    }
    async approve(token, spender, amount) { /* ... */ }
    async transfer(token, to, amount) { /* ... */ }
}

Testing

describe('Token Transfer', () => {
    it('should transfer tokens', async () => {
        const transfer = await tokenManager.transfer(usdcToken, recipient, amount);
        expect(transfer.status).toBe('success');
    });
});

Monitoring

class TransactionMonitor {
    async monitorTransaction(txHash, chain) {
        const status = await this.getTransactionStatus(txHash, chain);
        if (status === 'failed') await this.sendAlert(`Failed: ${txHash}`);
        return status;
    }
}

Conclusion

SPL token transfer and approval on Neon EVM revolutionize cross-chain interoperability. With a robust architecture, enhanced security, and optimization techniques, developers can build powerful applications spanning Solana and Ethereum.

Future Directions

  • Enhanced composability and security.

  • Better developer tools and standards.

Final Thoughts

This technology paves the way for a unified blockchain ecosystem, driving innovation in DeFi, gaming, and enterprise use cases.


References


0
Subscribe to my newsletter

Read articles from Goodness Mbakara directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Goodness Mbakara
Goodness Mbakara

Python | Django | Backend | Software | Rest API Developer