From 8d8fd93b8b58487d459d265d2d6d8a3d3753a381 Mon Sep 17 00:00:00 2001
From: Nuno
Date: Thu, 11 Sep 2025 10:56:13 +0100
Subject: [PATCH 01/29] feat: replacing require by errors
---
packages/foundry/contracts/RaylsHook.sol | 45 +++++++++++++++++++-----
packages/foundry/test/RaylsHook.t.sol | 18 +++++++---
2 files changed, 50 insertions(+), 13 deletions(-)
diff --git a/packages/foundry/contracts/RaylsHook.sol b/packages/foundry/contracts/RaylsHook.sol
index f4ccfed..2aee751 100644
--- a/packages/foundry/contracts/RaylsHook.sol
+++ b/packages/foundry/contracts/RaylsHook.sol
@@ -35,6 +35,16 @@ contract RaylsHook is BaseHook {
event CommitmentStored(uint256 id, address indexed sender);
event Revealed(uint256 id, address indexed revealer);
+ // Errors
+ error CommitmentMismatch(bytes pubId, uint256 expectedId);
+ error NotMarkedAsExecuted(uint256 signal);
+ error CommitmentNotReady(uint256 notBefore, uint256 currentTime);
+ error InvalidWallet(address provided, address expected);
+ error InvalidSuitabilityProof();
+ error AlreadyExecuted(uint256 id);
+ error InvalidPrivateSwapIntentProof();
+ error AlreadyExists(uint256 id);
+
struct Commitment {
bytes ciphertext; // AES/GCM ciphertext (includes tag)
bytes encKeyForAuditor; // encrypted symmetric key for auditor
@@ -83,10 +93,14 @@ contract RaylsHook is BaseHook {
uint256 walletInProof = pubSignals[2]; // index 2 because it's the 3rd public signal
address origin = _determineOrigin(msg.sender);
- require(walletInProof == uint256(uint160(origin)), "Invalid wallet for this proof");
+ if (walletInProof != uint256(uint160(origin))) {
+ revert InvalidWallet(address(uint160(walletInProof)), origin);
+ }
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);
}
@@ -136,7 +150,9 @@ contract RaylsHook is BaseHook {
}
function executeCommitment(uint256 id, bytes calldata data) external {
- require(!commitments[id].executed, "already executed");
+ if (commitments[id].executed) {
+ revert AlreadyExecuted(id);
+ }
(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]));
@@ -147,17 +163,30 @@ contract RaylsHook is BaseHook {
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");
+
+ if (uint256(keccak256(pubId)) != id) {
+ revert CommitmentMismatch(pubId, id);
+ }
+
+ if (pubSignals[2] != 1) {
+ revert NotMarkedAsExecuted(pubSignals[2]);
+ }
+
+ if (pubSignals[4] > block.timestamp) {
+ revert CommitmentNotReady(pubSignals[4], block.timestamp);
+ }
bool privateVerifierOk = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals);
- require(privateVerifierOk, "Invalid PrivateSwapIntent proof");
+ if (!privateVerifierOk) {
+ revert InvalidPrivateSwapIntentProof();
+ }
commitments[id].executed = true;
}
function storeCommitment(uint256 id, bytes calldata ciphertext, bytes calldata encKeyForAuditor) external {
- require(!commitments[id].exists, "already exists");
+ if (commitments[id].exists) {
+ revert AlreadyExists(id);
+ }
commitments[id] =
Commitment({ ciphertext: ciphertext, encKeyForAuditor: encKeyForAuditor, exists: true, executed: false });
emit CommitmentStored(id, msg.sender);
diff --git a/packages/foundry/test/RaylsHook.t.sol b/packages/foundry/test/RaylsHook.t.sol
index 44f5126..e95f286 100644
--- a/packages/foundry/test/RaylsHook.t.sol
+++ b/packages/foundry/test/RaylsHook.t.sol
@@ -201,14 +201,11 @@ contract RaylsHookTest is Test, Deployers {
bytes memory encKeyForAuditor = RaylsHookHelper.hexStringToBytes(encKeyForAuditorStr);
bytes memory ciphertext = RaylsHookHelper.hexStringToBytes(ciphertextStr);
- 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)
+ pubSignals[0], // Poseidon hash of (amountIn, zeroForOne, sender, timestamp)
+ ciphertext, // AES-encrypted message optional
encKeyForAuditor // optional: can be empty bytes
)
)
@@ -216,7 +213,18 @@ contract RaylsHookTest is Test, Deployers {
vm.startPrank(proofSender, proofSender);
hook.storeCommitment(id, ciphertext, encKeyForAuditor);
+
+ // Move time forward but not enough to be able to execute the commitment
+ vm.warp(pubSignals[4] - 1);
+ bytes memory expectedRevert =
+ abi.encodeWithSelector(RaylsHook.CommitmentNotReady.selector, pubSignals[4], block.timestamp);
+ vm.expectRevert(expectedRevert);
hook.executeCommitment(id, proofData);
+
+ // Move time forward to be able to execute the commitment
+ vm.warp(pubSignals[4]);
+ hook.executeCommitment(id, proofData);
+
vm.stopPrank();
(bytes memory onChainCiphertext, bytes memory onChainEncKeyForAuditor,, bool executed) = hook.commitments(id);
From 1442690ac0c669cf38d472b86566061637ba5097 Mon Sep 17 00:00:00 2001
From: Nuno
Date: Thu, 11 Sep 2025 19:04:05 +0100
Subject: [PATCH 02/29] feat: adding actual swap logic, permit, plus some
improvements
---
packages/foundry/contracts/RaylsHook.sol | 161 +++++++++++++++---
packages/foundry/foundry.toml | 2 +-
packages/foundry/test/RaylsHook.t.sol | 31 ++--
.../foundry/test/utils/RaylsHookHelper.sol | 41 +++++
4 files changed, 204 insertions(+), 31 deletions(-)
diff --git a/packages/foundry/contracts/RaylsHook.sol b/packages/foundry/contracts/RaylsHook.sol
index 2aee751..0fb17e9 100644
--- a/packages/foundry/contracts/RaylsHook.sol
+++ b/packages/foundry/contracts/RaylsHook.sol
@@ -13,9 +13,19 @@ import { SuitabilityAssessmentVerifier } from "./SuitabilityAssessmentVerifier.s
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,7 +40,7 @@ contract RaylsHook is BaseHook {
SuitabilityAssessmentVerifier public suitabilityVerifier;
PrivateSwapIntentVerifier public privateSwapIntentVerifier;
- mapping(uint256 => Commitment) public commitments;
+ mapping(PoolId poolId => mapping(uint256 => Commitment)) public commitments;
event CommitmentStored(uint256 id, address indexed sender);
event Revealed(uint256 id, address indexed revealer);
@@ -48,6 +58,7 @@ contract RaylsHook is BaseHook {
struct Commitment {
bytes ciphertext; // AES/GCM ciphertext (includes tag)
bytes encKeyForAuditor; // encrypted symmetric key for auditor
+ bytes permit;
bool exists;
bool executed;
}
@@ -149,46 +160,156 @@ contract RaylsHook is BaseHook {
}
}
- function executeCommitment(uint256 id, bytes calldata data) external {
- if (commitments[id].executed) {
+ function executeCommitment(PoolKey calldata key, uint256 id, bytes calldata data)
+ external
+ nonReentrant
+ returns (BalanceDelta)
+ {
+ if (commitments[key.toId()][id].executed) {
revert AlreadyExecuted(id);
}
(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
- );
-
- if (uint256(keccak256(pubId)) != id) {
- revert CommitmentMismatch(pubId, id);
+ if (pubSignals[4] > block.timestamp) {
+ revert CommitmentNotReady(pubSignals[4], block.timestamp);
}
if (pubSignals[2] != 1) {
revert NotMarkedAsExecuted(pubSignals[2]);
}
- if (pubSignals[4] > block.timestamp) {
- revert CommitmentNotReady(pubSignals[4], block.timestamp);
+ // Making sure we are executing the right commitment
+ bytes memory pubId = abi.encode(
+ pubSignals[0], // Poseidon hash of (amount, recipient, nonce)
+ commitments[key.toId()][id].ciphertext,
+ commitments[key.toId()][id].encKeyForAuditor
+ );
+
+ if (keccak256(pubId) != bytes32(id)) {
+ revert CommitmentMismatch(pubId, id);
}
bool privateVerifierOk = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals);
if (!privateVerifierOk) {
revert InvalidPrivateSwapIntentProof();
}
- commitments[id].executed = true;
+
+ // We can swap now
+
+ // Mark as executed before any external transfer/call
+ commitments[key.toId()][id].executed = true;
+
+ // Run the permit if needed
+ if (commitments[key.toId()][id].permit.length > 0) {
+ (uint8 v, bytes32 r, bytes32 s) = splitSig(commitments[key.toId()][id].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,
+ // We provide a negative value here to signify an "exact input for output" swap
+ 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 Executed(keyId, id, owner, /*...*/);
+ return delta;
}
- function storeCommitment(uint256 id, bytes calldata ciphertext, bytes calldata encKeyForAuditor) external {
- if (commitments[id].exists) {
+ function storeCommitment(
+ PoolKey calldata key,
+ uint256 id,
+ bytes calldata ciphertext,
+ bytes calldata encKeyForAuditor,
+ bytes calldata permit
+ ) external {
+ if (commitments[key.toId()][id].exists) {
revert AlreadyExists(id);
}
- commitments[id] =
- Commitment({ ciphertext: ciphertext, encKeyForAuditor: encKeyForAuditor, exists: true, executed: false });
+ commitments[key.toId()][id] = Commitment({
+ ciphertext: ciphertext,
+ encKeyForAuditor: encKeyForAuditor,
+ permit: permit,
+ exists: true,
+ executed: false
+ });
emit CommitmentStored(id, msg.sender);
}
+
+ 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)))
+ }
+ }
}
diff --git a/packages/foundry/foundry.toml b/packages/foundry/foundry.toml
index ab30ed6..bcfd5e4 100644
--- a/packages/foundry/foundry.toml
+++ b/packages/foundry/foundry.toml
@@ -7,7 +7,7 @@ fs_permissions = [{ access = "read-write", path = "./"}]
solc_version = '0.8.26'
evm_version = "cancun" # hard fork that enabled EIP-1153
optimizer_runs = 800
-via_ir = false
+via_ir = true
ffi = true
diff --git a/packages/foundry/test/RaylsHook.t.sol b/packages/foundry/test/RaylsHook.t.sol
index e95f286..b492ddc 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";
@@ -187,13 +191,13 @@ contract RaylsHookTest is Test, Deployers {
assertTrue(ok); // this will fail if verifyProof==false
}
- function testPrivateSwap() public {
+ function testAPrivateSwap() 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);
+ uint256 amountIn = pubSignals[1];
+ uint256 timestamp = pubSignals[4];
string memory encKeyForAuditorStr = jsonEncryptedPayload.readString(".encKeyForAuditor");
string memory ciphertextStr = jsonEncryptedPayload.readString(".ciphertext");
@@ -203,7 +207,7 @@ contract RaylsHookTest is Test, Deployers {
uint256 id = uint256(
keccak256(
- abi.encodePacked(
+ abi.encode(
pubSignals[0], // Poseidon hash of (amountIn, zeroForOne, sender, timestamp)
ciphertext, // AES-encrypted message optional
encKeyForAuditor // optional: can be empty bytes
@@ -212,22 +216,29 @@ contract RaylsHookTest is Test, Deployers {
);
vm.startPrank(proofSender, proofSender);
- hook.storeCommitment(id, ciphertext, encKeyForAuditor);
+ bytes memory permitSignature = RaylsHookHelper.buildPermitSignature(
+ vm, proofSenderPk, Currency.unwrap(currency0), timestamp, proofSender, address(hook), amountIn
+ );
+ hook.storeCommitment(poolKey, id, ciphertext, encKeyForAuditor, 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 to execute the commitment
vm.warp(pubSignals[4] - 1);
bytes memory expectedRevert =
abi.encodeWithSelector(RaylsHook.CommitmentNotReady.selector, pubSignals[4], block.timestamp);
vm.expectRevert(expectedRevert);
- hook.executeCommitment(id, proofData);
+ hook.executeCommitment(poolKey, id, proofData);
// Move time forward to be able to execute the commitment
vm.warp(pubSignals[4]);
- hook.executeCommitment(id, proofData);
-
+ BalanceDelta delta = hook.executeCommitment(poolKey, id, proofData);
+ assertEq(int256(delta.amount0()), -int256(amountIn));
vm.stopPrank();
- (bytes memory onChainCiphertext, bytes memory onChainEncKeyForAuditor,, bool executed) = hook.commitments(id);
+ (bytes memory onChainCiphertext, bytes memory onChainEncKeyForAuditor,, bool executed,) =
+ hook.commitments(poolKey.toId(), id);
assertEq(executed, true);
assertEq(onChainCiphertext, ciphertext);
diff --git a/packages/foundry/test/utils/RaylsHookHelper.sol b/packages/foundry/test/utils/RaylsHookHelper.sol
index 40f2646..b5cae35 100644
--- a/packages/foundry/test/utils/RaylsHookHelper.sol
+++ b/packages/foundry/test/utils/RaylsHookHelper.sol
@@ -4,6 +4,7 @@ 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;
@@ -140,4 +141,44 @@ 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);
+ }
}
From 8af635186ea6de1d78cae67cc351ff0b8e936ada Mon Sep 17 00:00:00 2001
From: Nuno
Date: Fri, 12 Sep 2025 11:19:15 +0100
Subject: [PATCH 03/29] feat: adding cancel commitment and cleaning up a bit
---
.../scripts/PrivateSwapIntent_input.json | 6 +
.../circom/scripts/Suitability_input.json | 12 ++
packages/foundry/contracts/RaylsHook.sol | 117 ++++++++++--------
packages/foundry/test/RaylsHook.t.sol | 9 +-
4 files changed, 87 insertions(+), 57 deletions(-)
create mode 100644 packages/circom/scripts/PrivateSwapIntent_input.json
create mode 100644 packages/circom/scripts/Suitability_input.json
diff --git a/packages/circom/scripts/PrivateSwapIntent_input.json b/packages/circom/scripts/PrivateSwapIntent_input.json
new file mode 100644
index 0000000..16bd6c6
--- /dev/null
+++ b/packages/circom/scripts/PrivateSwapIntent_input.json
@@ -0,0 +1,6 @@
+{
+ "amountIn": "1000000000000000",
+ "zeroForOne": "1",
+ "sender": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
+ "timestamp": "1697052800"
+}
diff --git a/packages/circom/scripts/Suitability_input.json b/packages/circom/scripts/Suitability_input.json
new file mode 100644
index 0000000..d1965f0
--- /dev/null
+++ b/packages/circom/scripts/Suitability_input.json
@@ -0,0 +1,12 @@
+{
+ "answer1": 3,
+ "answer2": 2,
+ "answer3": 1,
+ "answer4": 2,
+ "answer5": 3,
+ "wallet": "0x1234567890AbcdEF1234567890aBcdef12345678",
+ "thresholdScaled": 20,
+ "isSuitablePub": 1
+}
+
+
\ No newline at end of file
diff --git a/packages/foundry/contracts/RaylsHook.sol b/packages/foundry/contracts/RaylsHook.sol
index 0fb17e9..5fbab8c 100644
--- a/packages/foundry/contracts/RaylsHook.sol
+++ b/packages/foundry/contracts/RaylsHook.sol
@@ -42,25 +42,34 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
mapping(PoolId poolId => mapping(uint256 => Commitment)) public commitments;
- event CommitmentStored(uint256 id, address indexed sender);
- event Revealed(uint256 id, address indexed revealer);
+ event CommitmentStored(uint256 indexed id, address indexed sender, bytes, bytes, bytes);
+ event CommitmentExecuted(uint256 indexed id, address indexed sender);
+ event CommitmentCanceled(uint256 indexed id, address indexed canceller);
// Errors
error CommitmentMismatch(bytes pubId, uint256 expectedId);
- error NotMarkedAsExecuted(uint256 signal);
error CommitmentNotReady(uint256 notBefore, uint256 currentTime);
error InvalidWallet(address provided, address expected);
error InvalidSuitabilityProof();
error AlreadyExecuted(uint256 id);
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
+ Canceled // canceled by creator
+
+ }
struct Commitment {
bytes ciphertext; // AES/GCM ciphertext (includes tag)
bytes encKeyForAuditor; // encrypted symmetric key for auditor
bytes permit;
- bool exists;
- bool executed;
+ CommitmentStatus status;
}
constructor(IPoolManager _poolManager, address _suitabilityVerifier, address _privateSwapIntentVerifier)
@@ -74,12 +83,12 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
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,
@@ -116,32 +125,6 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
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);
- }
-
- function _beforeAddLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata)
- internal
- override
- returns (bytes4)
- {
- beforeAddLiquidityCount[key.toId()]++;
- return BaseHook.beforeAddLiquidity.selector;
- }
-
- function _beforeRemoveLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata)
- internal
- override
- returns (bytes4)
- {
- beforeRemoveLiquidityCount[key.toId()]++;
- return BaseHook.beforeRemoveLiquidity.selector;
- }
-
/**
* Determines the origin of the transaction.
*
@@ -165,8 +148,12 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
nonReentrant
returns (BalanceDelta)
{
- if (commitments[key.toId()][id].executed) {
- revert AlreadyExecuted(id);
+ Commitment storage commitment = commitments[key.toId()][id];
+ if (commitment.status == CommitmentStatus.None) {
+ revert CommitmentNotFound(id);
+ }
+ if (commitment.status != CommitmentStatus.Active) {
+ revert CommitmentNotActive(id);
}
(uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) =
@@ -176,15 +163,11 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
revert CommitmentNotReady(pubSignals[4], block.timestamp);
}
- if (pubSignals[2] != 1) {
- revert NotMarkedAsExecuted(pubSignals[2]);
- }
-
// Making sure we are executing the right commitment
bytes memory pubId = abi.encode(
pubSignals[0], // Poseidon hash of (amount, recipient, nonce)
- commitments[key.toId()][id].ciphertext,
- commitments[key.toId()][id].encKeyForAuditor
+ commitment.ciphertext,
+ commitment.encKeyForAuditor
);
if (keccak256(pubId) != bytes32(id)) {
@@ -197,13 +180,12 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
}
// We can swap now
-
// Mark as executed before any external transfer/call
- commitments[key.toId()][id].executed = true;
+ commitment.status = CommitmentStatus.Executed;
// Run the permit if needed
- if (commitments[key.toId()][id].permit.length > 0) {
- (uint8 v, bytes32 r, bytes32 s) = splitSig(commitments[key.toId()][id].permit);
+ 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
);
@@ -230,7 +212,7 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
);
(BalanceDelta delta) = abi.decode(callbackReturn, (BalanceDelta));
- // emit Executed(keyId, id, owner, /*...*/);
+ emit CommitmentExecuted(id, msg.sender);
return delta;
}
@@ -241,17 +223,50 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
bytes calldata encKeyForAuditor,
bytes calldata permit
) external {
- if (commitments[key.toId()][id].exists) {
+ if (commitments[key.toId()][id].status != CommitmentStatus.None) {
revert AlreadyExists(id);
}
+
commitments[key.toId()][id] = Commitment({
ciphertext: ciphertext,
encKeyForAuditor: encKeyForAuditor,
permit: permit,
- exists: true,
- executed: false
+ status: CommitmentStatus.Active
});
- emit CommitmentStored(id, msg.sender);
+ emit CommitmentStored(id, msg.sender, ciphertext, encKeyForAuditor, permit);
+ }
+
+ function cancelCommitment(PoolKey calldata key, uint256 id, bytes calldata data) external {
+ Commitment storage c = commitments[key.toId()][id];
+ if (c.status != CommitmentStatus.Active) {
+ revert CommitmentNotActive(id);
+ }
+
+ (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.encode(
+ pubSignals[0], // Poseidon hash of (amount, recipient, nonce)
+ c.ciphertext,
+ c.encKeyForAuditor
+ );
+
+ if (keccak256(pubId) != bytes32(id)) {
+ revert CommitmentMismatch(pubId, id);
+ }
+
+ bool privateVerifierOk = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals);
+ if (!privateVerifierOk) {
+ revert InvalidPrivateSwapIntentProof();
+ }
+
+ // We can cancel now
+ c.status = CommitmentStatus.Canceled;
+ delete c.ciphertext;
+ delete c.encKeyForAuditor;
+ delete c.permit;
+ emit CommitmentCanceled(id, msg.sender);
}
function swapAndSettleBalances(PoolKey memory key, SwapParams memory params) internal returns (BalanceDelta) {
diff --git a/packages/foundry/test/RaylsHook.t.sol b/packages/foundry/test/RaylsHook.t.sol
index b492ddc..a141c33 100644
--- a/packages/foundry/test/RaylsHook.t.sol
+++ b/packages/foundry/test/RaylsHook.t.sol
@@ -88,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);
@@ -237,10 +234,10 @@ contract RaylsHookTest is Test, Deployers {
assertEq(int256(delta.amount0()), -int256(amountIn));
vm.stopPrank();
- (bytes memory onChainCiphertext, bytes memory onChainEncKeyForAuditor,, bool executed,) =
+ (bytes memory onChainCiphertext, bytes memory onChainEncKeyForAuditor,, RaylsHook.CommitmentStatus status) =
hook.commitments(poolKey.toId(), id);
- assertEq(executed, true);
+ assertEq(uint8(status), uint8(RaylsHook.CommitmentStatus.Executed));
assertEq(onChainCiphertext, ciphertext);
assertEq(onChainEncKeyForAuditor, encKeyForAuditor);
string memory hexOnChainCiphertext = vm.toString(onChainCiphertext);
From e49deb0066ec2ea9a6dbdff288c2edb539e6e9d6 Mon Sep 17 00:00:00 2001
From: Nuno
Date: Fri, 12 Sep 2025 15:39:03 +0100
Subject: [PATCH 04/29] feat: changing to direct encryption using ECIES. adding
comments etc
---
packages/encryption/decrypt.cjs | 29 +--
packages/encryption/encrypt.js | 25 +--
packages/foundry/contracts/RaylsHook.sol | 178 ++++++++++--------
packages/foundry/test/RaylsHook.t.sol | 33 +---
.../foundry/test/utils/RaylsHookHelper.sol | 13 +-
5 files changed, 128 insertions(+), 150 deletions(-)
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 5fbab8c..ee5ff9e 100644
--- a/packages/foundry/contracts/RaylsHook.sol
+++ b/packages/foundry/contracts/RaylsHook.sol
@@ -42,7 +42,7 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
mapping(PoolId poolId => mapping(uint256 => Commitment)) public commitments;
- event CommitmentStored(uint256 indexed id, address indexed sender, bytes, bytes, bytes);
+ event CommitmentStored(uint256 indexed id, address indexed sender, bytes, bytes);
event CommitmentExecuted(uint256 indexed id, address indexed sender);
event CommitmentCanceled(uint256 indexed id, address indexed canceller);
@@ -66,8 +66,7 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
}
struct Commitment {
- bytes ciphertext; // AES/GCM ciphertext (includes tag)
- bytes encKeyForAuditor; // encrypted symmetric key for auditor
+ bytes ciphertextForAuditor; //ECIES-encrypted swap details for auditor
bytes permit;
CommitmentStatus status;
}
@@ -98,10 +97,10 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
});
}
- // -----------------------------------------------
- // 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
@@ -111,12 +110,16 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
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);
+ // 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);
if (!suitabilityOk) {
revert InvalidSuitabilityProof();
@@ -126,24 +129,82 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
}
/**
- * Determines the origin of the transaction.
- *
- * @param _sender The sender of the transaction
- *
- * @return origin_ The origin of the transaction
+ * @notice Stores a new encrypted swap commitment onchain.
+ * @dev Each commitment is uniquely identified by `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 id 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 _determineOrigin(address _sender) internal returns (address origin_) {
- // Set our default origin to the `tx.origin`
- origin_ = tx.origin;
+ function storeCommitment(
+ PoolKey calldata key,
+ uint256 id,
+ bytes calldata ciphertextForAuditor,
+ bytes calldata permit
+ ) external {
+ if (commitments[key.toId()][id].status != CommitmentStatus.None) {
+ revert AlreadyExists(id);
+ }
- // If the sender has a `msgSender` function, then we use that to determine the origin
- (bool success, bytes memory data) = _sender.call(abi.encodeWithSignature("msgSender()"));
- if (success && data.length >= 32) {
- origin_ = abi.decode(data, (address));
+ commitments[key.toId()][id] =
+ Commitment({ ciphertextForAuditor: ciphertextForAuditor, permit: permit, status: CommitmentStatus.Active });
+ emit CommitmentStored(id, msg.sender, ciphertextForAuditor, permit);
+ }
+
+ /**
+ * @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 id Unique identifier of the commitment to cancel.
+ * @param zkProof ABI-encoded zkSNARK proof data (pA, pB, pC, pubSignals) to authorize the cancellation.
+ * Emits a {CommitmentCanceled} event.
+ */
+ function cancelCommitment(PoolKey calldata key, uint256 id, bytes calldata zkProof) external {
+ Commitment storage c = commitments[key.toId()][id];
+ if (c.status != CommitmentStatus.Active) {
+ revert CommitmentNotActive(id);
+ }
+
+ (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) =
+ abi.decode(zkProof, (uint256[2], uint256[2][2], uint256[2], uint256[5]));
+
+ // Making sure we are executing the right commitment
+ bytes memory pubId = abi.encode(pubSignals[0], c.ciphertextForAuditor);
+
+ if (keccak256(pubId) != bytes32(id)) {
+ revert CommitmentMismatch(pubId, id);
+ }
+
+ bool privateVerifierOk = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals);
+ if (!privateVerifierOk) {
+ revert InvalidPrivateSwapIntentProof();
}
+
+ // We can cancel now
+ c.status = CommitmentStatus.Canceled;
+ delete c.ciphertextForAuditor;
+ delete c.permit;
+ emit CommitmentCanceled(id, msg.sender);
}
- function executeCommitment(PoolKey calldata key, uint256 id, bytes calldata data)
+ /**
+ * @notice Executes a previously stored encrypted swap commitment once its conditions are met.
+ * @dev Verifies a zkSNARK proof to ensure the executor knows the commitment’s plaintext
+ * and that the onchain commitment matches the provided proof. 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 id Unique identifier of the commitment to execute.
+ * @param zkProof 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 id, bytes calldata zkProof)
external
nonReentrant
returns (BalanceDelta)
@@ -157,7 +218,7 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
}
(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]));
+ abi.decode(zkProof, (uint256[2], uint256[2][2], uint256[2], uint256[5]));
if (pubSignals[4] > block.timestamp) {
revert CommitmentNotReady(pubSignals[4], block.timestamp);
@@ -166,8 +227,7 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
// Making sure we are executing the right commitment
bytes memory pubId = abi.encode(
pubSignals[0], // Poseidon hash of (amount, recipient, nonce)
- commitment.ciphertext,
- commitment.encKeyForAuditor
+ commitment.ciphertextForAuditor
);
if (keccak256(pubId) != bytes32(id)) {
@@ -203,7 +263,6 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
key,
SwapParams({
zeroForOne: zeroForOne,
- // We provide a negative value here to signify an "exact input for output" swap
amountSpecified: -int256(pubSignals[1]),
// No slippage limits (maximum slippage possible)
sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1
@@ -216,59 +275,6 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
return delta;
}
- function storeCommitment(
- PoolKey calldata key,
- uint256 id,
- bytes calldata ciphertext,
- bytes calldata encKeyForAuditor,
- bytes calldata permit
- ) external {
- if (commitments[key.toId()][id].status != CommitmentStatus.None) {
- revert AlreadyExists(id);
- }
-
- commitments[key.toId()][id] = Commitment({
- ciphertext: ciphertext,
- encKeyForAuditor: encKeyForAuditor,
- permit: permit,
- status: CommitmentStatus.Active
- });
- emit CommitmentStored(id, msg.sender, ciphertext, encKeyForAuditor, permit);
- }
-
- function cancelCommitment(PoolKey calldata key, uint256 id, bytes calldata data) external {
- Commitment storage c = commitments[key.toId()][id];
- if (c.status != CommitmentStatus.Active) {
- revert CommitmentNotActive(id);
- }
-
- (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.encode(
- pubSignals[0], // Poseidon hash of (amount, recipient, nonce)
- c.ciphertext,
- c.encKeyForAuditor
- );
-
- if (keccak256(pubId) != bytes32(id)) {
- revert CommitmentMismatch(pubId, id);
- }
-
- bool privateVerifierOk = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals);
- if (!privateVerifierOk) {
- revert InvalidPrivateSwapIntentProof();
- }
-
- // We can cancel now
- c.status = CommitmentStatus.Canceled;
- delete c.ciphertext;
- delete c.encKeyForAuditor;
- delete c.permit;
- emit CommitmentCanceled(id, msg.sender);
- }
-
function swapAndSettleBalances(PoolKey memory key, SwapParams memory params) internal returns (BalanceDelta) {
// Conduct the swap inside the Pool Manager
BalanceDelta delta = poolManager.swap(key, params, "");
@@ -327,4 +333,22 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
v := byte(0, mload(add(sig, 96)))
}
}
+
+ /**
+ * Determines the origin of the transaction.
+ *
+ * @param _sender The sender of the transaction
+ *
+ * @return origin_ The origin of the transaction
+ */
+ function _determineOrigin(address _sender) internal returns (address origin_) {
+ // Set our default origin to the `tx.origin`
+ origin_ = tx.origin;
+
+ // If the sender has a `msgSender` function, then we use that to determine the origin
+ (bool success, bytes memory data) = _sender.call(abi.encodeWithSignature("msgSender()"));
+ if (success && data.length >= 32) {
+ origin_ = abi.decode(data, (address));
+ }
+ }
}
diff --git a/packages/foundry/test/RaylsHook.t.sol b/packages/foundry/test/RaylsHook.t.sol
index a141c33..efaa031 100644
--- a/packages/foundry/test/RaylsHook.t.sol
+++ b/packages/foundry/test/RaylsHook.t.sol
@@ -196,27 +196,17 @@ contract RaylsHookTest is Test, Deployers {
uint256 amountIn = pubSignals[1];
uint256 timestamp = pubSignals[4];
- string memory encKeyForAuditorStr = jsonEncryptedPayload.readString(".encKeyForAuditor");
- string memory ciphertextStr = jsonEncryptedPayload.readString(".ciphertext");
-
- bytes memory encKeyForAuditor = RaylsHookHelper.hexStringToBytes(encKeyForAuditorStr);
- bytes memory ciphertext = RaylsHookHelper.hexStringToBytes(ciphertextStr);
-
- uint256 id = uint256(
- keccak256(
- abi.encode(
- pubSignals[0], // Poseidon hash of (amountIn, zeroForOne, sender, timestamp)
- ciphertext, // AES-encrypted message optional
- encKeyForAuditor // optional: can be empty bytes
- )
- )
- );
+ string memory ciphertextForAuditorStr = jsonEncryptedPayload.readString(".ciphertextForAuditor");
+
+ bytes memory ciphertextForAuditor = RaylsHookHelper.hexStringToBytes(ciphertextForAuditorStr);
+
+ uint256 id = uint256(keccak256(abi.encode(pubSignals[0], ciphertextForAuditor)));
vm.startPrank(proofSender, proofSender);
bytes memory permitSignature = RaylsHookHelper.buildPermitSignature(
vm, proofSenderPk, Currency.unwrap(currency0), timestamp, proofSender, address(hook), amountIn
);
- hook.storeCommitment(poolKey, id, ciphertext, encKeyForAuditor, permitSignature);
+ hook.storeCommitment(poolKey, id, ciphertextForAuditor, permitSignature);
// For now we just approve the swapRouter to spend the tokens
// IERC20Minimal(Currency.unwrap(currency0)).approve(address(hook), amountIn);
@@ -234,19 +224,16 @@ contract RaylsHookTest is Test, Deployers {
assertEq(int256(delta.amount0()), -int256(amountIn));
vm.stopPrank();
- (bytes memory onChainCiphertext, bytes memory onChainEncKeyForAuditor,, RaylsHook.CommitmentStatus status) =
- hook.commitments(poolKey.toId(), id);
+ (bytes memory onChainCiphertext,, RaylsHook.CommitmentStatus status) = hook.commitments(poolKey.toId(), id);
assertEq(uint8(status), uint8(RaylsHook.CommitmentStatus.Executed));
- assertEq(onChainCiphertext, ciphertext);
- assertEq(onChainEncKeyForAuditor, encKeyForAuditor);
+ assertEq(onChainCiphertext, ciphertextForAuditor);
+
string memory hexOnChainCiphertext = vm.toString(onChainCiphertext);
- string memory hexOnChainEncKeyForAuditor = vm.toString(onChainEncKeyForAuditor);
// 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 decryptedCommitmentId = RaylsHookHelper.decryptCiphertext(vm, auditorPk, hexOnChainCiphertext);
// Check that commitmentId is correct
assertEq(pubSignals[0], decryptedCommitmentId);
diff --git a/packages/foundry/test/utils/RaylsHookHelper.sol b/packages/foundry/test/utils/RaylsHookHelper.sol
index b5cae35..c8503a2 100644
--- a/packages/foundry/test/utils/RaylsHookHelper.sol
+++ b/packages/foundry/test/utils/RaylsHookHelper.sol
@@ -9,21 +9,18 @@ import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IER
library RaylsHookHelper {
using stdJson for string;
- function decryptCiphertext(
- Vm vm,
- string memory _auditorPk,
- string memory ciphertext,
- string memory encKeyForAuditor
- ) public returns (uint256 output) {
+ 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);
From fd2c13e9b384e55290cb45c503e44b4ca440eee7 Mon Sep 17 00:00:00 2001
From: Nuno
Date: Fri, 12 Sep 2025 18:41:11 +0100
Subject: [PATCH 05/29] feat: removing comment
---
packages/foundry/contracts/RaylsHook.sol | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/foundry/contracts/RaylsHook.sol b/packages/foundry/contracts/RaylsHook.sol
index ee5ff9e..a83cb38 100644
--- a/packages/foundry/contracts/RaylsHook.sol
+++ b/packages/foundry/contracts/RaylsHook.sol
@@ -226,7 +226,7 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
// Making sure we are executing the right commitment
bytes memory pubId = abi.encode(
- pubSignals[0], // Poseidon hash of (amount, recipient, nonce)
+ pubSignals[0], // Poseidon hash
commitment.ciphertextForAuditor
);
From 68b2ba500bb10814f79300afe7f755c68c437fd7 Mon Sep 17 00:00:00 2001
From: Nuno
Date: Mon, 15 Sep 2025 15:43:01 +0100
Subject: [PATCH 06/29] feat: adding more tests, reorganizing
---
.gitignore | 6 +-
packages/foundry/contracts/RaylsHook.sol | 43 ++---
packages/foundry/contracts/YourContract.sol | 88 ---------
packages/foundry/foundry.toml | 2 +-
packages/foundry/script/Deploy.s.sol | 4 -
.../foundry/script/DeployYourContract.s.sol | 30 ---
packages/foundry/test/RaylsHook.t.sol | 179 ++++++++++++++++--
packages/foundry/test/YourContract.t.sol | 17 --
.../foundry/test/utils/RaylsHookHelper.sol | 33 ++++
9 files changed, 218 insertions(+), 184 deletions(-)
delete mode 100644 packages/foundry/contracts/YourContract.sol
delete mode 100644 packages/foundry/script/DeployYourContract.s.sol
delete mode 100644 packages/foundry/test/YourContract.t.sol
diff --git a/.gitignore b/.gitignore
index 8bbbbd5..c6a3ff9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,8 @@ dist
# artifacts
packages/circom/artifacts
-packages/foundry/inputs
\ No newline at end of file
+packages/foundry/inputs
+
+# coverage
+packages/foundry/coverage
+lcov.info
\ No newline at end of file
diff --git a/packages/foundry/contracts/RaylsHook.sol b/packages/foundry/contracts/RaylsHook.sol
index a83cb38..8fb0f4d 100644
--- a/packages/foundry/contracts/RaylsHook.sol
+++ b/packages/foundry/contracts/RaylsHook.sol
@@ -44,14 +44,13 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
event CommitmentStored(uint256 indexed id, address indexed sender, bytes, bytes);
event CommitmentExecuted(uint256 indexed id, address indexed sender);
- event CommitmentCanceled(uint256 indexed id, address indexed canceller);
+ event CommitmentCancelled(uint256 indexed id, address indexed canceller);
// Errors
error CommitmentMismatch(bytes pubId, uint256 expectedId);
error CommitmentNotReady(uint256 notBefore, uint256 currentTime);
error InvalidWallet(address provided, address expected);
error InvalidSuitabilityProof();
- error AlreadyExecuted(uint256 id);
error InvalidPrivateSwapIntentProof();
error AlreadyExists(uint256 id);
error CommitmentNotActive(uint256 id);
@@ -61,7 +60,7 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
None, // default, not stored
Active, // stored but not yet executed
Executed, // executed successfully
- Canceled // canceled by creator
+ Cancelled // canceled by creator
}
@@ -130,7 +129,7 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
/**
* @notice Stores a new encrypted swap commitment onchain.
- * @dev Each commitment is uniquely identified by `id` under a specific pool key.
+ * @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.
@@ -161,23 +160,23 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
* 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 id Unique identifier of the commitment to cancel.
- * @param zkProof ABI-encoded zkSNARK proof data (pA, pB, pC, pubSignals) to authorize the cancellation.
- * Emits a {CommitmentCanceled} event.
+ * @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 id, bytes calldata zkProof) external {
+ function cancelCommitment(PoolKey calldata key, uint256 id, bytes calldata zkSnarkProof) external {
Commitment storage c = commitments[key.toId()][id];
if (c.status != CommitmentStatus.Active) {
revert CommitmentNotActive(id);
}
(uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) =
- abi.decode(zkProof, (uint256[2], uint256[2][2], uint256[2], uint256[5]));
+ abi.decode(zkSnarkProof, (uint256[2], uint256[2][2], uint256[2], uint256[5]));
// Making sure we are executing the right commitment
- bytes memory pubId = abi.encode(pubSignals[0], c.ciphertextForAuditor);
+ bytes memory commitmentId = abi.encode(pubSignals[0], c.ciphertextForAuditor);
- if (keccak256(pubId) != bytes32(id)) {
- revert CommitmentMismatch(pubId, id);
+ if (keccak256(commitmentId) != bytes32(id)) {
+ revert CommitmentMismatch(commitmentId, id);
}
bool privateVerifierOk = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals);
@@ -186,25 +185,25 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
}
// We can cancel now
- c.status = CommitmentStatus.Canceled;
+ c.status = CommitmentStatus.Cancelled;
delete c.ciphertextForAuditor;
delete c.permit;
- emit CommitmentCanceled(id, msg.sender);
+ emit CommitmentCancelled(id, msg.sender);
}
/**
* @notice Executes a previously stored encrypted swap commitment once its conditions are met.
- * @dev Verifies a zkSNARK proof to ensure the executor knows the commitment’s plaintext
- * and that the onchain commitment matches the provided proof. Uses ERC20 permit to
- * pull tokens from the original sender, then executes a Uniswap v4 swap through
+ * @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 id Unique identifier of the commitment to execute.
- * @param zkProof ABI-encoded zkSNARK proof data (pA, pB, pC, pubSignals) to authorize the execution.
+ * @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 id, bytes calldata zkProof)
+ function executeCommitment(PoolKey calldata key, uint256 id, bytes calldata zkSnarkProof)
external
nonReentrant
returns (BalanceDelta)
@@ -218,20 +217,20 @@ contract RaylsHook is BaseHook, IUnlockCallback, ReentrancyGuard {
}
(uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) =
- abi.decode(zkProof, (uint256[2], uint256[2][2], uint256[2], uint256[5]));
+ 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
- bytes memory pubId = abi.encode(
+ bytes memory commitmentId = abi.encode(
pubSignals[0], // Poseidon hash
commitment.ciphertextForAuditor
);
- if (keccak256(pubId) != bytes32(id)) {
- revert CommitmentMismatch(pubId, id);
+ if (keccak256(commitmentId) != bytes32(id)) {
+ revert CommitmentMismatch(commitmentId, id);
}
bool privateVerifierOk = privateSwapIntentVerifier.verifyProof(pA, pB, pC, pubSignals);
diff --git a/packages/foundry/contracts/YourContract.sol b/packages/foundry/contracts/YourContract.sol
deleted file mode 100644
index b653427..0000000
--- a/packages/foundry/contracts/YourContract.sol
+++ /dev/null
@@ -1,88 +0,0 @@
-//SPDX-License-Identifier: MIT
-pragma solidity >=0.8.0 <0.9.0;
-
-// Useful for debugging. Remove when deploying to a live network.
-import "forge-std/console.sol";
-
-// Use openzeppelin to inherit battle-tested implementations (ERC20, ERC721, etc)
-// import "@openzeppelin/contracts/access/Ownable.sol";
-
-/**
- * A smart contract that allows changing a state variable of the contract and tracking the changes
- * It also allows the owner to withdraw the Ether in the contract
- * @author BuidlGuidl
- */
-contract YourContract {
- // State Variables
- address public immutable owner;
- string public greeting = "Building Unstoppable Apps!!!";
- bool public premium = false;
- uint256 public totalCounter = 0;
- mapping(address => uint256) public userGreetingCounter;
- address public delegate;
-
- // Events: a way to emit log statements from smart contract that can be listened to by external parties
- event GreetingChange(address indexed greetingSetter, string newGreeting, bool premium, uint256 value);
-
- event DelegateChanged(address indexed newDelegate, address indexed oldDelegate);
-
- // Constructor: Called once on contract deployment
- // Check packages/foundry/deploy/Deploy.s.sol
- constructor(address _owner) {
- owner = _owner;
- }
-
- // Modifier: used to define a set of rules that must be met before or after a function is executed
- // Check the withdraw() function
- modifier isOwner() {
- // msg.sender: predefined variable that represents address of the account that called the current function
- require(msg.sender == owner, "Not the Owner");
- _;
- }
-
- function setDelegate(address _delegate) public isOwner {
- address oldDelegate = delegate;
- delegate = _delegate;
- emit DelegateChanged(_delegate, oldDelegate);
- }
-
- /**
- * Function that allows anyone to change the state variable "greeting" of the contract and increase the counters
- *
- * @param _newGreeting (string memory) - new greeting to save on the contract
- */
- function setGreeting(string memory _newGreeting) public payable {
- // Print data to the anvil chain console. Remove when deploying to a live network.
-
- console.logString("Setting new greeting");
- console.logString(_newGreeting);
-
- greeting = _newGreeting;
- totalCounter += 1;
- userGreetingCounter[msg.sender] += 1;
-
- // msg.value: built-in global variable that represents the amount of ether sent with the transaction
- if (msg.value > 0) {
- premium = true;
- } else {
- premium = false;
- }
-
- // emit: keyword used to trigger an event
- emit GreetingChange(msg.sender, _newGreeting, msg.value > 0, msg.value);
- }
-
- /**
- * Function that allows the owner to withdraw all the Ether in the contract
- * The function can only be called by the owner of the contract as defined by the isOwner modifier
- */
- function withdraw() public isOwner {
- (bool success,) = owner.call{ value: address(this).balance }("");
- require(success, "Failed to send Ether");
- }
-
- /**
- * Function that allows the contract to receive ETH
- */
- receive() external payable { }
-}
diff --git a/packages/foundry/foundry.toml b/packages/foundry/foundry.toml
index bcfd5e4..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 = true
ffi = true
-
[rpc_endpoints]
default_network = "http://127.0.0.1:8545"
diff --git a/packages/foundry/script/Deploy.s.sol b/packages/foundry/script/Deploy.s.sol
index 2ad973b..97945b6 100644
--- a/packages/foundry/script/Deploy.s.sol
+++ b/packages/foundry/script/Deploy.s.sol
@@ -2,7 +2,6 @@
pragma solidity ^0.8.19;
import "./DeployHelpers.s.sol";
-import { DeployYourContract } from "./DeployYourContract.s.sol";
import { DeploySuitabilityVerifier } from "./04_DeploySuitabilityVerifier.s.sol";
/**
@@ -16,9 +15,6 @@ contract DeployScript is ScaffoldETHDeploy {
// Deploys all your contracts sequentially
// Add new deployments here when needed
- DeployYourContract deployYourContract = new DeployYourContract();
- deployYourContract.run();
-
DeploySuitabilityVerifier deploySuitabilityVerifier = new DeploySuitabilityVerifier();
deploySuitabilityVerifier.run();
diff --git a/packages/foundry/script/DeployYourContract.s.sol b/packages/foundry/script/DeployYourContract.s.sol
deleted file mode 100644
index cafa004..0000000
--- a/packages/foundry/script/DeployYourContract.s.sol
+++ /dev/null
@@ -1,30 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.19;
-
-import "./DeployHelpers.s.sol";
-import "../contracts/YourContract.sol";
-
-/**
- * @notice Deploy script for YourContract contract
- * @dev Inherits ScaffoldETHDeploy which:
- * - Includes forge-std/Script.sol for deployment
- * - Includes ScaffoldEthDeployerRunner modifier
- * - Provides `deployer` variable
- * Example:
- * yarn deploy --file DeployYourContract.s.sol # local anvil chain
- * yarn deploy --file DeployYourContract.s.sol --network optimism # live network (requires keystore)
- */
-contract DeployYourContract is ScaffoldETHDeploy {
- /**
- * @dev Deployer setup based on `ETH_KEYSTORE_ACCOUNT` in `.env`:
- * - "scaffold-eth-default": Uses Anvil's account #9 (0xa0Ee7A142d267C1f36714E4a8F75612F20a79720), no password prompt
- * - "scaffold-eth-custom": requires password used while creating keystore
- *
- * Note: Must use ScaffoldEthDeployerRunner modifier to:
- * - Setup correct `deployer` account and fund it
- * - Export contract addresses & ABIs to `nextjs` packages
- */
- function run() external ScaffoldEthDeployerRunner {
- new YourContract(0xb5CD58d8d8D3A6138d5a7A68a3E51E1fCC63b3a3);
- }
-}
diff --git a/packages/foundry/test/RaylsHook.t.sol b/packages/foundry/test/RaylsHook.t.sol
index efaa031..5e07eeb 100644
--- a/packages/foundry/test/RaylsHook.t.sol
+++ b/packages/foundry/test/RaylsHook.t.sol
@@ -129,6 +129,31 @@ contract RaylsHookTest is Test, Deployers {
currency0.transfer(invalidProofSender, 1e18);
}
+ function testSwapRevertsWithInvalidProof() 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);
+
+ vm.expectRevert();
+ swapRouter.swapExactTokensForTokens({
+ amountIn: amountIn,
+ amountOutMin: 0,
+ zeroForOne: true,
+ poolKey: poolKey,
+ hookData: fakeProofData,
+ receiver: address(proofSender),
+ deadline: block.timestamp + 1
+ });
+
+ vm.stopPrank();
+ }
+
function testVerifyProofInBeforeSwap() public {
(uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) =
RaylsHookHelper.loadSuitabilityProof(jsonSuitability);
@@ -153,12 +178,13 @@ contract RaylsHookTest is Test, Deployers {
vm.startPrank(proofSender, proofSender);
IERC20Minimal(Currency.unwrap(currency0)).approve(address(swapRouter), amountIn);
+ // Sucessful swap
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
});
@@ -176,7 +202,7 @@ contract RaylsHookTest is Test, Deployers {
// 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);
}
function testPrivateSwapIntentVerifier() public view {
@@ -185,43 +211,76 @@ contract RaylsHookTest is Test, Deployers {
// 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);
}
function testAPrivateSwap() public {
- (uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) =
- RaylsHookHelper.loadPrivateSwapIntentProof(jsonPrivateSwap);
+ // Get the proof and public signals from the json file
+ (uint256 amountIn, uint256 timestamp, uint256 poseidonHash, bytes memory proofData) =
+ RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, false);
- bytes memory proofData = abi.encode(pA, pB, pC, pubSignals);
- uint256 amountIn = pubSignals[1];
- uint256 timestamp = pubSignals[4];
-
- string memory ciphertextForAuditorStr = jsonEncryptedPayload.readString(".ciphertextForAuditor");
+ // Get the ciphertext for the auditor from the json file
+ bytes memory ciphertextForAuditor = RaylsHookHelper.getJsonCiphertext(jsonEncryptedPayload);
- bytes memory ciphertextForAuditor = RaylsHookHelper.hexStringToBytes(ciphertextForAuditorStr);
-
- uint256 id = uint256(keccak256(abi.encode(pubSignals[0], ciphertextForAuditor)));
+ // Calculate the commitment ID off-chain using both the ZK Snark Poseidon Hash and the ciphertext
+ uint256 id = uint256(keccak256(abi.encode(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), timestamp, proofSender, address(hook), amountIn
);
+
+ // Call the hook to store the commitment
+ hook.storeCommitment(poolKey, id, ciphertextForAuditor, permitSignature);
+
+ // Revert if already exsits
+ bytes memory expectedRevert = abi.encodeWithSelector(RaylsHook.AlreadyExists.selector, id);
+ vm.expectRevert(expectedRevert);
hook.storeCommitment(poolKey, id, 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 to execute the commitment
- vm.warp(pubSignals[4] - 1);
- bytes memory expectedRevert =
- abi.encodeWithSelector(RaylsHook.CommitmentNotReady.selector, pubSignals[4], block.timestamp);
+ vm.warp(timestamp - 1);
+ expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotReady.selector, timestamp, block.timestamp);
vm.expectRevert(expectedRevert);
hook.executeCommitment(poolKey, id, proofData);
// Move time forward to be able to execute the commitment
- vm.warp(pubSignals[4]);
+ vm.warp(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, proofData);
+
+ // Revert if the public signal poseidon hash is invalid
+ (,, uint256 fakePoseidonHash, bytes memory fakePoseidonHashProof) =
+ RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, true);
+ bytes memory fakeCommitmentId = abi.encode(fakePoseidonHash, ciphertextForAuditor);
+ expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentMismatch.selector, fakeCommitmentId, id);
+ vm.expectRevert(expectedRevert);
+ hook.executeCommitment(poolKey, id, fakePoseidonHashProof);
+
+ // Revert if the proof is invalid
+ (,,, bytes memory fakeProof) =
+ RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, true, false);
+ expectedRevert = abi.encodeWithSelector(RaylsHook.InvalidPrivateSwapIntentProof.selector);
+ vm.expectRevert(expectedRevert);
+ hook.executeCommitment(poolKey, id, fakeProof);
+
+ // Execute the commitment successfully
BalanceDelta delta = hook.executeCommitment(poolKey, id, proofData);
assertEq(int256(delta.amount0()), -int256(amountIn));
+
+ // Revert if we want to execute it again
+ expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotActive.selector, id);
+ vm.expectRevert(expectedRevert);
+ hook.executeCommitment(poolKey, id, proofData);
vm.stopPrank();
(bytes memory onChainCiphertext,, RaylsHook.CommitmentStatus status) = hook.commitments(poolKey.toId(), id);
@@ -233,12 +292,90 @@ contract RaylsHookTest is Test, Deployers {
// 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);
+ uint256 decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, hexOnChainCiphertext);
// Check that commitmentId is correct
- assertEq(pubSignals[0], decryptedCommitmentId);
+ assertEq(poseidonHash, decryptedPoseidonHash);
+ }
- // assertEq(selector, IHooks.beforeSwap.selector, "selector mismatch");
- // delta and fee are placeholder, assert if needed
+ /**
+ * 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 test_EncryptedCommitmentForAuditor() public {
+ // Get the proof and public signals from the json file
+ (,, uint256 poseidonHash,) =
+ RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, false);
+
+ // Get the ciphertext for the auditor from the json file
+ bytes memory ciphertextForAuditor = RaylsHookHelper.getJsonCiphertext(jsonEncryptedPayload);
+ string memory hexOnChainCiphertext = vm.toString(ciphertextForAuditor);
+
+ // Decrypt off-chain and use the poseidonHash for comparison
+ // It must match to the one stored on-chain created by the circuit.
+ uint256 decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, hexOnChainCiphertext);
+
+ // Check that hashes are equal
+ assertEq(poseidonHash, decryptedPoseidonHash);
+ }
+
+ function test_cancelCommitment() public {
+ // Get the proof and public signals from the json file
+ (uint256 amountIn, uint256 timestamp, uint256 poseidonHash, bytes memory proofData) =
+ 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 id = uint256(keccak256(abi.encode(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), timestamp, proofSender, address(hook), amountIn
+ );
+
+ // Call the hook to store the commitment
+ hook.storeCommitment(poolKey, id, ciphertextForAuditor, permitSignature);
+
+ // Revert if the public signal poseidon hash is invalid
+ (,, uint256 fakePoseidonHash, bytes memory fakePoseidonHashProof) =
+ RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, true);
+ bytes memory fakeCommitmentId = abi.encode(fakePoseidonHash, ciphertextForAuditor);
+ bytes memory expectedRevert =
+ abi.encodeWithSelector(RaylsHook.CommitmentMismatch.selector, fakeCommitmentId, id);
+ vm.expectRevert(expectedRevert);
+ hook.cancelCommitment(poolKey, id, fakePoseidonHashProof);
+
+ // Revert if the proof is invalid
+ (,,, bytes memory fakeProof) =
+ RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, true, false);
+ expectedRevert = abi.encodeWithSelector(RaylsHook.InvalidPrivateSwapIntentProof.selector);
+ vm.expectRevert(expectedRevert);
+ hook.cancelCommitment(poolKey, id, fakeProof);
+
+ // Cancel it before it can be executed
+ hook.cancelCommitment(poolKey, id, proofData);
+
+ // Cancel it again shoule revvert
+ expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotActive.selector, id);
+ vm.expectRevert(expectedRevert);
+ hook.cancelCommitment(poolKey, id, proofData);
+
+ // Move time forward to be able to execute the commitment
+ vm.warp(timestamp + 1);
+
+ // Revert if we try to execute a cancelled commitment
+ expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotActive.selector, id);
+ vm.expectRevert(expectedRevert);
+ hook.executeCommitment(poolKey, id, proofData);
+ vm.stopPrank();
+
+ (,, RaylsHook.CommitmentStatus status) = hook.commitments(poolKey.toId(), id);
+
+ assertEq(uint8(status), uint8(RaylsHook.CommitmentStatus.Cancelled));
}
}
diff --git a/packages/foundry/test/YourContract.t.sol b/packages/foundry/test/YourContract.t.sol
deleted file mode 100644
index 5e6ff10..0000000
--- a/packages/foundry/test/YourContract.t.sol
+++ /dev/null
@@ -1,17 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import "forge-std/Test.sol";
-import "../contracts/YourContract.sol";
-
-contract YourContractTest is Test {
- YourContract public yourContract;
-
- function setUp() public {
- yourContract = new YourContract(vm.addr(1));
- }
-
- function testMessageOnDeployment() public view {
- require(keccak256(bytes(yourContract.greeting())) == keccak256("Building Unstoppable Apps!!!"));
- }
-}
diff --git a/packages/foundry/test/utils/RaylsHookHelper.sol b/packages/foundry/test/utils/RaylsHookHelper.sol
index c8503a2..2b639a4 100644
--- a/packages/foundry/test/utils/RaylsHookHelper.sol
+++ b/packages/foundry/test/utils/RaylsHookHelper.sol
@@ -178,4 +178,37 @@ library RaylsHookHelper {
(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 (uint256 amountIn, uint256 timestamp, uint256 poseidonHash, bytes memory proofData)
+ {
+ (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;
+ }
+
+ proofData = abi.encode(pA, pB, pC, pubSignals);
+ poseidonHash = pubSignals[0];
+ amountIn = pubSignals[1];
+ timestamp = pubSignals[4];
+ }
+
+ function getJsonCiphertext(string memory _jsonEncryptedPayload)
+ public
+ pure
+ returns (bytes memory ciphertextForAuditor)
+ {
+ string memory ciphertextForAuditorStr = _jsonEncryptedPayload.readString(".ciphertextForAuditor");
+ ciphertextForAuditor = RaylsHookHelper.hexStringToBytes(ciphertextForAuditorStr);
+ }
}
From 74b398a5be0485b451330db2c5d60017e4f86ba5 Mon Sep 17 00:00:00 2001
From: Nuno
Date: Mon, 15 Sep 2025 15:55:03 +0100
Subject: [PATCH 07/29] feat: removing scafold unneeded file
---
packages/foundry/script/00_DeployHook.s.sol | 30 ---------------------
1 file changed, 30 deletions(-)
delete mode 100644 packages/foundry/script/00_DeployHook.s.sol
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
From cca064b7b18051c6ac27eeeaff0d9cb1f2c4eb0e Mon Sep 17 00:00:00 2001
From: Nuno
Date: Mon, 15 Sep 2025 16:29:24 +0100
Subject: [PATCH 08/29] feat: adding correct example inputs for tests to pass
---
packages/circom/package.json | 2 +-
.../scripts/Suitability_input.example.json | 2 +-
packages/circom/scripts/Suitability_input.json | 18 ++++++++----------
3 files changed, 10 insertions(+), 12 deletions(-)
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
From 8d694fed5fc41842ecacefa4148c5285abf0db8c Mon Sep 17 00:00:00 2001
From: Nuno
Date: Mon, 15 Sep 2025 17:06:25 +0100
Subject: [PATCH 09/29] feat: adding struct in tests
---
packages/foundry/package.json | 1 +
packages/foundry/test/RaylsHook.t.sol | 75 +++++++++++--------
.../foundry/test/utils/RaylsHookHelper.sol | 17 +++--
3 files changed, 57 insertions(+), 36 deletions(-)
diff --git a/packages/foundry/package.json b/packages/foundry/package.json
index be52fdd..b5dbdd4 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",
diff --git a/packages/foundry/test/RaylsHook.t.sol b/packages/foundry/test/RaylsHook.t.sol
index 1a283da..c658d26 100644
--- a/packages/foundry/test/RaylsHook.t.sol
+++ b/packages/foundry/test/RaylsHook.t.sol
@@ -216,20 +216,26 @@ contract RaylsHookTest is Test, Deployers {
function testAPrivateSwap() public {
// Get the proof and public signals from the json file
- (uint256 amountIn, uint256 timestamp, uint256 poseidonHash, bytes memory proofData) =
+ 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 id = uint256(keccak256(abi.encode(poseidonHash, ciphertextForAuditor)));
+ uint256 id = uint256(keccak256(abi.encode(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), timestamp, proofSender, address(hook), amountIn
+ vm,
+ proofSenderPk,
+ Currency.unwrap(currency0),
+ proofCorrect.timestamp,
+ proofSender,
+ address(hook),
+ proofCorrect.amountIn
);
// Call the hook to store the commitment
@@ -244,43 +250,44 @@ contract RaylsHookTest is Test, Deployers {
// IERC20Minimal(Currency.unwrap(currency0)).approve(address(hook), amountIn);
// Move time forward but not enough to be able to execute the commitment
- vm.warp(timestamp - 1);
- expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotReady.selector, timestamp, block.timestamp);
+ vm.warp(proofCorrect.timestamp - 1);
+ expectedRevert =
+ abi.encodeWithSelector(RaylsHook.CommitmentNotReady.selector, proofCorrect.timestamp, block.timestamp);
vm.expectRevert(expectedRevert);
- hook.executeCommitment(poolKey, id, proofData);
+ hook.executeCommitment(poolKey, id, proofCorrect.proofData);
// Move time forward to be able to execute the commitment
- vm.warp(timestamp);
+ 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, proofData);
+ hook.executeCommitment(poolKey, fakeId, proofCorrect.proofData);
// Revert if the public signal poseidon hash is invalid
- (,, uint256 fakePoseidonHash, bytes memory fakePoseidonHashProof) =
+ RaylsHookHelper.PrivateSwapPublic memory proofWithWrongHash =
RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, true);
- bytes memory fakeCommitmentId = abi.encode(fakePoseidonHash, ciphertextForAuditor);
+ bytes memory fakeCommitmentId = abi.encode(proofWithWrongHash.poseidonHash, ciphertextForAuditor);
expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentMismatch.selector, fakeCommitmentId, id);
vm.expectRevert(expectedRevert);
- hook.executeCommitment(poolKey, id, fakePoseidonHashProof);
+ hook.executeCommitment(poolKey, id, proofWithWrongHash.proofData);
// Revert if the proof is invalid
- (,,, bytes memory fakeProof) =
+ RaylsHookHelper.PrivateSwapPublic memory proofWithWrongPA =
RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, true, false);
expectedRevert = abi.encodeWithSelector(RaylsHook.InvalidPrivateSwapIntentProof.selector);
vm.expectRevert(expectedRevert);
- hook.executeCommitment(poolKey, id, fakeProof);
+ hook.executeCommitment(poolKey, id, proofWithWrongPA.proofData);
// Execute the commitment successfully
- BalanceDelta delta = hook.executeCommitment(poolKey, id, proofData);
- assertEq(int256(delta.amount0()), -int256(amountIn));
+ BalanceDelta delta = hook.executeCommitment(poolKey, id, proofCorrect.proofData);
+ assertEq(int256(delta.amount0()), -int256(proofCorrect.amountIn));
// Revert if we want to execute it again
expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotActive.selector, id);
vm.expectRevert(expectedRevert);
- hook.executeCommitment(poolKey, id, proofData);
+ hook.executeCommitment(poolKey, id, proofCorrect.proofData);
vm.stopPrank();
(bytes memory onChainCiphertext,, RaylsHook.CommitmentStatus status) = hook.commitments(poolKey.toId(), id);
@@ -295,7 +302,7 @@ contract RaylsHookTest is Test, Deployers {
uint256 decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, hexOnChainCiphertext);
// Check that commitmentId is correct
- assertEq(poseidonHash, decryptedPoseidonHash);
+ assertEq(proofCorrect.poseidonHash, decryptedPoseidonHash);
}
/**
@@ -305,7 +312,7 @@ contract RaylsHookTest is Test, Deployers {
*/
function test_EncryptedCommitmentForAuditor() public {
// Get the proof and public signals from the json file
- (,, uint256 poseidonHash,) =
+ RaylsHookHelper.PrivateSwapPublic memory proofCorrect =
RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, false);
// Get the ciphertext for the auditor from the json file
@@ -317,61 +324,67 @@ contract RaylsHookTest is Test, Deployers {
uint256 decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, hexOnChainCiphertext);
// Check that hashes are equal
- assertEq(poseidonHash, decryptedPoseidonHash);
+ assertEq(proofCorrect.poseidonHash, decryptedPoseidonHash);
}
function test_cancelCommitment() public {
// Get the proof and public signals from the json file
- (uint256 amountIn, uint256 timestamp, uint256 poseidonHash, bytes memory proofData) =
+ 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 id = uint256(keccak256(abi.encode(poseidonHash, ciphertextForAuditor)));
+ uint256 id = uint256(keccak256(abi.encode(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), timestamp, proofSender, address(hook), amountIn
+ vm,
+ proofSenderPk,
+ Currency.unwrap(currency0),
+ proofCorrect.timestamp,
+ proofSender,
+ address(hook),
+ proofCorrect.amountIn
);
// Call the hook to store the commitment
hook.storeCommitment(poolKey, id, ciphertextForAuditor, permitSignature);
// Revert if the public signal poseidon hash is invalid
- (,, uint256 fakePoseidonHash, bytes memory fakePoseidonHashProof) =
+ RaylsHookHelper.PrivateSwapPublic memory proofWithWrongHash =
RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, false, true);
- bytes memory fakeCommitmentId = abi.encode(fakePoseidonHash, ciphertextForAuditor);
+ bytes memory fakeCommitmentId = abi.encode(proofWithWrongHash.poseidonHash, ciphertextForAuditor);
bytes memory expectedRevert =
abi.encodeWithSelector(RaylsHook.CommitmentMismatch.selector, fakeCommitmentId, id);
vm.expectRevert(expectedRevert);
- hook.cancelCommitment(poolKey, id, fakePoseidonHashProof);
+ hook.cancelCommitment(poolKey, id, proofWithWrongHash.proofData);
// Revert if the proof is invalid
- (,,, bytes memory fakeProof) =
+ RaylsHookHelper.PrivateSwapPublic memory proofWithWrongPA =
RaylsHookHelper.getPublicSignalsFromPrivateSwapIntentProof(jsonPrivateSwap, true, false);
expectedRevert = abi.encodeWithSelector(RaylsHook.InvalidPrivateSwapIntentProof.selector);
vm.expectRevert(expectedRevert);
- hook.cancelCommitment(poolKey, id, fakeProof);
+ hook.cancelCommitment(poolKey, id, proofWithWrongPA.proofData);
// Cancel it before it can be executed
- hook.cancelCommitment(poolKey, id, proofData);
+ hook.cancelCommitment(poolKey, id, proofCorrect.proofData);
// Cancel it again shoule revvert
expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotActive.selector, id);
vm.expectRevert(expectedRevert);
- hook.cancelCommitment(poolKey, id, proofData);
+ hook.cancelCommitment(poolKey, id, proofCorrect.proofData);
// Move time forward to be able to execute the commitment
- vm.warp(timestamp + 1);
+ vm.warp(proofCorrect.timestamp + 1);
// Revert if we try to execute a cancelled commitment
expectedRevert = abi.encodeWithSelector(RaylsHook.CommitmentNotActive.selector, id);
vm.expectRevert(expectedRevert);
- hook.executeCommitment(poolKey, id, proofData);
+ hook.executeCommitment(poolKey, id, proofCorrect.proofData);
vm.stopPrank();
(,, RaylsHook.CommitmentStatus status) = hook.commitments(poolKey.toId(), id);
diff --git a/packages/foundry/test/utils/RaylsHookHelper.sol b/packages/foundry/test/utils/RaylsHookHelper.sol
index 2b639a4..7ece6e0 100644
--- a/packages/foundry/test/utils/RaylsHookHelper.sol
+++ b/packages/foundry/test/utils/RaylsHookHelper.sol
@@ -9,6 +9,13 @@ import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IER
library RaylsHookHelper {
using stdJson for string;
+ struct PrivateSwapPublic {
+ uint256 amountIn;
+ uint256 timestamp;
+ uint256 poseidonHash;
+ bytes proofData;
+ }
+
function decryptCiphertext(Vm vm, string memory _auditorPk, string memory ciphertext)
public
returns (uint256 output)
@@ -182,7 +189,7 @@ library RaylsHookHelper {
function getPublicSignalsFromPrivateSwapIntentProof(string memory json, bool fakePa, bool fakeHash)
public
pure
- returns (uint256 amountIn, uint256 timestamp, uint256 poseidonHash, bytes memory proofData)
+ returns (PrivateSwapPublic memory out)
{
(uint256[2] memory pA, uint256[2][2] memory pB, uint256[2] memory pC, uint256[5] memory pubSignals) =
loadPrivateSwapIntentProof(json);
@@ -197,10 +204,10 @@ library RaylsHookHelper {
pubSignals[0] = 1;
}
- proofData = abi.encode(pA, pB, pC, pubSignals);
- poseidonHash = pubSignals[0];
- amountIn = pubSignals[1];
- timestamp = pubSignals[4];
+ 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)
From a41c046a38c6b06954c421ad9bc3346fcaaaf171 Mon Sep 17 00:00:00 2001
From: Nuno
Date: Mon, 15 Sep 2025 17:13:01 +0100
Subject: [PATCH 10/29] feat: addin lcov as dependency
---
packages/foundry/package.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/foundry/package.json b/packages/foundry/package.json
index b5dbdd4..5034803 100644
--- a/packages/foundry/package.json
+++ b/packages/foundry/package.json
@@ -29,6 +29,7 @@
"toml": "~3.0.0"
},
"devDependencies": {
+ "lcov": "^1.16.0",
"shx": "^0.3.4"
}
}
From 3d973ed9373f29f8d68ff88779ef5a076357fe60 Mon Sep 17 00:00:00 2001
From: Nuno
Date: Mon, 15 Sep 2025 17:19:09 +0100
Subject: [PATCH 11/29] feat: just addin new commands in README
---
README.md | 71 +++++++++++++++++++++++++++++++++----------------------
1 file changed, 43 insertions(+), 28 deletions(-)
diff --git a/README.md b/README.md
index 4b766e9..1a7f25d 100644
--- a/README.md
+++ b/README.md
@@ -46,22 +46,24 @@ Rayls Hook implements a comprehensive investor suitability assessment system tha
### 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
- **Public Outputs**: Commitment hash and verification data
- **Purpose**: Prove swap intent without revealing sensitive details
@@ -95,7 +97,7 @@ yarn install
```bash
# Start local Ethereum network (Scaffold-ETH 2)
-yarn chain
+yarn workspace 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 +105,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 circom setup-suitability # Suitability assessment circuit
+yarn workspace 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 +123,39 @@ 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
+
+```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,12 +192,14 @@ 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
### Phase 2: Frontend Development 🚧
+
- [ ] Complete questionnaire UI implementation
- [ ] ZK proof generation interface
- [ ] Real-time proof verification
@@ -194,6 +207,7 @@ yarn prove-private-swap
- [ ] Integration with wallet providers
### Phase 3: Advanced Features 📋
+
- [ ] Multi-circuit support and management
- [ ] Advanced risk assessment algorithms
- [ ] Compliance and regulatory features
@@ -201,6 +215,7 @@ yarn prove-private-swap
- [ ] Mobile-responsive design
### Phase 4: Production Ready 🎯
+
- [ ] Security audits and testing
- [ ] Performance optimization
- [ ] Documentation and tutorials
@@ -247,4 +262,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.
From df2719e251fcb7e28975c2fde70bb74f107cd33c Mon Sep 17 00:00:00 2001
From: Nuno
Date: Tue, 16 Sep 2025 10:19:03 +0100
Subject: [PATCH 12/29] feat: improved test for auditor decryption
---
packages/foundry/test/RaylsHook.t.sol | 20 ++++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)
diff --git a/packages/foundry/test/RaylsHook.t.sol b/packages/foundry/test/RaylsHook.t.sol
index c658d26..29d7221 100644
--- a/packages/foundry/test/RaylsHook.t.sol
+++ b/packages/foundry/test/RaylsHook.t.sol
@@ -290,16 +290,17 @@ contract RaylsHookTest is Test, Deployers {
hook.executeCommitment(poolKey, id, proofCorrect.proofData);
vm.stopPrank();
+ // Test the auditor part
(bytes memory onChainCiphertext,, RaylsHook.CommitmentStatus status) = hook.commitments(poolKey.toId(), id);
assertEq(uint8(status), uint8(RaylsHook.CommitmentStatus.Executed));
assertEq(onChainCiphertext, ciphertextForAuditor);
- string memory hexOnChainCiphertext = vm.toString(onChainCiphertext);
+ 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 decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, hexOnChainCiphertext);
+ uint256 decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, onChainCiphertextStr);
// Check that commitmentId is correct
assertEq(proofCorrect.poseidonHash, decryptedPoseidonHash);
@@ -317,11 +318,22 @@ contract RaylsHookTest is Test, Deployers {
// Get the ciphertext for the auditor from the json file
bytes memory ciphertextForAuditor = RaylsHookHelper.getJsonCiphertext(jsonEncryptedPayload);
- string memory hexOnChainCiphertext = vm.toString(ciphertextForAuditor);
+ 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.
- uint256 decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, hexOnChainCiphertext);
+ decryptedPoseidonHash = RaylsHookHelper.decryptCiphertext(vm, auditorPk, onChainCiphertextStr);
// Check that hashes are equal
assertEq(proofCorrect.poseidonHash, decryptedPoseidonHash);
From 141da8b8a9c8e4b11aae3ce406da413ee5fa08fb Mon Sep 17 00:00:00 2001
From: Nuno
Date: Tue, 16 Sep 2025 11:58:53 +0100
Subject: [PATCH 13/29] feat: adding README content
---
README.md | 32 ++++++++++--
docs/RaylsHook_diagram.svg | 102 +++++++++++++++++++++++++++++++++++++
docs/privateSwaps.md | 84 ++++++++++++++++++++++++++++++
3 files changed, 214 insertions(+), 4 deletions(-)
create mode 100644 docs/RaylsHook_diagram.svg
create mode 100644 docs/privateSwaps.md
diff --git a/README.md b/README.md
index 1a7f25d..544264e 100644
--- a/README.md
+++ b/README.md
@@ -14,13 +14,19 @@
-🧪 **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:
+
+1. 🛡️ Suitability Verifier Logic – 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 Swap Logic](./docs/privateSwaps.md) (click for more info) – Private swaps with an execution timestamp. Swap values remain hidden and are committed on-chain through a commitment ID, then later executed and validated and revealed with zkSNARK proofs. The values are also encrypted with an Auditor’s wallet public key (optional), and the ciphertext is stored on-chain, enabling the Auditor to independently verify commitments at any time.
+
+There's no integration with Hackaton partners but for private swaps, while commitments and encrypted payloads are currently stored fully on-chain, they could instead be stored in EigenDA with only lightweight references on-chain, reducing gas costs and improving scalability without compromising verifiability.
⚙️ Built using **Scaffold-ETH 2** as the foundation, with **NextJS**, **RainbowKit**, **Foundry**, **Wagmi**, **Circom**, **SnarkJS**, and **TypeScript**.
## 🎯 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 +35,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,6 +58,10 @@ 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 |
@@ -64,7 +82,7 @@ Rayls Hook implements a comprehensive investor suitability assessment system tha
#### 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
@@ -136,6 +154,8 @@ yarn workspace @se-2/foundry test
### 7. Check coverage
+(We focused on RaylsHook contract for full coverage)
+
```bash
yarn workspace @se-2/foundry coverage
```
@@ -197,10 +217,12 @@ yarn prove-private-swap
- [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
@@ -209,6 +231,8 @@ yarn prove-private-swap
### 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
diff --git a/docs/RaylsHook_diagram.svg b/docs/RaylsHook_diagram.svg
new file mode 100644
index 0000000..ad2bc1d
--- /dev/null
+++ b/docs/RaylsHook_diagram.svg
@@ -0,0 +1,102 @@
+
\ No newline at end of file
diff --git a/docs/privateSwaps.md b/docs/privateSwaps.md
new file mode 100644
index 0000000..2035953
--- /dev/null
+++ b/docs/privateSwaps.md
@@ -0,0 +1,84 @@
+## 📄 `docs/privateSwaps.md`
+
+🔐 Private Swap Commitments
+
+This Uniswap v4 hook extension introduces encrypted swap commitments, allowing users to conceal their swap parameters until execution while preserving permissionless execution and optional auditor oversight.
+
+Private swaps are created with an execution timestamp. Swap values are hidden and committed on-chain via a unique commitment ID, then later revealed and validated with zkSNARK proofs. Optionally, swap details are encrypted with an Auditor’s public key and the ciphertext is stored on-chain, enabling independent verification at any time.
+
+Key use cases include:
+
+- MEV protection – hiding swap intent reduces frontrunning risk; at reveal time, private bundlers can be used for additional protection.
+
+- Large swaps – users executing large trades can split them into multiple commitments to minimize pool price impact.
+
+- Compliance & oversight – DAOs and regulated protocols can prove swap schedules on-chain, with auditors able to verify encrypted commitments.
+
+## 🔄 Flow
+
+
+
+
+
+### 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.
+ - 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).
+ - `encKeyForAuditor`: ECIES encryption key for auditor.
+ - `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` + `encKeyForAuditor` onchain.
+ - Decrypt using it's own private key.
+ - Verify swap parameters offchain for compliance.
+ - Validates if permit matches the encrypted values
+
+---
+
+## Key notes
+
+- We use circom for zksnark and AES encryption for the auditor. Encryption in circom is too expensive.
+- We could enforce the auditor to approve a commitement cancelation.
From 655bce49d3af32db1caa85285d7fbd31653aeb03 Mon Sep 17 00:00:00 2001
From: Nuno
Date: Tue, 16 Sep 2025 12:06:03 +0100
Subject: [PATCH 14/29] feat: modifying diagram
---
docs/RaylsHook_diagram.svg | 103 +------------------------------------
docs/privateSwaps.md | 2 +-
2 files changed, 2 insertions(+), 103 deletions(-)
diff --git a/docs/RaylsHook_diagram.svg b/docs/RaylsHook_diagram.svg
index ad2bc1d..457a2d4 100644
--- a/docs/RaylsHook_diagram.svg
+++ b/docs/RaylsHook_diagram.svg
@@ -1,102 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/privateSwaps.md b/docs/privateSwaps.md
index 2035953..5bc097c 100644
--- a/docs/privateSwaps.md
+++ b/docs/privateSwaps.md
@@ -17,7 +17,7 @@ Key use cases include:
## 🔄 Flow
### 1. Create Commitment
From 44aa13d25f26e13e3f37e17c146fc7bf1a8ef85d Mon Sep 17 00:00:00 2001
From: Nuno
Date: Tue, 16 Sep 2025 14:08:41 +0100
Subject: [PATCH 16/29] feat: modifying README
---
README.md | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 544264e..a913ecd 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,11 @@
2. [🔐 Private Swap Logic](./docs/privateSwaps.md) (click for more info) – Private swaps with an execution timestamp. Swap values remain hidden and are committed on-chain through a commitment ID, then later executed and validated and revealed with zkSNARK proofs. The values are also encrypted with an Auditor’s wallet public key (optional), and the ciphertext is stored on-chain, enabling the Auditor to independently verify commitments at any time.
-There's no integration with Hackaton partners but for private swaps, while commitments and encrypted payloads are currently stored fully on-chain, they could instead be stored in EigenDA with only lightweight references on-chain, reducing gas costs and improving scalability without compromising verifiability.
+## 📌 Key Notes
-⚙️ Built using **Scaffold-ETH 2** as the foundation, with **NextJS**, **RainbowKit**, **Foundry**, **Wagmi**, **Circom**, **SnarkJS**, and **TypeScript**.
+- Private swap commitments and encrypted payloads are currently fully stored on-chain, but 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.
## 🎯 Project Overview
From caa0ff2128c46be67b739e4576651bed8fcbddb9 Mon Sep 17 00:00:00 2001
From: Nuno
Date: Tue, 16 Sep 2025 14:21:10 +0100
Subject: [PATCH 17/29] feat: adding simple suitability md
---
README.md | 2 +-
...vg => RaylsHook_private_swaps_diagram.svg} | 0
docs/RaylsHook_suitability_diagram.png | Bin 0 -> 359202 bytes
docs/privateSwaps.md | 2 +-
docs/suitability.md | 51 ++++++++++++++++++
5 files changed, 53 insertions(+), 2 deletions(-)
rename docs/{RaylsHook_sequence_diagram.svg => RaylsHook_private_swaps_diagram.svg} (100%)
create mode 100644 docs/RaylsHook_suitability_diagram.png
create mode 100644 docs/suitability.md
diff --git a/README.md b/README.md
index a913ecd..5a58742 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
**Rayls Hook** introduces two complementary ZK-SNARK enabled features built on Uniswap v4 hooks:
-1. 🛡️ Suitability Verifier Logic – 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.
+1. [🛡️ Suitability Verifier Logic](./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 Swap Logic](./docs/privateSwaps.md) (click for more info) – Private swaps with an execution timestamp. Swap values remain hidden and are committed on-chain through a commitment ID, then later executed and validated and revealed with zkSNARK proofs. The values are also encrypted with an Auditor’s wallet public key (optional), and the ciphertext is stored on-chain, enabling the Auditor to independently verify commitments at any time.
diff --git a/docs/RaylsHook_sequence_diagram.svg b/docs/RaylsHook_private_swaps_diagram.svg
similarity index 100%
rename from docs/RaylsHook_sequence_diagram.svg
rename to docs/RaylsHook_private_swaps_diagram.svg
diff --git a/docs/RaylsHook_suitability_diagram.png b/docs/RaylsHook_suitability_diagram.png
new file mode 100644
index 0000000000000000000000000000000000000000..d701e20cf409f0a5d097627aecdefd01cbb8295e
GIT binary patch
literal 359202
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