XRPL EVM Sidechain - Development | XRPL Interoperability | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
advanced60 min

XRPL EVM Sidechain - Development

Learning Objectives

Configure a complete development environment for XRPL EVM Sidechain

Deploy smart contracts using standard EVM tooling (Hardhat, Foundry)

Interact with the bridge programmatically for deposits and withdrawals

Build a simple DeFi application (DEX or lending pool) on the sidechain

Test and debug cross-chain functionality between mainnet and sidechain

Your Action Items0/3 completed

XRPL EVM Sidechain Network Parameters:

// Network configuration (values may change - verify with official docs)
const XRPL_EVM_TESTNET = {
  chainId: 1440002,                    // Testnet chain ID
  chainName: "XRPL EVM Sidechain Testnet",
  nativeCurrency: {
    name: "XRP",
    symbol: "XRP",
    decimals: 18
  },
  rpcUrls: ["https://rpc-evm-sidechain.xrpl.org"], 
  blockExplorerUrls: ["https://explorer.xrpl-evm-sidechain.org"] 
};

const XRPL_EVM_MAINNET = {
  chainId: 1440001,                    // Mainnet chain ID (when live)
  chainName: "XRPL EVM Sidechain",
  nativeCurrency: {
    name: "XRP",
    symbol: "XRP", 
    decimals: 18
  },
  rpcUrls: ["https://rpc.xrpl-evm-sidechain.org"],   // TBD
  blockExplorerUrls: ["https://explorer.xrpl-evm-sidechain.org"] 
};

Adding to MetaMask:

// Programmatically add network to MetaMask
async function addXRPLEVMNetwork() {
  try {
    await window.ethereum.request({
      method: 'wallet_addEthereumChain',
      params: [{
        chainId: '0x15F902',  // 1440002 in hex
        chainName: 'XRPL EVM Sidechain Testnet',
        nativeCurrency: {
          name: 'XRP',
          symbol: 'XRP',
          decimals: 18
        },
        rpcUrls: ['https://rpc-evm-sidechain.xrpl.org'],
        blockExplorerUrls: ['https://explorer.xrpl-evm-sidechain.org']
      }]
    });
    console.log('Network added successfully');
  } catch (error) {
    console.error('Failed to add network:', error);
  }
}

Initialize a New Project:

# Create project directory
mkdir xrpl-evm-project
cd xrpl-evm-project

# Initialize npm project
npm init -y

# Install Hardhat and dependencies
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox

# Initialize Hardhat
npx hardhat init

# Select: Create a JavaScript project

Configure Hardhat for XRPL EVM:

// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    xrplEvmTestnet: {
      url: "https://rpc-evm-sidechain.xrpl.org", 
      chainId: 1440002,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
      gasPrice: 1000000000  // 1 gwei (adjust as needed)
    },
    xrplEvmMainnet: {
      url: "https://rpc.xrpl-evm-sidechain.org",   // When available
      chainId: 1440001,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
    },
    hardhat: {
      forking: {
        url: "https://rpc-evm-sidechain.xrpl.org", 
        enabled: true
      }
    }
  },
  etherscan: {
    // Configure for sidechain explorer verification when available
    apiKey: {
      xrplEvmTestnet: process.env.EXPLORER_API_KEY || ""
    },
    customChains: [{
      network: "xrplEvmTestnet",
      chainId: 1440002,
      urls: {
        apiURL: "https://explorer.xrpl-evm-sidechain.org/api", 
        browserURL: "https://explorer.xrpl-evm-sidechain.org" 
      }
    }]
  }
};

Environment Setup:

# Create .env file
touch .env

# Add to .env (never commit this file!)
PRIVATE_KEY=your_private_key_here
EXPLORER_API_KEY=your_api_key_here
# Create .gitignore
echo "node_modules/
.env
coverage/
artifacts/
cache/
typechain-types/" > .gitignore

For developers preferring Foundry:

# Install Foundry
curl -L https://foundry.paradigm.xyz  | bash
foundryup

# Create new project
forge init xrpl-evm-foundry
cd xrpl-evm-foundry

# Configure foundry.toml
cat > foundry.toml << 'EOF'
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.20"
optimizer = true
optimizer_runs = 200

[rpc_endpoints]
xrpl_evm_testnet = "https://rpc-evm-sidechain.xrpl.org" 

[etherscan]
xrpl_evm_testnet = { key = "${EXPLORER_API_KEY}" }
EOF

Foundry Deployment Script:

// script/Deploy.s.sol
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/SimpleToken.sol";

contract DeployScript is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

SimpleToken token = new SimpleToken("XRPL Test Token", "XTT", 1000000 * 10**18);

console.log("Token deployed to:", address(token));

vm.stopBroadcast();
    }
}

Simple Token Contract:

// contracts/SimpleToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title SimpleToken
 * @dev A basic ERC20 token for XRPL EVM Sidechain
 */
contract SimpleToken is ERC20, Ownable {
    constructor(
        string memory name,
        string memory symbol,
        uint256 initialSupply
    ) ERC20(name, symbol) Ownable(msg.sender) {
        _mint(msg.sender, initialSupply);
    }

/**
     * @dev Mint new tokens (only owner)
     */
    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }

/**
     * @dev Burn tokens from caller's balance
     */
    function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }
}

Install OpenZeppelin:

npm install @openzeppelin/contracts

Deployment Script:

// scripts/deploy-token.js
const hre = require("hardhat");

async function main() {
  const [deployer] = await hre.ethers.getSigners();
  console.log("Deploying contracts with account:", deployer.address);

const balance = await hre.ethers.provider.getBalance(deployer.address);
  console.log("Account balance:", hre.ethers.formatEther(balance), "XRP");

// Deploy SimpleToken
  const SimpleToken = await hre.ethers.getContractFactory("SimpleToken");
  const token = await SimpleToken.deploy(
    "XRPL Test Token",
    "XTT",
    hre.ethers.parseEther("1000000")  // 1 million tokens
  );

await token.waitForDeployment();
  const tokenAddress = await token.getAddress();

console.log("SimpleToken deployed to:", tokenAddress);

// Verify deployment
  const totalSupply = await token.totalSupply();
  console.log("Total supply:", hre.ethers.formatEther(totalSupply), "XTT");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Deploy to Testnet:

# Compile contracts
npx hardhat compile

# Deploy to XRPL EVM testnet
npx hardhat run scripts/deploy-token.js --network xrplEvmTestnet

A minimal AMM for trading on XRPL EVM:

// contracts/SimpleAMM.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title SimpleAMM
 * @dev Constant product AMM (x * y = k) for XRPL EVM
 */
contract SimpleAMM is ERC20, ReentrancyGuard {
    IERC20 public immutable tokenA;
    IERC20 public immutable tokenB;

uint256 public reserveA;
    uint256 public reserveB;

uint256 public constant FEE_NUMERATOR = 3;      // 0.3% fee
    uint256 public constant FEE_DENOMINATOR = 1000;

event LiquidityAdded(
        address indexed provider,
        uint256 amountA,
        uint256 amountB,
        uint256 liquidity
    );

event LiquidityRemoved(
        address indexed provider,
        uint256 amountA,
        uint256 amountB,
        uint256 liquidity
    );

event Swap(
        address indexed trader,
        address tokenIn,
        uint256 amountIn,
        address tokenOut,
        uint256 amountOut
    );

constructor(
        address _tokenA,
        address _tokenB
    ) ERC20("XRPL AMM LP Token", "XRPLP") {
        require(_tokenA != _tokenB, "Identical tokens");
        tokenA = IERC20(_tokenA);
        tokenB = IERC20(_tokenB);
    }

/**
     * @dev Add liquidity to the pool
     */
    function addLiquidity(
        uint256 amountA,
        uint256 amountB
    ) external nonReentrant returns (uint256 liquidity) {
        tokenA.transferFrom(msg.sender, address(this), amountA);
        tokenB.transferFrom(msg.sender, address(this), amountB);

uint256 _totalSupply = totalSupply();

if (_totalSupply == 0) {
            // Initial liquidity
            liquidity = sqrt(amountA * amountB);
        } else {
            // Proportional liquidity
            liquidity = min(
                (amountA * _totalSupply) / reserveA,
                (amountB * _totalSupply) / reserveB
            );
        }

require(liquidity > 0, "Insufficient liquidity minted");
        _mint(msg.sender, liquidity);

reserveA += amountA;
        reserveB += amountB;

emit LiquidityAdded(msg.sender, amountA, amountB, liquidity);
    }

/**
     * @dev Remove liquidity from the pool
     */
    function removeLiquidity(
        uint256 liquidity
    ) external nonReentrant returns (uint256 amountA, uint256 amountB) {
        require(liquidity > 0, "Zero liquidity");

uint256 _totalSupply = totalSupply();
        amountA = (liquidity * reserveA) / _totalSupply;
        amountB = (liquidity * reserveB) / _totalSupply;

require(amountA > 0 && amountB > 0, "Insufficient amounts");

_burn(msg.sender, liquidity);

reserveA -= amountA;
        reserveB -= amountB;

tokenA.transfer(msg.sender, amountA);
        tokenB.transfer(msg.sender, amountB);

emit LiquidityRemoved(msg.sender, amountA, amountB, liquidity);
    }

/**
     * @dev Swap tokens
     */
    function swap(
        address tokenIn,
        uint256 amountIn,
        uint256 minAmountOut
    ) external nonReentrant returns (uint256 amountOut) {
        require(
            tokenIn == address(tokenA) || tokenIn == address(tokenB),
            "Invalid token"
        );
        require(amountIn > 0, "Zero amount");

bool isTokenA = tokenIn == address(tokenA);

(IERC20 _tokenIn, IERC20 _tokenOut, uint256 reserveIn, uint256 reserveOut) = 
            isTokenA 
                ? (tokenA, tokenB, reserveA, reserveB)
                : (tokenB, tokenA, reserveB, reserveA);

// Transfer in
        _tokenIn.transferFrom(msg.sender, address(this), amountIn);

// Calculate output with fee
        uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_NUMERATOR);
        amountOut = (amountInWithFee * reserveOut) / 
                    (reserveIn * FEE_DENOMINATOR + amountInWithFee);

require(amountOut >= minAmountOut, "Slippage exceeded");

// Transfer out
        _tokenOut.transfer(msg.sender, amountOut);

// Update reserves
        if (isTokenA) {
            reserveA += amountIn;
            reserveB -= amountOut;
        } else {
            reserveB += amountIn;
            reserveA -= amountOut;
        }

emit Swap(msg.sender, tokenIn, amountIn, address(_tokenOut), amountOut);
    }

/**
     * @dev Get quote for swap
     */
    function getAmountOut(
        address tokenIn,
        uint256 amountIn
    ) external view returns (uint256) {
        bool isTokenA = tokenIn == address(tokenA);
        uint256 reserveIn = isTokenA ? reserveA : reserveB;
        uint256 reserveOut = isTokenA ? reserveB : reserveA;

uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_NUMERATOR);
        return (amountInWithFee * reserveOut) / 
               (reserveIn * FEE_DENOMINATOR + amountInWithFee);
    }

// Helper functions
    function sqrt(uint256 y) internal pure returns (uint256 z) {
        if (y > 3) {
            z = y;
            uint256 x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }

function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }
}

Comprehensive Test Suite:

// test/SimpleAMM.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("SimpleAMM", function () {
  let tokenA, tokenB, amm;
  let owner, alice, bob;

const INITIAL_SUPPLY = ethers.parseEther("1000000");

beforeEach(async function () {
    [owner, alice, bob] = await ethers.getSigners();

// Deploy tokens
    const SimpleToken = await ethers.getContractFactory("SimpleToken");
    tokenA = await SimpleToken.deploy("Token A", "TKA", INITIAL_SUPPLY);
    tokenB = await SimpleToken.deploy("Token B", "TKB", INITIAL_SUPPLY);

// Deploy AMM
    const SimpleAMM = await ethers.getContractFactory("SimpleAMM");
    amm = await SimpleAMM.deploy(
      await tokenA.getAddress(),
      await tokenB.getAddress()
    );

// Setup: Transfer tokens to alice
    await tokenA.transfer(alice.address, ethers.parseEther("10000"));
    await tokenB.transfer(alice.address, ethers.parseEther("10000"));

// Approve AMM to spend tokens
    await tokenA.connect(alice).approve(await amm.getAddress(), ethers.MaxUint256);
    await tokenB.connect(alice).approve(await amm.getAddress(), ethers.MaxUint256);
  });

describe("Liquidity", function () {
    it("Should add initial liquidity", async function () {
      const amountA = ethers.parseEther("1000");
      const amountB = ethers.parseEther("1000");

await amm.connect(alice).addLiquidity(amountA, amountB);

expect(await amm.reserveA()).to.equal(amountA);
      expect(await amm.reserveB()).to.equal(amountB);
      expect(await amm.balanceOf(alice.address)).to.equal(amountA); // sqrt(1000*1000) = 1000
    });

it("Should remove liquidity proportionally", async function () {
      const amountA = ethers.parseEther("1000");
      const amountB = ethers.parseEther("1000");

await amm.connect(alice).addLiquidity(amountA, amountB);

const liquidity = await amm.balanceOf(alice.address);
      const halfLiquidity = liquidity / 2n;

await amm.connect(alice).removeLiquidity(halfLiquidity);

expect(await amm.reserveA()).to.equal(amountA / 2n);
      expect(await amm.reserveB()).to.equal(amountB / 2n);
    });
  });

describe("Swaps", function () {
    beforeEach(async function () {
      // Add liquidity
      await amm.connect(alice).addLiquidity(
        ethers.parseEther("1000"),
        ethers.parseEther("1000")
      );
    });

it("Should swap tokens correctly", async function () {
      // Transfer tokens to bob
      await tokenA.transfer(bob.address, ethers.parseEther("100"));
      await tokenA.connect(bob).approve(await amm.getAddress(), ethers.MaxUint256);

const amountIn = ethers.parseEther("10");
      const expectedOut = await amm.getAmountOut(await tokenA.getAddress(), amountIn);

const bobBalanceBefore = await tokenB.balanceOf(bob.address);

await amm.connect(bob).swap(
        await tokenA.getAddress(),
        amountIn,
        expectedOut * 99n / 100n  // 1% slippage tolerance
      );

const bobBalanceAfter = await tokenB.balanceOf(bob.address);
      expect(bobBalanceAfter - bobBalanceBefore).to.be.closeTo(
        expectedOut,
        ethers.parseEther("0.01")
      );
    });

it("Should revert on excessive slippage", async function () {
      await tokenA.transfer(bob.address, ethers.parseEther("100"));
      await tokenA.connect(bob).approve(await amm.getAddress(), ethers.MaxUint256);

const amountIn = ethers.parseEther("10");

await expect(
        amm.connect(bob).swap(
          await tokenA.getAddress(),
          amountIn,
          ethers.parseEther("100")  // Impossible output
        )
      ).to.be.revertedWith("Slippage exceeded");
    });
  });
});

Run Tests:

npx hardhat test
npx hardhat coverage  # For test coverage

Bridge Interface (Conceptual):

// interfaces/IBridge.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @title IBridge
 * @dev Interface for XRPL EVM Sidechain bridge
 * Note: Actual interface may differ - verify with official docs
 */
interface IBridge {
    /**
     * @dev Claim XRP deposited from mainnet
     * Called by bridge after witness attestation
     */
    function claim(
        bytes32 mainnetTxHash,
        address recipient,
        uint256 amount,
        bytes[] calldata signatures
    ) external;

/**
     * @dev Initiate withdrawal to mainnet
     * Burns wrapped XRP, emits event for witnesses
     */
    function withdraw(
        string calldata mainnetDestination,
        uint256 amount
    ) external;

/**
     * @dev Check if a mainnet transaction has been claimed
     */
    function isClaimed(bytes32 mainnetTxHash) external view returns (bool);

/**
     * @dev Get withdrawal status
     */
    function getWithdrawalStatus(bytes32 withdrawalId) external view returns (
        string memory destination,
        uint256 amount,
        uint256 timestamp,
        bool processed
    );

// Events
    event Deposit(
        bytes32 indexed mainnetTxHash,
        address indexed recipient,
        uint256 amount
    );

event WithdrawalInitiated(
        bytes32 indexed withdrawalId,
        address indexed sender,
        string mainnetDestination,
        uint256 amount
    );

event WithdrawalProcessed(
        bytes32 indexed withdrawalId,
        bytes32 mainnetTxHash
    );
}

Bridge Helper Library:

// lib/bridge.js
const { ethers } = require("ethers");

class XRPLBridge {
  constructor(provider, bridgeAddress, bridgeABI) {
    this.provider = provider;
    this.bridge = new ethers.Contract(bridgeAddress, bridgeABI, provider);
  }

/**
   * Initiate withdrawal from sidechain to mainnet
   */
  async withdraw(signer, mainnetAddress, amount) {
    const bridgeWithSigner = this.bridge.connect(signer);

// Validate mainnet address format (r...)
    if (!mainnetAddress.startsWith('r') || mainnetAddress.length < 25) {
      throw new Error('Invalid XRPL mainnet address');
    }

const tx = await bridgeWithSigner.withdraw(
      mainnetAddress,
      ethers.parseEther(amount.toString())
    );

const receipt = await tx.wait();

// Extract withdrawal ID from event
    const withdrawalEvent = receipt.logs.find(
      log => log.topics[0] === ethers.id("WithdrawalInitiated(bytes32,address,string,uint256)")
    );

return {
      txHash: receipt.hash,
      withdrawalId: withdrawalEvent?.topics[1],
      blockNumber: receipt.blockNumber
    };
  }

/**
   * Check deposit status
   */
  async checkDeposit(mainnetTxHash) {
    const claimed = await this.bridge.isClaimed(mainnetTxHash);
    return { mainnetTxHash, claimed };
  }

/**
   * Check withdrawal status
   */
  async checkWithdrawal(withdrawalId) {
    const status = await this.bridge.getWithdrawalStatus(withdrawalId);
    return {
      destination: status.destination,
      amount: ethers.formatEther(status.amount),
      timestamp: new Date(Number(status.timestamp) * 1000),
      processed: status.processed
    };
  }

/**
   * Wait for deposit confirmation
   */
  async waitForDeposit(mainnetTxHash, timeoutMs = 300000) {
    const startTime = Date.now();

while (Date.now() - startTime < timeoutMs) {
      const { claimed } = await this.checkDeposit(mainnetTxHash);
      if (claimed) {
        return true;
      }
      await new Promise(resolve => setTimeout(resolve, 5000));
    }

throw new Error('Deposit confirmation timeout');
  }
}

module.exports = { XRPLBridge };

End-to-End Bridge Example:

// scripts/bridge-demo.js
const { ethers } = require("hardhat");
const xrpl = require("xrpl");

async function main() {
  console.log("=== XRPL EVM Bridge Demo ===\n");

// Configuration
  const BRIDGE_ADDRESS = "0x..."; // Bridge contract address
  const DOOR_ACCOUNT = "r...";    // XRPL mainnet door account

// Step 1: Connect to both networks
  console.log("1. Connecting to networks...");

// EVM Sidechain
  const [evmSigner] = await ethers.getSigners();
  console.log(`   EVM Account: ${evmSigner.address}`);
  console.log(`   EVM Balance: ${ethers.formatEther(
    await ethers.provider.getBalance(evmSigner.address)
  )} XRP`);

// XRPL Mainnet (testnet for demo)
  const xrplClient = new xrpl.Client("wss://s.altnet.rippletest.net:51233");
  await xrplClient.connect();

const xrplWallet = xrpl.Wallet.fromSeed(process.env.XRPL_SECRET);
  console.log(`   XRPL Account: ${xrplWallet.address}`);

const xrplBalance = await xrplClient.getXrpBalance(xrplWallet.address);
  console.log(`   XRPL Balance: ${xrplBalance} XRP`);

// Step 2: Deposit from XRPL to Sidechain
  console.log("\n2. Initiating deposit (XRPL → Sidechain)...");

const depositAmount = "10";  // 10 XRP

const depositTx = {
    TransactionType: "Payment",
    Account: xrplWallet.address,
    Amount: xrpl.xrpToDrops(depositAmount),
    Destination: DOOR_ACCOUNT,
    Memos: [{
      Memo: {
        MemoType: Buffer.from("destination").toString("hex"),
        MemoData: Buffer.from(evmSigner.address.slice(2), "hex").toString("hex")
      }
    }]
  };

const prepared = await xrplClient.autofill(depositTx);
  const signed = xrplWallet.sign(prepared);
  const result = await xrplClient.submitAndWait(signed.tx_blob);

console.log(`   XRPL TX Hash: ${result.result.hash}`);
  console.log(`   Status: ${result.result.meta.TransactionResult}`);

// Step 3: Wait for bridge confirmation
  console.log("\n3. Waiting for bridge confirmation...");
  console.log("   (This typically takes 1-5 minutes)");

// In production, you'd poll the bridge contract or listen for events
  // For demo, we'll check balance after a delay

await new Promise(resolve => setTimeout(resolve, 60000));  // Wait 1 minute

const newEvmBalance = await ethers.provider.getBalance(evmSigner.address);
  console.log(`   New EVM Balance: ${ethers.formatEther(newEvmBalance)} XRP`);

// Step 4: Withdraw from Sidechain to XRPL
  console.log("\n4. Initiating withdrawal (Sidechain → XRPL)...");

const withdrawAmount = "5";  // 5 XRP

// Get bridge contract
  const bridgeABI = [
    "function withdraw(string calldata mainnetDestination, uint256 amount) external",
    "event WithdrawalInitiated(bytes32 indexed withdrawalId, address indexed sender, string mainnetDestination, uint256 amount)"
  ];
  const bridge = new ethers.Contract(BRIDGE_ADDRESS, bridgeABI, evmSigner);

const withdrawTx = await bridge.withdraw(
    xrplWallet.address,
    ethers.parseEther(withdrawAmount)
  );

const receipt = await withdrawTx.wait();
  console.log(`   EVM TX Hash: ${receipt.hash}`);

// Step 5: Wait for mainnet confirmation
  console.log("\n5. Withdrawal initiated. Monitor mainnet for confirmation.");
  console.log(`   Expected destination: ${xrplWallet.address}`);
  console.log(`   Amount: ${withdrawAmount} XRP`);

// Cleanup
  await xrplClient.disconnect();

console.log("\n=== Demo Complete ===");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Building a Token Launchpad:

APPLICATION: XRPL EVM TOKEN LAUNCHPAD

Features:
├── Create new tokens with custom parameters
├── Launch with liquidity pool (AMM)
├── Fair launch mechanism (no pre-allocation)
├── Automatic LP token locking
└── Bridge integration for cross-chain launches

Architecture:
┌─────────────────────────────────────────────────────────────┐
│                     Frontend (React)                         │
│  ├── Token creation form                                    │
│  ├── Launch configuration                                   │
│  ├── Portfolio dashboard                                    │
│  └── Bridge interface                                       │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│                   Smart Contracts                            │
│  ├── TokenFactory.sol     - Create new tokens               │
│  ├── LaunchPool.sol       - Fair launch mechanism           │
│  ├── LPLocker.sol         - Lock LP tokens                  │
│  └── Integration with AMM (SimpleAMM or Uniswap fork)       │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│                   XRPL EVM Sidechain                        │
└─────────────────────────────────────────────────────────────┘
// contracts/TokenFactory.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**

  • @title LaunchToken
  • @dev Token created by factory with configurable parameters
    */
    contract LaunchToken is ERC20, Ownable {
    uint8 private _decimals;
    bool public mintingFinished;

constructor(
string memory name,
string memory symbol,
uint8 decimals_,
uint256 initialSupply,
address initialHolder
) ERC20(name, symbol) Ownable(initialHolder) {
decimals = decimals;
if (initialSupply > 0) {
_mint(initialHolder, initialSupply);
}
}

function decimals() public view override returns (uint8) {
return _decimals;
}

function mint(address to, uint256 amount) external onlyOwner {
require(!mintingFinished, "Minting finished");
_mint(to, amount);
}

function finishMinting() external onlyOwner {
mintingFinished = true;
}
}

/**

  • @title TokenFactory
  • @dev Factory for creating new tokens with fair launch
    */
    contract TokenFactory {
    struct TokenInfo {
    address tokenAddress;
    address creator;
    string name;
    string symbol;
    uint256 totalSupply;
    uint256 createdAt;
    }

TokenInfo[] public tokens;
mapping(address => TokenInfo[]) public creatorTokens;

uint256 public creationFee = 0.1 ether; // 0.1 XRP
address public feeRecipient;

event TokenCreated(
address indexed tokenAddress,
address indexed creator,
string name,
string symbol,
uint256 totalSupply
);

constructor(address _feeRecipient) {
feeRecipient = _feeRecipient;
}

function createToken(
string memory name,
string memory symbol,
uint8 decimals,
uint256 initialSupply
) external payable returns (address) {
require(msg.value >= creationFee, "Insufficient fee");
require(bytes(name).length > 0, "Empty name");
require(bytes(symbol).length > 0, "Empty symbol");

// Create token
LaunchToken token = new LaunchToken(
name,
symbol,
decimals,
initialSupply,
msg.sender
);

address tokenAddress = address(token);

// Record token info
TokenInfo memory info = TokenInfo({
tokenAddress: tokenAddress,
creator: msg.sender,
name: name,
symbol: symbol,
totalSupply: initialSupply,
createdAt: block.timestamp
});

tokens.push(info);
creatorTokens[msg.sender].push(info);

// Transfer fee
payable(feeRecipient).transfer(msg.value);

emit TokenCreated(tokenAddress, msg.sender, name, symbol, initialSupply);

return tokenAddress;
}

function getTokenCount() external view returns (uint256) {
return tokens.length;
}

function getCreatorTokenCount(address creator) external view returns (uint256) {
return creatorTokens[creator].length;
}
}
```

React Component for Token Creation:

// frontend/src/components/CreateToken.jsx
import React, { useState } from 'react';
import { ethers } from 'ethers';

const TOKEN_FACTORY_ADDRESS = "0x...";
const TOKEN_FACTORY_ABI = [
  "function createToken(string name, string symbol, uint8 decimals, uint256 initialSupply) payable returns (address)",
  "function creationFee() view returns (uint256)",
  "event TokenCreated(address indexed tokenAddress, address indexed creator, string name, string symbol, uint256 totalSupply)"
];

export function CreateToken() {
  const [formData, setFormData] = useState({
    name: '',
    symbol: '',
    decimals: 18,
    initialSupply: '1000000'
  });
  const [status, setStatus] = useState('');
  const [createdToken, setCreatedToken] = useState(null);

const handleSubmit = async (e) => {
    e.preventDefault();

try {
      setStatus('Connecting to wallet...');

if (!window.ethereum) {
        throw new Error('MetaMask not installed');
      }

const provider = new ethers.BrowserProvider(window.ethereum);
      const signer = await provider.getSigner();

// Connect to factory
      const factory = new ethers.Contract(
        TOKEN_FACTORY_ADDRESS,
        TOKEN_FACTORY_ABI,
        signer
      );

// Get creation fee
      const fee = await factory.creationFee();
      setStatus(`Creation fee: ${ethers.formatEther(fee)} XRP`);

// Create token
      setStatus('Creating token...');
      const tx = await factory.createToken(
        formData.name,
        formData.symbol,
        formData.decimals,
        ethers.parseUnits(formData.initialSupply, formData.decimals),
        { value: fee }
      );

setStatus('Waiting for confirmation...');
      const receipt = await tx.wait();

// Get token address from event
      const event = receipt.logs.find(
        log => log.topics[0] === ethers.id(
          "TokenCreated(address,address,string,string,uint256)"
        )
      );

const tokenAddress = ethers.getAddress('0x' + event.topics[1].slice(26));

setCreatedToken(tokenAddress);
      setStatus('Token created successfully!');

} catch (error) {
      setStatus(`Error: ${error.message}`);
    }
  };

return (
    <div className="create-token">
      <h2>Create New Token</h2>

<form onSubmit={handleSubmit}>
        <div>
          <label>Token Name</label>
          <input
            type="text"
            value={formData.name}
            onChange={e => setFormData({...formData, name: e.target.value})}
            placeholder="My Token"
            required
          />
        </div>

<div>
          <label>Symbol</label>
          <input
            type="text"
            value={formData.symbol}
            onChange={e => setFormData({...formData, symbol: e.target.value})}
            placeholder="MTK"
            required
          />
        </div>

<div>
          <label>Decimals</label>
          <input
            type="number"
            value={formData.decimals}
            onChange={e => setFormData({...formData, decimals: parseInt(e.target.value)})}
            min="0"
            max="18"
          />
        </div>

<div>
          <label>Initial Supply</label>
          <input
            type="text"
            value={formData.initialSupply}
            onChange={e => setFormData({...formData, initialSupply: e.target.value})}
            placeholder="1000000"
            required
          />
        </div>

<button type="submit">Create Token</button>
      </form>

{status && <p className="status">{status}</p>}

{createdToken && (
        <div className="success">
          <p>Token Address: {createdToken}</p>
          <a 
            href={`https://explorer.xrpl-evm-sidechain.org/address/${createdToken}`} 
            target="_blank"
            rel="noopener noreferrer"
          >
            View on Explorer
          </a>
        </div>
      )}
    </div>
  );
}

// contracts/GasOptimized.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**

  • @title GasOptimizedPatterns
  • @dev Examples of gas optimization for XRPL EVM
    */
    contract GasOptimizedPatterns {

// ✅ Use immutable for values set once in constructor
address public immutable owner;
uint256 public immutable deploymentTime;

// ✅ Pack storage variables (same slot if possible)
struct PackedData {
uint128 valueA; // 16 bytes
uint64 valueB; // 8 bytes
uint64 timestamp; // 8 bytes = 32 bytes total (1 slot)
}

// ❌ Avoid: Each takes full slot
// uint256 valueA;
// uint256 valueB;
// uint256 timestamp;

// ✅ Use calldata for read-only array parameters
function processArray(uint256[] calldata data) external pure returns (uint256) {
uint256 sum;
for (uint256 i = 0; i < data.length; i++) {
sum += data[i];
}
return sum;
}

// ✅ Cache storage reads in memory
mapping(address => uint256) public balances;

function batchTransfer(address[] calldata recipients, uint256 amount) external {
uint256 senderBalance = balances[msg.sender]; // Cache
uint256 totalAmount = amount * recipients.length;

require(senderBalance >= totalAmount, "Insufficient balance");

for (uint256 i = 0; i < recipients.length; i++) {
balances[recipients[i]] += amount;
}

balances[msg.sender] = senderBalance - totalAmount;
}

// ✅ Use unchecked for safe math operations
function safeIncrement(uint256 i) internal pure returns (uint256) {
unchecked {
return i + 1; // Safe if we know i < max
}
}

constructor() {
owner = msg.sender;
deploymentTime = block.timestamp;
}
}
```

// Common errors on XRPL EVM and how to fix them

const COMMON_ERRORS = {
// Error: "insufficient funds for gas * price + value"
insufficientFunds: {
cause: "Account doesn't have enough XRP for gas",
solution: "Bridge more XRP from mainnet or get from faucet (testnet)"
},

// Error: "nonce too low" or "nonce too high"
nonceIssues: {
cause: "Transaction nonce mismatch",
solution: // Reset nonce in MetaMask: Settings > Advanced > Reset Account // Or specify nonce manually: const tx = await contract.method({ nonce: correctNonce });
},

// Error: "execution reverted"
executionReverted: {
cause: "Contract logic failed",
solution: // Get revert reason: try { await contract.method.staticCall(); // Simulate first } catch (error) { console.log("Revert reason:", error.reason); }
},

// Error: "network does not support ENS"
ensError: {
cause: "ENS lookup on non-Ethereum network",
solution: "Always use addresses, not ENS names on XRPL EVM"
},

// Error: Contract deployment fails silently
deploymentFailed: {
cause: "Contract size or constructor issues",
solution: // Check contract size: const code = await provider.getCode(contractAddress); console.log("Contract size:", code.length); // Max is ~24KB compiled bytecode
}
};

// Debug helper function
async function debugTransaction(provider, txHash) {
const tx = await provider.getTransaction(txHash);
const receipt = await provider.getTransactionReceipt(txHash);

console.log("Transaction Details:");
console.log(" From:", tx.from);
console.log(" To:", tx.to);
console.log(" Value:", ethers.formatEther(tx.value), "XRP");
console.log(" Gas Limit:", tx.gasLimit.toString());
console.log(" Gas Price:", ethers.formatUnits(tx.gasPrice, "gwei"), "gwei");

console.log("\nReceipt:");
console.log(" Status:", receipt.status === 1 ? "Success" : "Failed");
console.log(" Gas Used:", receipt.gasUsed.toString());
console.log(" Block:", receipt.blockNumber);

if (receipt.status === 0) {
// Try to get revert reason
try {
await provider.call(tx, tx.blockNumber);
} catch (error) {
console.log(" Revert Reason:", error.reason || error.message);
}
}
}
```



Development on XRPL EVM Sidechain is straightforward for anyone familiar with Ethereum development. The tooling works, contracts deploy, and basic functionality is accessible. However, this is a new environment—expect some friction, undiscovered edge cases, and the need for thorough testing. Start with small experiments, build confidence through successful deployments, and gradually increase complexity and value at risk.


Assignment: Build and deploy a complete DeFi application on XRPL EVM Sidechain testnet.

Requirements:

  • Simple AMM (token pair trading)

  • Token launchpad (create and launch new tokens)

  • Lending pool (deposit/borrow single asset)

  • NFT marketplace (mint/buy/sell)

  • Write contracts in Solidity (well-commented)

  • Include comprehensive test suite (>80% coverage)

  • Deploy to testnet

  • Verify contracts on explorer

  • Create React frontend (or similar)

  • Connect to MetaMask

  • Implement all core functions

  • Handle errors gracefully

  • Demonstrate deposit workflow (mainnet → sidechain)

  • Document withdrawal process

  • Handle bridge status checking

  • README with setup instructions

  • Architecture diagram

  • API documentation

  • Security considerations

  • Contract quality and testing (30%)

  • Frontend functionality (25%)

  • Bridge integration (20%)

  • Documentation quality (15%)

  • Code organization (10%)

Time investment: 8-12 hours
Value: Practical experience with XRPL EVM development that can be extended into real applications.


Knowledge Check

Question 1 of 5

(Tests Knowledge):

For Next Lesson:
Prepare for Axelar integration in Lesson 10, where we'll connect the XRPL ecosystem to the broader cross-chain world.


End of Lesson 9

Total words: ~6,800
Estimated completion time: 60 minutes reading + 8-12 hours for deliverable

Key Takeaways

1

Standard EVM tooling works:

Hardhat, Foundry, MetaMask, and ethers.js all function normally. Existing Solidity skills transfer directly.

2

Bridge is programmatically accessible:

Both deposits (mainnet → sidechain) and withdrawals can be automated, enabling cross-chain application workflows.

3

Gas optimization matters:

While XRP is cheap, efficient contracts improve UX and reduce costs. Apply standard EVM gas optimization techniques.

4

Testing is critical:

New environment means potential for unexpected issues. Thorough testnet testing before any mainnet deployment.

5

Start simple, iterate:

Begin with basic contracts, validate functionality, then build complexity. Don't attempt complex cross-chain applications until fundamentals are solid. ---