diff --git a/CLAUDE.md b/CLAUDE.md index 7bab26b..534af79 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,11 +52,11 @@ make deploy-idcard # IdCard contract to Base | Contract | Both Chains | |---|---| | Semaphore | `0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D` | -| CredentialRegistry | `0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db` | -| DefaultScorer | `0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c` | -| ScorerFactory | `0xAa03996D720C162Fdff246E1D3CEecc792986750` | +| CredentialRegistry | `0x17a22f130d4e1c4ba5C20a679a5a29F227083A62` | +| DefaultScorer | `0x6791B588dAdeb4323bc1C3d987130bC13cBe3625` | +| ScorerFactory | `0x016bC46169533a8d3284c5D8DD590C91783C8C06` | -Owner: `0x6F0CDcd334BA91A5E221582665Cce0431aD4Fc0b` +Owner: `0x677112864ED447866f8D461ABe284E5e907bB4F8` Trusted verifier (Sepolia): `0x3c50f7055D804b51e506Bc1EA7D082cB1548376C` Trusted verifier (mainnet): `0x9186aA65288bFfa67fB58255AeeaFfc4515535d9` diff --git a/README.md b/README.md index 070008c..588fd99 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,18 @@ Contract addresses are identical on both chains (same deployer, same nonce). | Contract | Address | |---|---| | Semaphore | [`0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D`](https://basescan.org/address/0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D) | -| CredentialRegistry | [`0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db`](https://basescan.org/address/0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db) | -| DefaultScorer | [`0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c`](https://basescan.org/address/0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c) | -| ScorerFactory | [`0xAa03996D720C162Fdff246E1D3CEecc792986750`](https://basescan.org/address/0xAa03996D720C162Fdff246E1D3CEecc792986750) | +| CredentialRegistry | [`0x17a22f130d4e1c4ba5C20a679a5a29F227083A62`](https://basescan.org/address/0x17a22f130d4e1c4ba5C20a679a5a29F227083A62) | +| DefaultScorer | [`0x6791B588dAdeb4323bc1C3d987130bC13cBe3625`](https://basescan.org/address/0x6791B588dAdeb4323bc1C3d987130bC13cBe3625) | +| ScorerFactory | [`0x016bC46169533a8d3284c5D8DD590C91783C8C06`](https://basescan.org/address/0x016bC46169533a8d3284c5D8DD590C91783C8C06) | ### Base Sepolia (chain ID 84532) | Contract | Address | |---|---| | Semaphore | [`0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D`](https://sepolia.basescan.org/address/0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D) | -| CredentialRegistry | [`0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db`](https://sepolia.basescan.org/address/0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db) | -| DefaultScorer | [`0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c`](https://sepolia.basescan.org/address/0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c) | -| ScorerFactory | [`0xAa03996D720C162Fdff246E1D3CEecc792986750`](https://sepolia.basescan.org/address/0xAa03996D720C162Fdff246E1D3CEecc792986750) | +| CredentialRegistry | [`0x17a22f130d4e1c4ba5C20a679a5a29F227083A62`](https://sepolia.basescan.org/address/0x17a22f130d4e1c4ba5C20a679a5a29F227083A62) | +| DefaultScorer | [`0x6791B588dAdeb4323bc1C3d987130bC13cBe3625`](https://sepolia.basescan.org/address/0x6791B588dAdeb4323bc1C3d987130bC13cBe3625) | +| ScorerFactory | [`0x016bC46169533a8d3284c5D8DD590C91783C8C06`](https://sepolia.basescan.org/address/0x016bC46169533a8d3284c5D8DD590C91783C8C06) | ### Credential Groups diff --git a/contracts/interfaces/Events.sol b/contracts/interfaces/Events.sol index 7d27d10..f7689b5 100644 --- a/contracts/interfaces/Events.sol +++ b/contracts/interfaces/Events.sol @@ -136,6 +136,10 @@ event AttestationValidityDurationSet(uint256 duration); /// @param status The new credential group status. event CredentialGroupStatusChanged(uint256 indexed credentialGroupId, ICredentialRegistry.CredentialGroupStatus status); +/// @notice Emitted when the future attestation buffer is updated. +/// @param buffer The new buffer duration in seconds. +event FutureAttestationBufferSet(uint256 buffer); + /// @notice Emitted when the registry-level default Merkle tree duration is updated. /// @param duration The new default duration in seconds. event DefaultMerkleTreeDurationSet(uint256 indexed duration); diff --git a/contracts/interfaces/ICredentialRegistry.sol b/contracts/interfaces/ICredentialRegistry.sol index e463c5f..e2c7faf 100644 --- a/contracts/interfaces/ICredentialRegistry.sol +++ b/contracts/interfaces/ICredentialRegistry.sol @@ -287,6 +287,10 @@ interface ICredentialRegistry { /// @param verifier_ The verifier address to remove. function removeTrustedVerifier(address verifier_) external; + /// @notice Updates the forward-tolerance buffer for future attestation timestamps. + /// @param buffer_ New buffer in seconds (0 to disable tolerance). + function setFutureAttestationBuffer(uint256 buffer_) external; + /// @notice Updates the registry-level default Merkle tree duration for new Semaphore groups. /// @param duration_ New default Merkle tree duration in seconds. function setDefaultMerkleTreeDuration(uint256 duration_) external; diff --git a/contracts/registry/base/AttestationVerifier.sol b/contracts/registry/base/AttestationVerifier.sol index 7d4ee63..6cfc4a4 100644 --- a/contracts/registry/base/AttestationVerifier.sol +++ b/contracts/registry/base/AttestationVerifier.sol @@ -30,7 +30,7 @@ abstract contract AttestationVerifier is RegistryStorage { if (apps[attestation_.appId].status != AppStatus.ACTIVE) revert AppNotActive(); if (attestation_.registry != address(this)) revert WrongRegistryAddress(); if (attestation_.chainId != block.chainid) revert WrongChain(); - if (attestation_.issuedAt > block.timestamp) revert FutureAttestation(); + if (attestation_.issuedAt > block.timestamp + futureAttestationBuffer) revert FutureAttestation(); if (block.timestamp > attestation_.issuedAt + attestationValidityDuration) revert AttestationExpired(); signer = keccak256(abi.encode(attestation_)).toEthSignedMessageHash().recover(v, r, s); diff --git a/contracts/registry/base/RegistryAdmin.sol b/contracts/registry/base/RegistryAdmin.sol index cd635a5..a216e61 100644 --- a/contracts/registry/base/RegistryAdmin.sol +++ b/contracts/registry/base/RegistryAdmin.sol @@ -72,6 +72,13 @@ abstract contract RegistryAdmin is RegistryStorage { emit AttestationValidityDurationSet(duration_); } + /// @notice Updates the forward-tolerance buffer for future attestation timestamps. + /// @param buffer_ New buffer in seconds (0 to disable tolerance). + function setFutureAttestationBuffer(uint256 buffer_) public onlyOwner { + futureAttestationBuffer = buffer_; + emit FutureAttestationBufferSet(buffer_); + } + /// @notice Updates the registry-level default Merkle tree duration for new Semaphore groups. /// @dev Does not propagate to existing groups. Only affects groups created after this call. /// @param duration_ New duration in seconds (must be > 0). diff --git a/contracts/registry/base/RegistryStorage.sol b/contracts/registry/base/RegistryStorage.sol index df48d3c..9063672 100644 --- a/contracts/registry/base/RegistryStorage.sol +++ b/contracts/registry/base/RegistryStorage.sol @@ -46,6 +46,11 @@ abstract contract RegistryStorage is ICredentialRegistry, Ownable2Step, Pausable /// @notice Maximum age (in seconds) an attestation is accepted. Default 30 minutes. uint256 public attestationValidityDuration = 30 minutes; + /// @notice Forward-tolerance buffer (in seconds) for attestation issuedAt timestamps. + /// On L2s a sequencer's block.timestamp can lag behind real-world time, + /// causing valid attestations to be rejected as "future". Default 10 minutes. + uint256 public futureAttestationBuffer = 10 minutes; + /// @notice Array of all registered credential group IDs (for enumeration). uint256[] public credentialGroupIds; diff --git a/docs/app-manager-specs.md b/docs/app-manager-specs.md index 82b8ce7..7c5af8b 100644 --- a/docs/app-manager-specs.md +++ b/docs/app-manager-specs.md @@ -14,7 +14,7 @@ A web dashboard for third-party app developers to self-manage their BringID inte All interactions go to two contracts on Base (mainnet 8453 / Sepolia 84532): -### CredentialRegistry (`0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db`) +### CredentialRegistry (`0x17a22f130d4e1c4ba5C20a679a5a29F227083A62`) | Function | Access | Description | |---|---|---| @@ -31,7 +31,7 @@ All interactions go to two contracts on Base (mainnet 8453 / Sepolia 84532): | `credentialGroups(uint256 id)` | View | Returns `(status, validityDuration, familyId)`. | | `getCredentialGroupIds()` | View | Returns all registered credential group IDs. | -### DefaultScorer (`0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c`) +### DefaultScorer (`0x6791B588dAdeb4323bc1C3d987130bC13cBe3625`) Read-only from the dashboard's perspective (only BringID owner can write): @@ -41,7 +41,7 @@ Read-only from the dashboard's perspective (only BringID owner can write): | `getScores(uint256[] credentialGroupIds)` | View | Scores for multiple groups. | | `getAllScores()` | View | All group IDs + scores. | -### ScorerFactory (`0xAa03996D720C162Fdff246E1D3CEecc792986750`) +### ScorerFactory (`0x016bC46169533a8d3284c5D8DD590C91783C8C06`) Deploys DefaultScorer instances owned by the caller. Same address on both chains. diff --git a/docs/fetching-scores.md b/docs/fetching-scores.md index c9d71f7..17812fa 100644 --- a/docs/fetching-scores.md +++ b/docs/fetching-scores.md @@ -2,7 +2,7 @@ The `DefaultScorer` contract stores global scores for each credential group. Scores can be read on-chain in a single call — no iteration or multicall needed. -**DefaultScorer address (Base mainnet & Sepolia):** `0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c` +**DefaultScorer address (Base mainnet & Sepolia):** `0x6791B588dAdeb4323bc1C3d987130bC13cBe3625` ## Get All Scores @@ -11,7 +11,7 @@ The `DefaultScorer` contract stores global scores for each credential group. Sco ### cast (Foundry) ```bash -cast call 0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c \ +cast call 0x6791B588dAdeb4323bc1C3d987130bC13cBe3625 \ "getAllScores()(uint256[],uint256[])" \ --rpc-url $BASE_RPC_URL ``` @@ -20,7 +20,7 @@ cast call 0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c \ ```js const scorer = new ethers.Contract( - "0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c", + "0x6791B588dAdeb4323bc1C3d987130bC13cBe3625", ["function getAllScores() view returns (uint256[], uint256[])"], provider ); @@ -33,7 +33,7 @@ const [groupIds, scores] = await scorer.getAllScores(); ```js const [groupIds, scores] = await publicClient.readContract({ - address: "0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c", + address: "0x6791B588dAdeb4323bc1C3d987130bC13cBe3625", abi: [{ name: "getAllScores", type: "function", diff --git a/docs/integration-test/test-verifier-prompt.md b/docs/integration-test/test-verifier-prompt.md new file mode 100644 index 0000000..a72307a --- /dev/null +++ b/docs/integration-test/test-verifier-prompt.md @@ -0,0 +1,115 @@ +# Task: Test the TLSN Verifier `/verify/oauth` endpoint + +Write and run a Node.js script (using ethers v6 which is already installed in this repo) to test the verifier's `/verify/oauth` endpoint running at `http://localhost:3000`. + +## What the endpoint does + +It accepts an OAuth credential message + signature, validates it, and returns a signed attestation. The server is running in **dev mode**, which means signer validation is skipped — but the signature must still be a valid ECDSA signature over the correct message hash. + +## How to construct the request + +### 1. Create a signer (any random wallet works in dev mode) + +```js +const { ethers } = require("ethers"); +const wallet = ethers.Wallet.createRandom(); +``` + +### 2. Build the OAuth message + +```js +const message = { + domain: "github.com", + userId: "testuser123", + score: "30", // must be >= credential group's min score (uint256 as string) + timestamp: "1700000000" // uint256 as string +}; +``` + +### 3. Sign the message + +The signature must be over `keccak256(abi.encode(string, string, uint256, uint256))`: + +```js +const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["string", "string", "uint256", "uint256"], + [message.domain, message.userId, message.score, message.timestamp] +); +const hash = ethers.keccak256(encoded); +const signature = await wallet.signMessage(ethers.getBytes(hash)); +``` + +### 4. Send the request + +```js +const body = { + message: message, + signature: signature, + registry: "0x17a22f130d4e1c4ba5C20a679a5a29F227083A62", + chain_id: "84532", // Base Sepolia + credential_group_id: "5", // github.com, min score 30 + app_id: "1", + semaphore_identity_commitment: "12345" +}; + +const res = await fetch("http://localhost:3000/verify/oauth", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body) +}); +``` + +### 5. Validate the response + +The response should be: + +```json +{ + "attestation": { + "registry": "0x17a22f130d4e1c4ba5c20a679a5a29f227083a62", + "chain_id": 84532, + "credential_group_id": "...", + "credential_id": "0x...", + "app_id": "1", + "semaphore_identity_commitment": "12345", + "issued_at": 1740268800 + }, + "verifier_hash": "0x...", + "signature": "0x..." +} +``` + +Verify: +- `attestation.chain_id` is `84532` (number, not string) +- `attestation.registry` is the address you sent +- `attestation.credential_group_id` is `"5"` +- `attestation.app_id` is `"1"` +- `attestation.semaphore_identity_commitment` is `"12345"` +- `attestation.issued_at` is a recent unix timestamp (number) +- `verifier_hash` matches `keccak256(abi.encode(registry, chainId, credentialGroupId, credentialId, appId, semaphoreIdentityCommitment, issuedAt))` with Solidity types `(address, uint256, uint256, bytes32, uint256, uint256, uint256)` +- `signature` recovers to verifier address `0x3c50f7055D804b51e506Bc1EA7D082cB1548376C` + +### 6. Test error cases + +Also test these should fail: +- **Missing `chain_id`** → should return 422 +- **Invalid `chain_id: "1"`** → should return 400 with "unsupported chain_id" +- **Wrong domain** (e.g. `domain: "x.com"` with `credential_group_id: "5"` which expects `github.com`) → should return 400 + +## Available credential groups for testing + +| ID | Domain | Min Score | +|----|--------|-----------| +| 1 | farcaster.xyz | 10 | +| 4 | github.com | 10 | +| 5 | github.com | 30 | +| 7 | x.com | 10 | +| 10 | zkpassport.id | 100 | +| 11 | self.xyz | 100 | + +## Important notes + +- Run the script from `/home/claude/credential-registry` (that's where `ethers` is installed) +- All uint256 values in the request body are **strings** +- `chain_id` in the request is a **string**, but in the response `attestation.chain_id` is a **number** +- The script should print clear pass/fail results for each check diff --git a/docs/migration-guide-v2.md b/docs/migration-guide-v2.md index aaca279..f684f3d 100644 --- a/docs/migration-guide-v2.md +++ b/docs/migration-guide-v2.md @@ -9,11 +9,11 @@ Contract addresses are identical on both chains (same deployer, same nonce). | Contract | Address | Chains | |---|---|---| | Semaphore | `0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D` | mainnet (8453), Sepolia (84532) | -| CredentialRegistry | `0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db` | mainnet (8453), Sepolia (84532) | -| DefaultScorer | `0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c` | mainnet (8453), Sepolia (84532) | -| ScorerFactory | `0xAa03996D720C162Fdff246E1D3CEecc792986750` | mainnet (8453), Sepolia (84532) | +| CredentialRegistry | `0x17a22f130d4e1c4ba5C20a679a5a29F227083A62` | mainnet (8453), Sepolia (84532) | +| DefaultScorer | `0x6791B588dAdeb4323bc1C3d987130bC13cBe3625` | mainnet (8453), Sepolia (84532) | +| ScorerFactory | `0x016bC46169533a8d3284c5D8DD590C91783C8C06` | mainnet (8453), Sepolia (84532) | -Owner: `0x6F0CDcd334BA91A5E221582665Cce0431aD4Fc0b` +Owner: `0x677112864ED447866f8D461ABe284E5e907bB4F8` Trusted verifier (Sepolia): `0x3c50f7055D804b51e506Bc1EA7D082cB1548376C` Trusted verifier (mainnet): `0x9186aA65288bFfa67fB58255AeeaFfc4515535d9` diff --git a/docs/migration-instructions/task-manager.md b/docs/migration-instructions/task-manager.md index 118fea3..c1fda31 100644 --- a/docs/migration-instructions/task-manager.md +++ b/docs/migration-instructions/task-manager.md @@ -138,7 +138,7 @@ Update user-facing error messages: ### 9. Environment Variables Add or update: -- `REGISTRY_ADDRESS` — new CredentialRegistry address: `0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db` (same on both Base mainnet 8453 and Sepolia 84532) +- `REGISTRY_ADDRESS` — new CredentialRegistry address: `0x17a22f130d4e1c4ba5C20a679a5a29F227083A62` (same on both Base mainnet 8453 and Sepolia 84532) ## No Changes Required diff --git a/docs/migration-instructions/verify-proof-api.md b/docs/migration-instructions/verify-proof-api.md index f9f0505..359259e 100644 --- a/docs/migration-instructions/verify-proof-api.md +++ b/docs/migration-instructions/verify-proof-api.md @@ -96,8 +96,8 @@ Contract addresses are identical on both chains (same deployer, same nonce). ```diff export const chainRegistries: Record = { - 84532: ['0x0b2Ab187a6FD2d2F05fACc158611838c284E3a9c'], -+ 84532: ['0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db'], -+ 8453: ['0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db'], ++ 84532: ['0x17a22f130d4e1c4ba5C20a679a5a29F227083A62'], ++ 8453: ['0x17a22f130d4e1c4ba5C20a679a5a29F227083A62'], } ``` diff --git a/docs/migration-v3/app-manager.md b/docs/migration-v3/app-manager.md new file mode 100644 index 0000000..d54ed47 --- /dev/null +++ b/docs/migration-v3/app-manager.md @@ -0,0 +1,271 @@ +# Migration Instructions — BringID App Manager Dashboard (v2 → v3) + +## Overview + +The App Manager is a Next.js web dashboard for third-party app developers to self-manage their BringID integration. App admins connect their wallet and manage their app's settings, custom scoring, and lifecycle via direct contract calls. See `docs/app-manager-specs.md` for the full spec. + +## Required Changes + +### 1. Contract Addresses + +Update all hardcoded or configured contract addresses: + +```diff +- REGISTRY_ADDRESS=0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe ++ REGISTRY_ADDRESS=0x17a22f130d4e1c4ba5C20a679a5a29F227083A62 + +- DEFAULT_SCORER_ADDRESS=0x6a0b5ba649C7667A0C4Cd7FE8a83484AEE6C5345 ++ DEFAULT_SCORER_ADDRESS=0x6791B588dAdeb4323bc1C3d987130bC13cBe3625 + +- SCORER_FACTORY_ADDRESS=0x05321FAAD6315a04d5024Ee5b175AB1C62a3fd44 ++ SCORER_FACTORY_ADDRESS=0x016bC46169533a8d3284c5D8DD590C91783C8C06 +``` + +Contract ABIs have changed — re-extract from `out/CredentialRegistry.sol/CredentialRegistry.json` and `out/DefaultScorer.sol/DefaultScorer.json` after building the v3 contracts. + +### 2. App ID Generation — Hash-Based, Non-Sequential (CRITICAL) + +**Affects:** My Apps list, app enumeration, Register App page + +App IDs are no longer sequential auto-increment integers. They are now derived from a hash: + +```diff +- appId = nextAppId++ // v2: sequential 1, 2, 3, … ++ appId = uint256(keccak256(abi.encodePacked(chainId, sender, nonce))) // v3: hash-based +``` + +The `nextAppId` storage variable still exists but it is now a **nonce counter** used as an input to the hash, not the next app ID itself. You cannot enumerate apps by iterating `1..nextAppId`. + +**Impact on "My Apps" list:** The enumeration strategy from the specs (using `nextAppId()` to bound iteration) no longer works. You **must** rely on event indexing to discover app IDs: + +```diff + // Old: iterate 1..nextAppId and check admin +- for (let id = 1; id < nextAppId; id++) { +- const app = await registry.read.apps([id]) +- if (app.admin === connectedAddress) myApps.push(id) +- } + + // New: index AppRegistered + AppAdminTransferred events ++ const created = await publicClient.getLogs({ ++ address: REGISTRY_ADDRESS, ++ event: parseAbiItem('event AppRegistered(uint256 indexed appId, address indexed admin, uint256 recoveryTimelock)'), ++ args: { admin: connectedAddress }, ++ fromBlock: DEPLOY_BLOCK, ++ }) ++ const received = await publicClient.getLogs({ ++ address: REGISTRY_ADDRESS, ++ event: parseAbiItem('event AppAdminTransferred(uint256 indexed appId, address indexed oldAdmin, address indexed newAdmin)'), ++ args: { newAdmin: connectedAddress }, ++ fromBlock: DEPLOY_BLOCK, ++ }) +``` + +The `registerApp()` return value is still the assigned `appId` — capture it from the transaction receipt. + +### 3. Admin Transfer — Two-Step Process (CRITICAL) + +**Affects:** App Detail / Settings page (Admin Transfer section) + +`setAppAdmin()` has been replaced with a two-step transfer pattern: + +```diff +- function setAppAdmin(uint256 appId, address newAdmin) external; ++ function transferAppAdmin(uint256 appId, address newAdmin) external; // Step 1: initiate ++ function acceptAppAdmin(uint256 appId) external; // Step 2: accept +``` + +**UI changes required:** + +- **Initiating admin (current admin):** Call `transferAppAdmin(appId, newAdmin)`. The warning "This is irreversible" is no longer accurate — the transfer is pending until accepted. +- **Accepting admin (new admin):** Add a new UI section for pending incoming transfers. Query `pendingAppAdmin(appId)` to check if the connected wallet has pending transfers to accept. +- **New event:** `AppAdminTransferInitiated(appId, currentAdmin, newAdmin)` fires on `transferAppAdmin()`. The existing `AppAdminTransferred(appId, oldAdmin, newAdmin)` fires on `acceptAppAdmin()`. + +```diff + // Old: single transaction +- await registry.write.setAppAdmin([appId, newAdmin]) + + // New: two-step ++ // Current admin initiates: ++ await registry.write.transferAppAdmin([appId, newAdmin]) ++ // New admin accepts: ++ await registry.write.acceptAppAdmin([appId]) +``` + +**Event indexing update** — add `AppAdminTransferInitiated` to track pending transfers: + +```diff + // Events to index + AppRegistered(appId, admin, recoveryTimelock) ++ AppAdminTransferInitiated(appId, currentAdmin, newAdmin) // NEW: pending transfers + AppAdminTransferred(appId, oldAdmin, newAdmin) + AppStatusChanged(appId, status) + AppScorerSet(appId, scorer) + AppRecoveryTimelockSet(appId, timelock) +``` + +### 4. Event Names — `AppStatusChanged` Replaces Separate Events + +**Affects:** My Apps list, App Detail page, event indexing + +The separate `AppSuspended` and `AppActivated` events have been replaced with a single `AppStatusChanged` event carrying an `AppStatus` enum: + +```diff +- event AppSuspended(uint256 indexed appId); +- event AppActivated(uint256 indexed appId); ++ event AppStatusChanged(uint256 indexed appId, ICredentialRegistry.AppStatus status); +``` + +`AppStatus` enum values: `UNDEFINED (0)`, `ACTIVE (1)`, `SUSPENDED (2)`. + +Update event indexing: + +```diff +- const suspended = await publicClient.getLogs({ +- event: parseAbiItem('event AppSuspended(uint256 indexed appId)'), +- }) +- const activated = await publicClient.getLogs({ +- event: parseAbiItem('event AppActivated(uint256 indexed appId)'), +- }) + ++ const statusChanges = await publicClient.getLogs({ ++ event: parseAbiItem('event AppStatusChanged(uint256 indexed appId, uint8 status)'), ++ fromBlock: DEPLOY_BLOCK, ++ }) ++ // status === 1 → ACTIVE, status === 2 → SUSPENDED +``` + +### 5. Scorer Validation — ERC165 On-Chain Check + +**Affects:** App Detail / Scorer Configuration section, Deploy Custom Scorer page + +`setAppScorer()` now validates the scorer contract on-chain via `ERC165Checker.supportsInterface()`. If the scorer does not implement the `IScorer` interface ID, the transaction reverts with `InvalidScorerContract()`. + +The specs recommended a client-side `getAllScores()` try-call before submitting. This is still useful for UX (prevents wasting gas), but the contract now enforces validation regardless: + +```diff + // Client-side validation (unchanged, still recommended for UX) + try { + await scorer.read.getAllScores() + } catch { + showError('This address does not implement the IScorer interface.') + return + } + + // Contract-side validation (NEW in v3 — will revert if scorer is invalid) + await registry.write.setAppScorer([appId, scorerAddress]) ++ // Reverts with InvalidScorerContract() if scorer doesn't support IScorer interface +``` + +**Custom scorer deployment** — scorers deployed via `ScorerFactory.create()` already implement `IERC165` and will pass validation. If app admins deploy custom scorer contracts manually, they **must** implement `supportsInterface()`: + +```solidity +import {IScorer} from "@bringid/contracts/interfaces/IScorer.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +contract MyScorer is IScorer { + function supportsInterface(bytes4 interfaceId) external pure returns (bool) { + return interfaceId == type(IScorer).interfaceId || interfaceId == type(IERC165).interfaceId; + } + // ... getScore, getScores, getAllScores implementations +} +``` + +### 6. Error Handling — Custom Errors Replace Strings + +**Affects:** All pages with contract interactions + +All `"BID::..."` error strings have been replaced with typed custom errors. Update error parsing throughout the dashboard: + +```diff +- if (error.message.includes('BID::app not active')) { +- showError('This app is currently suspended.') +- } + ++ // Decode custom error from revert data ++ import { decodeErrorResult } from 'viem' ++ const decoded = decodeErrorResult({ abi: registryAbi, data: error.data }) ++ switch (decoded.errorName) { ++ case 'AppNotActive': ++ showError('This app is currently suspended.') ++ break ++ case 'NotAppAdmin': ++ showError('You are not the admin of this app.') ++ break ++ case 'AppNotSuspended': ++ showError('This app is already active.') ++ break ++ } +``` + +App management error mapping: + +| Old (string) | New (custom error) | User Message | +|---|---|---| +| _(string match)_ | `NotAppAdmin()` | You are not the admin of this app. | +| _(string match)_ | `AppNotActive()` | This app is currently suspended. | +| _(string match)_ | `AppNotSuspended()` | This app is already active. | +| _(new)_ | `InvalidAdminAddress()` | Invalid admin address (cannot be zero). | +| _(new)_ | `NotPendingAdmin()` | You are not the pending admin for this app. | +| _(new)_ | `InvalidScorerContract()` | This address does not implement the IScorer interface. | +| _(new)_ | `InvalidScorerAddress()` | Invalid scorer address (cannot be zero). | + +### 7. New Feature — Merkle Tree Duration Configuration + +**Affects:** App Detail / Settings page (new section) + +v3 adds per-app Merkle tree duration configuration. App admins can override the registry-level default: + +```typescript +// Read current per-app override (0 = using registry default) +const appDuration = await registry.read.appMerkleTreeDuration([appId]) + +// Read registry default +const defaultDuration = await registry.read.defaultMerkleTreeDuration() + +// Set per-app override (admin-only) +await registry.write.setAppMerkleTreeDuration([appId, durationInSeconds]) + +// Clear override (revert to registry default) +await registry.write.setAppMerkleTreeDuration([appId, 0n]) +``` + +The dashboard should add a "Merkle Tree Duration" section to the App Detail page: +- Show current effective duration (per-app override if set, otherwise registry default) +- Input field for seconds with human-readable preview +- Note: setting to 0 reverts to the registry default +- Note: updating propagates to all existing Semaphore groups for the app + +New event to index: `AppMerkleTreeDurationSet(uint256 indexed appId, uint256 merkleTreeDuration)`. + +### 8. New View Function — `getAppSemaphoreGroupIds` + +**Affects:** App Detail page (optional enhancement) + +v3 adds a view function to retrieve all Semaphore group IDs for an app: + +```typescript +const groupIds = await registry.read.getAppSemaphoreGroupIds([appId]) +``` + +This can be used to show how many credential groups have active Semaphore groups for the app, or for debugging purposes. + +### 9. Re-registration + +All apps must be re-registered on the new contract. Existing app IDs from the previous deployment (`0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe`) are not valid on the new contract. + +The dashboard should: +- Clear any cached/stored app IDs from the old contract +- Re-index events starting from the new contract's deployment block +- Prompt returning users to re-register their apps + +## No Changes Required + +- Wallet connection flow (wagmi + viem + ConnectKit/RainbowKit) +- Chain configuration (Base mainnet 8453 + Base Sepolia 84532) +- `registerApp(recoveryTimelock)` function signature (unchanged) +- `suspendApp(appId)` / `activateApp(appId)` function signatures (unchanged) +- `setAppRecoveryTimelock(appId, timelock)` function signature (unchanged) +- `ScorerFactory.create()` flow (unchanged) +- `DefaultScorer` read functions (`getScore`, `getScores`, `getAllScores`) +- Score Explorer page (credential group IDs and structure unchanged) +- General architecture (no backend, direct contract calls, event-based indexing) diff --git a/docs/migration-v3/bringid.md b/docs/migration-v3/bringid.md index 866c5c9..87b17d9 100644 --- a/docs/migration-v3/bringid.md +++ b/docs/migration-v3/bringid.md @@ -12,13 +12,13 @@ Update all hardcoded or configured contract addresses: ```diff - CredentialRegistry: '0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe' -+ CredentialRegistry: '0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db' ++ CredentialRegistry: '0x17a22f130d4e1c4ba5C20a679a5a29F227083A62' - DefaultScorer: '0x6a0b5ba649C7667A0C4Cd7FE8a83484AEE6C5345' -+ DefaultScorer: '0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c' ++ DefaultScorer: '0x6791B588dAdeb4323bc1C3d987130bC13cBe3625' - ScorerFactory: '0x05321FAAD6315a04d5024Ee5b175AB1C62a3fd44' -+ ScorerFactory: '0xAa03996D720C162Fdff246E1D3CEecc792986750' ++ ScorerFactory: '0x016bC46169533a8d3284c5D8DD590C91783C8C06' ``` Semaphore address is unchanged: `0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D`. @@ -155,7 +155,7 @@ Rename the proof type to match the contract: All apps must re-register on the new contract via `registerApp()`. Existing app IDs from the old contract are not valid on the new contract. Store the new `appId` values. -All user credentials must be re-registered. Existing registrations on `0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe` are not accessible from `0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db`. +All user credentials must be re-registered. Existing registrations on `0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe` are not accessible from `0x17a22f130d4e1c4ba5C20a679a5a29F227083A62`. ## No Changes Required diff --git a/docs/migration-v3/overview.md b/docs/migration-v3/overview.md index d484849..9d01b92 100644 --- a/docs/migration-v3/overview.md +++ b/docs/migration-v3/overview.md @@ -1,6 +1,6 @@ # Migration Guide — Credential Registry v3 (Redeployment) -This guide covers breaking changes between the previous deployment (`0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe`) and the current deployment (`0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db`). All consuming apps must update. +This guide covers breaking changes between the previous deployment (`0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe`) and the current deployment (`0x17a22f130d4e1c4ba5C20a679a5a29F227083A62`). All consuming apps must update. ## Deployed Contracts @@ -19,11 +19,11 @@ Contract addresses are identical on both chains (same deployer, same nonce). | Contract | Address | Chains | |---|---|---| | Semaphore | `0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D` | mainnet (8453), Sepolia (84532) | -| CredentialRegistry | `0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db` | mainnet (8453), Sepolia (84532) | -| DefaultScorer | `0x315044578dd9480Dd25427E4a4d94b0fc2Fa4f8c` | mainnet (8453), Sepolia (84532) | -| ScorerFactory | `0xAa03996D720C162Fdff246E1D3CEecc792986750` | mainnet (8453), Sepolia (84532) | +| CredentialRegistry | `0x17a22f130d4e1c4ba5C20a679a5a29F227083A62` | mainnet (8453), Sepolia (84532) | +| DefaultScorer | `0x6791B588dAdeb4323bc1C3d987130bC13cBe3625` | mainnet (8453), Sepolia (84532) | +| ScorerFactory | `0x016bC46169533a8d3284c5D8DD590C91783C8C06` | mainnet (8453), Sepolia (84532) | -Owner: `0x6F0CDcd334BA91A5E221582665Cce0431aD4Fc0b` +Owner: `0x677112864ED447866f8D461ABe284E5e907bB4F8` Trusted verifier (Sepolia): `0x3c50f7055D804b51e506Bc1EA7D082cB1548376C` Trusted verifier (mainnet): `0x9186aA65288bFfa67fB58255AeeaFfc4515535d9` diff --git a/docs/migration-v3/relayer.md b/docs/migration-v3/relayer.md index 901979d..9140cb7 100644 --- a/docs/migration-v3/relayer.md +++ b/docs/migration-v3/relayer.md @@ -12,7 +12,7 @@ The Relayer executes blockchain operations via a relayer wallet with transaction ```diff - REGISTRY_ADDRESS=0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe -+ REGISTRY_ADDRESS=0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db ++ REGISTRY_ADDRESS=0x17a22f130d4e1c4ba5C20a679a5a29F227083A62 ``` Semaphore address is unchanged: `0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D`. diff --git a/docs/migration-v3/semaphore-indexer.md b/docs/migration-v3/semaphore-indexer.md index 9f79f2b..378aff2 100644 --- a/docs/migration-v3/semaphore-indexer.md +++ b/docs/migration-v3/semaphore-indexer.md @@ -12,7 +12,7 @@ Update the CredentialRegistry address used for event monitoring or group discove ```diff - REGISTRY_ADDRESS=0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe -+ REGISTRY_ADDRESS=0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db ++ REGISTRY_ADDRESS=0x17a22f130d4e1c4ba5C20a679a5a29F227083A62 ``` Semaphore address is unchanged: `0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D`. diff --git a/docs/migration-v3/task-manager.md b/docs/migration-v3/task-manager.md index 7094fb3..7629845 100644 --- a/docs/migration-v3/task-manager.md +++ b/docs/migration-v3/task-manager.md @@ -12,7 +12,7 @@ The Task Manager accepts, schedules, and batches verification/claim tasks before ```diff - REGISTRY_ADDRESS=0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe -+ REGISTRY_ADDRESS=0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db ++ REGISTRY_ADDRESS=0x17a22f130d4e1c4ba5C20a679a5a29F227083A62 ``` ### 2. Attestation Struct — `chainId` Field in Calldata (CRITICAL) diff --git a/docs/migration-v3/verifier.md b/docs/migration-v3/verifier.md index 2d29db9..32a5bda 100644 --- a/docs/migration-v3/verifier.md +++ b/docs/migration-v3/verifier.md @@ -56,7 +56,7 @@ Update the `registry` field in all attestations to the new contract address: ```diff - registry: '0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe' -+ registry: '0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db' ++ registry: '0x17a22f130d4e1c4ba5C20a679a5a29F227083A62' ``` The contract enforces `attestation.registry == address(this)` and reverts with `WrongRegistryAddress()` on mismatch. @@ -149,7 +149,7 @@ Update or add: ```diff - REGISTRY_ADDRESS=0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe -+ REGISTRY_ADDRESS=0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db ++ REGISTRY_ADDRESS=0x17a22f130d4e1c4ba5C20a679a5a29F227083A62 ``` Ensure `CHAIN_ID` or equivalent config is available if the verifier serves multiple chains. @@ -161,7 +161,7 @@ import { ethers } from 'ethers' const chainId = 84532 // Base Sepolia const attestation = { - registry: '0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db', + registry: '0x17a22f130d4e1c4ba5C20a679a5a29F227083A62', chainId, credentialGroupId, credentialId, diff --git a/docs/migration-v3/verify-proof-api.md b/docs/migration-v3/verify-proof-api.md index 1262127..539baeb 100644 --- a/docs/migration-v3/verify-proof-api.md +++ b/docs/migration-v3/verify-proof-api.md @@ -16,8 +16,8 @@ Update the registry whitelist: export const chainRegistries: Record = { - 84532: ['0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe'], - 8453: ['0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe'], -+ 84532: ['0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db'], -+ 8453: ['0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db'], ++ 84532: ['0x17a22f130d4e1c4ba5C20a679a5a29F227083A62'], ++ 8453: ['0x17a22f130d4e1c4ba5C20a679a5a29F227083A62'], } ``` diff --git a/docs/migration-v3/widget.md b/docs/migration-v3/widget.md index 036e178..cff990e 100644 --- a/docs/migration-v3/widget.md +++ b/docs/migration-v3/widget.md @@ -12,7 +12,7 @@ Update any hardcoded or configured contract addresses: ```diff - REGISTRY_ADDRESS=0xfd600B14Dc5A145ec9293Fd5768ae10Ccc1E91Fe -+ REGISTRY_ADDRESS=0xbF9b2556e6Dd64D60E08E3669CeF2a4293e006db ++ REGISTRY_ADDRESS=0x17a22f130d4e1c4ba5C20a679a5a29F227083A62 ``` ### 2. Scope Formula — `appId` Included (CRITICAL) diff --git a/script/deploy.sh b/script/deploy.sh index 7233d15..b19eeac 100755 --- a/script/deploy.sh +++ b/script/deploy.sh @@ -16,6 +16,14 @@ set -euo pipefail # --skip-scorer-factory Skip ScorerFactory deployment # --dry-run Simulate without broadcasting (no --broadcast flag) # +# Build strategy: +# Step 1 uses a two-step build to keep peak memory low (~1.2 GB): +# 1a. Scripts compiled with ci profile (via_ir=false) — fast, low memory +# 1b. Contracts compiled with default profile (via_ir=true) — optimized bytecode +# Steps 2 & 5 deploy contracts using `cast send --create` with pre-built +# bytecode from out/, avoiding any recompilation by forge. +# Steps 3 & 4 use `forge script` with ci profile (scripts already compiled). +# # Required env vars (in .env): # PRIVATE_KEY — Deployer private key (hex, without 0x prefix) # ALCHEMY_API_KEY — Alchemy API key for RPC @@ -142,14 +150,6 @@ if [[ "$DRY_RUN" == true ]]; then echo -e "${YELLOW}DRY RUN mode — no transactions will be broadcast${NC}" fi -# ── Foundry profile for forge script calls ─────────────────────────────────── -# Always use ci profile (via_ir=false) for fast script compilation/execution. -# Step 1 pre-builds contract artifacts with via_ir=true (default profile). -# forge script uses ci profile for speed — the deployed bytecode comes from -# the ci build. If you need BaseScan-verifiable bytecode (via_ir=true match), -# run `forge verify-contract` separately after deployment using the default profile. -FORGE_PROFILE="ci" - # ── Pre-flight checks ──────────────────────────────────────────────────────── echo "" echo -e "${BOLD}Pre-flight checks${NC}" @@ -228,77 +228,84 @@ fi echo "" # ══════════════════════════════════════════════════════════════════════════════ -# Step 1: Compile contracts +# Step 1: Compile contracts (two-step to reduce peak memory) +# 1a. Scripts with ci profile (via_ir=false) — fast, low memory +# 1b. Contracts with default profile (via_ir=true) — ~1.2 GB peak # ══════════════════════════════════════════════════════════════════════════════ -echo -e "${BOLD}Step 1/5: Compiling contracts (via_ir=true)${NC}" -echo -e "${YELLOW}Note: via_ir compilation may be slow and use significant memory${NC}" +echo -e "${BOLD}Step 1/5: Compiling contracts${NC}" echo "─────────────────────────────────────────" +echo -e " Building scripts (ci profile, via_ir=false)..." +FOUNDRY_PROFILE=ci forge build --skip test + +echo -e " Building contracts (default profile, via_ir=true)..." FOUNDRY_PROFILE=default forge build --skip test --skip script echo -e "${GREEN}✓ Compilation successful${NC}" + +# Extract all bytecodes now, before forge script (steps 3-4) can overwrite out/ +echo -e " Extracting via_ir bytecodes from out/..." +REGISTRY_BYTECODE=$(jq -r '.bytecode.object' "$PROJECT_DIR/out/CredentialRegistry.sol/CredentialRegistry.json") +if [[ -z "$REGISTRY_BYTECODE" || "$REGISTRY_BYTECODE" == "null" ]]; then + echo -e "${RED}Error: Could not extract CredentialRegistry bytecode from out/${NC}" + exit 1 +fi +FACTORY_BYTECODE=$(jq -r '.bytecode.object' "$PROJECT_DIR/out/ScorerFactory.sol/ScorerFactory.json") +if [[ -z "$FACTORY_BYTECODE" || "$FACTORY_BYTECODE" == "null" ]]; then + echo -e "${RED}Error: Could not extract ScorerFactory bytecode from out/${NC}" + exit 1 +fi +echo -e "${GREEN}✓ Bytecodes extracted${NC}" echo "" # ══════════════════════════════════════════════════════════════════════════════ # Step 2: Deploy CredentialRegistry (+ DefaultScorer via constructor) +# Uses `cast send --create` with pre-built bytecode to avoid recompilation. +# Constructor: CredentialRegistry(ISemaphore, address trustedVerifier, uint256 defaultMerkleTreeDuration) # ══════════════════════════════════════════════════════════════════════════════ echo -e "${BOLD}Step 2/5: Deploying CredentialRegistry${NC}" echo "─────────────────────────────────────────" -SEMAPHORE_ADDRESS="$SEMAPHORE_ADDRESS" \ -TRUSTED_VERIFIER="$TRUSTED_VERIFIER" \ -FOUNDRY_PROFILE="$FORGE_PROFILE" \ - forge script script/Deploy.s.sol:Deploy \ - --rpc-url "$RPC_URL" \ - $BROADCAST_FLAG \ - $VERIFY_FLAGS \ - -v - -echo "" - -# Extract addresses from broadcast JSON -REGISTRY_ADDRESS="" -DEFAULT_SCORER_ADDRESS="" - -BROADCAST_FILE="$PROJECT_DIR/broadcast/Deploy.s.sol/$CHAIN_ID/run-latest.json" +# Encode constructor args: (address semaphore, address trustedVerifier, uint256 defaultMerkleTreeDuration) +# defaultMerkleTreeDuration = 5 minutes = 300 seconds +CONSTRUCTOR_ARGS=$(cast abi-encode "constructor(address,address,uint256)" "$SEMAPHORE_ADDRESS" "$TRUSTED_VERIFIER" 300) if [[ "$DRY_RUN" == true ]]; then - # In dry-run mode, try to extract from dry-run broadcast - BROADCAST_FILE="$PROJECT_DIR/broadcast/Deploy.s.sol/$CHAIN_ID/dry-run/run-latest.json" -fi - -if [[ -f "$BROADCAST_FILE" ]]; then - # Extract CredentialRegistry address — it's a CREATE transaction - REGISTRY_ADDRESS=$(jq -r '.transactions[] | select(.transactionType == "CREATE" and .contractName == "CredentialRegistry") | .contractAddress' "$BROADCAST_FILE" | head -1) - - # Extract DefaultScorer address from additionalContracts (deployed by CredentialRegistry constructor) - DEFAULT_SCORER_ADDRESS=$(jq -r '.transactions[] | select(.contractName == "CredentialRegistry") | .additionalContracts[]? | select(.contractName == "DefaultScorer") | .address' "$BROADCAST_FILE" | head -1) + echo -e "${YELLOW} [DRY RUN] Would deploy CredentialRegistry with:${NC}" + echo -e " Semaphore: $SEMAPHORE_ADDRESS" + echo -e " TrustedVerifier: $TRUSTED_VERIFIER" + echo -e " MerkleTreeDur: 300 (5 minutes)" + echo -e " Bytecode size: $((${#REGISTRY_BYTECODE} / 2)) bytes" + REGISTRY_ADDRESS="0xDRY_RUN_REGISTRY_ADDRESS" + DEFAULT_SCORER_ADDRESS="0xDRY_RUN_SCORER_ADDRESS" +else + echo -e " Deploying CredentialRegistry..." + DEPLOY_OUTPUT=$(cast send \ + --private-key "$PRIVATE_KEY" \ + --rpc-url "$RPC_URL" \ + --json \ + --create "${REGISTRY_BYTECODE}${CONSTRUCTOR_ARGS#0x}") - # Fallback: query on-chain if not found in broadcast JSON (won't work in dry-run) - if [[ (-z "$DEFAULT_SCORER_ADDRESS" || "$DEFAULT_SCORER_ADDRESS" == "null") && -n "$REGISTRY_ADDRESS" && "$REGISTRY_ADDRESS" != "null" && "$DRY_RUN" == false ]]; then - DEFAULT_SCORER_ADDRESS=$(cast call "$REGISTRY_ADDRESS" "defaultScorer()(address)" --rpc-url "$RPC_URL" 2>/dev/null || true) + REGISTRY_ADDRESS=$(echo "$DEPLOY_OUTPUT" | jq -r '.contractAddress') + if [[ -z "$REGISTRY_ADDRESS" || "$REGISTRY_ADDRESS" == "null" ]]; then + echo -e "${RED}Error: CredentialRegistry deployment failed${NC}" + echo "$DEPLOY_OUTPUT" + exit 1 fi -fi -# Fallback: try to parse from forge script console output if broadcast JSON extraction failed -if [[ -z "$REGISTRY_ADDRESS" || "$REGISTRY_ADDRESS" == "null" ]]; then - echo -e "${YELLOW}Warning: Could not extract addresses from broadcast JSON${NC}" - echo -e "${YELLOW}Please enter the deployed addresses manually:${NC}" - read -r -p " CredentialRegistry address: " REGISTRY_ADDRESS - read -r -p " DefaultScorer address: " DEFAULT_SCORER_ADDRESS -fi - -if [[ -z "$REGISTRY_ADDRESS" || "$REGISTRY_ADDRESS" == "null" ]]; then - echo -e "${RED}Error: Could not determine CredentialRegistry address${NC}" - exit 1 + # Query DefaultScorer address (deployed by constructor) + # Wait briefly for the transaction to be indexed + sleep 2 + DEFAULT_SCORER_ADDRESS=$(cast call "$REGISTRY_ADDRESS" "defaultScorer()(address)" --rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]' || true) fi echo -e "${GREEN}✓ CredentialRegistry deployed: ${CYAN}$REGISTRY_ADDRESS${NC}" -echo -e "${GREEN}✓ DefaultScorer deployed: ${CYAN}$DEFAULT_SCORER_ADDRESS${NC}" +echo -e "${GREEN}✓ DefaultScorer deployed: ${CYAN}${DEFAULT_SCORER_ADDRESS:-N/A}${NC}" echo "" # ══════════════════════════════════════════════════════════════════════════════ # Step 3: Create credential groups + set scores +# Uses forge script with ci profile (scripts already compiled, no recompilation). # ══════════════════════════════════════════════════════════════════════════════ if [[ "$SKIP_CREDENTIAL_GROUPS" == true ]]; then echo -e "${YELLOW}Step 3/5: Skipping credential groups (--skip-credential-groups)${NC}" @@ -309,7 +316,7 @@ else echo "─────────────────────────────────────────" CREDENTIAL_REGISTRY_ADDRESS="$REGISTRY_ADDRESS" \ - FOUNDRY_PROFILE="$FORGE_PROFILE" \ + FOUNDRY_PROFILE="ci" \ forge script script/CredentialGroups.s.sol:DeployCredentialGroups \ --rpc-url "$RPC_URL" \ $BROADCAST_FLAG \ @@ -322,6 +329,7 @@ echo "" # ══════════════════════════════════════════════════════════════════════════════ # Step 4: Register apps +# Uses forge script with ci profile (scripts already compiled, no recompilation). # ══════════════════════════════════════════════════════════════════════════════ if [[ "$SKIP_APPS" == true ]]; then echo -e "${YELLOW}Step 4/5: Skipping app registration (--skip-apps)${NC}" @@ -333,7 +341,7 @@ else echo "─────────────────────────────────────────" CREDENTIAL_REGISTRY_ADDRESS="$REGISTRY_ADDRESS" \ - FOUNDRY_PROFILE="$FORGE_PROFILE" \ + FOUNDRY_PROFILE="ci" \ forge script script/RegisterApps.s.sol:RegisterApps \ --rpc-url "$RPC_URL" \ $BROADCAST_FLAG \ @@ -344,8 +352,21 @@ else fi echo "" +# ── Rebuild via_ir artifacts ────────────────────────────────────────────────── +# Steps 3-4 ran forge script with ci profile which overwrites out/ with +# via_ir=false artifacts. Rebuild so out/ contains the correct via_ir=true +# artifacts for BaseScan verification. +if [[ "$SKIP_CREDENTIAL_GROUPS" == false || "$SKIP_APPS" == false ]] && [[ "$DRY_RUN" == false ]]; then + echo -e " Rebuilding via_ir=true artifacts for verification..." + FOUNDRY_PROFILE=default forge build --skip test --skip script --quiet + echo -e "${GREEN}✓ via_ir=true artifacts restored in out/${NC}" + echo "" +fi + # ══════════════════════════════════════════════════════════════════════════════ # Step 5: Deploy ScorerFactory +# Uses `cast send --create` with pre-built bytecode to avoid recompilation. +# ScorerFactory has no constructor arguments. # ══════════════════════════════════════════════════════════════════════════════ if [[ "$SKIP_SCORER_FACTORY" == true ]]; then echo -e "${YELLOW}Step 5/5: Skipping ScorerFactory (--skip-scorer-factory)${NC}" @@ -353,27 +374,77 @@ else echo -e "${BOLD}Step 5/5: Deploying ScorerFactory${NC}" echo "─────────────────────────────────────────" - FOUNDRY_PROFILE="$FORGE_PROFILE" \ - forge script script/DeployScorerFactory.s.sol:DeployScorerFactory \ - --rpc-url "$RPC_URL" \ - $BROADCAST_FLAG \ - $VERIFY_FLAGS \ - -v + if [[ "$DRY_RUN" == true ]]; then + echo -e "${YELLOW} [DRY RUN] Would deploy ScorerFactory${NC}" + echo -e " Bytecode size: $((${#FACTORY_BYTECODE} / 2)) bytes" + SCORER_FACTORY_ADDRESS="0xDRY_RUN_FACTORY_ADDRESS" + else + echo -e " Deploying ScorerFactory..." + SF_OUTPUT=$(cast send \ + --private-key "$PRIVATE_KEY" \ + --rpc-url "$RPC_URL" \ + --json \ + --create "$FACTORY_BYTECODE") + + SCORER_FACTORY_ADDRESS=$(echo "$SF_OUTPUT" | jq -r '.contractAddress') + if [[ -z "$SCORER_FACTORY_ADDRESS" || "$SCORER_FACTORY_ADDRESS" == "null" ]]; then + echo -e "${RED}Error: ScorerFactory deployment failed${NC}" + echo "$SF_OUTPUT" + exit 1 + fi + fi - echo -e "${GREEN}✓ ScorerFactory deployed${NC}" + echo -e "${GREEN}✓ ScorerFactory deployed: ${CYAN}$SCORER_FACTORY_ADDRESS${NC}" fi echo "" -# ── Extract ScorerFactory address ───────────────────────────────────────────── -SCORER_FACTORY_ADDRESS="" -if [[ "$SKIP_SCORER_FACTORY" == false ]]; then - SF_BROADCAST="$PROJECT_DIR/broadcast/DeployScorerFactory.s.sol/$CHAIN_ID/run-latest.json" - if [[ "$DRY_RUN" == true ]]; then - SF_BROADCAST="$PROJECT_DIR/broadcast/DeployScorerFactory.s.sol/$CHAIN_ID/dry-run/run-latest.json" +# ══════════════════════════════════════════════════════════════════════════════ +# Step 6: Verify contracts on BaseScan +# ══════════════════════════════════════════════════════════════════════════════ +if [[ -n "${BASESCAN_API_KEY:-}" && "$DRY_RUN" == false ]]; then + echo -e "${BOLD}Step 6: Verifying contracts on BaseScan${NC}" + echo "─────────────────────────────────────────" + + # CredentialRegistry + echo -e " Verifying CredentialRegistry..." + FOUNDRY_PROFILE=default forge verify-contract "$REGISTRY_ADDRESS" \ + contracts/registry/CredentialRegistry.sol:CredentialRegistry \ + --chain-id "$CHAIN_ID" \ + --etherscan-api-key "$BASESCAN_API_KEY" \ + --constructor-args $(cast abi-encode "constructor(address,address,uint256)" "$SEMAPHORE_ADDRESS" "$TRUSTED_VERIFIER" 300) \ + --watch \ + && echo -e " ${GREEN}✓ CredentialRegistry verified${NC}" \ + || echo -e " ${YELLOW}⚠ CredentialRegistry verification failed (verify manually)${NC}" + + # DefaultScorer + if [[ -n "${DEFAULT_SCORER_ADDRESS:-}" && "$DEFAULT_SCORER_ADDRESS" != "null" ]]; then + echo -e " Verifying DefaultScorer..." + FOUNDRY_PROFILE=default forge verify-contract "$DEFAULT_SCORER_ADDRESS" \ + contracts/scoring/DefaultScorer.sol:DefaultScorer \ + --chain-id "$CHAIN_ID" \ + --etherscan-api-key "$BASESCAN_API_KEY" \ + --constructor-args $(cast abi-encode "constructor(address)" "$DEPLOYER_ADDRESS") \ + --watch \ + && echo -e " ${GREEN}✓ DefaultScorer verified${NC}" \ + || echo -e " ${YELLOW}⚠ DefaultScorer verification failed (verify manually)${NC}" fi - if [[ -f "$SF_BROADCAST" ]]; then - SCORER_FACTORY_ADDRESS=$(jq -r '.transactions[] | select(.transactionType == "CREATE" and .contractName == "ScorerFactory") | .contractAddress' "$SF_BROADCAST" | head -1) + + # ScorerFactory + if [[ -n "${SCORER_FACTORY_ADDRESS:-}" && "${SCORER_FACTORY_ADDRESS:-}" != "null" && "$SKIP_SCORER_FACTORY" == false ]]; then + echo -e " Verifying ScorerFactory..." + FOUNDRY_PROFILE=default forge verify-contract "$SCORER_FACTORY_ADDRESS" \ + contracts/scoring/ScorerFactory.sol:ScorerFactory \ + --chain-id "$CHAIN_ID" \ + --etherscan-api-key "$BASESCAN_API_KEY" \ + --watch \ + && echo -e " ${GREEN}✓ ScorerFactory verified${NC}" \ + || echo -e " ${YELLOW}⚠ ScorerFactory verification failed (verify manually)${NC}" fi + + echo "" +elif [[ "$DRY_RUN" == false ]]; then + echo -e "${YELLOW}Skipping BaseScan verification (no BASESCAN_API_KEY)${NC}" + echo "" fi # ══════════════════════════════════════════════════════════════════════════════ @@ -388,7 +459,7 @@ echo -e " ─────────────────────── echo -e " Semaphore: ${CYAN}$SEMAPHORE_ADDRESS${NC}" echo -e " CredentialRegistry: ${CYAN}$REGISTRY_ADDRESS${NC}" echo -e " DefaultScorer: ${CYAN}${DEFAULT_SCORER_ADDRESS:-N/A}${NC}" -[[ -n "$SCORER_FACTORY_ADDRESS" && "$SCORER_FACTORY_ADDRESS" != "null" ]] && \ +[[ -n "${SCORER_FACTORY_ADDRESS:-}" && "${SCORER_FACTORY_ADDRESS:-}" != "null" ]] && \ echo -e " ScorerFactory: ${CYAN}$SCORER_FACTORY_ADDRESS${NC}" echo "" echo -e " ${BOLD}Explorer Links${NC}" @@ -396,7 +467,7 @@ echo -e " ─────────────────────── echo -e " Registry: ${CYAN}$EXPLORER_URL/address/$REGISTRY_ADDRESS${NC}" [[ -n "${DEFAULT_SCORER_ADDRESS:-}" && "$DEFAULT_SCORER_ADDRESS" != "null" ]] && \ echo -e " DefaultScorer: ${CYAN}$EXPLORER_URL/address/$DEFAULT_SCORER_ADDRESS${NC}" -[[ -n "$SCORER_FACTORY_ADDRESS" && "$SCORER_FACTORY_ADDRESS" != "null" ]] && \ +[[ -n "${SCORER_FACTORY_ADDRESS:-}" && "${SCORER_FACTORY_ADDRESS:-}" != "null" ]] && \ echo -e " ScorerFactory: ${CYAN}$EXPLORER_URL/address/$SCORER_FACTORY_ADDRESS${NC}" echo "" echo -e " ${BOLD}Verification Commands${NC}" diff --git a/test/CredentialRegistry.t.sol b/test/CredentialRegistry.t.sol index 9d7cb4e..ef2fee7 100644 --- a/test/CredentialRegistry.t.sol +++ b/test/CredentialRegistry.t.sol @@ -1938,6 +1938,7 @@ contract CredentialRegistryTest is Test { bytes32 credentialId = keccak256("blinded-id"); uint256 commitment = COMMITMENT_12345; + // issuedAt exceeds the 10-minute buffer ICredentialRegistry.Attestation memory att = ICredentialRegistry.Attestation({ registry: address(registry), chainId: block.chainid, @@ -1953,6 +1954,71 @@ contract CredentialRegistryTest is Test { registry.registerCredential(att, v, r, s); } + function testRegisterCredentialFutureAttestationWithinBuffer() public { + uint256 credentialGroupId = 1; + registry.createCredentialGroup(credentialGroupId, 0, 0); + + bytes32 credentialId = keccak256("blinded-id"); + uint256 commitment = COMMITMENT_12345; + + // issuedAt is 5 minutes ahead, within the 10-minute default buffer + ICredentialRegistry.Attestation memory att = ICredentialRegistry.Attestation({ + registry: address(registry), + chainId: block.chainid, + credentialGroupId: credentialGroupId, + credentialId: credentialId, + appId: DEFAULT_APP_ID, + semaphoreIdentityCommitment: commitment, + issuedAt: block.timestamp + 5 minutes + }); + (uint8 v, bytes32 r, bytes32 s) = _signAttestation(att); + + registry.registerCredential(att, v, r, s); + } + + function testRegisterCredentialFutureAttestationBufferZero() public { + // Disable the buffer + registry.setFutureAttestationBuffer(0); + + uint256 credentialGroupId = 1; + registry.createCredentialGroup(credentialGroupId, 0, 0); + + bytes32 credentialId = keccak256("blinded-id"); + uint256 commitment = COMMITMENT_12345; + + // Even 1 second ahead should revert with buffer=0 + ICredentialRegistry.Attestation memory att = ICredentialRegistry.Attestation({ + registry: address(registry), + chainId: block.chainid, + credentialGroupId: credentialGroupId, + credentialId: credentialId, + appId: DEFAULT_APP_ID, + semaphoreIdentityCommitment: commitment, + issuedAt: block.timestamp + 1 + }); + (uint8 v, bytes32 r, bytes32 s) = _signAttestation(att); + + vm.expectRevert(FutureAttestation.selector); + registry.registerCredential(att, v, r, s); + } + + function testSetFutureAttestationBuffer() public { + assertEq(registry.futureAttestationBuffer(), 10 minutes); + + vm.expectEmit(false, false, false, true); + emit FutureAttestationBufferSet(5 minutes); + + registry.setFutureAttestationBuffer(5 minutes); + assertEq(registry.futureAttestationBuffer(), 5 minutes); + } + + function testSetFutureAttestationBufferOnlyOwner() public { + address notOwner = makeAddr("not-owner"); + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + registry.setFutureAttestationBuffer(5 minutes); + } + function testRegisterCredentialExpiredAttestation() public { uint256 credentialGroupId = 1; registry.createCredentialGroup(credentialGroupId, 0, 0);