# Gatekeeper deployer can permanently replace the aggregated bridge key with no signature, vote or delay

**URL:** https://ghostforum.org/t/gatekeeper-deployer-can-permanently-replace-the-aggregated-bridge-key-with-no-signature-vote-or-delay/160
**Category:** Submitted
**Tags:** ghost-node
**Created:** [October 6, 2026, 10:42am UTC](https://ghostforum.org/t/gatekeeper-deployer-can-permanently-replace-the-aggregated-bridge-key-with-no-signature-vote-or-delay/160 "2026-10-06T10:42:27Z")
**Posts on this page:** 7
**Page:** 1

<div class="post-metadata">

### Author: ![Sands](https://ghostforum.org/letter_avatar_proxy/v4/letter/s/22d042/32.png) [@Sands](https://ghostforum.org/u/Sands)
#### Post date: [October 6, 2026, 10:42am UTC](https://ghostforum.org/t/gatekeeper-deployer-can-permanently-replace-the-aggregated-bridge-key-with-no-signature-vote-or-delay/160/1 "2026-10-06T10:42:27Z")

</div>

Hi team

Finding: a single externally owned account, captured silently as transaction origin at Staking deployment, can at any future time replace the aggregated bridge key with no signature, no vote and no delay, then sign recalls against the planted key. The role never expires and is inherited by every successor Gatekeeper.

Location. Gatekeeper dot sol line 45 assigns deployer from tx dot origin inside the constructor. Staking dot sol line 65 constructs the Gatekeeper inside the Staking constructor, so the deployer value is the transaction origin of the Staking deployment transaction, never the Staking contract itself and never a multisig. updatePublicKeyMetadata at line 70 is guarded only by msg dot sender equals deployer at line 71. Lines 72 to 74 hold the commented-out disabling line under a TODO marking it testing-only. Lines 79 and 80 push the caller-supplied key into the checkpoint trace and store the packed rotation state. No timelock, no governance, no expiry, no parity check. Line 61 copies deployer forward from the previous Gatekeeper inside initialize, which line 51 restricts to the staking caller, so generations inherit the original deployer indefinitely. The signed rotate path at line 180 exists and requires a self-call plus a verified threshold signature, which makes the deployer path fully redundant as a key-write mechanism.

Executed in forge at the pinned HEAD, five of five passing: test\_deployerCanPlantKeyAndDrainViaRecall plants a key through the deployer path and drains through the real verify and recall path against the real staking and treasury stack. The negative control test\_mutatedSignatureFails reverts, proving the break is key possession and not signature forgery.

Deployment premise, stated honestly. Read-only live check: the Sepolia and Hoodi Gatekeepers are older minimal builds of about thirteen hundred bytes with neither the deployer selector nor the updatePublicKeyMetadata selector in the deployed bytecode, and the deployer view call reverts on both. Mordor did not answer. The vulnerable HEAD Gatekeeper is not deployed anywhere reachable today, so no live deployer EOA exists to classify yet. The mechanism is proven at 100 percent in the pinned source and becomes live in production the moment the HEAD stack is deployed. The transaction-origin binding also means the holder may not be who the team believes after any factory or Safe-mediated deployment.

Suggested fix: activate the disabling line, or remove the deployer override now that the threshold-signed rotate path performs this exact operation properly.

Shahmeer

---

<div class="post-metadata">

### Author: ![VoidDragon](https://ghostforum.org/user_avatar/ghostforum.org/voiddragon/32/16_2.png) [@VoidDragon](https://ghostforum.org/u/VoidDragon)
#### Post date: [October 7, 2026, 9:55am UTC](https://ghostforum.org/t/gatekeeper-deployer-can-permanently-replace-the-aggregated-bridge-key-with-no-signature-vote-or-delay/160/2 "2026-10-07T09:55:01Z")

</div>

Thank you for your report!

The same exact concern has been raised a couple of times before. It was done this way because we have a _chicken-or-the-egg_ problem here. We need `Gatekeeper` already deployed for the chain validators to run the DKG, but at the same time it needs the current `public key` - which is created by the DKG.

That’s why we’ve decided to use this dirty trick: initially, the deployer has to set the first key generated by the validators manually. As you can see, there’s a `NOTE` saying it should be done only once.

If you can envision more flexible ways to bootstrap the protocol, it would be nice if you could share your ideas here.

---

<div class="post-metadata">

### Author: ![Sands](https://ghostforum.org/letter_avatar_proxy/v4/letter/s/22d042/32.png) [@Sands](https://ghostforum.org/u/Sands)
#### Post date: [October 10, 2026, 4:51pm UTC](https://ghostforum.org/t/gatekeeper-deployer-can-permanently-replace-the-aggregated-bridge-key-with-no-signature-vote-or-delay/160/3 "2026-10-10T16:51:31Z")

</div>

Hi team

Fair point on the bootstrap problem: the first key has to come from somewhere before DKG completes. The finding stands on what happens after: the deployer role never expires, it is inherited by every successor Gatekeeper through initialize, and it is bound to tx origin rather than a known key. A middle path keeps your bootstrap flow: leave the one-time setter in place and activate the disabling line right after the first key is set, so the override cannot be reused or inherited. That preserves the manual first key without leaving a permanent backdoor.

Shahmeer

---

<div class="post-metadata">

### Author: ![VoidDragon](https://ghostforum.org/user_avatar/ghostforum.org/voiddragon/32/16_2.png) [@VoidDragon](https://ghostforum.org/u/VoidDragon)
#### Post date: [October 11, 2026, 4:27pm UTC](https://ghostforum.org/t/gatekeeper-deployer-can-permanently-replace-the-aggregated-bridge-key-with-no-signature-vote-or-delay/160/4 "2026-10-11T16:27:03Z")

</div>

Just to confirm: is that enough to uncomment the code below _NOTE_ section:

```auto
function updatePublicKeyMetadata(uint256 exodusSession, bytes32 publicKey, uint8 parity) external {
      require(msg.sender == deployer);
      // TODO: uncomment line below, needed only for testing purposes
      // and should push to hardcoded exodusSession 0 always
      // _deployer = address(0);

      // forge-lint: disable-next-line(unsafe-typecast)
      uint256 packedRotationState = RotationPacking.pack(parity, uint64(exodusSession));

      _aggregatedPublicKeys.push(exodusSession, uint256(publicKey));
      _packedRotationStates[publicKey] = packedRotationState;
  }

```

The code block is [taken from here](https://git.ghostchain.io/ghostchain/ghost-dao-contracts/src/commit/1359f920ca69d0ff401155662b05d292070c7e61/src/Gatekeeper.sol#L70).

---

<div class="post-metadata">

### Author: ![Sands](https://ghostforum.org/letter_avatar_proxy/v4/letter/s/22d042/32.png) [@Sands](https://ghostforum.org/u/Sands)
#### Post date: [October 11, 2026, 5:23pm UTC](https://ghostforum.org/t/gatekeeper-deployer-can-permanently-replace-the-aggregated-bridge-key-with-no-signature-vote-or-delay/160/5 "2026-10-11T17:23:20Z")

</div>

I tested it. Uncommenting the line as written does not compile: there is no `_deployer` variable in the contract, solc rejects it with an undeclared-identifier error. With the name corrected to `deployer = address(0)`, the deployer’s first call still sets the key and then burns the role, and every later call reverts for everyone, so the finding no longer reproduces. Two caveats from the tests: it permanently bricks the path rather than fixing it, and every fresh Gatekeeper deployment re-arms `deployer` to tx.origin in the constructor, so the line only protects deployments where the bootstrap call actually happens. Shahmeer

---

<div class="post-metadata">

### Author: ![VoidDragon](https://ghostforum.org/user_avatar/ghostforum.org/voiddragon/32/16_2.png) [@VoidDragon](https://ghostforum.org/u/VoidDragon)
#### Post date: [October 11, 2026, 5:57pm UTC](https://ghostforum.org/t/gatekeeper-deployer-can-permanently-replace-the-aggregated-bridge-key-with-no-signature-vote-or-delay/160/6 "2026-10-11T17:57:12Z")

</div>

You are correct about the renaming of `_deployer` to `deployer`, but in any case, compilation will fail - still, it would be nice to fix it now so we can forget about it down the road.

In production, it is intended to run only once, so your findings confirm that. As for the `constructor`, it is indeed setting `tx.origin` back to the `deployer`. But each `Gatekeeper` should call `initialize` on the `Staking` side - you can [take a look here](https://git.ghostchain.io/ghostchain/ghost-dao-contracts/src/commit/1359f920ca69d0ff401155662b05d292070c7e61/src/Staking.sol#L243). The idea is that only a non-initialized `Gatekeeper` can be set into the `Staking`, and during initialization it will reassign the `deployer` to the value from the previous one - for example, `address(0)` if it was already used, or the `tx.origin` of the first deployment.

Does that information fill all the gaps, or are there some other potential issues?

---

<div class="post-metadata">

### Author: ![Sands](https://ghostforum.org/letter_avatar_proxy/v4/letter/s/22d042/32.png) [@Sands](https://ghostforum.org/u/Sands)
#### Post date: [October 12, 2026, 2:07pm UTC](https://ghostforum.org/t/gatekeeper-deployer-can-permanently-replace-the-aggregated-bridge-key-with-no-signature-vote-or-delay/160/7 "2026-10-12T14:07:06Z")

</div>

Hi team

On the initialize() point: agreed — with the corrected burn line, address(0) propagates through initialize, so a spent role cannot be re-armed on successor Gatekeepers. That closes the inheritance leg.

The residual I would keep on the record is the arming window itself: deployer is tx.origin from the constructor, and nothing forces the bootstrap call to happen promptly — or at all. Until that first call burns the role, the deployment holds a live key-write, and with factory or Safe-mediated deployments the tx.origin holder may not be who the team expects. The single-run production assumption is exactly what makes the window easy to miss.

Shahmeer
