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¶
[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.
// 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¶
# 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)'
3. Deploy¶
# 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:
# 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
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¶
# 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
Sources at 72057fe3ec: developers/examples/first-contract/foundry.toml · developers/examples/languages/Counter.sol · developers/examples/first-contract/build.sh · developers/examples/first-contract/deploy.sh · developers/examples/first-contract/verify.sh · developers/examples/first-contract/interact.sh
Found a problem? Report a documentation issue · Build testnet-rc1+72057fe3ec