> ## Documentation Index
> Fetch the complete documentation index at: https://seilabs-docs-evm-cookbook.mintlify.site/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 && (status.type === 'error' ? <span className="text-red-600 dark:text-red-400" style={{
    fontSize: '11px'
  }}>
            {status.message}
          </span> : <span className="text-green-700 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 [frameSrc, setFrameSrc] = useState(null);
  const [btnHover, setBtnHover] = useState(false);
  const [isDark, setIsDark] = useState(true);
  useLayoutEffect(() => {
    const el = document.documentElement;
    const update = () => setIsDark(el.classList.contains('dark'));
    update();
    const obs = new MutationObserver(update);
    obs.observe(el, {
      attributes: true,
      attributeFilter: ['class']
    });
    return () => obs.disconnect();
  }, []);
  const themedSrc = (() => {
    if (!src) return src;
    try {
      const url = new URL(src);
      url.searchParams.set('theme', isDark ? 'dark' : 'light');
      return url.toString();
    } catch {
      return src;
    }
  })();
  const loadEditor = () => {
    if (themedSrc) setFrameSrc(themedSrc);
  };
  const loaded = frameSrc !== null;
  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 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="not-prose w-full rounded-lg border overflow-hidden my-4" style={{
    borderColor: HAIRLINE
  }}>
			<div className="flex items-center justify-between gap-3 px-4 py-2.5 border-b" 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-600 dark:text-neutral-400">{meta.name}</span>
				</div>
				<div className="flex items-center gap-3 shrink-0">
					{themedSrc ? <a href={themedSrc} 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 && themedSrc ? <button type="button" onClick={loadEditor} 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}

			{!themedSrc ? <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={frameSrc} 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={loadEditor} className="w-full flex flex-col items-center justify-center gap-2 text-neutral-600 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' ? 'Sei Mainnet' : network === 'testnet' ? 'Sei 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 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="not-prose w-full rounded-lg border overflow-hidden my-4" style={{
    borderColor: HAIRLINE
  }}>
			<div className="flex items-center justify-between gap-3 px-4 py-2.5 border-b" 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-600 dark:text-neutral-400">{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="text-xs uppercase tracking-wide text-neutral-600 dark:text-neutral-400">Request</span>
			</div>
			<pre className="m-0 px-4 py-3 text-sm overflow-x-auto text-neutral-700 dark:text-neutral-300" 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="text-xs uppercase tracking-wide text-neutral-600 dark:text-neutral-400">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="m-0 px-4 py-3 text-sm overflow-x-auto text-neutral-700 dark:text-neutral-300" style={codeStyle}>
						{resultString}
					</pre>
					{decoded ? <div className="px-4 pb-3 text-xs text-neutral-600 dark:text-neutral-400" 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="text-xs uppercase tracking-wide text-neutral-600 dark:text-neutral-400">Error</span>
					</div>
					<pre className="m-0 px-4 py-3 text-sm overflow-x-auto text-red-600 dark:text-red-400" style={codeStyle}>
						{errorMsg}
					</pre>
				</div> : null}
		</div>;
};

This tutorial shows how to set up Foundry for Sei EVM development. It covers environment setup, contract creation, and deployment. You use OpenZeppelin's pre-built components with the Foundry toolkit to build secure, standardized smart contracts.

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

<Tip>
  **Deploy to Sei Testnet first**

  We <strong>highly recommend</strong> that you deploy to <em>Sei Testnet</em> first. Make sure that everything works as expected before you deploy to Sei Mainnet. This helps you catch bugs early, avoid unnecessary gas costs, and keep your users safe.
</Tip>

## Try it before you install

It takes a few minutes to install Foundry and scaffold a project. If you only want to see a contract deploy to Sei, you can do both of these steps in the browser now.

First, confirm that the RPC endpoints you will use in `foundry.toml` respond:

<RunSnippet method="eth_chainId" network="testnet" title="eth_chainId · Sei Testnet" description="Testnet RPC. The result decodes to 1328." />

<RunSnippet method="eth_chainId" network="mainnet" title="eth_chainId · Sei Mainnet" description="Mainnet RPC. The result decodes to 1329." />

Then add Sei Testnet to your wallet and deploy a minimal `Counter.sol` from Remix. You do not need `forge` or a local install. In Remix, compile the contract under **Solidity Compiler**. Then deploy it under **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 you start, make sure that 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

If you have not installed Foundry yet, install it first. Follow the [installation guide](https://book.getfoundry.sh/getting-started/installation) or use the quick install command:

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

Create a new Foundry project:

```bash theme={null}
# 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 creates a standard Foundry project structure with `src/`, `test/`, and `script/` directories.

## Configuring Foundry for Sei EVM

To configure Foundry for Sei networks, create a `foundry.toml` file in your project root:

```toml title="foundry.toml" theme={null}
[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={null}
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. If you commit sensitive information such as your `PRIVATE_KEY`, you can lose funds.</Warning>

## Using OpenZeppelin contracts

OpenZeppelin has a library of secure, tested smart contract components. Install the OpenZeppelin contracts:

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

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

```bash theme={null}
forge remappings > remappings.txt
```

## Creating and deploying smart contracts

Each tab below creates a different type of smart contract. Choose the one that matches what you want to build:

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

    ```solidity title="src/Counter.sol" theme={null}
    // 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={null}
    // 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 with OpenZeppelin. Add the contract to `src/SeiToken.sol`:

    ```solidity title="src/SeiToken.sol" theme={null}
    // 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={null}
    // 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 with OpenZeppelin. Add the contract to `src/SeiNFT.sol`:

    ```solidity title="src/SeiNFT.sol" theme={null}
    // 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={null}
    // 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 has built-in support for testing. Create tests for your contracts.

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

    ```solidity title="test/Counter.t.sol" theme={null}
    // 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={null}
    // 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={null}
    // 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:

```bash theme={null}
forge test
```

For more verbose output, run:

```bash theme={null}
forge test -vvv
```

## Deploying to Sei Testnet and Mainnet

You can deploy your contracts to the Sei networks with Forge scripts or with direct deployment commands.

### Using Forge scripts (recommended)

Deploy to Sei Testnet:

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

Deploy to Sei Mainnet:

```bash theme={null}
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={null}
# Deploy Counter to testnet
forge create --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY --broadcast src/Counter.sol:Counter

# Deploy SeiToken to testnet
forge create --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY --broadcast src/SeiToken.sol:SeiToken --constructor-args YOUR_ADDRESS

# Deploy SeiNFT to testnet
forge create --rpc-url $SEI_TESTNET_RPC --private-key $PRIVATE_KEY --broadcast src/SeiNFT.sol:SeiNFT --constructor-args YOUR_ADDRESS "https://your-metadata-server.com/metadata/"
```

Without `--broadcast`, `forge create` only simulates the deployment. Put `--constructor-args` last, because it takes every value after it, and pass the plain values, not ABI-encoded data.

A successful deployment prints output similar to this:

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

## Interacting with deployed contracts

After deployment, you can interact with your contracts through Foundry's `cast` tool or ethers.js.

### Using Cast commands

```bash theme={null}
# 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

To interact with your contracts programmatically, install ethers.js:

```bash theme={null}
npm install ethers@latest
```

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

```tsx title="interact.js" theme={null}
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 contract ABIs in the `out` folder. You can use these ABIs to interact with contracts from other tools, such as `ethers.js` or `web3.js`.</Info>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.