diff --git a/CLAUDE.md b/CLAUDE.md index 7dd31c4..a2bd313 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1,14 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when working with +code in this repository. ## Project -rain.verify is a Solidity smart contract library for trust-minimized on-chain identity verification. It implements role-based (APPROVER, BANNER, REMOVER) verification with status lifecycle: NIL -> ADDED -> APPROVED/BANNED, with batch operations and callback hooks. +rain.verify is a Solidity smart contract library for trust-minimized on-chain +identity verification. It implements role-based (APPROVER, BANNER, REMOVER) +verification with status lifecycle: NIL -> ADDED -> APPROVED/BANNED, with batch +operations and callback hooks. ## Build & Test @@ -19,25 +23,43 @@ rainix-sol-legal # License compliance check Run `rainix-sol-prelude` before first test run, then: -Single test: `forge test --match-test testFunctionName` -Single contract: `forge test --match-contract ContractName` +Single test: `forge test --match-test testFunctionName` Single contract: +`forge test --match-contract ContractName` ## Architecture -- **Verify.sol** (`src/concrete/`) - Core contract. Uses OpenZeppelin AccessControl for role management. Stores per-account `VerifyStatus` with timestamps for each status transition. Supports batch approve/ban/remove with evidence. -- **AutoApprove.sol** (`src/concrete/`) - Callback that integrates with Rain Interpreter V4 to evaluate custom approval logic via bytecode. -- **VerifyCallback.sol** (`src/abstract/`) - Abstract base for verification callbacks (afterAdd/afterApprove/afterBan/afterRemove). OwnableUpgradeable. -- **LibEvidence.sol**, **LibVerifyStatus.sol** (`src/lib/`) - Assembly-optimized helper libraries. -- Interfaces live in the `rain.verify.interface` submodule under `lib/`. +- **Verify.sol** (`src/concrete/`) - Core contract. Uses OpenZeppelin + AccessControl for role management. Stores per-account `VerifyStatus` with + timestamps for each status transition. Supports batch approve/ban/remove with + evidence. +- **AutoApprove.sol** (`src/concrete/`) - Callback that integrates with Rain + Interpreter V4 to evaluate custom approval logic via bytecode. +- **VerifyCallback.sol** (`src/abstract/`) - Abstract base for verification + callbacks (afterAdd/afterApprove/afterBan/afterRemove). OwnableUpgradeable. +- **LibEvidence.sol**, **LibVerifyStatus.sol** (`src/lib/`) - Assembly-optimized + helper libraries. +- `IVerifyV1`, `IVerifyCallbackV1`, `Evidence` and `VerifyStatus` come from the + `rain-verify-interface` dependency, imported as + `rain-verify-interface-0.1.0/src/interface/...`. ## Dependencies -Managed as git submodules in `lib/`. Key deps: `rain.verify.interface`, `rain.interpreter.interface`, `rain.factory` (ICloneableV2 proxy pattern), `openzeppelin-contracts-upgradeable`. +Managed by soldeer. Declared in `foundry.toml` under `[dependencies]`, pinned by +`soldeer.lock`, and installed into the gitignored `dependencies/` directory +(`libs = ["dependencies"]`) by `forge soldeer install`. Imports carry the +version in the path prefix, per `remappings.txt` — e.g. +`rain-factory-0.1.1/src/...`. + +Key deps: `rain-verify-interface` 0.1.0, `rain-interpreter-interface` 0.1.0, +`rain-factory` 0.1.1 (ICloneableV2 proxy pattern), +`@openzeppelin-contracts-upgradeable` 5.6.1. ## Compiler Settings -Solidity 0.8.25, Paris EVM (pre-PUSH0 for cross-chain compatibility), 100k optimizer runs, no CBOR metadata. +Solidity 0.8.25, Paris EVM (pre-PUSH0 for cross-chain compatibility), 100k +optimizer runs, no CBOR metadata. ## License -LicenseRef-DCL-1.0 (Decentralized Community License). All source files require SPDX headers. +LicenseRef-DCL-1.0 (Decentralized Community License). All source files require +SPDX headers. diff --git a/REUSE.toml b/REUSE.toml index 21f181e..3e79ef2 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -2,24 +2,20 @@ version = 1 [[annotations]] path = [ - ".envrc", - ".gas-snapshot", - ".github/**/", - ".gitignore", - ".gitmodules", - "README.md", - "CLAUDE.md", - "flake.lock", - "flake.nix", - "foundry.toml", - "foundry.lock", - "slither.config.json", - "REUSE.toml", - "soldeer.lock", - "remappings.txt", - ".coderabbit.yaml", - ".env.example", - "audit/**/", + ".gas-snapshot", + ".github/**/", + ".gitignore", + "README.md", + "CLAUDE.md", + "flake.lock", + "flake.nix", + "foundry.toml", + "slither.config.json", + "REUSE.toml", + "soldeer.lock", + "remappings.txt", + ".coderabbit.yaml", + ".env.example", ] SPDX-FileCopyrightText = "Copyright (c) 2020 Rain Open Source Software Ltd" SPDX-License-Identifier = "LicenseRef-DCL-1.0" diff --git a/test/concrete/AutoApprove.t.sol b/test/concrete/AutoApprove.t.sol index 4f2bfad..3cd726f 100644 --- a/test/concrete/AutoApprove.t.sol +++ b/test/concrete/AutoApprove.t.sol @@ -19,61 +19,16 @@ import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "rain-factory-0.1.1/src/interf import { IInterpreterV4, StackItem, - EvalV4, SourceIndexV2, DEFAULT_STATE_NAMESPACE } from "rain-interpreter-interface-0.1.0/src/interface/IInterpreterV4.sol"; import {IInterpreterStoreV3} from "rain-interpreter-interface-0.1.0/src/interface/IInterpreterStoreV3.sol"; import {EvaluableV4} from "rain-interpreter-interface-0.1.0/src/interface/IInterpreterCallerV4.sol"; -import { - StateNamespace, - FullyQualifiedNamespace -} from "rain-interpreter-interface-0.1.0/src/interface/deprecated/v2/IInterpreterV3.sol"; +import {MockInterpreterV4} from "../mock/MockInterpreterV4.sol"; +import {MockInterpreterStoreV3} from "../mock/MockInterpreterStoreV3.sol"; import {LibVerifyStatus} from "../../src/lib/LibVerifyStatus.sol"; import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; -/// @dev Mock interpreter that returns a configurable stack value from `eval4`. -/// Does NOT inherit `IInterpreterV4` because the interface declares `calldata` -/// return types which cannot be produced from Solidity storage/memory. The ABI -/// encoding is identical so the caller (AutoApprove) can decode the response -/// through the interface pointer without issue. -contract MockInterpreterV4 { - /// @dev The stack to return from `eval4`. - StackItem[] public sStack; - - /// @dev Set the stack that `eval4` will return. - function setStack(StackItem[] memory stack) external { - delete sStack; - for (uint256 i = 0; i < stack.length; i++) { - sStack.push(stack[i]); - } - } - - /// @dev Convenience to set a single-element stack. - function setReturnValue(StackItem value) external { - delete sStack; - sStack.push(value); - } - - /// @dev Matches the `eval4` selector from `IInterpreterV4`. - function eval4(EvalV4 calldata) external view returns (StackItem[] memory stack, bytes32[] memory kvs) { - stack = sStack; - kvs = new bytes32[](0); - } -} - -/// @dev Mock store that implements all required functions of -/// `IInterpreterStoreV3` as no-ops. -contract MockInterpreterStoreV3 is IInterpreterStoreV3 { - /// @inheritdoc IInterpreterStoreV3 - function set(StateNamespace, bytes32[] calldata) external override {} - - /// @inheritdoc IInterpreterStoreV3 - function get(FullyQualifiedNamespace, bytes32) external pure override returns (bytes32) { - return bytes32(0); - } -} - /// @title AutoApproveTest /// @notice Tests for the `AutoApprove` callback contract, covering /// construction, auto-approval logic, denial, evidence-length filtering, diff --git a/test/concrete/Verify.callback.t.sol b/test/concrete/Verify.callback.t.sol index 3273a3a..7eafb98 100644 --- a/test/concrete/Verify.callback.t.sol +++ b/test/concrete/Verify.callback.t.sol @@ -12,105 +12,11 @@ import { VERIFY_STATUS_APPROVED, VERIFY_STATUS_BANNED } from "rain-verify-interface-0.1.0/src/interface/IVerifyV1.sol"; -import {IVerifyCallbackV1} from "rain-verify-interface-0.1.0/src/interface/IVerifyCallbackV1.sol"; +import {MockCallback} from "../mock/MockCallback.sol"; import {ICloneableV2} from "rain-factory-0.1.1/src/interface/ICloneableV2.sol"; import {LibVerifyStatus} from "../../src/lib/LibVerifyStatus.sol"; import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; -/// @dev Tracks which callback hooks were called and with what arguments. -/// Implements `IVerifyCallbackV1` directly (no access control) so the Verify -/// contract can call it without ownership setup. Suitable only for testing. -contract MockCallback is IVerifyCallbackV1 { - /// @dev Incremented each time `afterAdd` is called. - uint256 public afterAddCalls; - /// @dev Incremented each time `afterApprove` is called. - uint256 public afterApproveCalls; - /// @dev Incremented each time `afterBan` is called. - uint256 public afterBanCalls; - /// @dev Incremented each time `afterRemove` is called. - uint256 public afterRemoveCalls; - - /// @dev The `adder` from the most recent `afterAdd` call. - address public lastAddAdder; - /// @dev The evidences from the most recent `afterAdd` call. - Evidence[] public lastAddEvidences; - - /// @dev The `approver` from the most recent `afterApprove` call. - address public lastApproveApprover; - /// @dev The evidences from the most recent `afterApprove` call. - Evidence[] public lastApproveEvidences; - - /// @dev The `banner` from the most recent `afterBan` call. - address public lastBanBanner; - /// @dev The evidences from the most recent `afterBan` call. - Evidence[] public lastBanEvidences; - - /// @dev The `remover` from the most recent `afterRemove` call. - address public lastRemoveRemover; - /// @dev The evidences from the most recent `afterRemove` call. - Evidence[] public lastRemoveEvidences; - - /// @inheritdoc IVerifyCallbackV1 - function afterAdd(address adder, Evidence[] calldata evidences) external override { - afterAddCalls++; - lastAddAdder = adder; - delete lastAddEvidences; - for (uint256 i = 0; i < evidences.length; i++) { - lastAddEvidences.push(evidences[i]); - } - } - - /// @inheritdoc IVerifyCallbackV1 - function afterApprove(address approver, Evidence[] calldata evidences) external override { - afterApproveCalls++; - lastApproveApprover = approver; - delete lastApproveEvidences; - for (uint256 i = 0; i < evidences.length; i++) { - lastApproveEvidences.push(evidences[i]); - } - } - - /// @inheritdoc IVerifyCallbackV1 - function afterBan(address banner, Evidence[] calldata evidences) external override { - afterBanCalls++; - lastBanBanner = banner; - delete lastBanEvidences; - for (uint256 i = 0; i < evidences.length; i++) { - lastBanEvidences.push(evidences[i]); - } - } - - /// @inheritdoc IVerifyCallbackV1 - function afterRemove(address remover, Evidence[] calldata evidences) external override { - afterRemoveCalls++; - lastRemoveRemover = remover; - delete lastRemoveEvidences; - for (uint256 i = 0; i < evidences.length; i++) { - lastRemoveEvidences.push(evidences[i]); - } - } - - /// @dev Returns the number of evidences stored from the last `afterAdd`. - function lastAddEvidencesLength() external view returns (uint256) { - return lastAddEvidences.length; - } - - /// @dev Returns the number of evidences stored from the last `afterApprove`. - function lastApproveEvidencesLength() external view returns (uint256) { - return lastApproveEvidences.length; - } - - /// @dev Returns the number of evidences stored from the last `afterBan`. - function lastBanEvidencesLength() external view returns (uint256) { - return lastBanEvidences.length; - } - - /// @dev Returns the number of evidences stored from the last `afterRemove`. - function lastRemoveEvidencesLength() external view returns (uint256) { - return lastRemoveEvidences.length; - } -} - /// @title VerifyCallbackTest /// @notice Tests that callback hooks on `IVerifyCallbackV1` are invoked /// correctly after `add`, `approve`, `ban`, and `remove` actions in the diff --git a/test/lib/LibEvidence.t.sol b/test/lib/LibEvidence.t.sol index 875f662..e3efded 100644 --- a/test/lib/LibEvidence.t.sol +++ b/test/lib/LibEvidence.t.sol @@ -4,54 +4,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {Evidence} from "rain-verify-interface-0.1.0/src/interface/IVerifyV1.sol"; -import {LibEvidence} from "../../src/lib/LibEvidence.sol"; - -/// @title LibEvidenceHarness -/// @notice Exposes `LibEvidence` internal functions as external calls so they -/// can be exercised from Foundry tests. -contract LibEvidenceHarness { - using LibEvidence for uint256[]; - - /// Wraps `LibEvidence._updateEvidenceRef`. - function updateEvidenceRef(uint256[] memory refs, Evidence memory evidence, uint256 refsIndex) - external - pure - returns (uint256[] memory) - { - refs._updateEvidenceRef(evidence, refsIndex); - return refs; - } - - /// Wraps `LibEvidence.asEvidences`. - function asEvidences(uint256[] memory refs) external pure returns (Evidence[] memory) { - return refs.asEvidences(); - } - - /// Convenience: update a ref then immediately convert to `Evidence[]`. - function updateAndConvert(uint256[] memory refs, Evidence memory evidence, uint256 refsIndex) - external - pure - returns (Evidence[] memory) - { - refs._updateEvidenceRef(evidence, refsIndex); - return refs.asEvidences(); - } - - /// Batch update three refs then convert to `Evidence[]` in a single call. - /// Required because `_updateEvidenceRef` stores memory pointers that don't - /// survive ABI encoding across external call boundaries. - function updateThreeAndConvert(Evidence memory e0, Evidence memory e1, Evidence memory e2) - external - pure - returns (Evidence[] memory) - { - uint256[] memory refs = new uint256[](3); - refs._updateEvidenceRef(e0, 0); - refs._updateEvidenceRef(e1, 1); - refs._updateEvidenceRef(e2, 2); - return refs.asEvidences(); - } -} +import {LibEvidenceHarness} from "./LibEvidenceHarness.sol"; /// @title LibEvidenceTest /// @notice Foundry fuzz tests for `LibEvidence._updateEvidenceRef` and diff --git a/test/lib/LibEvidenceHarness.sol b/test/lib/LibEvidenceHarness.sol new file mode 100644 index 0000000..ffd87da --- /dev/null +++ b/test/lib/LibEvidenceHarness.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Evidence} from "rain-verify-interface-0.1.0/src/interface/IVerifyV1.sol"; +import {LibEvidence} from "../../src/lib/LibEvidence.sol"; + +/// @title LibEvidenceHarness +/// @notice Exposes `LibEvidence` internal functions as external calls so they +/// can be exercised from Foundry tests. +contract LibEvidenceHarness { + using LibEvidence for uint256[]; + + /// Wraps `LibEvidence._updateEvidenceRef`. + function updateEvidenceRef(uint256[] memory refs, Evidence memory evidence, uint256 refsIndex) + external + pure + returns (uint256[] memory) + { + refs._updateEvidenceRef(evidence, refsIndex); + return refs; + } + + /// Wraps `LibEvidence.asEvidences`. + function asEvidences(uint256[] memory refs) external pure returns (Evidence[] memory) { + return refs.asEvidences(); + } + + /// Convenience: update a ref then immediately convert to `Evidence[]`. + function updateAndConvert(uint256[] memory refs, Evidence memory evidence, uint256 refsIndex) + external + pure + returns (Evidence[] memory) + { + refs._updateEvidenceRef(evidence, refsIndex); + return refs.asEvidences(); + } + + /// Batch update three refs then convert to `Evidence[]` in a single call. + /// Required because `_updateEvidenceRef` stores memory pointers that don't + /// survive ABI encoding across external call boundaries. + function updateThreeAndConvert(Evidence memory e0, Evidence memory e1, Evidence memory e2) + external + pure + returns (Evidence[] memory) + { + uint256[] memory refs = new uint256[](3); + refs._updateEvidenceRef(e0, 0); + refs._updateEvidenceRef(e1, 1); + refs._updateEvidenceRef(e2, 2); + return refs.asEvidences(); + } +} diff --git a/test/mock/MockCallback.sol b/test/mock/MockCallback.sol new file mode 100644 index 0000000..4c4df95 --- /dev/null +++ b/test/mock/MockCallback.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Evidence} from "rain-verify-interface-0.1.0/src/interface/IVerifyV1.sol"; +import {IVerifyCallbackV1} from "rain-verify-interface-0.1.0/src/interface/IVerifyCallbackV1.sol"; + +/// @dev Tracks which callback hooks were called and with what arguments. +/// Implements `IVerifyCallbackV1` directly (no access control) so the Verify +/// contract can call it without ownership setup. Suitable only for testing. +contract MockCallback is IVerifyCallbackV1 { + /// @dev Incremented each time `afterAdd` is called. + uint256 public afterAddCalls; + /// @dev Incremented each time `afterApprove` is called. + uint256 public afterApproveCalls; + /// @dev Incremented each time `afterBan` is called. + uint256 public afterBanCalls; + /// @dev Incremented each time `afterRemove` is called. + uint256 public afterRemoveCalls; + + /// @dev The `adder` from the most recent `afterAdd` call. + address public lastAddAdder; + /// @dev The evidences from the most recent `afterAdd` call. + Evidence[] public lastAddEvidences; + + /// @dev The `approver` from the most recent `afterApprove` call. + address public lastApproveApprover; + /// @dev The evidences from the most recent `afterApprove` call. + Evidence[] public lastApproveEvidences; + + /// @dev The `banner` from the most recent `afterBan` call. + address public lastBanBanner; + /// @dev The evidences from the most recent `afterBan` call. + Evidence[] public lastBanEvidences; + + /// @dev The `remover` from the most recent `afterRemove` call. + address public lastRemoveRemover; + /// @dev The evidences from the most recent `afterRemove` call. + Evidence[] public lastRemoveEvidences; + + /// @inheritdoc IVerifyCallbackV1 + function afterAdd(address adder, Evidence[] calldata evidences) external override { + afterAddCalls++; + lastAddAdder = adder; + delete lastAddEvidences; + for (uint256 i = 0; i < evidences.length; i++) { + lastAddEvidences.push(evidences[i]); + } + } + + /// @inheritdoc IVerifyCallbackV1 + function afterApprove(address approver, Evidence[] calldata evidences) external override { + afterApproveCalls++; + lastApproveApprover = approver; + delete lastApproveEvidences; + for (uint256 i = 0; i < evidences.length; i++) { + lastApproveEvidences.push(evidences[i]); + } + } + + /// @inheritdoc IVerifyCallbackV1 + function afterBan(address banner, Evidence[] calldata evidences) external override { + afterBanCalls++; + lastBanBanner = banner; + delete lastBanEvidences; + for (uint256 i = 0; i < evidences.length; i++) { + lastBanEvidences.push(evidences[i]); + } + } + + /// @inheritdoc IVerifyCallbackV1 + function afterRemove(address remover, Evidence[] calldata evidences) external override { + afterRemoveCalls++; + lastRemoveRemover = remover; + delete lastRemoveEvidences; + for (uint256 i = 0; i < evidences.length; i++) { + lastRemoveEvidences.push(evidences[i]); + } + } + + /// @dev Returns the number of evidences stored from the last `afterAdd`. + function lastAddEvidencesLength() external view returns (uint256) { + return lastAddEvidences.length; + } + + /// @dev Returns the number of evidences stored from the last `afterApprove`. + function lastApproveEvidencesLength() external view returns (uint256) { + return lastApproveEvidences.length; + } + + /// @dev Returns the number of evidences stored from the last `afterBan`. + function lastBanEvidencesLength() external view returns (uint256) { + return lastBanEvidences.length; + } + + /// @dev Returns the number of evidences stored from the last `afterRemove`. + function lastRemoveEvidencesLength() external view returns (uint256) { + return lastRemoveEvidences.length; + } +} diff --git a/test/mock/MockInterpreterStoreV3.sol b/test/mock/MockInterpreterStoreV3.sol new file mode 100644 index 0000000..c0f2d87 --- /dev/null +++ b/test/mock/MockInterpreterStoreV3.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {IInterpreterStoreV3} from "rain-interpreter-interface-0.1.0/src/interface/IInterpreterStoreV3.sol"; +import { + StateNamespace, + FullyQualifiedNamespace +} from "rain-interpreter-interface-0.1.0/src/interface/deprecated/v2/IInterpreterV3.sol"; + +/// @dev Mock store that implements all required functions of +/// `IInterpreterStoreV3` as no-ops. +contract MockInterpreterStoreV3 is IInterpreterStoreV3 { + /// @inheritdoc IInterpreterStoreV3 + function set(StateNamespace, bytes32[] calldata) external override {} + + /// @inheritdoc IInterpreterStoreV3 + function get(FullyQualifiedNamespace, bytes32) external pure override returns (bytes32) { + return bytes32(0); + } +} diff --git a/test/mock/MockInterpreterV4.sol b/test/mock/MockInterpreterV4.sol new file mode 100644 index 0000000..b1629b1 --- /dev/null +++ b/test/mock/MockInterpreterV4.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {StackItem, EvalV4} from "rain-interpreter-interface-0.1.0/src/interface/IInterpreterV4.sol"; + +/// @dev Mock interpreter that returns a configurable stack value from `eval4`. +/// Does NOT inherit `IInterpreterV4` because the interface declares `calldata` +/// return types which cannot be produced from Solidity storage/memory. The ABI +/// encoding is identical so the caller (AutoApprove) can decode the response +/// through the interface pointer without issue. +contract MockInterpreterV4 { + /// @dev The stack to return from `eval4`. + StackItem[] public sStack; + + /// @dev Set the stack that `eval4` will return. + function setStack(StackItem[] memory stack) external { + delete sStack; + for (uint256 i = 0; i < stack.length; i++) { + sStack.push(stack[i]); + } + } + + /// @dev Convenience to set a single-element stack. + function setReturnValue(StackItem value) external { + delete sStack; + sStack.push(value); + } + + /// @dev Matches the `eval4` selector from `IInterpreterV4`. + function eval4(EvalV4 calldata) external view returns (StackItem[] memory stack, bytes32[] memory kvs) { + stack = sStack; + kvs = new bytes32[](0); + } +}