> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sei.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Sei EVM Smart Contract Development with Foundry

> Learn how to develop, test, and deploy smart contracts on Sei EVM using Foundry, with step-by-step examples of contract creation, testing, deployment, and interaction using both CLI and ethers.js.

export const AddSeiButton = ({network = 'mainnet', label = 'Add Sei to MetaMask'}) => {
  const SEI_MAINNET_CHAIN_PARAMS = {
    chainId: '0x531',
    chainName: 'Sei Network',
    rpcUrls: ['https://evm-rpc.sei-apis.com'],
    nativeCurrency: {
      name: 'Sei',
      symbol: 'SEI',
      decimals: 18
    },
    blockExplorerUrls: ['https://seiscan.io']
  };
  const SEI_TESTNET_CHAIN_PARAMS = {
    chainId: '0x530',
    chainName: 'Sei Testnet',
    rpcUrls: ['https://evm-rpc-testnet.sei-apis.com'],
    nativeCurrency: {
      name: 'Sei',
      symbol: 'SEI',
      decimals: 18
    },
    blockExplorerUrls: ['https://testnet.seiscan.io']
  };
  const chainParams = network === 'testnet' ? SEI_TESTNET_CHAIN_PARAMS : SEI_MAINNET_CHAIN_PARAMS;
  const [status, setStatus] = useState(null);
  const [isHovered, setIsHovered] = useState(false);
  const [isBusy, setIsBusy] = useState(false);
  const addOrSwitchSeiNetwork = async params => {
    if (typeof window === 'undefined' || !window.ethereum) {
      throw new Error('MetaMask is not installed');
    }
    const ethereum = window.ethereum;
    try {
      await ethereum.request({
        method: 'wallet_switchEthereumChain',
        params: [{
          chainId: params.chainId
        }]
      });
    } catch (switchError) {
      if (switchError && switchError.code === 4902) {
        await ethereum.request({
          method: 'wallet_addEthereumChain',
          params: [params]
        });
      } else {
        throw switchError;
      }
    }
  };
  const onClick = async e => {
    e.preventDefault();
    setIsBusy(true);
    setStatus(null);
    try {
      await addOrSwitchSeiNetwork(chainParams);
      setStatus({
        type: 'success',
        message: `${chainParams.chainName} added or switched.`
      });
    } catch (err) {
      const message = err && err.message ? err.message : 'Failed to add or switch network.';
      setStatus({
        type: 'error',
        message
      });
    } finally {
      setIsBusy(false);
    }
  };
  return <span className="inline-flex flex-col items-start gap-1">
      <button type="button" onClick={onClick} disabled={isBusy} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} className="inline-flex items-center gap-1 px-3 py-1.5 text-white transition-colors min-w-[160px]" style={{
    backgroundColor: isHovered ? 'var(--sei-maroon-200)' : 'var(--sei-maroon-100)',
    color: '#ffffff',
    fontFamily: 'var(--sei-font-mono)',
    textTransform: 'uppercase',
    letterSpacing: '0.04em',
    fontSize: '10px',
    opacity: isBusy ? 0.7 : 1,
    cursor: isBusy ? 'default' : 'pointer'
  }}>
        {isBusy ? 'Adding…' : label}
      </button>
      {status && <span className={status.type === 'error' ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'} style={{
    fontSize: '11px'
  }}>
          {status.message}
        </span>}
    </span>;
};

export const SandboxEmbed = props => {
  const {src, kind = 'codesandbox', title, description, height, label} = props || ({});
  const KINDS = {
    codesandbox: {
      name: 'CodeSandbox',
      host: 'codesandbox.io',
      defaultHeight: 500
    },
    remix: {
      name: 'Remix IDE',
      host: 'remix.ethereum.org',
      defaultHeight: 620
    },
    stackblitz: {
      name: 'StackBlitz',
      host: 'stackblitz.com',
      defaultHeight: 500
    }
  };
  const meta = KINDS[kind] || KINDS.codesandbox;
  const parsedHeight = Number(height);
  const frameHeight = Number.isFinite(parsedHeight) && parsedHeight > 0 ? parsedHeight : meta.defaultHeight;
  const allowAttr = 'clipboard-read; clipboard-write';
  const [loaded, setLoaded] = useState(false);
  const [btnHover, setBtnHover] = useState(false);
  const HAIRLINE = 'rgba(128, 128, 128, 0.25)';
  const surfaceStyle = {
    backgroundColor: 'rgba(128, 128, 128, 0.08)'
  };
  const monoStyle = {
    fontFamily: 'var(--sei-font-mono)'
  };
  const cardClass = 'not-prose w-full rounded-lg border overflow-hidden my-4';
  const headerClass = 'flex items-center justify-between gap-3 px-4 py-2.5 border-b';
  const buttonStyle = {
    backgroundColor: btnHover ? 'var(--sei-maroon-200)' : 'var(--sei-maroon-100)',
    color: '#ffffff',
    fontFamily: 'var(--sei-font-mono)',
    textTransform: 'uppercase',
    letterSpacing: '0.04em',
    fontSize: '10px',
    cursor: 'pointer'
  };
  const PlayIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
			<path d="M8 5v14l11-7z" />
		</svg>;
  const ExternalIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
			<path d="M14 5h5v5" />
			<path d="M19 5l-9 9" />
			<path d="M19 14v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h5" />
		</svg>;
  return <div className={cardClass} style={{
    borderColor: HAIRLINE
  }}>
			<div className={headerClass} style={{
    ...surfaceStyle,
    borderBottomColor: HAIRLINE
  }}>
				<div className="flex flex-col min-w-0">
					<span className="text-sm font-medium text-neutral-900 dark:text-white truncate" style={monoStyle}>
						{title || meta.name}
					</span>
					<span className="text-xs text-neutral-500 dark:text-neutral-500">{meta.name}</span>
				</div>
				<div className="flex items-center gap-3 shrink-0">
					{src ? <a href={src} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors">
							Open <ExternalIcon />
						</a> : null}
					{!loaded && src ? <button type="button" onClick={() => setLoaded(true)} onMouseEnter={() => setBtnHover(true)} onMouseLeave={() => setBtnHover(false)} className="inline-flex items-center gap-1.5 px-3 py-1.5 transition-colors" style={buttonStyle}>
							<PlayIcon />
							{label || 'Load editor'}
						</button> : null}
				</div>
			</div>

			{description ? <div className="px-4 pt-3 pb-1 text-sm text-neutral-600 dark:text-neutral-400">{description}</div> : null}

			{!src ? <div className="px-4 py-6 text-sm text-red-600 dark:text-red-400" style={monoStyle}>
					SandboxEmbed: missing required `src`.
				</div> : loaded ? <iframe src={src} title={title || meta.name} className="w-full block border-0" style={{
    height: frameHeight + 'px',
    backgroundColor: 'rgba(128, 128, 128, 0.05)'
  }} allow={allowAttr} loading="lazy" allowFullScreen /> : <button type="button" onClick={() => setLoaded(true)} className="w-full flex flex-col items-center justify-center gap-2 text-neutral-500 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200 transition-colors" style={{
    height: frameHeight + 'px',
    cursor: 'pointer',
    ...surfaceStyle
  }}>
					<PlayIcon />
					<span className="text-sm" style={monoStyle}>Click to load {meta.name}</span>
					<span className="text-xs">Loads {meta.host} in an embedded editor</span>
				</button>}
		</div>;
};

export const RunSnippet = props => {
  const {method = 'eth_blockNumber', params = [], network = 'testnet', endpoint, label, title, description, decode = 'auto'} = props || ({});
  const ENDPOINTS = {
    testnet: 'https://evm-rpc-testnet.sei-apis.com',
    mainnet: 'https://evm-rpc.sei-apis.com'
  };
  const rpcUrl = endpoint || ENDPOINTS[network] || ENDPOINTS.testnet;
  const networkLabel = network === 'mainnet' ? 'pacific-1 · mainnet' : network === 'testnet' ? 'atlantic-2 · testnet' : network;
  const requestBody = {
    jsonrpc: '2.0',
    id: 1,
    method,
    params
  };
  const requestJson = JSON.stringify(requestBody, null, 2);
  const [phase, setPhase] = useState('idle');
  const [result, setResult] = useState(null);
  const [errorMsg, setErrorMsg] = useState(null);
  const [elapsed, setElapsed] = useState(null);
  const [copied, setCopied] = useState(false);
  const [btnHover, setBtnHover] = useState(false);
  const groupThousands = s => s.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  const hexToDecimal = value => {
    if (typeof value !== 'string' || !(/^0x[0-9a-fA-F]+$/).test(value)) return null;
    if (value.length > 66) return null;
    try {
      return groupThousands(BigInt(value).toString(10));
    } catch (e) {
      return null;
    }
  };
  const run = async () => {
    setPhase('loading');
    setErrorMsg(null);
    setResult(null);
    setElapsed(null);
    const startedAt = typeof performance !== 'undefined' ? performance.now() : null;
    try {
      const response = await fetch(rpcUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(requestBody)
      });
      const data = await response.json();
      if (startedAt != null && typeof performance !== 'undefined') {
        setElapsed(Math.round(performance.now() - startedAt));
      }
      if (data && data.error) {
        setErrorMsg(data.error.message || 'RPC returned an error');
        setPhase('error');
        return;
      }
      setResult(data ? data.result : undefined);
      setPhase('success');
    } catch (err) {
      setErrorMsg(err && err.message ? err.message : 'Request failed');
      setPhase('error');
    }
  };
  const resultString = result === undefined ? 'undefined' : JSON.stringify(result, null, 2);
  const decoded = decode !== 'off' && typeof result === 'string' ? hexToDecimal(result) : null;
  const copyResult = () => {
    const flashCopied = () => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    };
    if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(resultString).then(flashCopied, () => {});
      return;
    }
    if (typeof document !== 'undefined') {
      try {
        const ta = document.createElement('textarea');
        ta.value = resultString;
        ta.style.position = 'fixed';
        ta.style.opacity = '0';
        document.body.appendChild(ta);
        ta.select();
        document.execCommand('copy');
        document.body.removeChild(ta);
        flashCopied();
      } catch (e) {}
    }
  };
  const HAIRLINE = 'rgba(128, 128, 128, 0.25)';
  const surfaceStyle = {
    backgroundColor: 'rgba(128, 128, 128, 0.08)'
  };
  const monoStyle = {
    fontFamily: 'var(--sei-font-mono)'
  };
  const codeStyle = {
    backgroundColor: 'rgba(128, 128, 128, 0.05)',
    fontFamily: 'var(--sei-font-mono)'
  };
  const cardClass = 'not-prose w-full rounded-lg border overflow-hidden my-4';
  const headerClass = 'flex items-center justify-between gap-3 px-4 py-2.5 border-b';
  const labelClass = 'text-xs uppercase tracking-wide text-neutral-500 dark:text-neutral-500';
  const preClass = 'm-0 px-4 py-3 text-sm overflow-x-auto text-neutral-700 dark:text-neutral-300';
  const buttonStyle = {
    backgroundColor: btnHover ? 'var(--sei-maroon-200)' : 'var(--sei-maroon-100)',
    color: '#ffffff',
    fontFamily: 'var(--sei-font-mono)',
    textTransform: 'uppercase',
    letterSpacing: '0.04em',
    fontSize: '10px',
    opacity: phase === 'loading' ? 0.7 : 1,
    cursor: phase === 'loading' ? 'default' : 'pointer'
  };
  return <div className={cardClass} style={{
    borderColor: HAIRLINE
  }}>
			<div className={headerClass} style={{
    ...surfaceStyle,
    borderBottomColor: HAIRLINE
  }}>
				<div className="flex flex-col min-w-0">
					<span className="text-sm font-medium text-neutral-900 dark:text-white truncate" style={monoStyle}>
						{title || method}
					</span>
					<span className="text-xs text-neutral-500 dark:text-neutral-500">{networkLabel}</span>
				</div>
				<button type="button" onClick={run} disabled={phase === 'loading'} onMouseEnter={() => setBtnHover(true)} onMouseLeave={() => setBtnHover(false)} className="inline-flex items-center gap-1.5 px-3 py-1.5 shrink-0 transition-colors" style={buttonStyle}>
					{phase === 'loading' ? <svg className="animate-spin h-4 w-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
								<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
								<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z" />
							</svg> : <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
								<path d="M8 5v14l11-7z" />
							</svg>}
					{phase === 'loading' ? 'Running…' : label || 'Run'}
				</button>
			</div>

			{description ? <div className="px-4 pt-3 text-sm text-neutral-600 dark:text-neutral-400">{description}</div> : null}

			<div className="px-4 pt-3 pb-1">
				<span className={labelClass}>Request</span>
			</div>
			<pre className={preClass} style={codeStyle}>
				{requestJson}
			</pre>

			{phase === 'success' ? <div className="border-t" style={{
    borderTopColor: HAIRLINE
  }}>
					<div className="flex items-center justify-between px-4 pt-3 pb-1">
						<span className={labelClass}>Response{elapsed != null ? ` · ${elapsed} ms` : ''}</span>
						<button type="button" onClick={copyResult} className="text-xs text-neutral-500 hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors">
							{copied ? 'Copied' : 'Copy'}
						</button>
					</div>
					<pre className={preClass} style={codeStyle}>
						{resultString}
					</pre>
					{decoded ? <div className="px-4 pb-3 text-xs text-neutral-500 dark:text-neutral-500" style={monoStyle}>
							= {decoded} (decimal)
						</div> : null}
				</div> : null}

			{phase === 'error' ? <div className="border-t" style={{
    borderTopColor: HAIRLINE
  }}>
					<div className="px-4 pt-3 pb-1">
						<span className={labelClass}>Error</span>
					</div>
					<pre className={`${preClass} text-red-600 dark:text-red-400`} style={codeStyle}>
						{errorMsg}
					</pre>
				</div> : null}
		</div>;
};

This tutorial will guide you through setting up Foundry for Sei EVM development and using OpenZeppelin contracts to build secure, standardized smart contracts. We'll cover environment setup, contract creation, deployment, and show how to leverage OpenZeppelin's pre-built components with the powerful Foundry toolkit.

<Info>Watch the video walkthrough for this topic in the [Video Tutorials](/evm/videos) section.</Info>

<Tip>
  **Deploy to Testnet First**

  It is <strong>highly recommended</strong> that you deploy to <em>testnet (atlantic-2)</em> first and verify everything works as expected before committing to mainnet. Doing so helps you catch bugs early, avoid unnecessary gas costs, and keep your users safe.
</Tip>

## Try it before you install

Installing Foundry and scaffolding a project takes a few minutes. If you just want to see a contract deploy to Sei, you can do both of these right now in the browser.

First, confirm the RPC endpoints you'll put in `foundry.toml` respond:

<RunSnippet method="eth_chainId" network="testnet" title="eth_chainId · atlantic-2" description="Testnet RPC — decodes to 1328." />

<RunSnippet method="eth_chainId" network="mainnet" title="eth_chainId · pacific-1" description="Mainnet RPC — decodes to 1329." />

Then add Sei testnet to your wallet and deploy a minimal `Counter.sol` from Remix — no `forge`, no install. In Remix: compile under **Solidity Compiler**, then **Deploy & Run** with **Environment** set to **Injected Provider — MetaMask**.

<AddSeiButton network="testnet" label="Add Sei testnet" />

<SandboxEmbed kind="remix" src="https://remix.ethereum.org/?#activate=solidity,fileManager&code=Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4yNDsKCi8vLyBAdGl0bGUgQ291bnRlciDigJQgbWluaW1hbCBjb250cmFjdCB0byBjb21waWxlICYgZGVwbG95IHRvIFNlaSB0ZXN0bmV0Lgpjb250cmFjdCBDb3VudGVyIHsKICAgIHVpbnQyNTYgcHVibGljIGNvdW50OwogICAgZnVuY3Rpb24gaW5jcmVtZW50KCkgZXh0ZXJuYWwgeyBjb3VudCsrOyB9CiAgICBmdW5jdGlvbiBkZWNyZW1lbnQoKSBleHRlcm5hbCB7IGNvdW50LS07IH0KfQ" title="Counter.sol · deploy to Sei testnet" description="Remix IDE with a minimal Counter contract preloaded. Compile in-browser, then deploy to Sei testnet." />

## Table of Contents

* [Try it before you install](#try-it-before-you-install)
* [Prerequisites](#prerequisites)
* [Setting Up Your Development Environment](#setting-up-your-development-environment)
* [Configuring Foundry for Sei EVM](#configuring-foundry-for-sei-evm)
* [Using OpenZeppelin Contracts](#using-openzeppelin-contracts)
* [Creating and Deploying Smart Contracts](#creating-and-deploying-smart-contracts)
* [Testing Your Smart Contracts](#testing-your-smart-contracts)
* [Deploying to Sei Testnet and Mainnet](#deploying-to-sei-testnet-and-mainnet)
* [Interacting with Deployed Contracts](#interacting-with-deployed-contracts)

## Prerequisites

Before we begin, ensure you have the following:

* [Foundry](https://book.getfoundry.sh/) installed on your system
* A basic understanding of Solidity and smart contract development
* A wallet with SEI tokens for gas
* [Node.js](https://nodejs.org/) (v18.0.0 or later) for ethers.js interactions

## Setting Up Your Development Environment

First, let's install Foundry if you haven't already. Follow the [installation guide](https://book.getfoundry.sh/getting-started/installation) or use the quick install command:

```bash theme={"dark"}
curl -L https://foundry.paradigm.xyz | bash
foundryup
```

Create a new Foundry project:

```bash theme={"dark"}
# Create a new directory for your project
mkdir sei-foundry-project
cd sei-foundry-project

# Initialize a new Foundry project
forge init --no-git

# Initialize git repository (required for installing dependencies)
git init
```

This will create a standard Foundry project structure with `src/`, `test/`, and `script/` directories.

## Configuring Foundry for Sei EVM

Create a `foundry.toml` file in your project root to configure Foundry for Sei networks:

```toml title="foundry.toml" theme={"dark"}
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.28"
optimizer = true
optimizer_runs = 200

# Sei testnet configuration
[rpc_endpoints]
sei_testnet = "https://evm-rpc-testnet.sei-apis.com"
sei_mainnet = "https://evm-rpc.sei-apis.com"
```

Create a `.env` file to store your private key and other sensitive information:

```bash title=".env" theme={"dark"}
PRIVATE_KEY=your_private_key_here
SEI_TESTNET_RPC=https://evm-rpc-testnet.sei-apis.com
SEI_MAINNET_RPC=https://evm-rpc.sei-apis.com
```

<Warning>Add `.env` to your `.gitignore` file to prevent committing sensitive information such as your `PRIVATE_KEY` and potentially lose funds.</Warning>

## Using OpenZeppelin Contracts

OpenZeppelin provides a library of secure, tested smart contract components. Let's install OpenZeppelin contracts:

```bash theme={"dark"}
forge install OpenZeppelin/openzeppelin-contracts
```

Create a `remappings.txt` file to properly map the imports:

```bash theme={"dark"}
forge remappings > remappings.txt
```

## Creating and Deploying Smart Contracts

Let's create different types of smart contracts. Choose from the tabs below based on what you want to build:

<Tabs>
  <Tab title="Counter Contract">
    Let's start with a simple counter contract. Update the default `src/Counter.sol` file:

    ```solidity title="src/Counter.sol" theme={"dark"}
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    contract Counter {
        uint256 public number;
        address public owner;

        event NumberChanged(uint256 newNumber, address changedBy);

        constructor() {
            owner = msg.sender;
        }

        modifier onlyOwner() {
            require(msg.sender == owner, "Only owner can call this function");
            _;
        }

        function setNumber(uint256 newNumber) public {
            number = newNumber;
            emit NumberChanged(newNumber, msg.sender);
        }

        function increment() public {
            number++;
            emit NumberChanged(number, msg.sender);
        }

        function getCount() public view returns (uint256) {
            return number;
        }

        function reset() public onlyOwner {
            number = 0;
            emit NumberChanged(0, msg.sender);
        }
    }
    ```

    Create a deployment script in `script/DeployCounter.s.sol`:

    ```solidity title="script/DeployCounter.s.sol" theme={"dark"}
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    import {Script} from "forge-std/Script.sol";
    import {Counter} from "../src/Counter.sol";

    contract DeployCounter is Script {
        function run() external returns (Counter) {
            vm.startBroadcast();
            Counter counter = new Counter();
            vm.stopBroadcast();
            return counter;
        }
    }
    ```
  </Tab>

  <Tab title="ERC20 Token">
    Create an ERC20 token using OpenZeppelin. Create `src/SeiToken.sol`:

    ```solidity title="src/SeiToken.sol" theme={"dark"}
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.22;

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

    contract SeiToken is ERC20, Ownable {
        constructor(address initialOwner)
            ERC20("Sei Token", "SEI")
            Ownable(initialOwner)
        {
            // Mint 1 million tokens to the contract deployer (with 18 decimals)
            _mint(msg.sender, 1000000 * 10 ** decimals());
        }

        // Function to mint new tokens (only owner)
        function mint(address to, uint256 amount) public onlyOwner {
            _mint(to, amount);
        }

        // Function to burn tokens
        function burn(uint256 amount) public {
            _burn(msg.sender, amount);
        }

        // Function to burn tokens from another account (with allowance)
        function burnFrom(address from, uint256 amount) public {
            _spendAllowance(from, msg.sender, amount);
            _burn(from, amount);
        }
    }
    ```

    Create a deployment script in `script/DeploySeiToken.s.sol`:

    ```solidity title="script/DeploySeiToken.s.sol" theme={"dark"}
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.22;

    import {Script} from "forge-std/Script.sol";
    import {SeiToken} from "../src/SeiToken.sol";

    contract DeploySeiToken is Script {
        function run() external returns (SeiToken) {
            vm.startBroadcast();
            SeiToken token = new SeiToken(msg.sender);
            vm.stopBroadcast();
            return token;
        }
    }
    ```
  </Tab>

  <Tab title="ERC721 NFT">
    Create an ERC721 NFT contract using OpenZeppelin. Create `src/SeiNFT.sol`:

    ```solidity title="src/SeiNFT.sol" theme={"dark"}
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.22;

    import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
    import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
    import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";

    contract SeiNFT is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
        uint256 private _nextTokenId;
        string private _baseTokenURI;

        constructor(address initialOwner, string memory baseTokenURI)
            ERC721("Sei NFT Collection", "SEINFT")
            Ownable(initialOwner)
        {
            _baseTokenURI = baseTokenURI;
        }

        // Function to update the base URI (only owner)
        function setBaseURI(string memory baseTokenURI) public onlyOwner {
            _baseTokenURI = baseTokenURI;
        }

        // Override the baseURI function
        function _baseURI() internal view override returns (string memory) {
            return _baseTokenURI;
        }

        function safeMint(address to, string memory uri) public onlyOwner returns (uint256) {
            uint256 tokenId = _nextTokenId++;
            _safeMint(to, tokenId);
            _setTokenURI(tokenId, uri);
            return tokenId;
        }

        // Public mint function with a fee
        function publicMint(string memory uri) public payable returns (uint256) {
            require(msg.value >= 0.01 ether, "Insufficient payment");
            uint256 tokenId = _nextTokenId++;
            _safeMint(msg.sender, tokenId);
            _setTokenURI(tokenId, uri);
            return tokenId;
        }

        // Withdraw contract balance (only owner)
        function withdraw() public onlyOwner {
            uint256 balance = address(this).balance;
            require(balance > 0, "No funds to withdraw");
            payable(owner()).transfer(balance);
        }

        // The following functions are overrides required by Solidity
        function _update(address to, uint256 tokenId, address auth)
            internal
            override(ERC721, ERC721Enumerable)
            returns (address)
        {
            return super._update(to, tokenId, auth);
        }

        function _increaseBalance(address account, uint128 value)
            internal
            override(ERC721, ERC721Enumerable)
        {
            super._increaseBalance(account, value);
        }

        function tokenURI(uint256 tokenId)
            public
            view
            override(ERC721, ERC721URIStorage)
            returns (string memory)
        {
            return super.tokenURI(tokenId);
        }

        function supportsInterface(bytes4 interfaceId)
            public
            view
            override(ERC721, ERC721Enumerable, ERC721URIStorage)
            returns (bool)
        {
            return super.supportsInterface(interfaceId);
        }
    }
    ```

    Create a deployment script in `script/DeploySeiNFT.s.sol`:

    ```solidity title="script/DeploySeiNFT.s.sol" theme={"dark"}
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.22;

    import {Script} from "forge-std/Script.sol";
    import {SeiNFT} from "../src/SeiNFT.sol";

    contract DeploySeiNFT is Script {
        function run() external returns (SeiNFT) {
            string memory baseURI = "https://your-metadata-server.com/metadata/";

            vm.startBroadcast();
            SeiNFT nft = new SeiNFT(msg.sender, baseURI);
            vm.stopBroadcast();

            return nft;
        }
    }
    ```
  </Tab>
</Tabs>

## Testing Your Smart Contracts

Foundry provides excellent testing capabilities. Let's create comprehensive tests for our contracts.

<Tabs>
  <Tab title="Counter Tests">
    Update `test/Counter.t.sol`:

    ```solidity title="test/Counter.t.sol" theme={"dark"}
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.22;

    import {Test, console} from "forge-std/Test.sol";
    import {Counter} from "../src/Counter.sol";

    contract CounterTest is Test {
        Counter public counter;
        address public owner;
        address public user;

        function setUp() public {
            owner = address(this);
            user = address(0x1);
            counter = new Counter();
            counter.setNumber(0);
        }

        function test_Increment() public {
            counter.increment();
            assertEq(counter.number(), 1);
        }

        function test_SetNumber() public {
            counter.setNumber(42);
            assertEq(counter.number(), 42);
        }

        function test_GetCount() public {
            uint256 initialCount = counter.getCount();
            counter.increment();
            assertEq(counter.getCount(), initialCount + 1);
        }

        function test_Reset() public {
            counter.setNumber(100);
            counter.reset();
            assertEq(counter.number(), 0);
        }

        function test_OnlyOwnerCanReset() public {
            vm.prank(user);
            vm.expectRevert("Only owner can call this function");
            counter.reset();
        }

        function test_EventEmitted() public {
            vm.expectEmit(true, true, false, true);
            emit Counter.NumberChanged(42, address(this));
            counter.setNumber(42);
        }

        function testFuzz_SetNumber(uint256 x) public {
            counter.setNumber(x);
            assertEq(counter.number(), x);
        }
    }
    ```
  </Tab>

  <Tab title="ERC20 Tests">
    Create `test/SeiToken.t.sol`:

    ```solidity title="test/SeiToken.t.sol" theme={"dark"}
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.22;

    import {Test} from "forge-std/Test.sol";
    import {SeiToken} from "../src/SeiToken.sol";

    contract SeiTokenTest is Test {
        SeiToken public token;
        address public owner;
        address public user1;
        address public user2;

        function setUp() public {
            owner = address(this);
            user1 = address(0x1);
            user2 = address(0x2);
            token = new SeiToken(owner);
        }

        function test_InitialSupply() public {
            assertEq(token.totalSupply(), 1000000 * 10 ** 18);
            assertEq(token.balanceOf(owner), 1000000 * 10 ** 18);
        }

        function test_Transfer() public {
            token.transfer(user1, 1000);
            assertEq(token.balanceOf(user1), 1000);
            assertEq(token.balanceOf(owner), 1000000 * 10 ** 18 - 1000);
        }

        function test_Mint() public {
            token.mint(user1, 500);
            assertEq(token.balanceOf(user1), 500);
            assertEq(token.totalSupply(), 1000000 * 10 ** 18 + 500);
        }

        function test_Burn() public {
            token.burn(1000);
            assertEq(token.totalSupply(), 1000000 * 10 ** 18 - 1000);
        }

        function test_OnlyOwnerCanMint() public {
            vm.prank(user1);
            vm.expectRevert();
            token.mint(user2, 1000);
        }
    }
    ```
  </Tab>

  <Tab title="ERC721 Tests">
    Create `test/SeiNFT.t.sol`:

    ```solidity title="test/SeiNFT.t.sol" theme={"dark"}
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.22;

    import {Test} from "forge-std/Test.sol";
    import {SeiNFT} from "../src/SeiNFT.sol";

    contract SeiNFTTest is Test {
        SeiNFT public nft;
        address public owner;
        address public user1;
        string public baseURI = "https://example.com/metadata/";

        function setUp() public {
            owner = address(this);
            user1 = address(0x1);
            nft = new SeiNFT(owner, baseURI);
        }

        function test_SafeMint() public {
            uint256 tokenId = nft.safeMint(user1, "1.json");
            assertEq(nft.ownerOf(tokenId), user1);
            assertEq(nft.tokenURI(tokenId), string(abi.encodePacked(baseURI, "1.json")));
        }

        function test_PublicMint() public {
            vm.deal(user1, 1 ether);
            vm.prank(user1);
            uint256 tokenId = nft.publicMint{value: 0.01 ether}("2.json");
            assertEq(nft.ownerOf(tokenId), user1);
        }

        function test_PublicMintInsufficientPayment() public {
            vm.deal(user1, 0.005 ether);
            vm.prank(user1);
            vm.expectRevert("Insufficient payment");
            nft.publicMint{value: 0.005 ether}("3.json");
        }

        function test_TotalSupply() public {
            assertEq(nft.totalSupply(), 0);
            nft.safeMint(user1, "1.json");
            assertEq(nft.totalSupply(), 1);
        }

        function test_Withdraw() public {
            vm.deal(user1, 1 ether);
            vm.prank(user1);
            nft.publicMint{value: 0.01 ether}("1.json");

            uint256 contractBalance = address(nft).balance;
            assertEq(contractBalance, 0.01 ether);

            nft.withdraw();
            assertEq(address(nft).balance, 0);
        }
    }
    ```
  </Tab>
</Tabs>

Run your tests with:

```bash theme={"dark"}
forge test
```

For more verbose output:

```bash theme={"dark"}
forge test -vvv
```

## Deploying to Sei Testnet and Mainnet

Now let's deploy our contracts to the Sei networks. You can use either Forge scripts or direct deployment commands.

### Using Forge Scripts (Recommended)

Deploy to Sei testnet:

```bash theme={"dark"}
forge script script/DeployCounter.s.sol --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY --broadcast
```

Deploy to Sei mainnet:

```bash theme={"dark"}
forge script script/DeployCounter.s.sol --rpc-url $SEI_MAINNET_RPC --private-key $PRIVATE_KEY --broadcast
```

### Using Direct Forge Create Commands

Alternatively, you can deploy directly:

```bash theme={"dark"}
# Deploy Counter to testnet
forge create --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY src/Counter.sol:Counter

# Deploy SeiToken to testnet
forge create --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY src/SeiToken.sol:SeiToken --constructor-args $(cast abi-encode "constructor(address)" "YOUR_ADDRESS")

# Deploy SeiNFT to testnet
forge create --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY src/SeiNFT.sol:SeiNFT --constructor-args $(cast abi-encode "constructor(address,string)" "YOUR_ADDRESS" "https://your-metadata-server.com/metadata/")
```

Successful deployment will output:

```bash theme={"dark"}
[⠒] Compiling...
No files changed, compilation skipped
Deployer: 0xYOUR_DEPLOYER_ADDRESS
Deployed to: 0xYOUR_CONTRACT_ADDRESS
Transaction hash: 0xYOUR_TX_HASH
```

## Interacting with Deployed Contracts

Once deployed, you can interact with your contracts using Foundry's `cast` tool or ethers.js.

### Using Cast Commands

```bash theme={"dark"}
# Query the counter value
cast call $CONTRACT_ADDRESS "getCount()(uint256)" --rpc-url $SEI_TESTNET_RPC

# Increment the counter
cast send $CONTRACT_ADDRESS "increment()" --private-key $PRIVATE_KEY --rpc-url $SEI_TESTNET_RPC

# Set a specific number
cast send $CONTRACT_ADDRESS "setNumber(uint256)" 42 --private-key $PRIVATE_KEY --rpc-url $SEI_TESTNET_RPC

# Check ERC20 token balance
cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" $YOUR_ADDRESS --rpc-url $SEI_TESTNET_RPC

# Transfer ERC20 tokens
cast send $TOKEN_ADDRESS "transfer(address,uint256)" $RECIPIENT_ADDRESS 1000 --private-key $PRIVATE_KEY --rpc-url $SEI_TESTNET_RPC
```

### Using ethers.js

Install ethers.js to interact with your contracts programmatically:

```bash theme={"dark"}
npm install ethers@latest
```

Create a Node.js script to interact with your deployed contracts:

```tsx title="interact.js" theme={"dark"}
import { ethers } from 'ethers';

const privateKey = process.env.PRIVATE_KEY;
const evmRpcEndpoint = process.env.SEI_TESTNET_RPC;
const provider = new ethers.JsonRpcProvider(evmRpcEndpoint);
const signer = new ethers.Wallet(privateKey, provider);

// Counter contract interaction
const counterAbi = ['function getCount() public view returns (uint256)', 'function increment() public', 'function setNumber(uint256 newNumber) public', 'event NumberChanged(uint256 newNumber, address changedBy)'];

const counterAddress = 'YOUR_COUNTER_CONTRACT_ADDRESS';
const counterContract = new ethers.Contract(counterAddress, counterAbi, signer);

// ERC20 token interaction
const tokenAbi = ['function balanceOf(address owner) view returns (uint256)', 'function transfer(address to, uint256 amount) returns (bool)', 'function mint(address to, uint256 amount)', 'event Transfer(address indexed from, address indexed to, uint256 value)'];

const tokenAddress = 'YOUR_TOKEN_CONTRACT_ADDRESS';
const tokenContract = new ethers.Contract(tokenAddress, tokenAbi, signer);

async function interactWithContracts() {
  try {
    // Counter interactions
    console.log('Current count:', await counterContract.getCount());

    const incrementTx = await counterContract.increment();
    await incrementTx.wait();
    console.log('Incremented! New count:', await counterContract.getCount());

    // Token interactions
    const balance = await tokenContract.balanceOf(signer.address);
    console.log('Token balance:', ethers.formatEther(balance));

    // Transfer tokens
    const transferTx = await tokenContract.transfer('0xRecipientAddress', ethers.parseEther('100'));
    await transferTx.wait();
    console.log('Tokens transferred!');
  } catch (error) {
    console.error('Error:', error);
  }
}

interactWithContracts();
```

<Info>Foundry generates the ABI for contracts in the `out` folder. You can use this ABI to interact with contracts from other tools like `ethers.js` or `web3.js`.</Info>
