Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ doesn't describe endpoints the released server lacks.
(`pkpId → master account`): a wallet address can only ever be registered —
or re-registered after deletion — by the account that first registered it,
closing a cross-account PKP hijack via publicly visible derivation paths
(#575). Requires a one-time on-chain backfill for wallets registered before
the upgrade (`backfillPkpOwners`).
(#575). `getWalletDerivation` enforces the same binding at read time
(fail-closed), so the node never resolves a key for an account that doesn't
own the PKP. The one-time backfill of pre-existing PKPs is complete on-chain;
the migration helper (`backfillPkpOwners`) and the pre-backfill compatibility
fallback have been removed.

## v1.1.10 — 2026-06-22

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,26 +167,27 @@ contract ViewsFacet {
// Cross-account hijack defense (#575). This view is the exact read the
// node uses to resolve a derivation path before signing/decrypting, so
// enforce the global first-owner binding HERE, not just at registration:
// if the wallet has an owner and the resolving (master) account is not
// that owner, the local pkpData entry is a stale pre-fix hijack
// registration — fail closed instead of leaking the victim's path.
// owner == 0 means a pre-migration wallet not yet backfilled: fall
// through so signing keeps working until backfillPkpOwners runs.
// the resolving (master) account must be the pkpId's owner, otherwise
// the local pkpData entry is a stale hijack registration — fail closed
// instead of leaking the victim's path.
//
// registerWalletDerivation always sets pkpIdToOwnerMaster when it writes
// pkpData, and the one-time #575 backfill bound every pre-existing PKP,
// so a non-zero derivation always has a non-zero owner. An owner of 0
// here therefore means an unexpected/legacy state and fails closed.
AppStorage.AccountConfigStorage storage s = AppStorage.getStorage();
uint256 owner = s.pkpIdToOwnerMaster[walletAddress];
if (owner != 0) {
uint256 resolvedMaster = s.allApiKeyHashesToMaster[apiKeyHash];
if (owner != resolvedMaster) {
revert AppStorage.InvalidRequest("PKP owned by another account");
}
uint256 resolvedMaster = s.allApiKeyHashesToMaster[apiKeyHash];
if (owner != resolvedMaster) {
revert AppStorage.InvalidRequest("PKP owned by another account");
}
return derivation;
}

/// @notice Return the master apiKeyHash that first registered a pkpId, or 0 if
/// the pkpId has never been bound (pre-migration wallet or never registered).
/// @dev The binding survives removeWalletDerivation by design; used to audit the
/// global first-owner rule and the one-time backfill migration.
/// @notice Return the master apiKeyHash that owns a pkpId, or 0 if the pkpId
/// has never been registered.
/// @dev The binding survives removeWalletDerivation by design; used to audit
/// the global first-owner rule.
function getPkpOwnerMaster(address pkpId) public view returns (uint256) {
AppStorage.AccountConfigStorage storage s = AppStorage.getStorage();
return s.pkpIdToOwnerMaster[pkpId];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ contract WritesFacet {
uint256 indexed apiKeyHash,
address indexed pkpId
);
event PkpOwnerBackfilled(
address indexed pkpId,
uint256 indexed masterHash
);
event UsageApiKeyRemoved(
uint256 indexed accountApiKeyHash,
uint256 indexed usageApiKeyHash
Expand Down Expand Up @@ -762,35 +758,6 @@ contract WritesFacet {
emit WalletDerivationRemoved(apiKeyHash, pkpId);
}

/// @notice One-time migration helper: bind wallets registered before the global
/// owner binding existed to their original master account.
/// @dev Pairs should be derived off-chain from the EARLIEST
/// `WalletDerivationRegistered(masterHash, pkpId, ...)` event per pkpId
/// (first registration wins, matching the rule `registerWalletDerivation`
/// now enforces). Already-bound pkpIds are skipped, never re-assigned, so
/// the call is idempotent and safe to run in batches / re-run. Restricted
/// to the diamond owner or config operator.
function backfillPkpOwners(
address[] calldata pkpIds,
uint256[] calldata masterHashes
) public {
SecurityLib.revertIfNotConfigOperatorOrOwner(msg.sender);
if (pkpIds.length != masterHashes.length) {
revert AppStorage.InvalidRequest("array length mismatch");
}
AppStorage.AccountConfigStorage storage s = AppStorage.getStorage();
for (uint256 i = 0; i < pkpIds.length; i++) {
if (masterHashes[i] == 0) {
revert AppStorage.InvalidRequest("masterHash must be non-zero");
}
if (s.pkpIdToOwnerMaster[pkpIds[i]] != 0) {
continue; // already bound — never re-assign ownership
}
s.pkpIdToOwnerMaster[pkpIds[i]] = masterHashes[i];
emit PkpOwnerBackfilled(pkpIds[i], masterHashes[i]);
}
}

function setNodeConfiguration(
string memory key,
string memory value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import "./tasks/propose-add-device";
import "./tasks/transfer-ownership";
import "./tasks/verify-compose-hash";
import "./tasks/verify-diamond-facets";
import "./tasks/backfill-pkp-owners";

const config: HardhatUserConfig = {
solidity: {
Expand Down

This file was deleted.

Loading
Loading