Skip to content
Available on private testnetTestnet: yesMainnet: Not availableSoftware: Core bad4a06ea · protocol pqwerai-v8-testnet/1Updated

First smart contract

Prerequisites: the quickstart account (funded) and env.sh, Foundry 1.8.5. Result: a Counter contract deployed, verified byte-for-byte and called.

1. Project layout

first-contract/
├── foundry.toml
└── src/Counter.sol
foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.26"     # pinned; matches contracts/studio/compilers.json
evm_version = "cancun"      # chain 77240 runs PUSH0/MCOPY/TSTORE; never use blobhash()/blobbasefee
optimizer = true
optimizer_runs = 200
bytecode_hash = "none"      # no metadata hash: deterministic runtime bytes for byte-for-byte verification
remappings = ["@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"]

evm_version = "cancun" is correct for chain 77240: it executes PUSH0, MCOPY and transient storage but not the blob opcodes, which Solidity emits only if you call blobhash() or read block.blobbasefee (EVM compatibility). The Wallet Studio's precompiled templates use london, which is also valid.

src/Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

/// Starter counter (Solidity 0.8.26). Unaudited example.
contract Counter {
    uint256 private count;
    address public immutable creator;

    event Incremented(uint256 value);

    constructor() {
        creator = msg.sender;
    }

    function increment() external {
        count += 1;
        emit Incremented(count);
    }

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

2. Compile

build.sh
# Compile with the pinned solc 0.8.26 and print the ABI and the runtime size.
forge build
forge inspect Counter abi --json
python3 -c 'import json; o=json.load(open("out/Counter.sol/Counter.json")); print("runtime bytes:", (len(o["deployedBytecode"]["object"])-2)//2)'
Compiling 1 files with Solc 0.8.26
Compiler run successful!
[ …ABI… ]
runtime bytes: 288

3. Deploy

deploy.sh
# Deploy (one EIP-1559 transaction). forge prints "Deployed to: 0x...".
forge create src/Counter.sol:Counter --broadcast \
  --keystore "$KEYSTORE" --password-file "$PASSWORD_FILE" --rpc-url "$RPC" | tee deploy.log
COUNTER=$(sed -n 's/^Deployed to: //p' deploy.log)
echo "COUNTER=$COUNTER"
Deployer: 0x42C913Cd758146169E3623b5Ad9Bd74872665621
Deployed to: 0x5EdDF6d21aFd7FeF521b25EB8bCff86F2dC54B28
Transaction hash: 0x…
COUNTER=0x5EdDF6d21aFd7FeF521b25EB8bCff86F2dC54B28

4. Verify

The PQWERAI explorer does not offer source verification yet. Verification therefore means proving that the code at the address is exactly what your source compiles to:

verify.sh
# Verification = the on-chain runtime code equals the locally compiled runtime, byte for byte, except the
# ranges the compiler reserves for immutables (here `creator`), whose values are written at deployment.
# (The PQWERAI explorer has no source-verification service yet; this is the same check the Wallet Studio runs.)
cast code "$COUNTER" --rpc-url "$RPC" > onchain.hex
python3 - out/Counter.sol/Counter.json onchain.hex <<'PY'
import json, sys
art = json.load(open(sys.argv[1]))["deployedBytecode"]
local = bytearray.fromhex(art["object"][2:])
chain = bytearray.fromhex(open(sys.argv[2]).read().strip()[2:])
masked = 0
for refs in art.get("immutableReferences", {}).values():
    for r in refs:  # copy the deployed immutable value into the local image before comparing
        local[r["start"]:r["start"] + r["length"]] = chain[r["start"]:r["start"] + r["length"]]
        masked += r["length"]
if local == chain:
    print(f"VERIFIED: {len(chain)} runtime bytes match ({masked} immutable bytes taken from chain)")
else:
    sys.exit("MISMATCH: on-chain code differs from the compiled artifact")
PY
VERIFIED: 288 runtime bytes match (32 immutable bytes taken from chain)
Why immutables need special handling

creator is immutable: the compiler leaves 32 zero bytes in the runtime code and the constructor writes the deployer address there. A naive comparison of cast code with forge inspect … deployedBytecode therefore fails. The script copies exactly the ranges listed in immutableReferences from the chain and compares everything else. Publish your foundry.toml, source and compiler version so anyone can repeat the check.

5. Interact

interact.sh
# Read, write, read again, then decode the Incremented event from the chain logs.
cast call "$COUNTER" "get()(uint256)" --rpc-url "$RPC"
TX=$(cast send "$COUNTER" "increment()" --async \
       --keystore "$KEYSTORE" --password-file "$PASSWORD_FILE" --rpc-url "$RPC")
cast receipt "$TX" status --rpc-url "$RPC"
cast call "$COUNTER" "get()(uint256)" --rpc-url "$RPC"
cast call "$COUNTER" "creator()(address)" --rpc-url "$RPC"
BLOCK=$(cast receipt "$TX" blockNumber --rpc-url "$RPC")
cast logs --from-block "$BLOCK" --to-block "$BLOCK" --address "$COUNTER" "Incremented(uint256)" --rpc-url "$RPC"
0
true
1
0x42C913Cd758146169E3623b5Ad9Bd74872665621
- address: 0x5EdDF6d21aFd7FeF521b25EB8bCff86F2dC54B28
  blockNumber: 4505
  data: 0x0000000000000000000000000000000000000000000000000000000000000001
  topics: [
    0x20d8a6f5a693f9d1d627a598e8820f7a55ee74c183aa8f1a30e8d4e8dd9a8d84
  ]
  decoded:
    param0: 1

0x20d8a6f5… is keccak256("Incremented(uint256)"), the event signature topic.

Troubleshooting

Error Cause and fix
invalid opcode: BLOBHASH / BLOBBASEFEE Code uses blob features the chain does not support. Remove them.
Unable to locate solc 0.8.26 offline Install solc 0.8.26 once, or set FOUNDRY_SOLC=/path/to/solc
Constructor argument count mismatch --constructor-args must be the last option of forge create
MISMATCH in verify Different compiler settings (optimizer, evm_version, bytecode_hash) or different source
max code size exceeded Runtime > 24,576 bytes (EIP-170). Split the contract or use libraries

Next: Create a token · Language center


Found a problem? Report a documentation issue · Build testnet-rc1+72057fe3ec