Skip to content

Commit 4842f2a

Browse files
authored
feat: make the validator ERC-7780 & ERC-1271 compatible (#20)
* added gas limit in test and remove gas meter tinkering in verifySignature * Removed LibBytes lib inclusion * updated * updated * Some fix on solhint
1 parent daecd5c commit 4842f2a

File tree

5 files changed

+129
-83
lines changed

5 files changed

+129
-83
lines changed

src/SemaphoreMSAValidator.sol

Lines changed: 97 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity >=0.8.23 <=0.8.29;
33

4+
// Rhinestone module-kit
45
import { ERC7579ValidatorBase } from "modulekit/Modules.sol";
5-
import { VALIDATION_SUCCESS } from "modulekit/accounts/common/interfaces/IERC7579Module.sol";
6+
import { IStatelessValidator } from "modulekit/module-bases/interfaces/IStatelessValidator.sol";
67
import { PackedUserOperation } from "modulekit/external/ERC4337.sol";
7-
import { LibSort, LibBytes } from "solady/Milady.sol";
8+
9+
import { LibSort } from "solady/Milady.sol";
810

911
import { ISemaphore, ISemaphoreGroups } from "./utils/Semaphore.sol";
1012
import { ValidatorLibBytes } from "./utils/ValidatorLibBytes.sol";
1113
import { Identity } from "./utils/Identity.sol";
1214
// import { console } from "forge-std/console.sol";
1315

14-
contract SemaphoreMSAValidator is ERC7579ValidatorBase {
16+
contract SemaphoreMSAValidator is ERC7579ValidatorBase, IStatelessValidator {
1517
using LibSort for *;
1618
using ValidatorLibBytes for bytes;
1719

1820
// Constants
1921
uint8 public constant MAX_MEMBERS = 32;
20-
uint8 internal constant CMT_BYTELEN = 32;
22+
uint8 public constant CMT_BYTELEN = 32;
2123

2224
// Ensure the following match with the 3 function calls.
23-
bytes4[3] internal ALLOWED_SELECTORS =
25+
bytes4[3] public ALLOWED_SELECTORS =
2426
[this.initiateTx.selector, this.signTx.selector, this.executeTx.selector];
2527

2628
struct ExtCallCount {
@@ -45,7 +47,6 @@ contract SemaphoreMSAValidator is ERC7579ValidatorBase {
4547
error InvalidSignatureLen(address account, uint256 len);
4648
error InvalidSignature(address account, bytes signature);
4749
error InvalidSemaphoreProof(bytes reason);
48-
error NonAllowedSelector(address account, bytes4 funcSel);
4950
error NonValidatorCallBanned(address targetAddr, address selfAddr);
5051
error InitiateTxWithNullAddress(address account);
5152
error InitiateTxWithNullCallDataAndNullValue(address account, address targetAddr);
@@ -230,7 +231,7 @@ contract SemaphoreMSAValidator is ERC7579ValidatorBase {
230231
// 1. targetAddr cannot be 0
231232
// 2. if txCallData is blank, then msg.value must be > 0, else revert
232233
if (targetAddr == address(0)) revert InitiateTxWithNullAddress(account);
233-
if (LibBytes.cmp(txCallData, "") == 0 && msg.value == 0) {
234+
if (txCallData.length == 0 && msg.value == 0) {
234235
revert InitiateTxWithNullCallDataAndNullValue(account, targetAddr);
235236
}
236237

@@ -322,80 +323,127 @@ contract SemaphoreMSAValidator is ERC7579ValidatorBase {
322323
bytes32 userOpHash
323324
)
324325
external
325-
// view
326+
virtual
326327
override
327328
returns (ValidationData)
328329
{
329-
// you want to exclude initiateTx, signTx, executeTx from needing tx count.
330-
// you just need to ensure they are a valid proof from the semaphore group members
331330
address account = userOp.sender;
332-
uint256 groupId = groupMapping[account];
333-
334-
// The userOp.signature is 160 bytes containing:
335-
// (uint256 pubX (32 bytes), uint256 pubY (32 bytes), bytes[96] signature (96 bytes))
336-
if (userOp.signature.length != 160) {
337-
revert InvalidSignatureLen(account, userOp.signature.length);
331+
bytes calldata targetCallData = userOp.callData[100:];
332+
if (_validateSignatureWithConfig(account, userOpHash, userOp.signature, targetCallData)) {
333+
return VALIDATION_SUCCESS;
338334
}
339-
340-
// Verify signature using the public key
341-
if (!Identity.verifySignature(userOpHash, userOp.signature)) {
342-
revert InvalidSignature(account, userOp.signature);
343-
}
344-
345-
// Verify if the identity commitment is one of the semaphore group members
346-
bytes memory pubKey = LibBytes.slice(userOp.signature, 0, 66);
347-
uint256 cmt = Identity.getCommitment(pubKey);
348-
if (!groups.hasMember(groupId, cmt)) revert MemberNotExists(account, cmt);
349-
350-
// We don't allow call to other contracts.
351-
address targetAddr = address(bytes20(userOp.callData[100:120]));
352-
if (targetAddr != address(this)) revert NonValidatorCallBanned(targetAddr, address(this));
353-
354-
// For callData, the first 120 bytes are reserved by ERC-7579 use. Then 32 bytes of value,
355-
// then the remaining as the callData passed in getExecOps
356-
bytes memory valAndCallData = userOp.callData[120:];
357-
bytes4 funcSel = bytes4(LibBytes.slice(valAndCallData, 32, 36));
358-
359-
// We only allow calls to `initiateTx()`, `signTx()`, and `executeTx()` to pass,
360-
// and reject the rest.
361-
if (_isAllowedSelector(funcSel)) return VALIDATION_SUCCESS;
362-
revert NonAllowedSelector(account, funcSel);
335+
return VALIDATION_FAILED;
363336
}
364337

338+
/**
339+
* Validates an ERC-1271 signature with the sender
340+
*
341+
* @param hash bytes32 hash of the data
342+
* @param data bytes data containing the signatures, and target calldata
343+
*
344+
* @return bytes4 EIP1271_SUCCESS if the signature is valid, EIP1271_FAILED otherwise
345+
*/
365346
function isValidSignatureWithSender(
366347
address sender,
367348
bytes32 hash,
368-
bytes calldata signature
349+
bytes calldata data
369350
)
370351
external
371352
view
372353
virtual
373354
override
374-
returns (bytes4 sugValidationResult)
355+
returns (bytes4)
375356
{
376-
return EIP1271_SUCCESS;
357+
bytes calldata signature = data[0:160];
358+
bytes calldata targetCallData = data[160:];
359+
if (_validateSignatureWithConfig(sender, hash, signature, targetCallData)) {
360+
return EIP1271_SUCCESS;
361+
}
362+
return EIP1271_FAILED;
377363
}
378364

365+
/**
366+
* Validates a signature given some data
367+
* For [ERC-7780](https://eips.ethereum.org/EIPS/eip-7780) Stateless Validator
368+
*
369+
* @param hash The data that was signed over
370+
* @param signature The signature to verify
371+
* @param data The data to validate the verified signature agains
372+
*
373+
* MUST validate that the signature is a valid signature of the hash
374+
* MUST compare the validated signature against the data provided
375+
* MUST return true if the signature is valid and false otherwise
376+
*/
379377
function validateSignatureWithData(
380-
bytes32,
381-
bytes calldata,
382-
bytes calldata
378+
bytes32 hash,
379+
bytes calldata signature,
380+
bytes calldata data
383381
)
384382
external
385383
view
386384
virtual
387-
returns (bool validSig)
385+
returns (bool)
388386
{
389-
return true;
387+
address account = address(bytes20(data[0:20]));
388+
bytes calldata targetCallData = data[20:];
389+
return _validateSignatureWithConfig(account, hash, signature, targetCallData);
390390
}
391391

392+
/*//////////////////////////////////////////////////////////////////////////
393+
INTERNAL FUNCTIONS
394+
//////////////////////////////////////////////////////////////////////////*/
395+
392396
function _isAllowedSelector(bytes4 sel) internal view returns (bool allowed) {
393397
for (uint256 i = 0; i < ALLOWED_SELECTORS.length; ++i) {
394398
if (sel == ALLOWED_SELECTORS[i]) return true;
395399
}
396400
return false;
397401
}
398402

403+
function _validateSignatureWithConfig(
404+
address account,
405+
bytes32 hash,
406+
bytes calldata signature,
407+
bytes calldata targetCallData
408+
)
409+
internal
410+
view
411+
returns (bool)
412+
{
413+
// you want to exclude initiateTx, signTx, executeTx from needing tx count.
414+
// you just need to ensure they are a valid proof from the semaphore group members
415+
uint256 groupId = groupMapping[account];
416+
417+
// The userOp.signature is 160 bytes containing:
418+
// (uint256 pubX (32 bytes), uint256 pubY (32 bytes), bytes[96] signature (96 bytes))
419+
if (signature.length != 160) {
420+
revert InvalidSignatureLen(account, signature.length);
421+
}
422+
423+
// Verify signature using the public key
424+
if (!Identity.verifySignature(hash, signature)) {
425+
revert InvalidSignature(account, signature);
426+
}
427+
428+
// Verify if the identity commitment is one of the semaphore group members
429+
bytes memory pubKey = signature[0:64];
430+
uint256 cmt = Identity.getCommitment(pubKey);
431+
if (!groups.hasMember(groupId, cmt)) revert MemberNotExists(account, cmt);
432+
433+
// We don't allow call to other contracts.
434+
address targetAddr = address(bytes20(targetCallData[0:20]));
435+
if (targetAddr != address(this)) revert NonValidatorCallBanned(targetAddr, address(this));
436+
437+
// For callData, the first 120 bytes are reserved by ERC-7579 use. Then 32 bytes of value,
438+
// then the remaining as the callData passed in getExecOps
439+
bytes calldata valAndCallData = targetCallData[20:];
440+
bytes4 funcSel = bytes4(valAndCallData[32:36]);
441+
442+
// We only allow calls to `initiateTx()`, `signTx()`, and `executeTx()` to pass,
443+
// and reject the rest.
444+
return _isAllowedSelector(funcSel);
445+
}
446+
399447
/*//////////////////////////////////////////////////////////////////////////
400448
METADATA
401449
//////////////////////////////////////////////////////////////////////////*/
@@ -426,6 +474,6 @@ contract SemaphoreMSAValidator is ERC7579ValidatorBase {
426474
* @return true if the module is of the given type, false otherwise
427475
*/
428476
function isModuleType(uint256 typeID) external pure override returns (bool) {
429-
return typeID == TYPE_VALIDATOR;
477+
return typeID == TYPE_VALIDATOR || typeID == TYPE_STATELESS_VALIDATOR;
430478
}
431479
}

src/utils/CurveBabyJubJub.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity >=0.8.23 <=0.8.29;
33

44
// ref: https://github.com/yondonfu/sol-baby-jubjub
5-
// with: https://github.com/yondonfu/sol-baby-jubjub/pull/1
5+
// with PR#1: https://github.com/yondonfu/sol-baby-jubjub/pull/1
66

77
library CurveBabyJubJub {
88
// Curve parameters
@@ -138,6 +138,7 @@ library CurveBabyJubJub {
138138
* @dev Helper function to call the bigModExp precompile
139139
*/
140140
function expmod(uint256 _b, uint256 _e, uint256 _m) internal view returns (uint256 o) {
141+
// solhint-disable-next-line no-inline-assembly
141142
assembly {
142143
let memPtr := mload(0x40)
143144
mstore(memPtr, 0x20) // Length of base _b

src/utils/Identity.sol

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@ pragma solidity >=0.8.23 <=0.8.29;
33

44
import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol";
55
import { PoseidonT6 } from "poseidon-solidity/PoseidonT6.sol";
6-
import { Vm } from "forge-std/Vm.sol";
7-
// import { console } from "forge-std/console.sol";
8-
import { LibString } from "solady/Milady.sol";
96
import { CurveBabyJubJub } from "./CurveBabyJubJub.sol";
10-
11-
Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
7+
// import { LibString } from "solady/Milady.sol";
8+
// import { Vm } from "forge-std/Vm.sol";
9+
// import { console } from "forge-std/console.sol";
1210

1311
library Identity {
1412
uint256 internal constant base8x = CurveBabyJubJub.Base8x;
@@ -19,48 +17,44 @@ library Identity {
1917
cmt = PoseidonT3.hash([pkX, pkY]);
2018
}
2119

22-
function verifySignatureFFI(bytes32 message, bytes memory signature) public returns (bool) {
23-
(uint256 pkX, uint256 pkY, uint256 s0, uint256 s1, uint256 s2) =
24-
abi.decode(signature, (uint256, uint256, uint256, uint256, uint256));
20+
// function verifySignatureFFI(bytes32 message, bytes memory signature) public returns (bool) {
21+
// (uint256 pkX, uint256 pkY, uint256 s0, uint256 s1, uint256 s2) =
22+
// abi.decode(signature, (uint256, uint256, uint256, uint256, uint256));
2523

26-
string[] memory inputs = new string[](6);
27-
inputs[0] = "pnpm";
28-
inputs[1] = "semaphore-identity";
29-
inputs[2] = "verify";
30-
inputs[3] = vm.toString(abi.encodePacked(pkX, pkY));
31-
inputs[4] = vm.toString(message);
32-
inputs[5] = vm.toString(abi.encodePacked(s0, s1, s2));
24+
// Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
3325

34-
bytes memory res = vm.ffi(inputs);
35-
string memory resStr = string(res);
36-
return LibString.eq(resStr, "true");
37-
}
26+
// string[] memory inputs = new string[](6);
27+
// inputs[0] = "pnpm";
28+
// inputs[1] = "semaphore-identity";
29+
// inputs[2] = "verify";
30+
// inputs[3] = vm.toString(abi.encodePacked(pkX, pkY));
31+
// inputs[4] = vm.toString(message);
32+
// inputs[5] = vm.toString(abi.encodePacked(s0, s1, s2));
33+
34+
// bytes memory res = vm.ffi(inputs);
35+
// string memory resStr = string(res);
36+
// return LibString.eq(resStr, "true");
37+
// }
3838

39-
function verifySignature(bytes32 message, bytes memory signature) public returns (bool) {
39+
function verifySignature(bytes32 message, bytes memory signature) public view returns (bool) {
4040
// Implement eddsa-poseidon verifySignature() method in solidity.
4141
// https://github.com/privacy-scaling-explorations/zk-kit/blob/388f72b7a029a14bf5c20861d5f54bdaa98b3ac7/packages/eddsa-poseidon/src/eddsa-poseidon-factory.ts#L127-L158
4242
(uint256 pkX, uint256 pkY, uint256 s0, uint256 s1, uint256 s2) =
4343
abi.decode(signature, (uint256, uint256, uint256, uint256, uint256));
4444

4545
uint256 hm = PoseidonT6.hash([s0, s1, pkX, pkY, uint256(message)]);
4646

47-
// TODO: remove this after you can increase gas limit in getExecOps()
48-
vm.pauseGasMetering();
49-
5047
(uint256 pLeftx, uint256 pLefty) = CurveBabyJubJub.pointMul(base8x, base8y, s2);
5148

5249
// This is suppose to be: CurveBabyJubJub.pointMul(pkX, pkY, mulmod(8, hm, FM)),
53-
// but I'm not sure the field modulus to use. No, not `CurveBabyJubJub.Q`.
50+
// but I'm not sure the field modulo to use. No, not `CurveBabyJubJub.Q`.
5451
(uint256 pRightx, uint256 pRighty) = CurveBabyJubJub.pointMul(pkX, pkY, hm);
5552
(pRightx, pRighty) = CurveBabyJubJub.pointAdd(pRightx, pRighty, pRightx, pRighty);
5653
(pRightx, pRighty) = CurveBabyJubJub.pointAdd(pRightx, pRighty, pRightx, pRighty);
5754
(pRightx, pRighty) = CurveBabyJubJub.pointAdd(pRightx, pRighty, pRightx, pRighty);
5855

5956
(uint256 pSumx, uint256 pSumy) = CurveBabyJubJub.pointAdd(s0, s1, pRightx, pRighty);
6057

61-
// TODO: remove this after you can increase gas limit in getExecOps()
62-
vm.resumeGasMetering();
63-
6458
return (pLeftx == pSumx && pLefty == pSumy);
6559
}
6660
}

test/Identity.t.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ contract IdentityTest is Test {
2020
assertEq(true, IdentityT.verifySignature(hash, signature));
2121
}
2222

23-
function test_verifySignatureAcceptCorrectSignature2() public {
23+
function test_verifySignatureAcceptCorrectSignature2() public view {
2424
bytes32 hash = hex"00b917632b69261f21d20e0cabdf9f3fa1255c6e500021997a16cf3a46d80297";
2525
bytes memory signature =
26+
// solhint-disable-next-line max-line-length
2627
hex"26c3a847609100b3fd926d3c0a61324a32479d5989f01383aca537869cb23a851d67a417abb29f71e1f7c3d0bcd93cb68f89203b046174f03c3822a9139b512611b5289e52e9f70ff4a30cb9a19d66de49266887d3d17ed35f2dfc30f44573dc0c44756c4e4c5a5e5eeacc68f39b4e2238041e70ca926139ea039e260ea7ca5000b8d0dfc37fc5de7b0f80b722f8966a43caa10c8068cf863e5d06f82ae7c9d8";
2728

2829
assertEq(true, IdentityT.verifySignature(hash, signature));

test/SemaphoreMSAValidator.t.sol

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,11 @@ contract SemaphoreValidatorUnitTest is RhinestoneModuleKit, Test {
304304
txValidator: address(semaphoreValidator)
305305
});
306306

307-
// TODO: We need to increase the accountGasLimits, default 2e6 is not enough to verify
308-
// signature, for all those elliptic curve computation.
309-
// userOpData.userOp.accountGasLimits = bytes32(uint256(2e7));
310-
// userOpData.userOpHash = smartAcct.aux.entrypoint.getUserOpHash(userOpData.userOp);
311-
307+
// We need to increase the accountGasLimits, default 2e6 is not enough to verify
308+
// signature, for all those elliptic curve computation.
309+
// Encoding two fields here, validation and execution gas
310+
userOpData.userOp.accountGasLimits = bytes32(abi.encodePacked(uint128(2e7), uint128(2e7)));
311+
userOpData.userOpHash = smartAcct.aux.entrypoint.getUserOpHash(userOpData.userOp);
312312
userOpData.userOp.signature = id.signHash(userOpData.userOpHash);
313313
}
314314

@@ -486,6 +486,8 @@ contract SemaphoreValidatorUnitTest is RhinestoneModuleKit, Test {
486486
callData: abi.encodeCall(SimpleContract.setVal, (testVal)),
487487
txValidator: address(semaphoreValidator)
488488
});
489+
userOpData.userOp.accountGasLimits = bytes32(abi.encodePacked(uint128(2e7), uint128(2e7)));
490+
userOpData.userOpHash = smartAcct.aux.entrypoint.getUserOpHash(userOpData.userOp);
489491
userOpData.userOp.signature = member.identity.signHash(userOpData.userOpHash);
490492

491493
smartAcct.expect4337Revert(SemaphoreMSAValidator.NonValidatorCallBanned.selector);

0 commit comments

Comments
 (0)