Skip to content

Commit 250cee4

Browse files
authored
Merge pull request #36 from zerodevapp/fix/tob-kernel-1
fix: timelock policy create proposal using erc4337 with no-op calldata
2 parents 6b81e58 + cc349f5 commit 250cee4

20 files changed

Lines changed: 4604 additions & 225 deletions

src/policies/CallerPolicy.sol

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ import {
2828
* If you need to validate who signed, use a signer module instead.
2929
*/
3030
contract CallerPolicy is PolicyBase, IStatelessValidatorWithSender {
31+
error PolicyAlreadyInstalled();
32+
error PolicyNotLive();
33+
error EmptyCallers();
34+
error ZeroAddressCaller();
35+
3136
mapping(bytes32 id => mapping(address => Status)) public status;
3237
/// @notice Maps policy ID => requesting protocol => wallet => whether protocol is allowed
3338
mapping(bytes32 id => mapping(address caller => mapping(address wallet => bool))) public allowedCaller;
@@ -82,18 +87,18 @@ contract CallerPolicy is PolicyBase, IStatelessValidatorWithSender {
8287
}
8388

8489
function _policyOninstall(bytes32 id, bytes calldata _data) internal override {
85-
require(status[id][msg.sender] == Status.NA, "Already installed");
90+
require(status[id][msg.sender] == Status.NA, PolicyAlreadyInstalled());
8691
address[] memory callers = abi.decode(_data, (address[]));
87-
require(callers.length > 0, "Empty callers array");
92+
require(callers.length > 0, EmptyCallers());
8893
for (uint256 i = 0; i < callers.length; i++) {
89-
require(callers[i] != address(0), "Zero address caller");
94+
require(callers[i] != address(0), ZeroAddressCaller());
9095
allowedCaller[id][callers[i]][msg.sender] = true;
9196
}
9297
status[id][msg.sender] = Status.Live;
9398
}
9499

95100
function _policyOnUninstall(bytes32 id, bytes calldata _data) internal override {
96-
require(status[id][msg.sender] == Status.Live);
101+
require(status[id][msg.sender] == Status.Live, PolicyNotLive());
97102
status[id][msg.sender] = Status.Deprecated;
98103
}
99104

src/policies/TimelockPolicy.sol

Lines changed: 126 additions & 177 deletions
Large diffs are not rendered by default.

src/signers/ECDSASigner.sol

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
contract ECDSASigner is SignerBase, IStatelessValidator, IStatelessValidatorWithSender {
1818
error InvalidDataLength();
1919
error ZeroAddressSigner();
20+
error SignerAlreadySet();
21+
error SignerNotSet();
2022

2123
mapping(bytes32 id => mapping(address wallet => address)) public signer;
2224

@@ -48,6 +50,12 @@ contract ECDSASigner is SignerBase, IStatelessValidator, IStatelessValidatorWith
4850
: SIG_VALIDATION_FAILED_UINT;
4951
}
5052

53+
/// @notice Validate an ERC-1271 signature
54+
/// @dev The `sender` parameter (requesting protocol) is intentionally unused.
55+
/// This signer authenticates the SIGNER (owner), not the requesting protocol.
56+
/// WARNING: Because sender is ignored, any protocol can request signature
57+
/// validation. If you need to restrict which protocols can request signatures,
58+
/// pair this signer with a CallerPolicy.
5159
function checkSignature(bytes32 id, address sender, bytes32 hash, bytes calldata sig)
5260
external
5361
view
@@ -61,15 +69,15 @@ contract ECDSASigner is SignerBase, IStatelessValidator, IStatelessValidatorWith
6169
}
6270

6371
function _signerOninstall(bytes32 id, bytes calldata _data) internal override {
64-
require(signer[id][msg.sender] == address(0), "Already installed");
65-
if (_data.length != 20) revert InvalidDataLength();
72+
require(signer[id][msg.sender] == address(0), SignerAlreadySet());
73+
require(_data.length == 20, InvalidDataLength());
6674
address signerAddr = address(bytes20(_data[0:20]));
67-
if (signerAddr == address(0)) revert ZeroAddressSigner();
75+
require(signerAddr != address(0), ZeroAddressSigner());
6876
signer[id][msg.sender] = signerAddr;
6977
}
7078

7179
function _signerOnUninstall(bytes32 id, bytes calldata) internal override {
72-
require(signer[id][msg.sender] != address(0));
80+
require(signer[id][msg.sender] != address(0), SignerNotSet());
7381
delete signer[id][msg.sender];
7482
}
7583

src/signers/WeightedECDSASigner.sol

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ contract WeightedECDSASigner is EIP712, SignerBase, IStatelessValidator, IStatel
3636
keccak256("Proposal(address account,bytes32 id,bytes callData,uint256 nonce)");
3737

3838
error ZeroWeightSigner();
39+
error LengthMismatch();
40+
error EmptyGuardians();
41+
error ZeroThreshold();
42+
error GuardianCannotBeSelf();
43+
error ZeroAddressGuardian();
44+
error ZeroWeight();
45+
error GuardianAlreadyEnabled();
46+
error SignersNotSorted();
47+
error ThresholdExceedsTotalWeight();
3948

4049
mapping(bytes32 id => mapping(address kernel => WeightedECDSASignerStorage)) public weightedStorage;
4150
mapping(address guardian => mapping(bytes32 id => mapping(address kernel => GuardianStorage))) public guardian;
@@ -53,22 +62,23 @@ contract WeightedECDSASigner is EIP712, SignerBase, IStatelessValidator, IStatel
5362

5463
(address[] memory _guardians, uint24[] memory _weights, uint24 _threshold) =
5564
abi.decode(_data, (address[], uint24[], uint24));
56-
require(_guardians.length == _weights.length, "Length mismatch");
57-
require(_guardians.length > 0, "No guardians");
58-
require(_threshold > 0, "Zero threshold");
65+
require(_guardians.length == _weights.length, LengthMismatch());
66+
require(_guardians.length > 0, EmptyGuardians());
67+
require(_threshold > 0, ZeroThreshold());
5968

6069
weightedStorage[id][msg.sender].firstGuardian = msg.sender;
6170
for (uint256 i = 0; i < _guardians.length; i++) {
62-
require(_guardians[i] != msg.sender, "Guardian cannot be self");
63-
require(_guardians[i] != address(0), "Guardian cannot be 0");
64-
require(_weights[i] != 0, "Weight cannot be 0");
65-
require(guardian[_guardians[i]][id][msg.sender].weight == 0, "Guardian already enabled");
71+
require(_guardians[i] != msg.sender, GuardianCannotBeSelf());
72+
require(_guardians[i] != address(0), ZeroAddressGuardian());
73+
require(_weights[i] != 0, ZeroWeight());
74+
require(guardian[_guardians[i]][id][msg.sender].weight == 0, GuardianAlreadyEnabled());
6675
guardian[_guardians[i]][id][msg.sender] =
6776
GuardianStorage({weight: _weights[i], nextGuardian: weightedStorage[id][msg.sender].firstGuardian});
6877
weightedStorage[id][msg.sender].firstGuardian = _guardians[i];
6978
weightedStorage[id][msg.sender].totalWeight += _weights[i];
7079
emit GuardianAdded(_guardians[i], msg.sender, _weights[i]);
7180
}
81+
require(_threshold <= weightedStorage[id][msg.sender].totalWeight, ThresholdExceedsTotalWeight());
7282
weightedStorage[id][msg.sender].threshold = _threshold;
7383
}
7484

@@ -102,6 +112,12 @@ contract WeightedECDSASigner is EIP712, SignerBase, IStatelessValidator, IStatel
102112
return _validateUserOpSignature(id, userOp, userOpHash, userOp.signature, msg.sender);
103113
}
104114

115+
/// @notice Validate an ERC-1271 signature
116+
/// @dev The `sender` parameter (requesting protocol) is intentionally unused.
117+
/// This signer authenticates the SIGNERS (guardians), not the requesting protocol.
118+
/// WARNING: Because sender is ignored, any protocol can request signature
119+
/// validation. If you need to restrict which protocols can request signatures,
120+
/// pair this signer with a CallerPolicy.
105121
function checkSignature(bytes32 id, address, bytes32 hash, bytes calldata sig)
106122
external
107123
view
@@ -138,6 +154,15 @@ contract WeightedECDSASigner is EIP712, SignerBase, IStatelessValidator, IStatel
138154
/**
139155
* @notice Internal function to validate user operation signatures
140156
* @dev Shared logic for both installed and stateless validator modes
157+
*
158+
* SECURITY: Split Signature Scheme
159+
* The first N-1 signatures verify a proposalHash (EIP-712 typed data covering
160+
* account, id, callData, and nonce). The last signature MUST verify the full
161+
* userOpHash to bind the complete UserOp (including gas fields).
162+
* This prevents a scenario where guardians approve a proposal but an attacker
163+
* manipulates gas parameters in the final UserOp.
164+
* A double-counting check ensures a guardian who signed both the proposalHash
165+
* and userOpHash only has their weight counted once.
141166
*/
142167
function _validateUserOpSignature(
143168
bytes32 id,
@@ -188,7 +213,7 @@ contract WeightedECDSASigner is EIP712, SignerBase, IStatelessValidator, IStatel
188213
signer = ECDSA.tryRecoverCalldata(proposalHash, sig[i * 65:(i + 1) * 65]);
189214

190215
// Enforce sorted order to prevent signature reuse
191-
require(signer > lastSigner, "Signers not sorted");
216+
require(signer > lastSigner, SignersNotSorted());
192217
lastSigner = signer;
193218
proposalSigners[i] = signer;
194219

src/validators/ECDSAValidator.sol

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ contract ECDSAValidator is IValidator, IHook, IStatelessValidator, IStatelessVal
3232

3333
error InvalidDataLength();
3434
error ZeroAddressOwner();
35+
error SenderNotOwner();
3536

3637
function onInstall(bytes calldata _data) external payable override {
3738
if (_isInitialized(msg.sender)) revert AlreadyInitialized(msg.sender);
38-
if (_data.length != 20) revert InvalidDataLength();
39+
require(_data.length == 20, InvalidDataLength());
3940
address owner = address(bytes20(_data[0:20]));
40-
if (owner == address(0)) revert ZeroAddressOwner();
41+
require(owner != address(0), ZeroAddressOwner());
4142
ecdsaValidatorStorage[msg.sender].owner = owner;
4243
emit OwnerRegistered(msg.sender, owner);
4344
}
@@ -72,18 +73,28 @@ contract ECDSAValidator is IValidator, IHook, IStatelessValidator, IStatelessVal
7273
returns (uint256)
7374
{
7475
address owner = ecdsaValidatorStorage[msg.sender].owner;
76+
// Fail if owner is not set (prevents matching with failed recovery returning address(0))
77+
if (owner == address(0)) return SIG_VALIDATION_FAILED_UINT;
7578
return _verifySignature(userOpHash, userOp.signature, owner)
7679
? SIG_VALIDATION_SUCCESS_UINT
7780
: SIG_VALIDATION_FAILED_UINT;
7881
}
7982

83+
/// @notice Validate an ERC-1271 signature
84+
/// @dev The `sender` parameter (requesting protocol) is intentionally unused.
85+
/// This validator authenticates the SIGNER (owner), not the requesting protocol.
86+
/// WARNING: Because sender is ignored, any protocol can request signature
87+
/// validation. If you need to restrict which protocols can request signatures,
88+
/// pair this validator with a CallerPolicy.
8089
function isValidSignatureWithSender(address, bytes32 hash, bytes calldata sig)
8190
external
8291
view
8392
override
8493
returns (bytes4)
8594
{
8695
address owner = ecdsaValidatorStorage[msg.sender].owner;
96+
// Fail if owner is not set (prevents matching with failed recovery returning address(0))
97+
if (owner == address(0)) return ERC1271_INVALID;
8798
return _verifySignature(hash, sig, owner) ? ERC1271_MAGICVALUE : ERC1271_INVALID;
8899
}
89100

@@ -106,7 +117,7 @@ contract ECDSAValidator is IValidator, IHook, IStatelessValidator, IStatelessVal
106117
}
107118

108119
function preCheck(address msgSender, uint256, bytes calldata) external payable override returns (bytes memory) {
109-
require(msgSender == ecdsaValidatorStorage[msg.sender].owner, "ECDSAValidator: sender is not owner");
120+
require(msgSender == ecdsaValidatorStorage[msg.sender].owner, SenderNotOwner());
110121
return hex"";
111122
}
112123

0 commit comments

Comments
 (0)