Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0578d2a
fix(TimelockPolicy): enforce timelock for ERC-1271 signatures
leekt Feb 2, 2026
b2f5abc
fix(TimelockPolicy): invalidate stale proposals on reinstall
leekt Feb 2, 2026
14cb24f
Fix dead proposal creation code
leekt Feb 2, 2026
2f88be1
test: add BTT tests for TimelockEpochValidation
leekt Feb 2, 2026
d6c7871
test: add BTT tests for TimelockSignaturePolicy
leekt Feb 2, 2026
baa9fa6
test: add comprehensive BTT tests for TOB-KERNEL-1 fix
leekt Feb 2, 2026
ca7137b
update
leekt Feb 2, 2026
97ded5e
fix: timelockpolicy does not accept erc1271
leekt Feb 2, 2026
27ec844
Merge pull request #37 from zerodevapp/fix/tob-kernel-2
leekt Feb 6, 2026
c2381b0
Merge pull request #42 from zerodevapp/fix/tob-kernel-20
leekt Feb 6, 2026
37da3aa
fix(TimelockPolicy): add upper bounds for delay and expirationPeriod
leekt Feb 6, 2026
ecb7630
fix(TimelockPolicy): correct ERC-7579 no-op detection encoding
leekt Feb 6, 2026
d4c3302
Merge pull request #48 from zerodevapp/fix/tob-kernel-18
leekt Feb 6, 2026
6d864e5
Merge pull request #47 from zerodevapp/fix/tob-kernel-5
leekt Feb 6, 2026
df553e7
fix(TimelockPolicy): add grace period to prevent race conditions
leekt Feb 2, 2026
7709731
test: add BTT tests for TimelockCancellationRace
leekt Feb 2, 2026
17f78f7
fix(TimelockPolicy): make permissionless proposals inert until sessio…
leekt Feb 9, 2026
4b66879
feat(TimelockPolicy): add proposer to ProposalCreated event
leekt Feb 9, 2026
17d950e
fix(TimelockPolicy): remove createProposal, proposals only via no-op …
leekt Feb 9, 2026
28ee229
fix(TimelockPolicy): remove dead code and add callDataLength overflow…
leekt Feb 9, 2026
3705223
Merge pull request #44 from zerodevapp/fix/tob-kernel-21
leekt Feb 9, 2026
351b99e
Merge fix/tob-kernel-21 (grace period) into fix/tob-kernel-1
leekt Feb 9, 2026
4a83f3f
fix(TimelockPolicy): add mode check and fix executeUserOp offset in n…
leekt Feb 9, 2026
810bc68
fix(TimelockPolicy): correct ERC-7579 no-op detection encoding
leekt Feb 9, 2026
d0bbbb1
test: add EntryPoint integration tests for TimelockPolicy
leekt Feb 11, 2026
52dac36
Merge pull request #49 from zerodevapp/fix/delete_createProposal
leekt Feb 11, 2026
9f966f5
fix(TimelockPolicy): replace grace period with guardian cancellation
leekt Feb 12, 2026
d043f60
fix(TimelockPolicy): use LibERC7579.decodeSingle for no-op detection
leekt Feb 12, 2026
c9f666b
test(TimelockPolicy): update tests for guardian cancellation and no-o…
leekt Feb 12, 2026
8033d31
test(TimelockPolicy): add comprehensive guardian cancellation test cases
leekt Feb 12, 2026
edbc28a
refactor: remove redundant signature length check in TimelockPolicy
leekt Feb 12, 2026
d307e68
test: add TimelockNoOpDetection test cases
leekt Feb 23, 2026
f438af3
Merge pull request #50 from zerodevapp/fix/tob-kernel-21-attempt-2
leekt Feb 24, 2026
60406c0
Merge branch 'dev/0.2.0' into fix/tob-kernel-1
leekt Feb 24, 2026
79e908f
Merge branch 'fix/tob-kernel-1' of github.com:zerodevapp/kernel-7579-…
leekt Feb 24, 2026
574e94c
fix: add zero-owner guard in ECDSAValidator validation functions
leekt Feb 24, 2026
6344004
fix: add install-time input validation with require-customError pattern
leekt Feb 24, 2026
33c161c
chore: replace string literal requires/reverts with custom errors
leekt Feb 24, 2026
a7614be
test: update TimelockPolicy tests for custom error selectors
leekt Feb 24, 2026
3162c8f
docs: add security assumption NatSpec to TimelockPolicy, ECDSAValidat…
leekt Feb 24, 2026
e13d652
test: fix broken 64-byte signature test in TimelockPolicy
leekt Feb 24, 2026
d78afb3
Merge pull request #53 from zerodevapp/fix/tob-kernel-5-v2
leekt Apr 7, 2026
338401e
Merge pull request #54 from zerodevapp/fix/tob-kernel-13-zero-signer
leekt Apr 7, 2026
d556bb0
Merge branch 'fix/tob-kernel-1' into chore/custom_errors
leekt Apr 7, 2026
39ceadc
Merge pull request #55 from zerodevapp/chore/custom_errors
leekt Apr 7, 2026
cc349f5
Merge pull request #52 from zerodevapp/docs/security_warnings
leekt Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/policies/CallerPolicy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import {
* If you need to validate who signed, use a signer module instead.
*/
contract CallerPolicy is PolicyBase, IStatelessValidatorWithSender {
error PolicyAlreadyInstalled();
error PolicyNotLive();
error EmptyCallers();
error ZeroAddressCaller();

mapping(bytes32 id => mapping(address => Status)) public status;
/// @notice Maps policy ID => requesting protocol => wallet => whether protocol is allowed
mapping(bytes32 id => mapping(address caller => mapping(address wallet => bool))) public allowedCaller;
Expand Down Expand Up @@ -82,18 +87,18 @@ contract CallerPolicy is PolicyBase, IStatelessValidatorWithSender {
}

function _policyOninstall(bytes32 id, bytes calldata _data) internal override {
require(status[id][msg.sender] == Status.NA, "Already installed");
require(status[id][msg.sender] == Status.NA, PolicyAlreadyInstalled());
address[] memory callers = abi.decode(_data, (address[]));
require(callers.length > 0, "Empty callers array");
require(callers.length > 0, EmptyCallers());
for (uint256 i = 0; i < callers.length; i++) {
require(callers[i] != address(0), "Zero address caller");
require(callers[i] != address(0), ZeroAddressCaller());
allowedCaller[id][callers[i]][msg.sender] = true;
}
status[id][msg.sender] = Status.Live;
}

function _policyOnUninstall(bytes32 id, bytes calldata _data) internal override {
require(status[id][msg.sender] == Status.Live);
require(status[id][msg.sender] == Status.Live, PolicyNotLive());
status[id][msg.sender] = Status.Deprecated;
}

Expand Down
303 changes: 126 additions & 177 deletions src/policies/TimelockPolicy.sol

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions src/signers/ECDSASigner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
contract ECDSASigner is SignerBase, IStatelessValidator, IStatelessValidatorWithSender {
error InvalidDataLength();
error ZeroAddressSigner();
error SignerAlreadySet();
error SignerNotSet();

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

Expand Down Expand Up @@ -48,6 +50,12 @@ contract ECDSASigner is SignerBase, IStatelessValidator, IStatelessValidatorWith
: SIG_VALIDATION_FAILED_UINT;
}

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

function _signerOninstall(bytes32 id, bytes calldata _data) internal override {
require(signer[id][msg.sender] == address(0), "Already installed");
if (_data.length != 20) revert InvalidDataLength();
require(signer[id][msg.sender] == address(0), SignerAlreadySet());
require(_data.length == 20, InvalidDataLength());
address signerAddr = address(bytes20(_data[0:20]));
if (signerAddr == address(0)) revert ZeroAddressSigner();
require(signerAddr != address(0), ZeroAddressSigner());
signer[id][msg.sender] = signerAddr;
}

function _signerOnUninstall(bytes32 id, bytes calldata) internal override {
require(signer[id][msg.sender] != address(0));
require(signer[id][msg.sender] != address(0), SignerNotSet());
delete signer[id][msg.sender];
}

Expand Down
41 changes: 33 additions & 8 deletions src/signers/WeightedECDSASigner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ contract WeightedECDSASigner is EIP712, SignerBase, IStatelessValidator, IStatel
keccak256("Proposal(address account,bytes32 id,bytes callData,uint256 nonce)");

error ZeroWeightSigner();
error LengthMismatch();
error EmptyGuardians();
error ZeroThreshold();
error GuardianCannotBeSelf();
error ZeroAddressGuardian();
error ZeroWeight();
error GuardianAlreadyEnabled();
error SignersNotSorted();
error ThresholdExceedsTotalWeight();

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

(address[] memory _guardians, uint24[] memory _weights, uint24 _threshold) =
abi.decode(_data, (address[], uint24[], uint24));
require(_guardians.length == _weights.length, "Length mismatch");
require(_guardians.length > 0, "No guardians");
require(_threshold > 0, "Zero threshold");
require(_guardians.length == _weights.length, LengthMismatch());
require(_guardians.length > 0, EmptyGuardians());
require(_threshold > 0, ZeroThreshold());

weightedStorage[id][msg.sender].firstGuardian = msg.sender;
for (uint256 i = 0; i < _guardians.length; i++) {
require(_guardians[i] != msg.sender, "Guardian cannot be self");
require(_guardians[i] != address(0), "Guardian cannot be 0");
require(_weights[i] != 0, "Weight cannot be 0");
require(guardian[_guardians[i]][id][msg.sender].weight == 0, "Guardian already enabled");
require(_guardians[i] != msg.sender, GuardianCannotBeSelf());
require(_guardians[i] != address(0), ZeroAddressGuardian());
require(_weights[i] != 0, ZeroWeight());
require(guardian[_guardians[i]][id][msg.sender].weight == 0, GuardianAlreadyEnabled());
guardian[_guardians[i]][id][msg.sender] =
GuardianStorage({weight: _weights[i], nextGuardian: weightedStorage[id][msg.sender].firstGuardian});
weightedStorage[id][msg.sender].firstGuardian = _guardians[i];
weightedStorage[id][msg.sender].totalWeight += _weights[i];
emit GuardianAdded(_guardians[i], msg.sender, _weights[i]);
}
require(_threshold <= weightedStorage[id][msg.sender].totalWeight, ThresholdExceedsTotalWeight());
weightedStorage[id][msg.sender].threshold = _threshold;
}

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

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

// Enforce sorted order to prevent signature reuse
require(signer > lastSigner, "Signers not sorted");
require(signer > lastSigner, SignersNotSorted());
lastSigner = signer;
proposalSigners[i] = signer;

Expand Down
17 changes: 14 additions & 3 deletions src/validators/ECDSAValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ contract ECDSAValidator is IValidator, IHook, IStatelessValidator, IStatelessVal

error InvalidDataLength();
error ZeroAddressOwner();
error SenderNotOwner();

function onInstall(bytes calldata _data) external payable override {
if (_isInitialized(msg.sender)) revert AlreadyInitialized(msg.sender);
if (_data.length != 20) revert InvalidDataLength();
require(_data.length == 20, InvalidDataLength());
address owner = address(bytes20(_data[0:20]));
if (owner == address(0)) revert ZeroAddressOwner();
require(owner != address(0), ZeroAddressOwner());
ecdsaValidatorStorage[msg.sender].owner = owner;
emit OwnerRegistered(msg.sender, owner);
}
Expand Down Expand Up @@ -72,18 +73,28 @@ contract ECDSAValidator is IValidator, IHook, IStatelessValidator, IStatelessVal
returns (uint256)
{
address owner = ecdsaValidatorStorage[msg.sender].owner;
// Fail if owner is not set (prevents matching with failed recovery returning address(0))
if (owner == address(0)) return SIG_VALIDATION_FAILED_UINT;
return _verifySignature(userOpHash, userOp.signature, owner)
? SIG_VALIDATION_SUCCESS_UINT
: SIG_VALIDATION_FAILED_UINT;
}

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

Expand All @@ -106,7 +117,7 @@ contract ECDSAValidator is IValidator, IHook, IStatelessValidator, IStatelessVal
}

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

Expand Down
Loading