F-2026-0001·incorrect-validation-order

New users cannot onboard after upgrade due to registerKeyLog regression

Fixedbridgecross-chainkey-registrygithub.com/pdxwebdev/yadakeyeventwallet
TL;DR

After upgrading to KeyLogRegistryUpgrade, registerKeyLog always reverts because validateTransaction unconditionally hashes the empty confirming key before the isPair guard. No new user can register an inception key on the bridge.

Severity
CRITICAL
Impact
HIGH
Likelihood
HIGH
Method
MManual review
CAT.
Complexity
LOW
Exploitability
HIGH
02Section · Description

Description

Note: The problem described in [F-2026-0002] is fixed in KeyLogRegistryUpgrade.sol but it introduces a different type of regression described here. KeyLogRegistryUpgrade.sol and KeyLogRegistry.sol should not be used in production before the recommended fix described below.

The [F-2026-0002] fix in KeyLogRegistryUpgrade changed line 184 of validateTransaction from:

solidity
// Original (buggy):
address confirmingPublicKeyHash =
getAddressFromPublicKey(unconfirmed.publicKey);
// Upgrade (fixed for pairs, but breaks single registration):
address confirmingPublicKeyHash =
getAddressFromPublicKey(confirming.publicKey);

This fix is correct for registerKeyLogPair (where confirming.publicKey is a real 64-byte key). However, registerKeyLog passes an empty confirming key:

solidity
function registerKeyLog(KeyData memory key) external onlyAuthorized {
address publicKeyHash = getAddressFromPublicKey(key.publicKey);
(KeyEventFlag flag, ) = validateTransaction(
key,
KeyData({
publicKey: "", // <-- empty bytes
prerotatedKeyHash: address(0),
twicePrerotatedKeyHash: address(0),
outputAddress: address(0),
prevPublicKeyHash: address(0)
}),
false // isPair = false
);
}

validateTransaction calls getAddressFromPublicKey(confirming.publicKey) at line 184 unconditionally, before the if (!isPair) return guard at line 211. Since confirming.publicKey is "" (0 bytes), getAddressFromPublicKey reverts with "Public key must be 64 bytes".

This means registerKeyLog always reverts after upgrading to KeyLogRegistryUpgrade.

Vulnerable Scenario

  1. Protocol deploys KeyLogRegistryUpgrade.
  2. New user calls Bridge.registerKeyPairWithTransfer() with no confirming key (ctx.confirming.outputAddress == address(0)).
  3. Bridge evaluates isPair = false.
  4. Bridge calls keyLogRegistry.registerKeyLog(unconfirmedKeyData).
  5. registerKeyLog constructs an empty confirming KeyData with publicKey: "" and calls validateTransaction(key, emptyConfirming, false).
  6. validateTransaction executes getAddressFromPublicKey(confirming.publicKey), this is getAddressFromPublicKey("").
  7. getAddressFromPublicKey checks require(publicKey.length == 64), fails because length is 0.
  8. Transaction reverts with "Public key must be 64 bytes" , inception never registered.
  9. Result: no new user can register their first key on the bridge.
03Section · Impact

Impact

After deploying KeyLogRegistryUpgrade:

  • All new user onboarding breaks, no inception events can be registered.
  • Existing users with confirmed key pairs can still rotate (via registerKeyLogPair).
  • The protocol cannot onboard any new users until a second upgrade fixes this regression.
04Section · Recommendation

Recommendation

Move the getAddressFromPublicKey(confirming.publicKey) call inside the isPair block:

solidity
function validateTransaction(
KeyData memory unconfirmed,
KeyData memory confirming,
bool isPair
) public view returns (KeyEventFlag, KeyEventFlag) {
(KeyLogEntry memory lastEntry, bool hasEntries) =
getLatestChainEntry(unconfirmed.publicKey);
address unconfirmedPublicKeyHash =
getAddressFromPublicKey(unconfirmed.publicKey);
- address confirmingPublicKeyHash =
- getAddressFromPublicKey(confirming.publicKey);
+ address confirmingPublicKeyHash; // only computed for pairs
// ... existing validation for unconfirmed ...
if (!isPair) {
require(unconfirmed.prerotatedKeyHash == unconfirmed.outputAddress, "...");
require(confirming.outputAddress == address(0), "...");
return (firstFlag, KeyEventFlag.UNCONFIRMED);
}
+ confirmingPublicKeyHash =
+ getAddressFromPublicKey(confirming.publicKey);
// ... rest of pair validation ...
05Section · Resolution

Resolution

YadaCoin, Confirmed. Moved confirmingPublicKeyHash computation inside the isPair block so that non-pair registerKeyLog calls no longer revert on the missing confirming public key.

Zealynx, Fixed. Verified the fix resolves the regression for non-pair registrations. Also identified and confirmed removal of a redundant require statement that was unreachable after the restructuring.

Status
Fixed
F-2026-0001

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx