diff --git a/.gitignore b/.gitignore index db63668..f77875f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,11 +22,12 @@ node_modules # cli dist -# artifacts -packages/circom/artifacts -packages/foundry/inputs +# coverage +packages/foundry/coverage +lcov.info # Build artifacts +packages/foundry/inputs packages/foundry/out/ packages/foundry/broadcast/ packages/foundry/cache/ @@ -42,4 +43,4 @@ packages/circom/artifacts/ packages/circom/scripts/*.wtns packages/circom/scripts/proof.json packages/circom/scripts/public.json -packages/circom/scripts/input.json \ No newline at end of file +packages/circom/scripts/input.json diff --git a/README.md b/README.md index 4b766e9..883424e 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,23 @@ TypeScript

-πŸ§ͺ **Rayls Hook** is a privacy-preserving investor suitability assessment system built on Uniswap v4 hooks. It allows users to prove their investment suitability without revealing their specific questionnaire responses using Zero-Knowledge Proofs. +**Rayls Hook** introduces two complementary ZK-SNARK enabled features built on Uniswap v4 hooks: -βš™οΈ Built using **Scaffold-ETH 2** as the foundation, with **NextJS**, **RainbowKit**, **Foundry**, **Wagmi**, **Circom**, **SnarkJS**, and **TypeScript**. +1. [πŸ›‘οΈ Suitability Verifier](./docs/suitability.md) (click for more info) – A privacy-preserving investor suitability assessment system. It allows users to prove their investment suitability without revealing their specific questionnaire responses using Zero-Knowledge Proofs. + +2. [πŸ” Private Swaps](./docs/privateSwaps.md) (click for more info) – Private swaps that allow users to conceal their swap parameters until an execution timestamp is reached. Hidden swap values are committed on-chain via a unique commitment ID, then at execution time they are revealed and validated using zkSNARK proofs. Swap details are also encrypted using the Auditor’s public key and the generated ciphertext is stored on-chain, enabling independent verification at any time. The commitment Id is the result of running the cryptographic function keccak256 against the auditor ciphertext and the poseidon hash generated by the zk snark proof. + +## πŸ“Œ Key Notes + +- More details about circom and zkSNARK implementation [here](./packages/circom/README.md) +- There's no partner integration but although Private swap commitments and encrypted payloads are currently fully stored on-chain, they could be stored in EigenDA with only lightweight references on-chain to reduce gas costs and improve scalability without compromising verifiability. +- Only the beforeSwap hook is used, but the logic can be extended to beforeAddLiquidity as well. +- The two features β€” Suitability Verifier and Private Swap Commitments β€” are independent, though private swap execution could optionally require passing the suitability check. +- The frontend provides an example of a Suitability questionaire, but there's currently no FE->BE integration. ## 🎯 Project Overview -Rayls Hook implements a comprehensive investor suitability assessment system that: +πŸ›‘οΈ Suitability Verifier Logic - βœ… **Private Questionnaire**: Users answer 5 suitability questions without revealing their responses - πŸ” **Zero-Knowledge Proofs**: Prove investment suitability using Circom circuits @@ -29,6 +39,14 @@ Rayls Hook implements a comprehensive investor suitability assessment system tha - πŸ›‘οΈ **Privacy-First**: Never reveal private questionnaire data - ⚑ **On-Chain Verification**: Smart contract verification of ZK proofs +πŸ” Private Swap Logic + +- βœ… Encrypted Commitments: Users (or backend services) create encrypted swap commitments +- ⏳ Deferred Execution: Commitments become executable only after a timestamp +- πŸ” ZK Proof of Intent: Execution requires a zkSNARK proof proving knowledge of commitment id +- πŸ“‘ Auditor Access: Commitments include encrypted values for auditors to decrypt +- πŸͺ Uniswap v4 Integration: Hook contract executes swaps using permit + safe transfer logic + ## πŸ—οΈ Architecture ### System Components @@ -44,25 +62,31 @@ Rayls Hook implements a comprehensive investor suitability assessment system tha β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` +- Suitability Verifier Logic lives in circuits + verifier contracts + +- Private Swap Logic lives in the hook contracts + zk circuits + auditor encrypt/decrypt scripts. + ### Technology Stack -| Layer | Technology | Purpose | -|-------|------------|---------| -| **ZK Layer** | Circom + SnarkJS | Zero-knowledge proof generation | -| **Smart Contracts** | Solidity + Foundry | On-chain verification | -| **Frontend** | NextJS + Scaffold-ETH 2 | User interface | -| **Integration** | Uniswap v4 Hooks | DEX integration | -| **Development** | TypeScript + Wagmi | Type-safe development | +| Layer | Technology | Purpose | +| ------------------- | ----------------------- | ------------------------------- | +| **ZK Layer** | Circom + SnarkJS | Zero-knowledge proof generation | +| **Smart Contracts** | Solidity + Foundry | On-chain verification | +| **Frontend** | NextJS + Scaffold-ETH 2 | User interface | +| **Integration** | Uniswap v4 Hooks | DEX integration | +| **Development** | TypeScript + Wagmi | Type-safe development | ### Circuit Architecture #### Suitability Assessment Circuit + - **Private Inputs**: 5 questionnaire responses (0-3 scale) - **Public Inputs**: Risk threshold and calculated profile - **Output**: Suitability verification (0 or 1) #### Private Swap Intent Circuit -- **Private Inputs**: Amount, direction, sender, timestamp + +- **Private Inputs**: amountIn, zeroForOne, sender, timestamp - **Public Outputs**: Commitment hash and verification data - **Purpose**: Prove swap intent without revealing sensitive details @@ -75,6 +99,13 @@ Before you begin, you need to install the following tools: - [Git](https://git-scm.com/downloads) - [Circom](https://docs.circom.io/getting-started/installation/) (for ZK circuits) - [SnarkJS](https://github.com/iden3/snarkjs) (for ZK proofs) +- [Foundry](https://getfoundry.sh/introduction/installation/) + +To better run circom you might need to add this to your PATH: + +```bash +export PATH=$PATH:$HOME/.cargo/bin +``` ## πŸš€ Quick Start @@ -85,7 +116,7 @@ To get started with Rayls Hook, follow these steps: ```bash # Clone the repository git clone https://github.com/raylsnetwork/uniswap-incubator.git -cd rayls-hook +cd uniswap-incubator # Install all dependencies yarn install @@ -95,7 +126,7 @@ yarn install ```bash # Start local Ethereum network (Scaffold-ETH 2) -yarn chain +yarn workspace @se-2/foundry chain ``` This command starts a local Ethereum network using Foundry. The network runs on your local machine and can be used for testing and development. @@ -103,19 +134,16 @@ This command starts a local Ethereum network using Foundry. The network runs on ### 3. Setup Zero-Knowledge Circuits ```bash -# Setup ZK circuits and generate proofs -yarn setup - # Or setup specific circuits -yarn setup-suitability # Suitability assessment circuit -yarn setup-private-swap # Private swap intent circuit +yarn workspace rayls-hook-circom setup-suitability # Suitability assessment circuit +yarn workspace rayls-hook-circom setup-private-swap # Private swap intent circuit ``` ### 4. Deploy Smart Contracts ```bash # Deploy contracts to local network -yarn deploy +yarn workspace @se-2/foundry deploy ``` This command deploys the Rayls Hook smart contracts to the local network, including the ZK verifiers and Uniswap v4 hooks. @@ -124,27 +152,41 @@ This command deploys the Rayls Hook smart contracts to the local network, includ ```bash # Start the NextJS frontend -yarn start +yarn workspace @se-2/nextjs start ``` Visit your app on: `http://localhost:3000`. You can interact with the suitability assessment and test the ZK proof verification. +### 6. Running tests + +```bash +yarn workspace @se-2/foundry test +``` + +### 7. Check coverage + +(We focused on RaylsHook contract for full coverage) + +```bash +yarn workspace @se-2/foundry coverage +``` + ## πŸ› οΈ Development ### Available Commands -| Command | Description | -|---------|-------------| -| `yarn chain` | Start local blockchain | -| `yarn deploy` | Deploy smart contracts | -| `yarn start` | Start frontend | -| `yarn setup` | Setup ZK circuits (default: Suitability) | -| `yarn prove` | Generate new ZK proof | -| `yarn setup-suitability` | Setup Suitability circuit | -| `yarn prove-suitability` | Generate Suitability proof | -| `yarn setup-private-swap` | Setup PrivateSwapIntent circuit | -| `yarn prove-private-swap` | Generate PrivateSwapIntent proof | -| `yarn test` | Run tests | +| Command | Description | +| ------------------------- | ---------------------------------------- | +| `yarn chain` | Start local blockchain | +| `yarn deploy` | Deploy smart contracts | +| `yarn start` | Start frontend | +| `yarn setup` | Setup ZK circuits (default: Suitability) | +| `yarn prove` | Generate new ZK proof | +| `yarn setup-suitability` | Setup Suitability circuit | +| `yarn prove-suitability` | Generate Suitability proof | +| `yarn setup-private-swap` | Setup PrivateSwapIntent circuit | +| `yarn prove-private-swap` | Generate PrivateSwapIntent proof | +| `yarn test` | Run tests | ### Project Structure @@ -181,26 +223,34 @@ yarn prove-private-swap ## πŸ“‹ Roadmap ### Phase 1: Core Infrastructure βœ… + - [x] ZK circuits implementation (Suitability + PrivateSwapIntent) - [x] Smart contract verifiers - [x] Basic Uniswap v4 hook integration - [x] ZK proof generation and verification pipeline +- [x] Auditor encryption feature +- [x] Multiple tests ### Phase 2: Frontend Development 🚧 -- [ ] Complete questionnaire UI implementation + +- [ ] Complete UI + BE implementation - [ ] ZK proof generation interface - [ ] Real-time proof verification - [ ] User dashboard and profile management - [ ] Integration with wallet providers ### Phase 3: Advanced Features πŸ“‹ + - [ ] Multi-circuit support and management +- [ ] Private Swap multi-auditors support and management +- [ ] Private Swap multi-executors support and management - [ ] Advanced risk assessment algorithms - [ ] Compliance and regulatory features - [ ] Integration with external KYC providers - [ ] Mobile-responsive design ### Phase 4: Production Ready 🎯 + - [ ] Security audits and testing - [ ] Performance optimization - [ ] Documentation and tutorials @@ -247,4 +297,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file --- -**Note**: This project is part of the Uniswap Hook Incubator 6 program. For production use, consider security audits and additional compliance requirements. \ No newline at end of file +**Note**: This project is part of the Uniswap Hook Incubator 6 program. For production use, consider security audits and additional compliance requirements. diff --git a/docs/RaylsHook_private_swaps_block_diagram.svg b/docs/RaylsHook_private_swaps_block_diagram.svg new file mode 100644 index 0000000..11099fe --- /dev/null +++ b/docs/RaylsHook_private_swaps_block_diagram.svg @@ -0,0 +1 @@ +

Start
Private Swap Flow

User

RAYLS Middleware

RAYLS Uniswap Hook

Pool Manager

Auditor (optional)

creates swap commitment

executes swap

reads ciphertext

executeCommitment

storeCommitment

cancelCommitment

\ No newline at end of file diff --git a/docs/RaylsHook_private_swaps_diagram.svg b/docs/RaylsHook_private_swaps_diagram.svg new file mode 100644 index 0000000..22e6dde --- /dev/null +++ b/docs/RaylsHook_private_swaps_diagram.svg @@ -0,0 +1,102 @@ +AuditorRayls HookRayls MiddlewareUserAuditorRayls HookRayls MiddlewareUserhas (sk = a, pk = aG)knows auditor pkSigns a permit message <hookAddress, amount>checks permitm = < amount, direction, sender, timestamp >generates a zkSNARK proof zk, outputs poseidon hash ph = Hash(m)ciphertext = ENC(m)commitmentId = keccak256(ciphertext, ph)m = DEC(sk, ciphertext)Monitors timestampVerifies zkSNARK proof validity.Verifies if commitmentId matches zk public signal.Verifies timestamp >= block.timestampExecutes permitEecutes swap via Uniswap v4 `PoolManager`.Settles balances on callbacktrigger private swap req = <amount, direction, sender, timestamp, permit>calls storeCommitment<poolKey, commitmentId, ciphertext, permit>new commitment: < ciphertext >User can cancel the commitmentcalls cancelCommitment<poolKey, commitmentId, zk>calls executeCommitment<poolKey, commitmentId, zk> diff --git a/docs/RaylsHook_suitability_diagram.png b/docs/RaylsHook_suitability_diagram.png new file mode 100644 index 0000000..d701e20 Binary files /dev/null and b/docs/RaylsHook_suitability_diagram.png differ diff --git a/docs/SuitabilityQuestionaire.png b/docs/SuitabilityQuestionaire.png new file mode 100644 index 0000000..950de02 Binary files /dev/null and b/docs/SuitabilityQuestionaire.png differ diff --git a/docs/privateSwaps.md b/docs/privateSwaps.md new file mode 100644 index 0000000..21a96b1 --- /dev/null +++ b/docs/privateSwaps.md @@ -0,0 +1,92 @@ +## πŸ“„ `docs/privateSwaps.md` + +πŸ” Private Swap Commitments + +This Uniswap v4 hook extension introduces private swaps that allow users to conceal their swap parameters until an execution timestamp is reached. Hidden swap values are committed on-chain via a unique commitment ID, then at execution time they are revealed and validated using zkSNARK proofs. Swap details are also encrypted using the Auditor’s public key and the generated ciphertext is stored on-chain, enabling independent verification at any time. The commitment Id is the result of running the cryptographic function keccak256 against the auditor ciphertext and the poseidon hash generated by the zk snark proof. + +Key use cases include: + +- MEV protection – hiding swap intent reduces frontrunning risk. + +- Price impact mitigation for large swaps – users executing large trades can split them into multiple commitments to minimize token price impact in the pool. + +- Compliance & oversight – DAOs and regulated protocols can prove onchain to an auditor the agreed swap schedules according to the defined tokenomics. + +

+ Private Swap Block Diagram +

+ +## πŸ”„ Flow + +

+ Private Swap Diagram +

+ +### 1. Create Commitment + +- **User** through a UI: + + - Creates a swap commitment by defining amountIn, direction, timestamp. + - Signs and sends along an ERC20 permit + +- **Rayls Middleware** + + - Encrypts swap params using Auditor's pub key (file encrypt.js). + - Creates and holds zkSNARK proofs of knowledge of swap params for commitment `id`.. + - Generates commitment id using Auditor's encryption + Poseidon hash from zk proof. + - Calls `storeCommitment(id, ciphertext, permit)` with: + - `id`: unique hash of the commitment. + - `ciphertext`: encrypted swap details (amount, direction, timestamp). + - `permit`: ERC20 permit signature. + - Contract records commitment and emits `CommitmentStored`. + +### 2. Execute Commitment + +- **Rayls Middleware** + + - Monitors for commitments with expired timestamps. + - Triggers commitment execution when timestamp is reached + - When time is reached, calls `executeCommitment(id, zkProof)`. + +- **Rayls Hook** + + - Verifies: + - zkSNARK proof validity. + - Commitment matches proof. + - Permit authorizes token pull. + - Contract executes swap via Uniswap v4 `PoolManager`. + - Settles balances on callback + - Emits `CommitmentExecuted`. + +### 3. Cancel Commitment + +- **User** + - Triggers a commitment cancellation through a UI, before execution +- **Rayls Middleware** + - Calls `cancelCommitment(id, zkProof)`. +- **Rayls Hook** + - Marks commitment as canceled, clears heavy storage. + - Emits `CommitmentCanceled`. + +### 4. Auditor Flow + +- **Auditor** can always: + + - Read `ciphertext` onchain. + - Decrypt using it's own private key (file decrypt.js). + - Verify swap parameters offchain for compliance. + - Validates if permit matches the encrypted values + +--- + +## Key notes + +- We use circom for zkSNARK and ECIES encryption for the auditor (using nodejs scripts). Encryption in circom is too expensive. +- We could enforce the auditor to approve a commitement cancelation. +- When calling executeCommitment, Rayls Middleware would use private bundlers for additional MEV protection. + +## Future Extensions and Use Cases + +- Decentralized Executors: Anyone could register as an executor and earn a percentage of swap fees for executing commitments, creating an open marketplace of executors. +- Auditor-Gated Cancellation: Cancellation requests could require auditor approval, preventing users from revoking commitments that already passed compliance checks. +- Multi-Auditor Support: Commitments could be associated with multiple auditors, allowing collaborative oversight or redundancy in regulatory validation. diff --git a/docs/suitability.md b/docs/suitability.md new file mode 100644 index 0000000..4e77327 --- /dev/null +++ b/docs/suitability.md @@ -0,0 +1,55 @@ +## πŸ›‘οΈ Suitability Verifier Logic + +The Suitability Verifier Logic ensures that **investors can prove compliance** with regulatory requirements without revealing their raw questionnaire responses. This is done using a **zkSNARK proof**. + +

+ Suitability Questionnaire +

+ +## πŸ”„ Flow + +

+ Suitability Diagram +

+ +1. **Investor (User)** + + - Fills out a suitability questionnaire offchain. + - FE (frontend) generates a commitment hash of the answers. + - BE (backend) computes zkSNARK proof: + - Shows that the answers meet required conditions. + - Keeps raw answers private. + +2. **Frontend (FE)** + + - Collects user answers. + - Passes them securely to BE. + - Displays status to user (proof verified or not). + +3. **Backend (BE)** + + - Generates proof using Circom circuit. + - Sends proof + public signals to smart contract. + +4. **Smart Contract (Verifier)** + - Receives proof + signals. + - Runs `verifyProof(...)` onchain. + - Marks user as "suitable" if proof is valid. + +--- + +## πŸ” Onchain Verification + +The contract only verifies: + +- Proof validity. +- Public signals match expected format (e.g., user address, KYC hash). + +Raw answers **never leave the BE** and are not revealed onchain. + +--- + +## πŸ“‹ Events + +- `SuitabilityVerified(address indexed user)` – user passed the suitability check. +- `SuitabilityRevoked(address indexed user)` – regulator/admin revoked suitability. diff --git a/packages/circom/README.md b/packages/circom/README.md index dc519bd..e26753b 100644 --- a/packages/circom/README.md +++ b/packages/circom/README.md @@ -1,47 +1,40 @@ -# Suitability Assessment with Zero-Knowledge Proofs +# Zero-Knowledge Proofs -This project implements an investor suitability assessment system using Zero-Knowledge Proofs (ZKP) with Circom and SnarkJS. The system allows a user to prove they have an adequate risk profile without revealing their specific questionnaire responses. - -## 🎯 Objective - -The goal is to create a system where: -- A user answers 5 suitability questions -- Each answer has a specific weight -- The system calculates a risk profile (0-10) -- The user can prove their profile meets a minimum threshold -- **Without revealing their specific responses** +This project implements an investor suitability assessment system and private swaps using Zero-Knowledge Proofs (ZKP) with Circom and SnarkJS. The system allows a user to prove they have an adequate risk profile without revealing their specific questionnaire responses. ## πŸ—οΈ Architecture -### Available Circuits - The project includes two main circuits: -#### 1. Suitability Circuit (`circuits/Suitability.circom`) +### 1. Suitability Circuit (`circuits/Suitability.circom`) The suitability assessment circuit implements: + - **Private inputs**: 5 questionnaire responses (0-3 each) - **Public inputs**: minimum threshold and calculated risk profile - **Public output**: indicates if the profile meets the threshold (0 or 1) -#### 2. Private Swap Intent Circuit (`circuits/PrivateSwapIntent.circom`) +#### Objective -The private swap intent circuit implements: -- **Private inputs**: amount, direction, sender, timestamp -- **Public outputs**: commitment hash and public parameters -- **Purpose**: Prove swap intent without revealing sensitive details +The goal is to create a system where: + +- A user answers 5 suitability questions +- Each answer has a specific weight +- The system calculates a risk profile (0-10) +- The user can prove their profile meets a minimum threshold +- **Without revealing their specific responses** -### Question Weights +#### Question Weights -| Question | Weight | Description | -|----------|--------|-------------| -| 1 | 2 | Investment experience | -| 2 | 3 | Risk tolerance | -| 3 | 2 | Time horizon | -| 4 | 1 | Financial objectives | -| 5 | 2 | Market knowledge | +| Question | Weight | Description | +| -------- | ------ | --------------------- | +| 1 | 2 | Investment experience | +| 2 | 3 | Risk tolerance | +| 3 | 2 | Time horizon | +| 4 | 1 | Financial objectives | +| 5 | 2 | Market knowledge | -### Risk Profile Calculation +#### Risk Profile Calculation ``` weightedSum = answer1*2 + answer2*3 + answer3*2 + answer4*1 + answer5*2 @@ -49,6 +42,14 @@ maxPossibleScore = 4 * (2+3+2+1+2) = 40 riskProfile = (weightedSum * 10) / maxPossibleScore ``` +### 2. Private Swap Intent Circuit (`circuits/PrivateSwapIntent.circom`) + +The private swap intent circuit implements: + +- **Private inputs**: amount, direction, sender, timestamp +- **Public outputs**: poseidon hash and public parameters +- **Purpose**: Prove it knows valid private inputs (swap params) that hash to the poseidon hash, without revealing sensitive details + ## πŸš€ Installation and Setup ### Prerequisites @@ -123,12 +124,14 @@ The `zk_pipeline.sh` script handles the entire ZK workflow: ### 2. What the Pipeline Does #### Compilation Phase: + - Compiles the Circom circuit - Generates R1CS constraints - Creates WASM and JavaScript files - Generates circuit symbols #### Setup Phase (--force-setup): + - Generates Powers of Tau (Phase 1) - Contributes to Powers of Tau - Prepares Phase 2 @@ -138,6 +141,7 @@ The `zk_pipeline.sh` script handles the entire ZK workflow: - Generates Solidity verifier contract #### Proof Generation: + - Calculates circuit witness - Generates ZK proof - Verifies proof off-chain @@ -148,6 +152,7 @@ The `zk_pipeline.sh` script handles the entire ZK workflow: The pipeline creates all necessary files: ``` +Example for Suitability: artifacts/ β”œβ”€β”€ Suitability.r1cs # Circuit constraints β”œβ”€β”€ Suitability_js/ # JavaScript witness calculator @@ -173,26 +178,25 @@ scripts/ # Input files β”œβ”€β”€ solidityInputs.json # Proof data for contract calls β”œβ”€β”€ solidityInputs.decimal.json # Decimal format β”œβ”€β”€ solidityInputs.ui.json # UI format -β”œβ”€β”€ solidityCalldata.txt # Raw calldata -└── cast_call.sh # Cast call script +└── solidityCalldata.txt # Raw calldata ``` ## πŸ”§ Available Scripts -| Script | Description | -|--------|-------------| -| `yarn compile` | Compiles the Circom circuit only | -| `yarn setup` | Complete ZK setup for Suitability (default) | -| `yarn prove` | Generate new proof for Suitability (reuses existing setup) | -| `yarn test` | Runs system tests | +| Script | Description | +| -------------- | ---------------------------------------------------------- | +| `yarn compile` | Compiles the Circom circuit only | +| `yarn setup` | Complete ZK setup for Suitability (default) | +| `yarn prove` | Generate new proof for Suitability (reuses existing setup) | +| `yarn test` | Runs system tests | ### Circuit-Specific Commands -| Script | Description | -|--------|-------------| -| `yarn setup-suitability` | Complete ZK setup for Suitability circuit | -| `yarn prove-suitability` | Generate new proof for Suitability circuit | -| `yarn setup-private-swap` | Complete ZK setup for PrivateSwapIntent circuit | +| Script | Description | +| ------------------------- | ------------------------------------------------ | +| `yarn setup-suitability` | Complete ZK setup for Suitability circuit | +| `yarn prove-suitability` | Generate new proof for Suitability circuit | +| `yarn setup-private-swap` | Complete ZK setup for PrivateSwapIntent circuit | | `yarn prove-private-swap` | Generate new proof for PrivateSwapIntent circuit | ## πŸ§ͺ Tests @@ -204,8 +208,9 @@ yarn test ``` Tests scenarios such as: + - Low risk profile -- Medium risk profile +- Medium risk profile - High risk profile - Different thresholds - Constraint validation @@ -218,18 +223,19 @@ The Suitability circuit requires specific input format with questionnaire respon ```json { - "answer1": 3, // Investment experience (0-3) - "answer2": 2, // Risk tolerance (0-3) - "answer3": 1, // Time horizon (0-3) - "answer4": 2, // Financial objectives (0-3) - "answer5": 3, // Market knowledge (0-3) + "answer1": 3, // Investment experience (0-3) + "answer2": 2, // Risk tolerance (0-3) + "answer3": 1, // Time horizon (0-3) + "answer4": 2, // Financial objectives (0-3) + "answer5": 3, // Market knowledge (0-3) "wallet": "0x1234567890AbcdEF1234567890aBcdef12345678", // User wallet address "thresholdScaled": 20, // Minimum threshold (0-100, where 100 = 10.0) - "isSuitablePub": 1 // Expected result (0=Unsuitable, 1=Suitable) + "isSuitablePub": 1 // Expected result (0=Unsuitable, 1=Suitable) } ``` #### Field Descriptions: + - **answer1-5**: Questionnaire responses (0-3 scale) - `0`: Lowest risk/conservative option - `3`: Highest risk/aggressive option @@ -243,14 +249,15 @@ The PrivateSwapIntent circuit requires swap parameters and commitment data: ```json { - "amountIn": "100", // Amount to swap (string for large numbers) - "zeroForOne": "1", // Swap direction (0=Token1β†’Token0, 1=Token0β†’Token1) + "amountIn": "100", // Amount to swap (string for large numbers) + "zeroForOne": "1", // Swap direction (0=Token1β†’Token0, 1=Token0β†’Token1) "sender": "0x1234567890AbcdEF1234567890aBcdef12345678", // Sender address - "timestamp": "1697052800" // Unix timestamp of swap intent + "timestamp": "1697052800" // Unix timestamp of swap intent } ``` #### Field Descriptions: + - **amountIn**: Swap amount (use string format for large numbers) - **zeroForOne**: Direction flag (0 or 1) - **sender**: Address of the swap initiator @@ -274,22 +281,35 @@ yarn prove-private-swap #### Example Scenarios: **Suitable Profile (High Risk Tolerance):** + ```json { - "answer1": 3, "answer2": 3, "answer3": 2, "answer4": 3, "answer5": 3, - "thresholdScaled": 15, "isSuitablePub": 1 + "answer1": 3, + "answer2": 3, + "answer3": 2, + "answer4": 3, + "answer5": 3, + "thresholdScaled": 15, + "isSuitablePub": 1 } ``` **Unsuitable Profile (Low Risk Tolerance):** + ```json { - "answer1": 0, "answer2": 0, "answer3": 0, "answer4": 0, "answer5": 0, - "thresholdScaled": 25, "isSuitablePub": 0 + "answer1": 0, + "answer2": 0, + "answer3": 0, + "answer4": 0, + "answer5": 0, + "thresholdScaled": 25, + "isSuitablePub": 0 } ``` **Large Swap Intent:** + ```json { "amountIn": "1000000000000000000", // 1 ETH in wei @@ -336,12 +356,14 @@ const proofData = require('../foundry/solidityInputs.json'); ## πŸ›‘οΈ Security ### Privacy + - **Private responses**: Never revealed - **Calculated profile**: Can be public - **Threshold**: Can be public - **Result**: Can be public ### Validation + - Constraints ensure valid responses (0-3) - Consistency verification between profile and threshold - Boundary validation (0-10) @@ -351,11 +373,13 @@ const proofData = require('../foundry/solidityInputs.json'); To integrate with the frontend: 1. **Run the ZK pipeline**: + ```bash yarn setup ``` 2. **Deploy the contract**: + ```bash cd ../foundry forge build @@ -363,16 +387,19 @@ forge script script/DeploySuitabilityVerifier.s.sol --rpc-url http://localhost:8 ``` 3. **Use Scaffold-ETH hooks**: + ```typescript -const { writeContractAsync: verifySuitabilityAsync } = useScaffoldWriteContract({ - contractName: "SuitabilityVerifier" -}); +const { writeContractAsync: verifySuitabilityAsync } = useScaffoldWriteContract( + { + contractName: "SuitabilityVerifier", + } +); // Verify suitability using generated proof data -const proofData = require('../foundry/solidityInputs.json'); +const proofData = require("../foundry/solidityInputs.json"); await verifySuitabilityAsync({ functionName: "verifyProof", - args: [proofData[0], proofData[1], proofData[2], proofData[3]] + args: [proofData[0], proofData[1], proofData[2], proofData[3]], }); ``` @@ -399,10 +426,11 @@ MIT License - see the [LICENSE](../LICENSE) file for details. ## πŸ†˜ Support For questions or issues: + 1. Check the documentation 2. Run the tests 3. Open a GitHub issue --- -**Note**: This is an educational project. For production use, consider security audits and more robust implementations. \ No newline at end of file +**Note**: This is an educational project. For production use, consider security audits and more robust implementations. diff --git a/packages/circom/package.json b/packages/circom/package.json index 62f390e..e5945d0 100644 --- a/packages/circom/package.json +++ b/packages/circom/package.json @@ -1,5 +1,5 @@ { - "name": "suitability-zk", + "name": "rayls-hook-circom", "version": "1.0.0", "description": "Zero-Knowledge Suitability Assessment System", "main": "index.js", diff --git a/packages/circom/scripts/Suitability_input.example.json b/packages/circom/scripts/Suitability_input.example.json index 568b45b..6560639 100644 --- a/packages/circom/scripts/Suitability_input.example.json +++ b/packages/circom/scripts/Suitability_input.example.json @@ -4,7 +4,7 @@ "answer3": 1, "answer4": 2, "answer5": 3, - "wallet": "0x1234567890AbcdEF1234567890aBcdef12345678", + "wallet": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "thresholdScaled": 20, "isSuitablePub": 1 } diff --git a/packages/circom/scripts/Suitability_input.json b/packages/circom/scripts/Suitability_input.json index d1965f0..6560639 100644 --- a/packages/circom/scripts/Suitability_input.json +++ b/packages/circom/scripts/Suitability_input.json @@ -1,12 +1,10 @@ { - "answer1": 3, - "answer2": 2, - "answer3": 1, - "answer4": 2, - "answer5": 3, - "wallet": "0x1234567890AbcdEF1234567890aBcdef12345678", - "thresholdScaled": 20, - "isSuitablePub": 1 + "answer1": 3, + "answer2": 2, + "answer3": 1, + "answer4": 2, + "answer5": 3, + "wallet": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "thresholdScaled": 20, + "isSuitablePub": 1 } - - \ No newline at end of file diff --git a/packages/encryption/decrypt.cjs b/packages/encryption/decrypt.cjs index 2f04726..41f66ea 100644 --- a/packages/encryption/decrypt.cjs +++ b/packages/encryption/decrypt.cjs @@ -7,17 +7,14 @@ const eccrypto = require("eccrypto"); const crypto = require("crypto"); const { buildPoseidon } = require("circomlibjs"); - - // ECIES encrypted symmetric key (from encrypt.js) -if (process.argv.length !== 5) { - console.error("Usage: node decrypt.cjs "); +if (process.argv.length !== 4) { + console.error("Usage: node decrypt.cjs "); process.exit(1); } const auditorPrivKey = process.argv[2]; -const ciphertextHex = process.argv[3]; -const encKeyForAuditor = process.argv[4]; +const encKeyForAuditor = process.argv[3]; // Auditor private key (Buffer) const auditorPriv = Buffer.from( @@ -25,7 +22,6 @@ const auditorPriv = Buffer.from( "hex" ); -const ciphertextBuffer = Buffer.from(ciphertextHex.replace(/^0x/, ""), "hex"); const encKeyForAuditorBuffer = Buffer.from(encKeyForAuditor.replace(/^0x/, ""), "hex"); // ---------------------- @@ -44,18 +40,6 @@ async function decryptSymmetricKey(encKeyForAuditorBuffer, auditorPriv) { return K; } -// 2️⃣ Decrypt AES-GCM ciphertext -function decryptMessage(K, ciphertextBuffer) { - const iv = ciphertextBuffer.slice(0, 12); - const tag = ciphertextBuffer.slice(12, 28); // 16 bytes tag - const enc = ciphertextBuffer.slice(28); - - const decipher = crypto.createDecipheriv("aes-256-gcm", K, iv); - decipher.setAuthTag(tag); - const plaintext = Buffer.concat([decipher.update(enc), decipher.final()]); - return plaintext; -} - // 3️⃣ Parse plaintext buffer into circuit inputs function parseMessage(plaintext) { let offset = 0; @@ -97,13 +81,10 @@ async function computeCommitmentId(amountIn, zeroForOne, sender, timestamp) { // ---------------------- (async () => { // Recover symmetric key - const K = await decryptSymmetricKey(encKeyForAuditorBuffer, auditorPriv); - - // Decrypt message - const plaintext = decryptMessage(K, ciphertextBuffer); + const decryptedMessage = await decryptSymmetricKey(encKeyForAuditorBuffer, auditorPriv); // Parse back to circuit inputs - const parsed = parseMessage(plaintext); + const parsed = parseMessage(decryptedMessage); // Compute commitment ID const commitmentId = await computeCommitmentId( diff --git a/packages/encryption/encrypt.js b/packages/encryption/encrypt.js index 4bc8678..7d16ab7 100644 --- a/packages/encryption/encrypt.js +++ b/packages/encryption/encrypt.js @@ -34,32 +34,21 @@ async function main() { const message = Buffer.concat([amountBuf, zeroForOneBuf, senderBuf, timestampBuf]); - // Symmetric key K - const K = crypto.randomBytes(32); - - // Encrypt the message with AES-GCM - const ciphertext = aesGcmEncrypt(K, message); - // Encrypt K with auditor’s public key (ECIES) - const encKeyForAuditor = await eccrypto.encrypt( + const encForAuditor = await eccrypto.encrypt( Buffer.from(pubKeyUncompressed.slice(2), "hex"), // drop 0x - K + message ); const encryptedBuffer = Buffer.concat([ - encKeyForAuditor.iv, // 16 bytes - encKeyForAuditor.ephemPublicKey, // 65 bytes - encKeyForAuditor.ciphertext, // variable - encKeyForAuditor.mac // 32 bytes + encForAuditor.iv, // 16 bytes + encForAuditor.ephemPublicKey, // 65 bytes + encForAuditor.ciphertext, // variable + encForAuditor.mac // 32 bytes ]); - // Convert to BytesLike - const encKeyForAuditorBytes = ethers.getBytes(encryptedBuffer); - const ciphertextBytes = ethers.getBytes("0x" + ciphertext.toString("hex")); - const jsonData = { - encKeyForAuditor: ethers.hexlify(encKeyForAuditorBytes), - ciphertext: ethers.hexlify(ciphertextBytes) + ciphertextForAuditor: ethers.hexlify(encryptedBuffer) }; await fs.writeFile("../foundry/inputs/encryptedPayload.json", JSON.stringify(jsonData, null, 2)); diff --git a/packages/foundry/contracts/RaylsHook.sol b/packages/foundry/contracts/RaylsHook.sol index 4291688..e286f08 100644 --- a/packages/foundry/contracts/RaylsHook.sol +++ b/packages/foundry/contracts/RaylsHook.sol @@ -13,9 +13,19 @@ import { SuitabilityVerifier } from "./SuitabilityVerifier.sol"; import { console } from "forge-std/console.sol"; import { PrivateSwapIntentVerifier } from "./PrivateSwapIntentVerifier.sol"; +import { BalanceDelta } from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import { Currency } from "@uniswap/v4-core/src/types/Currency.sol"; +import { IUnlockCallback } from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { console } from "forge-std/console.sol"; -contract RaylsHook is BaseHook { +contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard { using PoolIdLibrary for PoolKey; + using SafeERC20 for IERC20; // NOTE: --------------------------------------------------------- // state variables should typically be unique to a pool @@ -30,16 +40,34 @@ contract RaylsHook is BaseHook { SuitabilityVerifier public suitabilityVerifier; PrivateSwapIntentVerifier public privateSwapIntentVerifier; - mapping(uint256 => Commitment) public commitments; + mapping(PoolId poolId => mapping(uint256 => Commitment)) public commitments; + + event CommitmentStored(uint256 indexed id, address indexed sender, bytes, bytes); + event CommitmentExecuted(uint256 indexed id, address indexed sender); + event CommitmentCancelled(uint256 indexed id, address indexed canceller); - event CommitmentStored(uint256 id, address indexed sender); - event Revealed(uint256 id, address indexed revealer); + // Errors + error CommitmentMismatch(bytes32 pubId, uint256 expectedId); + error CommitmentNotReady(uint256 notBefore, uint256 currentTime); + error InvalidWallet(address provided, address expected); + error InvalidSuitabilityProof(); + error InvalidPrivateSwapIntentProof(); + error AlreadyExists(uint256 id); + error CommitmentNotActive(uint256 id); + error CommitmentNotFound(uint256 id); + + enum CommitmentStatus { + None, // default, not stored + Active, // stored but not yet executed + Executed, // executed successfully + Cancelled // canceled by creator + + } struct Commitment { - bytes ciphertext; // AES/GCM ciphertext (includes tag) - bytes encKeyForAuditor; // encrypted symmetric key for auditor - bool exists; - bool executed; + bytes ciphertextForAuditor; //ECIES-encrypted swap details for auditor + bytes permit; + CommitmentStatus status; } constructor(IPoolManager _poolManager, address _suitabilityVerifier, address _privateSwapIntentVerifier) @@ -53,12 +81,12 @@ contract RaylsHook is BaseHook { return Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, - beforeAddLiquidity: true, + beforeAddLiquidity: false, afterAddLiquidity: false, - beforeRemoveLiquidity: true, + beforeRemoveLiquidity: false, afterRemoveLiquidity: false, beforeSwap: true, - afterSwap: true, + afterSwap: false, beforeDonate: false, afterDonate: false, beforeSwapReturnDelta: false, @@ -68,10 +96,10 @@ contract RaylsHook is BaseHook { }); } - // ----------------------------------------------- - // NOTE: see IHooks.sol for function documentation - // ----------------------------------------------- - + /** + * Here we verify the suitability proof and that the wallet in the proof matches the transaction origin + * + */ function _beforeSwap(address, PoolKey calldata key, SwapParams calldata, bytes calldata data) internal override @@ -81,40 +109,228 @@ contract RaylsHook is BaseHook { abi.decode(data, (uint256[2], uint256[2][2], uint256[2], uint256[5])); uint256 walletInProof = pubSignals[2]; // index 2 because it's the 3rd public signal + + // We want to identify the originator of the transaction address origin = _determineOrigin(msg.sender); - require(walletInProof == uint256(uint160(origin)), "Invalid wallet for this proof"); + // Verify the wallet address in the proof matches the transaction origin + if (walletInProof != uint256(uint160(origin))) { + revert InvalidWallet(address(uint160(walletInProof)), origin); + } + // Verify the Suitability proof bool suitabilityOk = suitabilityVerifier.verifyProof(pA, pB, pC, pubSignals); - require(suitabilityOk, "Invalid Suitability proof"); + if (!suitabilityOk) { + revert InvalidSuitabilityProof(); + } return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } - function _afterSwap(address, PoolKey calldata key, SwapParams calldata, BalanceDelta, bytes calldata) - internal - override - returns (bytes4, int128) - { - return (BaseHook.afterSwap.selector, 0); + /** + * @notice Stores a new encrypted swap commitment onchain. + * @dev Each commitment is uniquely identified by a commitment `id` under a specific pool key. + * Reverts if a commitment with the same `id` already exists. + * The ciphertextForAuditor allows a designated auditor to decrypt the swap parameters offchain. + * @param key Pool key identifying the Uniswap v4 pool this commitment belongs to. + * @param commitmentId Unique identifier for the commitment (Poseidon/keccak hash). + * @param ciphertextForAuditor Encrypted swap data for the auditor + * @param permit ERC20 permit signature data, allowing token transfers at execution. + * Emits a {CommitmentStored} event. + */ + function storeCommitment( + PoolKey calldata key, + uint256 commitmentId, + bytes calldata ciphertextForAuditor, + bytes calldata permit + ) external { + if (commitments[key.toId()][commitmentId].status != CommitmentStatus.None) { + revert AlreadyExists(commitmentId); + } + + commitments[key.toId()][commitmentId] = + Commitment({ ciphertextForAuditor: ciphertextForAuditor, permit: permit, status: CommitmentStatus.Active }); + emit CommitmentStored(commitmentId, msg.sender, ciphertextForAuditor, permit); } - function _beforeAddLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata) - internal - override - returns (bytes4) - { - beforeAddLiquidityCount[key.toId()]++; - return BaseHook.beforeAddLiquidity.selector; + /** + * @notice Cancels a previously stored commitment before execution. + * @dev Marks the commitment as canceled so it cannot be executed. + * Reverts if the commitment does not exist, was already executed, or already canceled. + * Large storage fields may be cleared to save gas, but the status is retained for auditability. + * @param key Pool key identifying the Uniswap v4 pool this commitment belongs to. + * @param commitmentId Unique identifier of the commitment to cancel. + * @param zkSnarkProof ABI-encoded zkSNARK proof data (pA, pB, pC, pubSignals) to authorize the cancellation. + * Emits a {CommitmentCancelled} event. + */ + function cancelCommitment(PoolKey calldata key, uint256 commitmentId, bytes calldata zkSnarkProof) external { + Commitment storage c = commitments[key.toId()][commitmentId]; + if (c.status != CommitmentStatus.Active) { + revert CommitmentNotActive(commitmentId); + } + + (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) = + abi.decode(zkSnarkProof, (uint256[2], uint256[2][2], uint256[2], uint256[5])); + + // Making sure we are executing the right commitment + bytes32 commitmentIdFromZK = getCommitmentId(pubSignals[0], c.ciphertextForAuditor); + + if (commitmentIdFromZK != bytes32(commitmentId)) { + revert CommitmentMismatch(commitmentIdFromZK, commitmentId); + } + + bool privateVerifierOk = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals); + if (!privateVerifierOk) { + revert InvalidPrivateSwapIntentProof(); + } + + // We can cancel now + c.status = CommitmentStatus.Cancelled; + delete c.ciphertextForAuditor; + delete c.permit; + emit CommitmentCancelled(commitmentId, msg.sender); } - function _beforeRemoveLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata) - internal - override - returns (bytes4) + /** + * @notice Executes a previously stored encrypted swap commitment once its conditions are met. + * @dev Verifies a zkSNARK proof to ensure the executor knows the swap commitment’s plaintext + * and that the onchain commitment id is the result of the provided proof + encryption for auditor. + * It uses ERC20 permit to pull tokens from the original sender, then executes a Uniswap v4 swap through + * the PoolManager. Marks the commitment as executed to prevent replay. + * @param key Pool key identifying the Uniswap v4 pool this commitment belongs to. + * @param commitmentId Unique identifier of the commitment to execute. + * @param zkSnarkProof ABI-encoded zkSNARK proof data (pA, pB, pC, pubSignals) to authorize the execution. + * @return delta Net balance delta returned from the swap execution. + * Emits a {CommitmentExecuted} event (if you add one). + */ + function executeCommitment(PoolKey calldata key, uint256 commitmentId, bytes calldata zkSnarkProof) + external + nonReentrant + returns (BalanceDelta) { - beforeRemoveLiquidityCount[key.toId()]++; - return BaseHook.beforeRemoveLiquidity.selector; + Commitment storage commitment = commitments[key.toId()][commitmentId]; + if (commitment.status == CommitmentStatus.None) { + revert CommitmentNotFound(commitmentId); + } + if (commitment.status != CommitmentStatus.Active) { + revert CommitmentNotActive(commitmentId); + } + + (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) = + abi.decode(zkSnarkProof, (uint256[2], uint256[2][2], uint256[2], uint256[5])); + + if (pubSignals[4] > block.timestamp) { + revert CommitmentNotReady(pubSignals[4], block.timestamp); + } + + // Making sure we are executing the right commitment + bytes32 commitmentIdFromZK = getCommitmentId( + pubSignals[0], // Poseidon hash + commitment.ciphertextForAuditor + ); + + if (commitmentIdFromZK != bytes32(commitmentId)) { + revert CommitmentMismatch(commitmentIdFromZK, commitmentId); + } + + bool privateVerifierOk = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals); + if (!privateVerifierOk) { + revert InvalidPrivateSwapIntentProof(); + } + + // We can swap now + // Mark as executed before any external transfer/call + commitment.status = CommitmentStatus.Executed; + + // Run the permit if needed + if (commitment.permit.length > 0) { + (uint8 v, bytes32 r, bytes32 s) = splitSig(commitment.permit); + IERC20Permit(Currency.unwrap(key.currency0)).permit( + address(uint160(pubSignals[3])), address(this), pubSignals[1], pubSignals[4] + 1 days, v, r, s + ); + } + + // First transfer the tokens to the hook contract + IERC20(Currency.unwrap(key.currency0)).safeTransferFrom( + address(uint160(pubSignals[3])), address(this), pubSignals[1] + ); + + // Then unlock the PoolManager to execute the swap + bool zeroForOne = pubSignals[2] == 1 ? true : false; + bytes memory callbackReturn = poolManager.unlock( + abi.encode( + key, + SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: -int256(pubSignals[1]), + // No slippage limits (maximum slippage possible) + sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1 + }) + ) + ); + + (BalanceDelta delta) = abi.decode(callbackReturn, (BalanceDelta)); + emit CommitmentExecuted(commitmentId, msg.sender); + return delta; + } + + function swapAndSettleBalances(PoolKey memory key, SwapParams memory params) internal returns (BalanceDelta) { + // Conduct the swap inside the Pool Manager + BalanceDelta delta = poolManager.swap(key, params, ""); + + // If we just did a zeroForOne swap + // We need to send Token 0 to PM, and receive Token 1 from PM + if (params.zeroForOne) { + // Negative Value => Money leaving user's wallet + // Settle with PoolManager + if (delta.amount0() < 0) { + _settle(key.currency0, uint128(-delta.amount0())); + } + + // Positive Value => Money coming into user's wallet + // Take from PM + if (delta.amount1() > 0) { + _take(key.currency1, uint128(delta.amount1())); + } + } else { + if (delta.amount1() < 0) { + _settle(key.currency1, uint128(-delta.amount1())); + } + + if (delta.amount0() > 0) { + _take(key.currency0, uint128(delta.amount0())); + } + } + + return delta; + } + + function _settle(Currency currency, uint128 amount) internal { + // Transfer tokens to PM and let it know + poolManager.sync(currency); + currency.transfer(address(poolManager), amount); + poolManager.settle(); + } + + function _take(Currency currency, uint128 amount) internal { + // Take tokens out of PM to our hook contract + poolManager.take(currency, address(this), amount); + } + + function unlockCallback(bytes calldata data) external returns (bytes memory) { + (PoolKey memory key, SwapParams memory params) = abi.decode(data, (PoolKey, SwapParams)); + BalanceDelta delta = swapAndSettleBalances(key, params); + return abi.encode(delta); + } + + function splitSig(bytes memory sig) internal pure returns (uint8 v, bytes32 r, bytes32 s) { + require(sig.length == 65, "bad sig length"); + + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } } /** @@ -135,31 +351,7 @@ contract RaylsHook is BaseHook { } } - function executeCommitment(uint256 id, bytes calldata data) external { - require(!commitments[id].executed, "already executed"); - - (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) = - abi.decode(data, (uint256[2], uint256[2][2], uint256[2], uint256[5])); - - // Making sure we are executing the right commitment - bytes memory pubId = abi.encodePacked( - pubSignals[0], // Poseidon hash of (amount, recipient, nonce) - commitments[id].ciphertext, - commitments[id].encKeyForAuditor - ); - require(uint256(keccak256(pubId)) == id, "commitment mismatch"); - require(pubSignals[2] == 1, "Not marked as executed"); - require(pubSignals[4] <= block.timestamp, "Commitment can not be executed yet"); - - bool privateVerifierOk = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals); - require(privateVerifierOk, "Invalid PrivateSwapIntent proof"); - commitments[id].executed = true; - } - - function storeCommitment(uint256 id, bytes calldata ciphertext, bytes calldata encKeyForAuditor) external { - require(!commitments[id].exists, "already exists"); - commitments[id] = - Commitment({ ciphertext: ciphertext, encKeyForAuditor: encKeyForAuditor, exists: true, executed: false }); - emit CommitmentStored(id, msg.sender); + function getCommitmentId(uint256 poseidonHash, bytes memory cipherText) public pure returns (bytes32) { + return keccak256(abi.encode(poseidonHash, cipherText)); } } diff --git a/packages/foundry/foundry.toml b/packages/foundry/foundry.toml index ab30ed6..0ac6812 100644 --- a/packages/foundry/foundry.toml +++ b/packages/foundry/foundry.toml @@ -6,11 +6,11 @@ fs_permissions = [{ access = "read-write", path = "./"}] solc_version = '0.8.26' evm_version = "cancun" # hard fork that enabled EIP-1153 +optimizer = true optimizer_runs = 800 -via_ir = false +via_ir = true ffi = true - [rpc_endpoints] default_network = "http://127.0.0.1:8545" diff --git a/packages/foundry/package.json b/packages/foundry/package.json index be52fdd..5034803 100644 --- a/packages/foundry/package.json +++ b/packages/foundry/package.json @@ -9,6 +9,7 @@ "account:reveal-pk": "node scripts-js/revealPK.js", "chain": "make chain", "clean": "forge clean", + "coverage": "forge coverage --ir-minimum --report lcov && genhtml lcov.info --output-directory coverage && open coverage/index.html", "compile": "make compile", "deploy": "node scripts-js/parseArgs.js", "flatten": "make flatten", @@ -28,6 +29,7 @@ "toml": "~3.0.0" }, "devDependencies": { + "lcov": "^1.16.0", "shx": "^0.3.4" } } diff --git a/packages/foundry/script/00_DeployHook.s.sol b/packages/foundry/script/00_DeployHook.s.sol deleted file mode 100644 index 8a84674..0000000 --- a/packages/foundry/script/00_DeployHook.s.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; - -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {HookMiner} from "@uniswap/v4-periphery/src/utils/HookMiner.sol"; - -import {BaseScript} from "./base/BaseScript.sol"; - -import {Counter} from "../contracts/Counter.sol"; - -/// @notice Mines the address and deploys the Counter.sol Hook contract -contract DeployHookScript is BaseScript { - function run() public { - uint160 flags = uint160( - Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG - | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG - ); - - // Mine a salt that will produce a hook address with the correct flags - bytes memory constructorArgs = abi.encode(poolManager); - (address hookAddress, bytes32 salt) = - HookMiner.find(CREATE2_FACTORY, flags, type(Counter).creationCode, constructorArgs); - - vm.startBroadcast(); - Counter counter = new Counter{salt: salt}(poolManager); - vm.stopBroadcast(); - - require(address(counter) == hookAddress, "DeployHookScript: Hook Address Mismatch"); - } -} \ No newline at end of file diff --git a/packages/foundry/test/RaylsHook.t.sol b/packages/foundry/test/RaylsHook.t.sol index ed4dd48..907424b 100644 --- a/packages/foundry/test/RaylsHook.t.sol +++ b/packages/foundry/test/RaylsHook.t.sol @@ -36,8 +36,12 @@ contract RaylsHookTest is Test, Deployers { using CurrencyLibrary for Currency; using StateLibrary for IPoolManager; - address proofSender = 0x1234567890AbcdEF1234567890aBcdef12345678; address invalidProofSender = 0x876543210FedCBa9876543210fedcBA987654321; + + // Private key for proofSender for wallet 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 + uint256 proofSenderPk = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; + address proofSender = vm.addr(proofSenderPk); + // Private key for Auditor for wallet 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 string auditorPk = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; @@ -84,10 +88,7 @@ contract RaylsHookTest is Test, Deployers { // Deploy the hook to an address with the correct flags address flags = address( - uint160( - Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG - | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG - ) ^ (0x4444 << 144) // Namespace the hook to avoid collisions + uint160(Hooks.BEFORE_SWAP_FLAG) ^ (0x4444 << 144) // Namespace the hook to avoid collisions ); bytes memory constructorArgs = abi.encode(poolManager, suitabilityVerifier, privateSwapIntentVerifier); // Add all the necessary constructor arguments from the hook deployCodeTo("RaylsHook.sol:RaylsHook", constructorArgs, flags); @@ -128,13 +129,48 @@ contract RaylsHookTest is Test, Deployers { currency0.transfer(invalidProofSender, 1e18); } - function testVerifyProofInBeforeSwap() public { + /** + * Tests that a swap reverts if the suitability proof is invalid. + */ + function testSuitabilitySwapWithInvalidProof() public { + (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) = + RaylsHookHelper.loadSuitabilityProof(jsonSuitability); + + uint256[2] memory fakePA = [uint256(1), pA[1]]; + bytes memory fakeProofData = abi.encode(fakePA, pB, pC, pubSignals); + + uint256 amountIn = 1e16; + vm.startPrank(proofSender, proofSender); + IERC20Minimal(Currency.unwrap(currency0)).approve(address(swapRouter), amountIn); + + // Revert if the proof is invalid + vm.expectRevert(); + swapRouter.swapExactTokensForTokens({ + amountIn: amountIn, + amountOutMin: 0, + zeroForOne: true, + poolKey: poolKey, + hookData: fakeProofData, + receiver: address(proofSender), + deadline: block.timestamp + 1 + }); + + vm.stopPrank(); + } + + /** + * Tests the suitability check before a swap. + */ + function testSuitabilitySwap() public { + // We load our zkSNARK proof from the json file (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) = RaylsHookHelper.loadSuitabilityProof(jsonSuitability); bytes memory proofData = abi.encode(pA, pB, pC, pubSignals); + uint256 amountIn = 1e16; + // Revert if the proof is valid but the sender is not the one in the public signals vm.startPrank(invalidProofSender, invalidProofSender); IERC20Minimal(Currency.unwrap(currency0)).approve(address(swapRouter), amountIn); vm.expectRevert(); @@ -149,93 +185,261 @@ contract RaylsHookTest is Test, Deployers { }); vm.stopPrank(); + // Now we use the user that matches the public signal of the zkSNARK proof vm.startPrank(proofSender, proofSender); IERC20Minimal(Currency.unwrap(currency0)).approve(address(swapRouter), amountIn); + // Now it should succeed BalanceDelta swapDelta = swapRouter.swapExactTokensForTokens({ amountIn: amountIn, amountOutMin: 0, zeroForOne: true, poolKey: poolKey, - hookData: proofData, // pass proof here + hookData: proofData, receiver: address(proofSender), deadline: block.timestamp + 1 }); + vm.stopPrank(); assertEq(int256(swapDelta.amount0()), -int256(amountIn)); - - // assertEq(selector, IHooks.beforeSwap.selector, "selector mismatch"); - // delta and fee are placeholder, assert if needed } + /** + * Tests the suitability verifier directly. + */ function testSuitabilityVerifier() public view { (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) = RaylsHookHelper.loadSuitabilityProof(jsonSuitability); // If your verifier is public in the hook contract, call it directly: bool ok = suitabilityVerifier.verifyProof(pA, pB, pC, pubSignals); - assertTrue(ok); // this will fail if verifyProof==false + assertTrue(ok); } + /** + * Tests the private swap intent verifier directly. + */ function testPrivateSwapIntentVerifier() public view { (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) = RaylsHookHelper.loadPrivateSwapIntentProof(jsonPrivateSwap); // If your verifier is public in the hook contract, call it directly: bool ok = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals); - assertTrue(ok); // this will fail if verifyProof==false + assertTrue(ok); } + /** + * Tests the full flow of a private swap: + * 1. Loads proof and public signals from the json file + * 2. Loads the ciphertext for the auditor from the json file + * 3. Calculates the commitment ID off-chain using both the ZK Snark Poseidon hash and the ciphertext + * 4. Stores the commitment on-chain + * 5. Does multiple negative revert tests. + * 6. Executes the commitment successfully + * 7. Checks the pool delta from the pool + * 8. Tests the auditor part by decrypting the ciphertext and checking that the poseidon hash matches the one from the proof + * This proves that the values in the encrypted payload: amountIn, zeroForOne, sender, timestamp are correct. + * And that the commitment ID is correct. + */ function testPrivateSwap() public { - (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) = - RaylsHookHelper.loadPrivateSwapIntentProof(jsonPrivateSwap); - - bytes memory proofData = abi.encode(pA, pB, pC, pubSignals); - uint256 amountIn = 1e16; - IERC20Minimal(Currency.unwrap(currency0)).approve(address(swapRouter), amountIn); + // Get the proof and public signals from the json file + RaylsHookHelper.PrivateSwapPublic memory proofCorrect = + RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, false); - string memory encKeyForAuditorStr = jsonEncryptedPayload.readString(".encKeyForAuditor"); - string memory ciphertextStr = jsonEncryptedPayload.readString(".ciphertext"); + // Get the ciphertext for the auditor from the json file + bytes memory ciphertextForAuditor = RaylsHookHelper.getJsonCiphertext(jsonEncryptedPayload); - bytes memory encKeyForAuditor = RaylsHookHelper.hexStringToBytes(encKeyForAuditorStr); - bytes memory ciphertext = RaylsHookHelper.hexStringToBytes(ciphertextStr); + // Calculate the commitment ID off-chain using both the ZK Snark Poseidon Hash and the ciphertext + uint256 commitmentId = uint256(hook.getCommitmentId(proofCorrect.poseidonHash, ciphertextForAuditor)); - vm.warp(pubSignals[4]); - console.log("Block timestamp:", block.timestamp); - - uint256 id = uint256( - keccak256( - abi.encodePacked( - pubSignals[0], // Poseidon hash of (amount, recipient, nonce) - ciphertext, // AES-encrypted message (always present) - encKeyForAuditor // optional: can be empty bytes - ) - ) + // Store the commitment on-chain + vm.startPrank(proofSender, proofSender); + // Build the permit signature to approve the hook to spend the tokens + bytes memory permitSignature = RaylsHookHelper.buildPermitSignature( + vm, + proofSenderPk, + Currency.unwrap(currency0), + proofCorrect.timestamp, + proofSender, + address(hook), + proofCorrect.amountIn ); - vm.startPrank(proofSender, proofSender); - hook.storeCommitment(id, ciphertext, encKeyForAuditor); - hook.executeCommitment(id, proofData); + // Call the hook to store the commitment + hook.storeCommitment(poolKey, commitmentId, ciphertextForAuditor, permitSignature); + + // Revert if already exsits + bytes memory expectedRevert = abi.encodeWithSelector(RaylsHook.AlreadyExists.selector, commitmentId); + vm.expectRevert(expectedRevert); + hook.storeCommitment(poolKey, commitmentId, ciphertextForAuditor, permitSignature); + + // For now we just approve the swapRouter to spend the tokens + // IERC20Minimal(Currency.unwrap(currency0)).approve(address(hook), amountIn); + + // Move time forward but not enough to be able tcommitmentIdo execute the commitment + vm.warp(proofCorrect.timestamp - 1); + expectedRevert = + abi.encodeWithSelector(RaylsHook.CommitmentNotReady.selector, proofCorrect.timestamp, block.timestamp); + vm.expectRevert(expectedRevert); + hook.executeCommitment(poolKey, commitmentId, proofCorrect.proofData); + + // Move time forward to be able to execute the commitment + vm.warp(proofCorrect.timestamp); + + // Revert if commitementId is incorrect and doesnt match the proof + uint256 fakeId = 123456; + expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotFound.selector, fakeId); + vm.expectRevert(expectedRevert); + hook.executeCommitment(poolKey, fakeId, proofCorrect.proofData); + + // Revert if the public signal poseidon hash is invalid + RaylsHookHelper.PrivateSwapPublic memory proofWithWrongHash = + RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, true); + bytes32 fakeCommitmentId = hook.getCommitmentId(proofWithWrongHash.poseidonHash, ciphertextForAuditor); + expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentMismatch.selector, fakeCommitmentId, commitmentId); + vm.expectRevert(expectedRevert); + hook.executeCommitment(poolKey, commitmentId, proofWithWrongHash.proofData); + + // Revert if the proof is invalid + RaylsHookHelper.PrivateSwapPublic memory proofWithWrongPA = + RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, true, false); + expectedRevert = abi.encodeWithSelector(RaylsHook.InvalidPrivateSwapIntentProof.selector); + vm.expectRevert(expectedRevert); + hook.executeCommitment(poolKey, commitmentId, proofWithWrongPA.proofData); + + // Execute the commitment successfully + BalanceDelta delta = hook.executeCommitment(poolKey, commitmentId, proofCorrect.proofData); + assertEq(int256(delta.amount0()), -int256(proofCorrect.amountIn)); + + // Revert if we want to execute it again + expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotActive.selector, commitmentId); + vm.expectRevert(expectedRevert); + hook.executeCommitment(poolKey, commitmentId, proofCorrect.proofData); vm.stopPrank(); - (bytes memory onChainCiphertext, bytes memory onChainEncKeyForAuditor,, bool executed) = hook.commitments(id); + // Test the auditor part + (bytes memory onChainCiphertext,, RaylsHook.CommitmentStatus status) = + hook.commitments(poolKey.toId(), commitmentId); - assertEq(executed, true); - assertEq(onChainCiphertext, ciphertext); - assertEq(onChainEncKeyForAuditor, encKeyForAuditor); - string memory hexOnChainCiphertext = vm.toString(onChainCiphertext); - string memory hexOnChainEncKeyForAuditor = vm.toString(onChainEncKeyForAuditor); + assertEq(uint8(status), uint8(RaylsHook.CommitmentStatus.Executed)); + assertEq(onChainCiphertext, ciphertextForAuditor); + + string memory onChainCiphertextStr = vm.toString(onChainCiphertext); // Decrypt off-chain and use the private values to calculate the commitment ID // It must match to the one stored on-chain created by the circuit. - uint256 decryptedCommitmentId = - RaylsHookHelper.decryptCiphertext(vm, auditorPk, hexOnChainCiphertext, hexOnChainEncKeyForAuditor); + uint256 decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, onChainCiphertextStr); // Check that commitmentId is correct - assertEq(pubSignals[0], decryptedCommitmentId); + assertEq(proofCorrect.poseidonHash, decryptedPoseidonHash); + } + + /** + * Loads the poseidon hash from the ZK Snark proof, decrypts the ciphertext from the encrypted payload and checks that they are equal. + * This simulates the auditor decrypting the ciphertext and checking that the commitment ID is correct. + * Which proves that the values in the encrypted payload: amountIn, zeroForOne, sender, timestamp are correct. + */ + function testEncryptedCommitmentForAuditor() public { + // Get the proof and public signals from the json file + RaylsHookHelper.PrivateSwapPublic memory proofCorrect = + RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, false); + + // Get the ciphertext for the auditor from the json file + bytes memory ciphertextForAuditor = RaylsHookHelper.getJsonCiphertext(jsonEncryptedPayload); + string memory onChainCiphertextStr = vm.toString(ciphertextForAuditor); + + // First lets try with a fake ciphertext and see that cannot decrypt it + // The wallet here was set at an invalid value so decryption will fail + bytes memory fakeCiphertext = + hex"ee86f15b901b6c155b8c3ec570e50f4c04b86d3cf44aafa7cc640a3e141354da40eef4c6ed78fd8077be72c3e853e435cf9fa2d70164501cd37efec0dc0a04119f66ecf007b190100c856d5aae7a5fec4d38b39c99bed1d8923635820c81bb9d8d52794056400a7d19f6e8ce663c79684cc19a7935ca8189811169a5fbe5c1147fd80f3a83207217c1a895862b162c89ce85dc51c53571c1202b90e1e710cb801d47b4aabeeac981197ba285c399c344ff09d56a386f26169726e8f29c9ff4c45c033816426f6d8d4befadb6f008d2bf8537022f5aaa9e93920fa8e959c0ca30e0edabd55bcfcbc3050343426ea294e61321e1885efe3db4be70f4bc9b8a0b2f4c"; + string memory fakeCiphertextStr = vm.toString(fakeCiphertext); + uint256 decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, fakeCiphertextStr); + + // Check that hashes are different + assertNotEq(proofCorrect.poseidonHash, decryptedPoseidonHash); + + // Now a successful decryption + // Decrypt off-chain and use the poseidonHash for comparison + // It must match to the one stored on-chain created by the circuit. + decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, onChainCiphertextStr); + + // Check that hashes are equal + assertEq(proofCorrect.poseidonHash, decryptedPoseidonHash); + } + + /** + * Tests the full flow of cancelling a private swap commitment: + * 1. Loads proof and public signals from the json file + * 2. Loads the ciphertext for the auditor from the json file + * 3. Calculates the commitment ID off-chain using both the ZK Snark Poseidon hash and the ciphertext + * 4. Stores the commitment on-chain + * 5. Does multiple negative revert tests. + * 6. Cancels the commitment successfully + * 7. Tries to execute it and reverts + */ + function testCancelCommitment() public { + // Get the proof and public signals from the json file + RaylsHookHelper.PrivateSwapPublic memory proofCorrect = + RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, false); + + // Get the ciphertext for the auditor from the json file + bytes memory ciphertextForAuditor = RaylsHookHelper.getJsonCiphertext(jsonEncryptedPayload); + + // Calculate the commitment ID off-chain using both the ZK Snark Poseidon Hash and the ciphertext + uint256 commitmentId = uint256(hook.getCommitmentId(proofCorrect.poseidonHash, ciphertextForAuditor)); + + // Store the commitment on-chain + vm.startPrank(proofSender, proofSender); + // Build the permit signature to approve the hook to spend the tokens + bytes memory permitSignature = RaylsHookHelper.buildPermitSignature( + vm, + proofSenderPk, + Currency.unwrap(currency0), + proofCorrect.timestamp, + proofSender, + address(hook), + proofCorrect.amountIn + ); + + // Call the hook to store the commitment + hook.storeCommitment(poolKey, commitmentId, ciphertextForAuditor, permitSignature); + + // Revert if the public signal poseidon hash is invalid + RaylsHookHelper.PrivateSwapPublic memory proofWithWrongHash = + RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, true); + bytes32 fakeCommitmentId = hook.getCommitmentId(proofWithWrongHash.poseidonHash, ciphertextForAuditor); + bytes memory expectedRevert = + abi.encodeWithSelector(RaylsHook.CommitmentMismatch.selector, fakeCommitmentId, commitmentId); + vm.expectRevert(expectedRevert); + hook.cancelCommitment(poolKey, commitmentId, proofWithWrongHash.proofData); + + // Revert if the proof is invalid + RaylsHookHelper.PrivateSwapPublic memory proofWithWrongPA = + RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, true, false); + expectedRevert = abi.encodeWithSelector(RaylsHook.InvalidPrivateSwapIntentProof.selector); + vm.expectRevert(expectedRevert); + hook.cancelCommitment(poolKey, commitmentId, proofWithWrongPA.proofData); + + // Cancel it before it can be executed + hook.cancelCommitment(poolKey, commitmentId, proofCorrect.proofData); + + // Cancel it again shoule revvert + expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotActive.selector, commitmentId); + vm.expectRevert(expectedRevert); + hook.cancelCommitment(poolKey, commitmentId, proofCorrect.proofData); + + // Move time forward to be able to execute the commitment + vm.warp(proofCorrect.timestamp + 1); + + // Revert if we try to execute a cancelled commitment + expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotActive.selector, commitmentId); + vm.expectRevert(expectedRevert); + hook.executeCommitment(poolKey, commitmentId, proofCorrect.proofData); + vm.stopPrank(); + + (,, RaylsHook.CommitmentStatus status) = hook.commitments(poolKey.toId(), commitmentId); - // assertEq(selector, IHooks.beforeSwap.selector, "selector mismatch"); - // delta and fee are placeholder, assert if needed + assertEq(uint8(status), uint8(RaylsHook.CommitmentStatus.Cancelled)); } } diff --git a/packages/foundry/test/utils/RaylsHookHelper.sol b/packages/foundry/test/utils/RaylsHookHelper.sol index 40f2646..7ece6e0 100644 --- a/packages/foundry/test/utils/RaylsHookHelper.sol +++ b/packages/foundry/test/utils/RaylsHookHelper.sol @@ -4,25 +4,30 @@ pragma solidity ^0.8.21; import { console } from "forge-std/console.sol"; import "forge-std/StdJson.sol"; import { Vm } from "forge-std/Test.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; library RaylsHookHelper { using stdJson for string; - function decryptCiphertext( - Vm vm, - string memory _auditorPk, - string memory ciphertext, - string memory encKeyForAuditor - ) public returns (uint256 output) { + struct PrivateSwapPublic { + uint256 amountIn; + uint256 timestamp; + uint256 poseidonHash; + bytes proofData; + } + + function decryptCiphertext(Vm vm, string memory _auditorPk, string memory ciphertext) + public + returns (uint256 output) + { // You can implement this function to validate the encryption on-chain if needed. // For example, you might want to check the length of the ciphertext or other properties. // Enable FFI - string[] memory cmds = new string[](5); + string[] memory cmds = new string[](4); cmds[0] = "node"; cmds[1] = "../encryption/decrypt.cjs"; cmds[2] = _auditorPk; cmds[3] = cmds[3] = ciphertext; - cmds[4] = encKeyForAuditor; bytes memory result = vm.ffi(cmds); @@ -140,4 +145,77 @@ library RaylsHookHelper { if (c >= 97 && c <= 102) return c - 87; // 'a'-'f' revert("invalid hex char"); } + + function splitSig(bytes memory sig) internal pure returns (uint8 v, bytes32 r, bytes32 s) { + require(sig.length == 65, "bad sig length"); + + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } + } + + function buildPermitSignature( + Vm vm, + uint256 privateKey, + address token, + uint256 timestamp, + address sender, + address receiver, + uint256 amount + ) public view returns (bytes memory) { + uint256 nonce = IERC20Permit(token).nonces(sender); + uint256 deadline = timestamp + 1 days; + + // EIP-712 domain separator + bytes32 DOMAIN_SEPARATOR = IERC20Permit(token).DOMAIN_SEPARATOR(); + + // Permit typehash (same as OZ ERC20Permit) + bytes32 PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + // Build struct hash + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, sender, address(receiver), amount, nonce, deadline)); + + // Final digest (EIP-712) + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); + + // Sign with Foundry’s vm.sign + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return abi.encodePacked(r, s, v); + } + + function getPublicSignalsFromPrivateSwapIntentProof(string memory json, bool fakePa, bool fakeHash) + public + pure + returns (PrivateSwapPublic memory out) + { + (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) = + loadPrivateSwapIntentProof(json); + + if (fakePa) { + // Change the poseidon hash to an incorrect one + pA = [uint256(1), pA[1]]; + } + + if (fakeHash) { + // Change the poseidon hash to an incorrect one + pubSignals[0] = 1; + } + + out.proofData = abi.encode(pA, pB, pC, pubSignals); + out.poseidonHash = pubSignals[0]; + out.amountIn = pubSignals[1]; + out.timestamp = pubSignals[4]; + } + + function getJsonCiphertext(string memory _jsonEncryptedPayload) + public + pure + returns (bytes memory ciphertextForAuditor) + { + string memory ciphertextForAuditorStr = _jsonEncryptedPayload.readString(".ciphertextForAuditor"); + ciphertextForAuditor = RaylsHookHelper.hexStringToBytes(ciphertextForAuditorStr); + } }