Skip to content
This repository was archived by the owner on Jan 9, 2026. It is now read-only.

Commit 65a3f92

Browse files
committed
feat: add chainweb chain id precompile
1 parent e1cefa5 commit 65a3f92

File tree

5 files changed

+93
-68
lines changed

5 files changed

+93
-68
lines changed

src/Chainweb.sol

Lines changed: 60 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@ import {console2} from "forge-std/console2.sol";
77
import {Test} from "forge-std/test.sol";
88
import {Script, console} from "forge-std/Script.sol";
99

10+
interface IChainwebChainId {
11+
function getChainId() external view returns (uint32);
12+
}
13+
1014
contract Chainweb is CommonBase {
1115
uint24 private _chains;
1216
uint256 private _chainIdOffset;
1317
uint24 private _chainwebChainIdOffset;
1418
uint256[] private _chainForks;
1519
string private _hostUrl;
1620

17-
constructor(
18-
uint24 chainNumbers,
19-
uint256 chainIdOffset,
20-
uint24 chainwebChainIdOffset,
21-
string memory hostUrl
22-
) {
21+
// @notice Precompile that provides the chainweb-chain-id
22+
address public constant CHAIN_ID_PRECOMPILE = address(0x9b02c3e2dF42533e0FD166798B5A616f59DBd2cc);
23+
24+
constructor(uint24 chainNumbers, uint256 chainIdOffset, uint24 chainwebChainIdOffset, string memory hostUrl) {
2325
_chains = chainNumbers;
2426
_chainIdOffset = chainIdOffset;
2527
_chainwebChainIdOffset = chainwebChainIdOffset;
@@ -37,12 +39,13 @@ contract Chainweb is CommonBase {
3739
return path;
3840
}
3941

40-
function rpcUrl(string memory chainId) private returns (string memory) {
42+
function rpcUrl(string memory chainId, string memory chainwebChainId) private returns (string memory) {
4143
// TODO - support forking from a custom RPC URL if provided
42-
string[] memory cmds = new string[](3);
44+
string[] memory cmds = new string[](4);
4345
cmds[0] = getNodePath();
4446
cmds[1] = "start";
4547
cmds[2] = chainId;
48+
cmds[3] = chainwebChainId;
4649
bytes memory output = vm.ffi(cmds);
4750
string memory url = string(output);
4851
return url;
@@ -52,40 +55,53 @@ contract Chainweb is CommonBase {
5255
console.log("Setting main RPC node for", _chains, "chains");
5356
for (uint256 i = 0; i < _chains; i++) {
5457
if (bytes(_hostUrl).length > 0) {
55-
string memory url = string(
56-
abi.encodePacked(
57-
_hostUrl,
58-
"/chain/",
59-
vm.toString(i + _chainwebChainIdOffset),
60-
"/evm/rpc"
61-
)
62-
);
58+
string memory url =
59+
string(abi.encodePacked(_hostUrl, "/chain/", vm.toString(i + _chainwebChainIdOffset), "/evm/rpc"));
6360
console.log("Using custom RPC URL:", url);
6461
_chainForks.push(vm.createFork(url));
6562
} else {
66-
string memory url = rpcUrl(vm.toString(i + _chainIdOffset));
63+
string memory url = rpcUrl(vm.toString(i + _chainIdOffset), vm.toString(i + _chainwebChainIdOffset));
6764
console.log("Forking", url);
6865
_chainForks.push(vm.createFork(url));
6966
}
7067
}
7168
}
7269

70+
// set the contract in the same address as devnet
71+
function deployChainWebChainIdContract(uint256 chainId) public {
72+
// get runtime bytecode
73+
switchChain(chainId);
74+
75+
bytes memory bytecode = hex"5f545f526004601cf3";
76+
77+
// place code at the target address
78+
vm.etch(CHAIN_ID_PRECOMPILE, bytecode);
79+
80+
// set storage slot 0 = desired chainId (e.g., 1337)
81+
vm.store(CHAIN_ID_PRECOMPILE, bytes32(uint256(0)), bytes32(chainId));
82+
}
83+
84+
function getActiveChainId() public view returns (uint256) {
85+
(bool ok, bytes memory data) = CHAIN_ID_PRECOMPILE.staticcall("");
86+
require(ok, "call failed");
87+
88+
uint32 chainId = uint32(bytes4(data));
89+
return chainId;
90+
}
91+
7392
function setupChainsForTest() public {
7493
console.log("Setting main RPC node for", _chains, "chains");
75-
string memory url = rpcUrl(vm.toString(_chainIdOffset));
94+
string memory url = rpcUrl(vm.toString(_chainIdOffset), vm.toString(_chainwebChainIdOffset));
7695
console.log("Forking", url);
7796
for (uint24 i = 0; i < _chains; i++) {
78-
_chainForks.push(vm.createFork(url));
97+
uint256 forkId = vm.createFork(url);
98+
_chainForks.push(forkId);
99+
deployChainWebChainIdContract(i + _chainwebChainIdOffset);
79100
}
80101
}
81102

82103
function switchChain(uint256 chainId) public {
83-
console.log(
84-
"Switching to chain:",
85-
chainId,
86-
"_chainIdOffset",
87-
_chainwebChainIdOffset
88-
);
104+
console.log("Switching to chain:", chainId, "_chainIdOffset", _chainwebChainIdOffset);
89105
require(chainId >= _chainwebChainIdOffset, "Invalid chain ID");
90106
uint24 chainIndex = uint24(chainId - _chainwebChainIdOffset);
91107
require(chainIndex < _chains, "Invalid chain ID");
@@ -112,15 +128,23 @@ struct ChainwebConfig {
112128
}
113129

114130
contract ChainwebConfigReader is CommonBase {
115-
function readOptionalJsonUint(string memory json, string memory key, uint256 defaultValue) internal pure returns (uint256) {
131+
function readOptionalJsonUint(string memory json, string memory key, uint256 defaultValue)
132+
internal
133+
pure
134+
returns (uint256)
135+
{
116136
try vm.parseJsonUint(json, key) returns (uint256 value) {
117137
return value;
118138
} catch {
119139
return defaultValue;
120140
}
121141
}
122142

123-
function readOptionalJsonString(string memory json, string memory key, string memory defaultValue) internal pure returns (string memory) {
143+
function readOptionalJsonString(string memory json, string memory key, string memory defaultValue)
144+
internal
145+
pure
146+
returns (string memory)
147+
{
124148
try vm.parseJsonString(json, key) returns (string memory value) {
125149
return value;
126150
} catch {
@@ -138,8 +162,10 @@ contract ChainwebConfigReader is CommonBase {
138162
// Parse all fields from config
139163
uint256 numberOfChains = readOptionalJsonUint(json, string(abi.encodePacked(envPath, ".numberOfChains")), 1);
140164
uint256 chainIdOffset = readOptionalJsonUint(json, string(abi.encodePacked(envPath, ".chainIdOffset")), 31337);
141-
uint256 chainwebChainIdOffset = readOptionalJsonUint(json, string(abi.encodePacked(envPath, ".chainwebChainIdOffset")), 0);
142-
string memory extenalHostUrl = readOptionalJsonString(json, string(abi.encodePacked(envPath, ".extenalHostUrl")), "");
165+
uint256 chainwebChainIdOffset =
166+
readOptionalJsonUint(json, string(abi.encodePacked(envPath, ".chainwebChainIdOffset")), 0);
167+
string memory extenalHostUrl =
168+
readOptionalJsonString(json, string(abi.encodePacked(envPath, ".extenalHostUrl")), "");
143169

144170
return ChainwebConfig({
145171
numberOfChains: numberOfChains,
@@ -154,12 +180,7 @@ contract ChainwebTest is Test {
154180
Chainweb public chainweb;
155181

156182
constructor(uint24 chainNumbers, uint24 chainwebChainIdOffset) {
157-
chainweb = new Chainweb(
158-
chainNumbers,
159-
block.chainid,
160-
chainwebChainIdOffset,
161-
""
162-
);
183+
chainweb = new Chainweb(chainNumbers, block.chainid, chainwebChainIdOffset, "");
163184
chainweb.setupChainsForTest();
164185
}
165186
}
@@ -169,24 +190,16 @@ contract ChainwebScript is Script {
169190
ChainwebConfigReader private configReader;
170191

171192
constructor() {
172-
string memory environment = vm.envExists("CHAINWEB")
173-
? vm.envString("CHAINWEB")
174-
: "anvil";
193+
string memory environment = vm.envExists("CHAINWEB") ? vm.envString("CHAINWEB") : "anvil";
175194

176195
configReader = new ChainwebConfigReader();
177196
ChainwebConfig memory config = configReader.readChainwebConfig(environment);
178197

179-
string memory nodeUrl = vm.envExists("CHAINWEB_HOST")
180-
? vm.envString("CHAINWEB_HOST")
181-
: config.extenalHostUrl;
198+
string memory nodeUrl = vm.envExists("CHAINWEB_HOST") ? vm.envString("CHAINWEB_HOST") : config.extenalHostUrl;
182199
console.log("Using node URL:", nodeUrl);
183200

184-
chainweb = new Chainweb(
185-
uint24(config.numberOfChains),
186-
block.chainid,
187-
uint24(config.chainwebChainIdOffset),
188-
nodeUrl
189-
);
201+
chainweb =
202+
new Chainweb(uint24(config.numberOfChains), block.chainid, uint24(config.chainwebChainIdOffset), nodeUrl);
190203
chainweb.setupChainsForScript();
191204
}
192205
}

src/example/script/Counter.s.sol

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import {Script, console} from "forge-std/Script.sol";
55
import {Counter} from "../src/Counter.sol";
66
import {ChainwebScript} from "kadena-io/foundry-chainweb/Chainweb.sol";
77

8-
contract CounterScript is ChainwebScript() {
9-
Counter public counter;
10-
8+
contract CounterScript is ChainwebScript {
119
function run() public {
1210
uint256[] memory chainIds = chainweb.getChainIds();
1311
for (uint256 i = 0; i < chainIds.length; i++) {
@@ -18,5 +16,10 @@ contract CounterScript is ChainwebScript() {
1816
counter1.setNumber(1);
1917
vm.stopBroadcast();
2018
}
19+
20+
for (uint256 i = 0; i < chainIds.length; i++) {
21+
chainweb.switchChain(chainIds[i]);
22+
console.log("Active chain ID:", chainweb.getActiveChainId());
23+
}
2124
}
2225
}

src/example/src/Counter.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ pragma solidity ^0.8.13;
33

44
contract Counter {
55
uint256 public number;
6+
address public owner;
67

78
function setNumber(uint256 newNumber) public {
89
number = newNumber;
10+
owner = msg.sender;
911
}
1012

1113
function increment() public {

src/example/test/Counter.t.sol

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,25 @@ import {ChainwebTest, ChainwebConfig} from "kadena-io/foundry-chainweb/Chainweb.
88
contract CounterTest is ChainwebTest(4, 0) {
99
function test_MultiChains() public {
1010
uint256[] memory chainIds = chainweb.getChainIds();
11-
console.log("Available chain IDs:", chainIds.length);
12-
13-
chainweb.switchChain(chainIds[0]);
14-
Counter counter0 = new Counter();
15-
console.log("Running test on chain:", block.chainid);
16-
counter0.setNumber(10);
17-
18-
chainweb.switchChain(chainIds[1]);
19-
Counter counter1 = new Counter();
20-
counter1.setNumber(11);
21-
22-
chainweb.switchChain(chainIds[0]);
23-
counter0.increment();
24-
assertEq(counter0.number(), 11);
11+
for (uint256 i = 0; i < chainIds.length; i++) {
12+
console.log("msg.sender in test:", msg.sender);
13+
chainweb.switchChain(chainIds[i]);
14+
console.log("Running script on chain:", block.chainid);
15+
Counter counter = new Counter();
16+
console.log("counter.owner() in test:", counter.owner());
17+
console.log("counter.owner() nonce:", vm.getNonce(counter.owner()));
18+
counter.setNumber(1);
19+
console.log("contract address:", address(counter));
20+
}
21+
}
2522

26-
chainweb.switchChain(chainIds[1]);
27-
counter1.increment();
28-
assertEq(counter1.number(), 12);
23+
function test_ActiveChainId() public {
24+
uint256[] memory chainIds = chainweb.getChainIds();
25+
for (uint256 i = 0; i < chainIds.length; i++) {
26+
chainweb.switchChain(chainIds[i]);
27+
uint256 activeChainId = chainweb.getActiveChainId();
28+
assertEq(activeChainId, chainIds[i]);
29+
console.log("Active chain ID:", activeChainId);
30+
}
2931
}
3032
}

src/scripts/node.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env bash
22

33
CHAIN_ID=$2
4+
CHAINWEB_CHAIN_ID=$3
45
PID_FILE="/tmp/chainweb-anvil$CHAIN_ID.pid"
56

67

@@ -66,7 +67,11 @@ start_anvil() {
6667
rm -f "$PID_FILE"
6768
exit 1
6869
fi
69-
echo "http://127.0.0.1:$PORT"
70+
RPC_URL="http://127.0.0.1:$PORT"
71+
VALUE=$(printf "0x%064x" "$CHAINWEB_CHAIN_ID")
72+
cast rpc --rpc-url "$RPC_URL" anvil_setCode 0x9b02c3e2dF42533e0FD166798B5A616f59DBd2cc 0x5f545f526004601cf3 > /dev/null 2>&1
73+
cast rpc --rpc-url "$RPC_URL" anvil_setStorageAt 0x9b02c3e2dF42533e0FD166798B5A616f59DBd2cc 0x0 $VALUE > /dev/null 2>&1
74+
echo "$RPC_URL"
7075
}
7176

7277

0 commit comments

Comments
 (0)