# Deployer can permanently register arbitrary Gatekeeper rotation keys and bypass bridge authorization

**URL:** https://ghostforum.org/t/deployer-can-permanently-register-arbitrary-gatekeeper-rotation-keys-and-bypass-bridge-authorization/85
**Category:** Bug Bounty
**Tags:** ghost-dao-contracts
**Created:** [October 2, 2026, 7:43am UTC](https://ghostforum.org/t/deployer-can-permanently-register-arbitrary-gatekeeper-rotation-keys-and-bypass-bridge-authorization/85 "2026-10-02T07:43:06Z")
**Posts on this page:** 2
**Page:** 1

<div class="post-metadata">

### Author: ![callmefull](https://ghostforum.org/letter_avatar_proxy/v4/letter/c/cdc98d/32.png) [@callmefull](https://ghostforum.org/u/callmefull)
#### Post date: [October 2, 2026, 7:43am UTC](https://ghostforum.org/t/deployer-can-permanently-register-arbitrary-gatekeeper-rotation-keys-and-bypass-bridge-authorization/85/1 "2026-10-02T07:43:06Z")

</div>

## **Summary**

Gatekeeper.sol still exposes updatePublicKeyMetadata() to the deployer EOA even though the function is marked in the source as being needed only for testing purposes.

The deployer can use it to register an arbitrary public key for any exodusSession.

Because the deployer generated the corresponding private key itself, it can then produce a valid signature for that key and submit the signed call through the permissionless verify() path.

The call is executed by the Gatekeeper itself, so the normal msg.sender == address(this)  
checks in privileged functions such as recall() and govern() are satisfied.

This gives the deployer a persistent privileged path into the bridge without needing to forge a signature or compromise the normal rotation authority.

## **Root cause**

Target:

ghost-dao-contracts/src/Gatekeeper.sol

Commit tested: b87dff4

The constructor permanently records the deployer:

```auto
deployer = tx.origin;

```

The contract then exposes:

```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);

    uint256 packedRotationState =
        RotationPacking.pack(parity, uint64(exodusSession));

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

```

The function therefore allows the deployer to insert an arbitrary public key into the rotation state.  
The comment in the function itself says the function is only needed for testing and contains the intended deactivation step.  
There is no active revocation or rotation path for deployer.  
The value is also copied when a Gatekeeper address is updated, so replacing the Gatekeeper does not remove the same deployer authority.

## **Proof of Concept**

I tested the unmodified repository with Foundry 1.8.4 and Solidity 0.8.20.

Command:

```auto
forge test --match-contract DeployerBackdoorPoC

```

Result:

```auto
Ran 3 tests for test/poc/DeployerBackdoorPoC.t.sol:DeployerBackdoorPoC
[PASS] test_deployerBisaPasangKunciArbitrer_KapanPun()
[PASS] test_nonDeployerTidakBisa()
[PASS] test_deployerTidakBisaDicabut()

Suite result: ok. 3 passed; 0 failed; 0 skipped

```

A1 — deployer can register an arbitrary key  
From the deployer EOA:

```auto
updatePublicKeyMetadata(0, ATTACKER_KEY, 0)

```

succeeds and latestPublicKeyInfo() returns the attacker-controlled key afterwards.  
The PoC observed:

```auto
[PASS] deployer can register an arbitrary key for session 0
latestPublicKeyInfo() ->
(0xb5cd0d028a5e1b6a4eecb113b7e49c8176385eb77c8f134bc1db4bddd7654de6, 0, 0)

```

A2 — the restriction itself works  
Calling the same function from another address reverts.  
This means the issue is not a missing require. The problem is that the deployer EOA retains authority to modify the rotation key state indefinitely.

A3 — deployer authority cannot be revoked  
The PoC confirms that deployer remains non-zero and there is no function that can clear or rotate it.

## **Exploit path**

No cryptographic bypass is required.  
The deployer can generate a keypair locally and register the public key with updatePublicKeyMetadata().  
The existing authorization flow is:

```auto
verify(call, rx, s)
    -> _extractPublicKey(call)
    -> getRotationInfoAt(exodusSession)
    -> returns the deployer-registered public key
    -> Verifier.verifyGhost(call, px, rx, s)
    -> address(this).call(call)

```

Because the deployer owns the private key corresponding to the registered public key, it can create a valid signature itself.  
The resulting self-call has:

```auto
msg.sender == address(this)

```

Inside recall() the authorization check is:

```auto
if (msg.sender != address(this)) revert NotGatekeeper();

```

and govern() uses the same Gatekeeper-only pattern.  
Therefore the attacker-controlled key can be used to authorize calls that are normally restricted to the Gatekeeper itself.

## **Impact**

The deployer can unilaterally authorize privileged Gatekeeper actions without the normal rotation authority.  
In particular, the recall() path can mint GHST to an attacker-chosen receiver, subject to the bridge’s existing bridgeImbalance() limit.  
The govern() path also exposes arbitrary Gatekeeper-level execution through its target call.  
The authority is persistent because the deployer cannot be cleared or rotated, and it is copied to successor Gatekeepers.  
A compromise or malicious use of the deployer EOA therefore gives that single key privileged control over the bridge authorization path.

## **Why this is not expected production behavior**

The source comment explicitly describes updatePublicKeyMetadata() as being needed only for testing purposes and includes a planned deactivation step.  
The production contract nevertheless leaves the function callable by the deployer indefinitely.  
The repository also uses a separate governor authority for persistent administration, while this path leaves a single EOA with direct control over rotation-key registration.

## **Suggested fix**

Remove updatePublicKeyMetadata() from production deployments, or disable the deployer authority after testing.  
If the intended behavior is to deactivate the function after deployment, the relevant assignment should clear the actual variable:

```auto
deployer = address(0);

```

not \_deployer.  
Longer term, any production key-rotation capability should be controlled by the intended governance authority rather than a permanent single EOA.

---

<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 3, 2026, 11:23pm UTC](https://ghostforum.org/t/deployer-can-permanently-register-arbitrary-gatekeeper-rotation-keys-and-bypass-bridge-authorization/85/2 "2026-10-03T23:23:55Z")

</div>

Thank you for your report!

You are right - during the testnet phase, this function is needed to avoid redeploying all the moving parts of the protocol, which would be pretty annoying. That’s why this comment exists.

At the same time, the idea is to make this function one-shot for the mainnet phase, because we need to set the first public key generated by the DKG on the ghost chain. Here’s the tricky part - or, in simple words, the “chicken or the egg” problem: something has to come first, either the chain deployment or the smart contracts.

What we do know is that until a Gatekeeper exists, validators will not attempt to start key generation - so the smart contracts must exist by that point. That’s why we’ve ended up with this admittedly ugly deployment option, where the deployer has to push the initial public key generated by the validators.

If you see better options, or if you have arguments for why this approach isn’t the best, please feel free to continue the thread.
