Smart Contract Deployment Guide#

Deploy and interact with smart contracts on Shell-Chain.

See also: Quickstart Guide · JSON-RPC API Reference · Testnet Operator Guide · PQ Crypto Guide


Overview#

Shell-Chain is fully EVM-compatible (Cancun spec). Any contract written in Solidity or Vyper that compiles to EVM bytecode will work on Shell-Chain without modification. Standard tooling — Hardhat, Foundry, Remix — all work out of the box.


Prerequisites#

  • Node.js 18+ (for Hardhat)
  • Hardhat or Foundry installed
  • A running shell-chain node (see Quickstart)
  • A funded account (pre-allocated in genesis or received via transfer)

Connecting to Shell Chain#

NetworkRPC URLChain ID
Localhttp://localhost:85451337
Alpha Testnethttp://testnet.shell.xyz1337

The local endpoint is the default JSON-RPC server started by shell-node run. The alpha testnet endpoint is served via nginx reverse proxy (see Testnet Operator Guide).


Hardhat Setup#

Install Hardhat#

mkdir my-shell-project && cd my-shell-project
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init

Configure networks#

// hardhat.config.js
module.exports = {
  solidity: "0.8.26",
  networks: {
    shell: {
      url: "http://localhost:8545",
      chainId: 1337,
    },
    shellAlpha: {
      url: "http://testnet.shell.xyz",
      chainId: 1337,
    }
  }
};

Deploy to a local node:

npx hardhat run scripts/deploy.js --network shell

Deploy to the alpha testnet:

npx hardhat run scripts/deploy.js --network shellAlpha

Foundry Setup#

Install Foundry#

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

Deploy with Foundry#

forge create --rpc-url http://localhost:8545 --chain-id 1337 src/Counter.sol:Counter

For the alpha testnet:

forge create --rpc-url http://testnet.shell.xyz --chain-id 1337 src/Counter.sol:Counter

Example: Deploy a Counter Contract#

1. Write the contract#

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Counter {
    uint256 public count;

    event CountChanged(uint256 newCount);

    function get() public view returns (uint256) {
        return count;
    }

    function increment() public {
        count += 1;
        emit CountChanged(count);
    }

    function decrement() public {
        require(count > 0, "Counter: cannot decrement below zero");
        count -= 1;
        emit CountChanged(count);
    }

    function reset() public {
        count = 0;
        emit CountChanged(count);
    }
}

2. Deploy with Hardhat#

Create scripts/deploy.js:

const hre = require("hardhat");

async function main() {
  const Counter = await hre.ethers.getContractFactory("Counter");
  const counter = await Counter.deploy();
  await counter.waitForDeployment();
  console.log("Counter deployed to:", await counter.getAddress());
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
npx hardhat run scripts/deploy.js --network shell

3. Deploy with Foundry#

forge create \
  --rpc-url http://localhost:8545 \
  --chain-id 1337 \
  src/Counter.sol:Counter

Interacting with a Deployed Contract#

Read calls (no gas required)#

Use eth_call to read state without submitting a transaction:

# Call the get() function (selector: 0x6d4ce63c)
curl -s http://localhost:8545 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_call",
    "params":[{
      "to":"0xYOUR_CONTRACT_ADDRESS",
      "data":"0x6d4ce63c"
    },"latest"],
    "id":1
  }'

Or with Hardhat:

const counter = await hre.ethers.getContractAt("Counter", "0xYOUR_CONTRACT_ADDRESS");
const count = await counter.get();
console.log("Current count:", count.toString());

Write calls (submits a transaction)#

Use eth_sendRawTransaction or shell_sendTransaction to modify state:

# Increment the counter (selector: 0xd09de08a)
curl -s http://localhost:8545 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_sendRawTransaction",
    "params":["0x...signed_tx_bytes..."],
    "id":1
  }'

Or with Hardhat:

const counter = await hre.ethers.getContractAt("Counter", "0xYOUR_CONTRACT_ADDRESS");
const tx = await counter.increment();
await tx.wait();
console.log("Incremented! New count:", (await counter.get()).toString());

Using PQ Signatures for Deployment#

Shell-Chain uses post-quantum Dilithium3 signatures instead of ECDSA. To deploy contracts using PQ signatures, use the shell_sendTransaction RPC method:

# Sign the deployment transaction with the shell-node CLI
shell-node tx deploy \
  --code 0x608060405234801561001057600080fd5b50... \
  --keystore my-key.json \
  --rpc-url http://127.0.0.1:8545

# Or submit via JSON-RPC
curl -s http://localhost:8545 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"shell_sendTransaction",
    "params":[{
      "from": "0xYOUR_ADDRESS",
      "data": "0x608060405234801561001057600080fd5b50...",
      "gas": "0x100000",
      "maxFeePerGas": "0x3b9aca00",
      "maxPriorityFeePerGas": "0x0",
      "nonce": "0x0",
      "pqSignature": "0x...dilithium3_signature...",
      "pqPubkey": "0x...dilithium3_pubkey..."
    }],
    "id":1
  }'

Note: Standard Ethereum wallets (MetaMask, etc.) use ECDSA signatures. For full PQ security, use the shell-node CLI or PQ-aware SDKs. See PQ Crypto Guide for details.


Verifying Contracts with debug_traceTransaction#

After deploying a contract, use debug_traceTransaction to inspect the execution trace:

curl -s http://localhost:8545 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"debug_traceTransaction",
    "params":["0xYOUR_TX_HASH"],
    "id":1
  }' | python3 -m json.tool

The trace shows the full call tree including:

  • CREATE / CREATE2 frames for contract deployment
  • Gas consumption per opcode
  • Storage reads and writes
  • Internal calls between contracts

Note: The debug namespace must be enabled on the node with --rpc-api eth,net,web3,shell,debug.


EVM Compatibility Notes#

Shell-Chain implements the Cancun EVM specification. Key compatibility details:

Supported Cancun opcodes#

OpcodeEIPDescription
TSTORE / TLOADEIP-1153Transient storage (cleared after each tx)
MCOPYEIP-5656Efficient memory copy
BLOBHASHEIP-4844Access blob versioned hashes
BLOBBASEFEEEIP-7516Read blob base fee

Signature behavior#

  • ecrecover always returns address(0) — Shell-Chain uses post-quantum Dilithium3 signatures, not ECDSA. Contracts that rely on ecrecover for signature verification will not work as expected. Use the PQ signature scheme instead.

Transaction types supported#

TypeEIPDescription
Legacy (type 0)Traditional transactions
Access list (type 1)EIP-2930Transactions with access lists for gas savings
EIP-1559 (type 2)EIP-1559Dynamic fee transactions with base fee + priority fee
Blob (type 3)EIP-4844Blob-carrying transactions for data availability

Gas model#

Shell-Chain uses the EIP-1559 gas model:

  • baseFeePerGas adjusts per-block based on gas utilization
  • maxPriorityFeePerGas is always 0x0 on this PoA chain
  • Use eth_gasPrice to get the current base fee
  • Use eth_feeHistory for historical fee data

Gas Estimation Tips#

  1. Use eth_estimateGas before submitting transactions. The estimate includes a 20% buffer (gas_used × 1.2) with a minimum of 21,000.

  2. Check the base fee with eth_gasPrice. Set maxFeePerGas ≥ the base fee or the transaction will be rejected.

  3. Access lists save gas for contracts that touch many storage slots. Use eth_createAccessList to generate one:

    curl -s http://localhost:8545 \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc":"2.0",
        "method":"eth_createAccessList",
        "params":[{
          "to":"0xYOUR_CONTRACT",
          "data":"0x..."
        },"latest"],
        "id":1
      }'
    
  4. Transient storage (TSTORE/TLOAD) is cheaper than regular storage for data only needed within a single transaction.

  5. Gas limit is set in genesis (default: 30,000,000). Check with eth_getBlockByNumber.


Further Reading#


Last updated: 2025