diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1df60390 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Rain Protocol metadata system — Solidity contracts, Rust CLI/bindings, and a Graph subgraph for emitting and indexing on-chain metadata following the [MetadataV1 spec](https://github.com/rainprotocol/specs/blob/main/metadata-v1.md). + +The core contract is **MetaBoard** — deterministically deployed at `0xfb8437AeFBB8031064E274527C5fc08e30Ac6928` across all supported networks. It emits `MetaV1_2` events that the subgraph indexes. + +## Build & Test Commands + +All commands require the Nix development shell. Use `nix develop` to enter it, or prefix commands with `nix develop -c`. + +| Task | Command | +|------|---------| +| Solidity tests | `nix develop -c rainix-sol-test` | +| Solidity static analysis | `nix develop -c rainix-sol-static` | +| Solidity build artifacts | `nix develop -c rainix-sol-artifacts` | +| Rust tests | `nix develop -c rainix-rs-test` | +| Rust static analysis | `nix develop -c rainix-rs-static` | +| Rust build artifacts | `nix develop -c rainix-rs-artifacts` | +| Subgraph build | `nix develop -c subgraph-build` | +| Subgraph tests | `nix develop -c subgraph-test` | +| REUSE license check | `nix develop -c rainix-sol-legal` | + +Run a single Solidity test (inside nix shell): + +```sh +forge test --match-test testFunctionName +forge test --match-contract MetaBoardTest +``` + +Run a single Rust test (inside nix shell): + +```sh +cargo test test_name +``` + +## Architecture + +### Solidity (`src/`) +- `src/concrete/MetaBoard.sol` — Main contract; emits `MetaV1_2` events with sender, subject, and metadata bytes +- `src/lib/LibMeta.sol` — Metadata validation; checks magic number prefix `0xff0a89c674ee7874` +- `src/lib/LibDescribedByMeta.sol` — Helper for contracts implementing `IDescribedByMetaV1` +- `src/lib/deploy/LibMetaBoardDeploy.sol` — Deterministic deployment using Zoltu deployer pattern + +### Rust (`crates/`) +- `crates/cli` — `rain-metadata` binary; metadata generation/validation for multiple types (authoring, dotrain, Solidity ABI, etc.) +- `crates/bindings` — Solidity bindings generated via `alloy::sol!` from JSON ABIs in `/out` +- `crates/metaboard` — GraphQL client (Cynic) for querying MetaBoard subgraph data + +### Subgraph (`subgraph/`) +- AssemblyScript handlers indexing `MetaV1_2` events from MetaBoard +- Deployed across ~15 networks (Arbitrum, Base, Polygon, Flare, etc.) + +## Key Configuration + +- **Solidity**: `foundry.toml` — solc 0.8.25, Cancun EVM, optimizer 1M runs, `bytecode_hash = "none"`, `cbor_metadata = false` +- **Rust workspace**: `Cargo.toml` at root, three crates +- **Fuzz runs**: 5,096 (foundry.toml `[fuzz]`) +- **Remappings**: `rain.deploy/=lib/rain.deploy/src/` + +## Licensing + +DecentraLicense 1.0 (DCL-1.0). REUSE 3.2 compliant — all files need SPDX license headers. diff --git a/REUSE.toml b/REUSE.toml index 29965817..32fd7b98 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -2,6 +2,8 @@ version = 1 [[annotations]] path = [ + ".cargo/config.toml", + ".envrc", ".gas-snapshot", ".github/workflows/**/", ".gitignore", @@ -9,7 +11,9 @@ path = [ "audit/**/", "Cargo.lock", "Cargo.toml", + "CLAUDE.md", "crates/**/", + "foundry.lock", "subgraph/**/", "README.md", "flake.lock", diff --git a/flake.lock b/flake.lock index 9a12e0c4..e9f0d4de 100644 --- a/flake.lock +++ b/flake.lock @@ -75,11 +75,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1769324704, - "narHash": "sha256-aef15vEgiMEls1hTMt46rJuKNSO2cIOfiP99patq9yc=", + "lastModified": 1773213477, + "narHash": "sha256-Pv1Z3QqGkSGEUV+9pM5vYIiI7VJo7Tfm6ZmR+JSp1zo=", "owner": "shazow", "repo": "foundry.nix", - "rev": "e830409ba1bdecdc5ef9a1ec92660fc2da9bc68d", + "rev": "3c73daa86c823d706824fd9bbcb85aa23fd0f668", "type": "github" }, "original": { @@ -120,11 +120,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1769364508, - "narHash": "sha256-Wy8EVYSLq5Fb/rYH3LRxAMCnW75f9hOg2562AXVFmPk=", + "lastModified": 1773260212, + "narHash": "sha256-lYbvufol78/ziIlZm043NT2byudb6NQpGHhYhVSlkVY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6077bc4fb29be43d525984f63b69d37b9b1e62fe", + "rev": "37e1923adb6229cea0d12a5921b75c976017ccfe", "type": "github" }, "original": { @@ -151,11 +151,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1766653575, - "narHash": "sha256-TPgxCS7+hWc4kPhzkU5dD2M5UuPhLuuaMNZ/IpwKQvI=", + "lastModified": 1771923393, + "narHash": "sha256-Fy0+UXELv9hOE8WjYhJt8fMDLYTU2Dqn3cX4BwoGBos=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3c1016e6acd16ad96053116d0d3043029c9e2649", + "rev": "ea7f1f06811ce7fcc81d6c6fd4213150c23edcf2", "type": "github" }, "original": { @@ -175,11 +175,11 @@ "solc": "solc" }, "locked": { - "lastModified": 1769366341, - "narHash": "sha256-jeYOweTuJdKshW9lqVoNxvl4+flyRzWxEctRGabTW/8=", + "lastModified": 1773326212, + "narHash": "sha256-NDEwqacK1T59486Nlb/Kx2ks1vh92TiHVHcEL9dZuYc=", "owner": "rainprotocol", "repo": "rainix", - "rev": "e7bfe9c39d2de818eac241f88ecabc69e86ed734", + "rev": "266fd905bb0797c614b6f4baf8a747c2c07376cb", "type": "github" }, "original": { @@ -199,11 +199,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1769309768, - "narHash": "sha256-AbOIlNO+JoqRJkK1VrnDXhxuX6CrdtIu2hSuy4pxi3g=", + "lastModified": 1773216618, + "narHash": "sha256-iZlowevS+xKLGOXtZwpIrz3SWe7PtoGUfEeVZNib+WE=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "140c9dc582cb73ada2d63a2180524fcaa744fad5", + "rev": "07d7dc6fcc5eae76b4fb0e19d4afd939437bec97", "type": "github" }, "original": { @@ -219,11 +219,11 @@ "solc-macos-amd64-list-json": "solc-macos-amd64-list-json" }, "locked": { - "lastModified": 1768831671, - "narHash": "sha256-0mmlYRtZK+eomevkQCCH7PL8QlSuALZQsjLroCWGE08=", + "lastModified": 1772085240, + "narHash": "sha256-+NEcuhT2A0QQumVx9Ze6g2iuNicyuW028Jq/HUJHGh4=", "owner": "hellwolf", "repo": "solc.nix", - "rev": "80ad871b93d15c7bccf71617f78f73c2d291a9c7", + "rev": "d3cc119973e484ea366f4b997b404bb00d7829ca", "type": "github" }, "original": { @@ -235,13 +235,13 @@ "solc-macos-amd64-list-json": { "flake": false, "locked": { - "narHash": "sha256-P+ZslplK4cQ/wnV/wykVKb+yTCviI0eylA3sk9uHmRo=", + "narHash": "sha256-oEiXc95EghuYCudzkPA9XBFOnMdgWFfTO2/4XUfSTpc=", "type": "file", - "url": "https://github.com/argotorg/solc-bin/raw/a11f1ad/macosx-amd64/list.json" + "url": "https://github.com/argotorg/solc-bin/raw/83cb756/macosx-amd64/list.json" }, "original": { "type": "file", - "url": "https://github.com/argotorg/solc-bin/raw/a11f1ad/macosx-amd64/list.json" + "url": "https://github.com/argotorg/solc-bin/raw/83cb756/macosx-amd64/list.json" } }, "systems": { diff --git a/foundry.lock b/foundry.lock index a6749934..e3cffd73 100644 --- a/foundry.lock +++ b/foundry.lock @@ -3,6 +3,6 @@ "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" }, "lib/rain.deploy": { - "rev": "ccfdb378d2ab1ed95461ecf1355682250e487e4c" + "rev": "43a6ed3a98f0141e1963b4b9136e8c80e2889bd1" } } \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 8896ee71..db80bbb6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,7 @@ [profile.default] solc = "0.8.25" +auto_detect_solc = false # optimizer settings for snapshotting. optimizer = true @@ -9,11 +10,17 @@ evm_version = "cancun" bytecode_hash = "none" cbor_metadata = false +ffi = true remappings = [ "rain.deploy/=lib/rain.deploy/src/" ] +fs_permissions = [ + { access = "read", path = "subgraph/networks.json" }, + { access = "read", path = "subgraph/subgraph.yaml" } +] + [fuzz] runs = 5096 diff --git a/lib/rain.deploy b/lib/rain.deploy index ccfdb378..43a6ed3a 160000 --- a/lib/rain.deploy +++ b/lib/rain.deploy @@ -1 +1 @@ -Subproject commit ccfdb378d2ab1ed95461ecf1355682250e487e4c +Subproject commit 43a6ed3a98f0141e1963b4b9136e8c80e2889bd1 diff --git a/src/interface/deprecated/IMetaV1.sol b/src/interface/deprecated/IMetaV1.sol index acf04c18..95c45ee6 100644 --- a/src/interface/deprecated/IMetaV1.sol +++ b/src/interface/deprecated/IMetaV1.sol @@ -27,5 +27,6 @@ interface IMetaV1 { /// hash of some data/thing that this metadata is about. /// @param meta Rain metadata V1 compliant metadata bytes. /// https://github.com/rainprotocol/specs/blob/main/metadata-v1.md + //slither-disable-next-line unindexed-event-address event MetaV1(address sender, uint256 subject, bytes meta); } diff --git a/src/interface/unstable/IMetaV1_2.sol b/src/interface/unstable/IMetaV1_2.sol index dbca2d27..c739ea65 100644 --- a/src/interface/unstable/IMetaV1_2.sol +++ b/src/interface/unstable/IMetaV1_2.sol @@ -20,6 +20,6 @@ interface IMetaV1_2 { /// hash of some data/thing that this metadata is about. /// @param meta Rain metadata V1 compliant metadata bytes. /// https://github.com/rainprotocol/specs/blob/main/metadata-v1.md - //slither-disable-next-line naming-convention + //slither-disable-next-line naming-convention,unindexed-event-address event MetaV1_2(address sender, bytes32 subject, bytes meta); } diff --git a/src/lib/deploy/LibMetaBoardDeploy.sol b/src/lib/deploy/LibMetaBoardDeploy.sol index a7d051bc..29058ffa 100644 --- a/src/lib/deploy/LibMetaBoardDeploy.sol +++ b/src/lib/deploy/LibMetaBoardDeploy.sol @@ -19,4 +19,10 @@ library LibMetaBoardDeploy { /// than just checking the address. bytes32 constant METABOARD_DEPLOYED_CODEHASH = bytes32(0x60e0735a3406074fd8f85adb2813d0d7c346337ea4bcc6f2ef4eb25077a4933c); + + uint256 constant METABOARD_START_BLOCK_ARBITRUM = 431042729; + uint256 constant METABOARD_START_BLOCK_BASE = 42021282; + uint256 constant METABOARD_START_BLOCK_BASE_SEPOLIA = 38683088; + uint256 constant METABOARD_START_BLOCK_FLARE = 55347067; + uint256 constant METABOARD_START_BLOCK_POLYGON = 82855948; } diff --git a/subgraph/networks.json b/subgraph/networks.json index 321c3a11..712145e6 100644 --- a/subgraph/networks.json +++ b/subgraph/networks.json @@ -1,38 +1,32 @@ { "matic": { "metaboard0": { - "address": "0x23F77e7Bc935503e437166498D7D72f2Ea290E1f", - "startBlock": 55856789 + "address": "0xfb8437AeFBB8031064E274527C5fc08e30Ac6928", + "startBlock": 82855948 } }, "arbitrum-one": { "metaboard0": { - "address": "0x1eFd85E6C384fAD9B80C6D508E9098Eb91C4eD30", - "startBlock": 256572417 + "address": "0xfb8437AeFBB8031064E274527C5fc08e30Ac6928", + "startBlock": 431042729 } }, "base": { "metaboard0": { - "address": "0x59401C9302E79Eb8AC6aea659B8B3ae475715e86", - "startBlock": 18309350 + "address": "0xfb8437AeFBB8031064E274527C5fc08e30Ac6928", + "startBlock": 42021282 } }, - "mainnet": { + "base-sepolia": { "metaboard0": { - "address": "0x8F61d274aaB5D8CFD82dc266529EAe33020386a9", - "startBlock": 21041769 + "address": "0xfb8437AeFBB8031064E274527C5fc08e30Ac6928", + "startBlock": 38683088 } }, - "berachain-mainnet": { + "flare": { "metaboard0": { - "address": "0x8F61d274aaB5D8CFD82dc266529EAe33020386a9", - "startBlock": 3092258 - } - }, - "sonic": { - "metaboard0": { - "address": "0x8F61d274aaB5D8CFD82dc266529EAe33020386a9", - "startBlock": 17707513 + "address": "0xfb8437AeFBB8031064E274527C5fc08e30Ac6928", + "startBlock": 55347067 } } } diff --git a/subgraph/subgraph.yaml b/subgraph/subgraph.yaml index f7957ce5..27016f0d 100644 --- a/subgraph/subgraph.yaml +++ b/subgraph/subgraph.yaml @@ -6,7 +6,7 @@ dataSources: name: metaboard0 network: arbitrum-one source: - address: "0x017F5651eB8fa4048BBc17433149c6c035d391A6" + address: "0xfb8437AeFBB8031064E274527C5fc08e30Ac6928" abi: MetaBoard startBlock: 266485075 mapping: diff --git a/subgraph/tests/address.ts b/subgraph/tests/address.ts new file mode 100644 index 00000000..67abe7ef --- /dev/null +++ b/subgraph/tests/address.ts @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +import { Address } from "@graphprotocol/graph-ts"; + +export const CONTRACT_ADDRESS = Address.fromString("0xfb8437AeFBB8031064E274527C5fc08e30Ac6928"); diff --git a/subgraph/tests/utils.ts b/subgraph/tests/utils.ts index 85c4937d..dfdec5bd 100644 --- a/subgraph/tests/utils.ts +++ b/subgraph/tests/utils.ts @@ -1,7 +1,8 @@ -import { MetaV1_2 } from "../generated/metaboard0/MetaBoard"; // Update the path as per your file structure +import { MetaV1_2 } from "../generated/metaboard0/MetaBoard"; import { ethereum, Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; import { newMockEvent } from "matchstick-as"; import { handleMetaV1_2 } from "../src/metaBoard"; +import { CONTRACT_ADDRESS } from "./address"; export function createNewMetaV1Event(sender: string, subject: Bytes, meta: Bytes, transactionHash: string, transactionBlockNumber: number, transactionTimestamp: number): MetaV1_2 { @@ -32,4 +33,4 @@ export function handleNewMetaV1Events(events: MetaV1_2[]): void { }); } -export const CONTRACT_ADDRESS = Address.fromString("0x23F77e7Bc935503e437166498D7D72f2Ea290E1f"); +export { CONTRACT_ADDRESS } from "./address"; diff --git a/test/lib/deploy/LibMetaBoardDeploy.t.sol b/test/lib/deploy/LibMetaBoardDeploy.t.sol index e64ba24f..94a9d2d8 100644 --- a/test/lib/deploy/LibMetaBoardDeploy.t.sol +++ b/test/lib/deploy/LibMetaBoardDeploy.t.sol @@ -8,6 +8,9 @@ import {LibMetaBoardDeploy} from "src/lib/deploy/LibMetaBoardDeploy.sol"; import {MetaBoard} from "src/concrete/MetaBoard.sol"; contract LibMetaBoardDeployTest is Test { + /// Arbitrum Nitro genesis block. Archive RPCs can't serve blocks before this. + uint256 constant ARBITRUM_NITRO_GENESIS_BLOCK = 22207817; + function testDeployAddress() external { vm.createSelectFork(vm.envString("CI_FORK_ETH_RPC_URL")); @@ -55,4 +58,143 @@ contract LibMetaBoardDeployTest is Test { function testProdDeployPolygon() external { checkProdDeployment("CI_FORK_POLYGON_RPC_URL"); } + + function findStartBlock(string memory rpcEnvVar, uint256 searchFrom) internal returns (uint256) { + vm.createSelectFork(vm.envString(rpcEnvVar)); + return LibRainDeploy.findDeployBlock( + vm, + LibMetaBoardDeploy.METABOARD_DEPLOYED_ADDRESS, + LibMetaBoardDeploy.METABOARD_DEPLOYED_CODEHASH, + searchFrom + ); + } + + /// findDeployBlock binary searches via rollFork which hits RPC rate limits + /// in CI. Skipped there; the isStartBlock tests verify correctness cheaply. + /// Arbitrum is always skipped because Foundry's rollFork maps to L1 block + /// numbers, not L2. The Arbitrum start block was found via manual binary + /// search using eth_getCode RPC calls against L2 block numbers. + // function testStartBlockArbitrum() external { + // assertEq( + // findStartBlock("CI_FORK_ARB_RPC_URL", ARBITRUM_NITRO_GENESIS_BLOCK), + // LibMetaBoardDeploy.METABOARD_START_BLOCK_ARBITRUM + // ); + // } + + function testStartBlockBase() external { + vm.skip(vm.envOr("CI", false)); + assertEq(findStartBlock("CI_FORK_BASE_RPC_URL", 0), LibMetaBoardDeploy.METABOARD_START_BLOCK_BASE); + } + + function testStartBlockBaseSepolia() external { + vm.skip(vm.envOr("CI", false)); + assertEq( + findStartBlock("CI_FORK_BASE_SEPOLIA_RPC_URL", 0), LibMetaBoardDeploy.METABOARD_START_BLOCK_BASE_SEPOLIA + ); + } + + function testStartBlockFlare() external { + vm.skip(vm.envOr("CI", false)); + assertEq(findStartBlock("CI_FORK_FLARE_RPC_URL", 0), LibMetaBoardDeploy.METABOARD_START_BLOCK_FLARE); + } + + function testStartBlockPolygon() external { + vm.skip(vm.envOr("CI", false)); + assertEq(findStartBlock("CI_FORK_POLYGON_RPC_URL", 0), LibMetaBoardDeploy.METABOARD_START_BLOCK_POLYGON); + } + + function checkIsStartBlock(string memory rpcEnvVar, uint256 startBlock) internal { + vm.createSelectFork(vm.envString(rpcEnvVar)); + assertTrue( + LibRainDeploy.isStartBlock( + vm, + LibMetaBoardDeploy.METABOARD_DEPLOYED_ADDRESS, + LibMetaBoardDeploy.METABOARD_DEPLOYED_CODEHASH, + startBlock + ), + string.concat("not start block: ", rpcEnvVar) + ); + } + + function testIsStartBlockArbitrum() external { + checkIsStartBlock("CI_FORK_ARB_RPC_URL", LibMetaBoardDeploy.METABOARD_START_BLOCK_ARBITRUM); + } + + function testIsStartBlockBase() external { + checkIsStartBlock("CI_FORK_BASE_RPC_URL", LibMetaBoardDeploy.METABOARD_START_BLOCK_BASE); + } + + function testIsStartBlockBaseSepolia() external { + checkIsStartBlock("CI_FORK_BASE_SEPOLIA_RPC_URL", LibMetaBoardDeploy.METABOARD_START_BLOCK_BASE_SEPOLIA); + } + + function testIsStartBlockFlare() external { + checkIsStartBlock("CI_FORK_FLARE_RPC_URL", LibMetaBoardDeploy.METABOARD_START_BLOCK_FLARE); + } + + function testIsStartBlockPolygon() external { + checkIsStartBlock("CI_FORK_POLYGON_RPC_URL", LibMetaBoardDeploy.METABOARD_START_BLOCK_POLYGON); + } + + function testSubgraphYamlAddress() external { + string[] memory inputs = new string[](3); + inputs[0] = "yq"; + inputs[1] = ".dataSources[0].source.address"; + inputs[2] = "subgraph/subgraph.yaml"; + bytes memory result = vm.ffi(inputs); + address addr = address(bytes20(result)); + assertEq(addr, LibMetaBoardDeploy.METABOARD_DEPLOYED_ADDRESS, "subgraph.yaml address mismatch"); + } + + function testSubgraphTestAddressTs() external { + string[] memory inputs = new string[](4); + inputs[0] = "grep"; + inputs[1] = "-oP"; + inputs[2] = "0x[0-9a-fA-F]{40}"; + inputs[3] = "subgraph/tests/address.ts"; + bytes memory result = vm.ffi(inputs); + address addr = address(bytes20(result)); + assertEq(addr, LibMetaBoardDeploy.METABOARD_DEPLOYED_ADDRESS, "subgraph/tests/address.ts address mismatch"); + } + + function testNetworksJsonAddresses() external view { + string memory json = vm.readFile("subgraph/networks.json"); + string[] memory networks = vm.parseJsonKeys(json, "$"); + for (uint256 i = 0; i < networks.length; i++) { + string memory path = string.concat(".", networks[i], ".metaboard0.address"); + address addr = vm.parseJsonAddress(json, path); + assertEq( + addr, + LibMetaBoardDeploy.METABOARD_DEPLOYED_ADDRESS, + string.concat("networks.json address mismatch: ", networks[i]) + ); + } + } + + function checkNetworksJsonStartBlock(string memory networkKey, uint256 expectedStartBlock) internal view { + string memory json = vm.readFile("subgraph/networks.json"); + string memory path = string.concat(".", networkKey, ".metaboard0.startBlock"); + uint256 startBlock = vm.parseJsonUint(json, path); + assertEq(startBlock, expectedStartBlock, string.concat("networks.json startBlock mismatch: ", networkKey)); + } + + function testNetworksJsonStartBlockArbitrum() external view { + checkNetworksJsonStartBlock("arbitrum-one", LibMetaBoardDeploy.METABOARD_START_BLOCK_ARBITRUM); + } + + function testNetworksJsonStartBlockBase() external view { + checkNetworksJsonStartBlock("base", LibMetaBoardDeploy.METABOARD_START_BLOCK_BASE); + } + + function testNetworksJsonStartBlockBaseSepolia() external view { + checkNetworksJsonStartBlock("base-sepolia", LibMetaBoardDeploy.METABOARD_START_BLOCK_BASE_SEPOLIA); + } + + function testNetworksJsonStartBlockFlare() external view { + checkNetworksJsonStartBlock("flare", LibMetaBoardDeploy.METABOARD_START_BLOCK_FLARE); + } + + function testNetworksJsonStartBlockPolygon() external view { + checkNetworksJsonStartBlock("matic", LibMetaBoardDeploy.METABOARD_START_BLOCK_POLYGON); + } }