# GHOST-E02 — Third-party dust stake can extend an existing user's warmup expiry

**URL:** https://ghostforum.org/t/ghost-e02-third-party-dust-stake-can-extend-an-existing-users-warmup-expiry/131
**Category:** Submitted
**Tags:** ghost-dao-contracts
**Created:** [October 4, 2026, 2:46am UTC](https://ghostforum.org/t/ghost-e02-third-party-dust-stake-can-extend-an-existing-users-warmup-expiry/131 "2026-10-04T02:46:25Z")
**Posts on this page:** 8
**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 4, 2026, 2:46am UTC](https://ghostforum.org/t/ghost-e02-third-party-dust-stake-can-extend-an-existing-users-warmup-expiry/131/1 "2026-10-04T02:46:25Z")

</div>

GHOST-E02 — Third-party dust stake can extend an existing user’s warmup expiry

**Severity:** Medium

**Component:** `ghost-dao-contracts / src/Warmup.sol`, `src/Staking.sol`

**Repository:** `ghost-dao-contracts`

**Commit:** `b87dff4`

## Summary

`GhostStaking.stake()` allows a caller to stake FTSO for another address.

For recipients that have not enabled `locks`, an external deposit is accepted and passed to `GhostWarmup.addToWarmup()` together with a new expiry:

```auto
uint48 expiry = epoch.number + warmupPeriod;
IGhostWarmup(warmup).addToWarmup(returnAmount, to, expiry);

```

`GhostWarmup.addToWarmup()` aggregates the recipient’s position, but replaces the existing expiry with the expiry of the latest deposit:

```auto
info.deposit += payout;
info.payout += ghstPayout;
info.expiry = expiry;

```

Because of this, a third party can make a negligible deposit, such as `1 wei`, to another user’s address and move the expiry of that user’s entire existing warmup position forward.

The attacker does not need any privileged role or access to the victim’s wallet.

## Root Cause

The external-deposit check in `Staking.stake()` only rejects the deposit when the recipient has enabled their lock:

```auto
if (locks[to] && to != msg.sender) revert ExternalDepositsLocked();

```

When the lock is not enabled, the staking contract creates a new expiry and passes it to `Warmup.addToWarmup()`.

`Warmup.addToWarmup()` stores the position as a single aggregate entry per address. New deposits increase the aggregate `deposit` and `payout`, but the `expiry` is overwritten rather than kept independently per deposit.

This means a small third-party deposit can change the maturity of the recipient’s entire existing position.

## Attack Scenario

Assume `warmupPeriod = 5`.

The victim initially stakes `1000` FTSO at epoch `1`:

```auto
Victim deposit: 1000 FTSO
Expiry: epoch 6

```

At epoch `2`, an unrelated user deposits only `1 wei` to the victim:

```auto
stake(1, victim, false, false)

```

The aggregate position becomes:

```auto
Deposit: 1000 FTSO + 1 wei
Expiry: epoch 7

```

The original `1000` FTSO deposit has therefore become unclaimable at its original expiry even though the victim did not make another deposit.

If the attacker repeats the same action before the new expiry is reached, the victim’s claimable state can be pushed forward again.

## Proof of Concept

The attached Foundry PoC reproduces the behavior using the repository contracts.

The main test creates two accounts with the same initial stake:

- `VICTIM`
- `CONTROL`

The attacker then deposits `1 wei` to the victim.

The test confirms that the victim’s expiry moves from epoch `6` to epoch `7`. It then advances to epoch `6` and compares the two accounts: the control account can claim, while the attacked account cannot.

The relevant PoC assertions are:

```auto
CONTROL:
  claim at epoch 6 -> succeeds

VICTIM:
  claim at epoch 6 -> returns 0
  warmup deposit -> still present

```

The PoC also repeats the attack three times and verifies that the victim remains unclaimable after each extension.

The test suite completes successfully:

```auto
[PASS] test_strangerExtendsVictimWarmupLock()
[PASS] test_attackRepeatableKeepsVictimLockedForever()
[PASS] test_victimEscapeHatches()

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

```

The uploaded PoC contains the full setup and assertions.

## Impact

Any user can repeatedly delay another user’s warmup position by spending only a negligible amount of FTSO.

No privileged role is required, and the attacker does not need to control or interact with the victim’s wallet.

The attack affects the victim’s entire aggregated warmup position, not only the attacker’s `1 wei` deposit.

The practical result is a griefing/DoS condition where the victim can repeatedly miss the point at which their existing position should become claimable.

The attack can continue for as long as the attacker keeps making third-party deposits before the current expiry.

## Existing Lock Mechanism

The contract provides `toggleLock()` to prevent future external deposits.

However, enabling the lock does not restore an expiry that has already been extended.

For example:

```auto
Victim expiry = 6

Attacker deposits 1 wei
Victim expiry = 7

Victim enables external-deposit lock
Future attacker deposits -> reverted

Victim expiry remains 7

```

Therefore the lock can stop subsequent extensions, but it does not undo an extension that has already occurred.

## Why This Is a Security Issue

Allowing users to stake on behalf of another address is an existing feature, and the `locks` mechanism provides a way for an address to reject future external deposits.

The issue is the effect of an external deposit on an already-existing warmup position.

A third party’s negligible deposit can overwrite the expiry of the recipient’s entire aggregate position. This allows an unrelated user to postpone when someone else’s already-deposited funds become claimable.

The attacker does not need to add a meaningful amount to the victim’s position. A `1 wei` deposit is sufficient to move the expiry forward.

This makes the external-deposit primitive usable as a low-cost griefing mechanism against existing warmup positions.

## Recommended Fix

The warmup accounting should not allow a third-party deposit to move the expiry of an existing position forward.

A minimal mitigation is to preserve the existing expiry rather than replacing it with the newest expiry:

```auto
if (info.expiry == 0 || expiry < info.expiry) {
    info.expiry = expiry;
}

```

A stronger design would track expiry per deposit rather than maintaining a single expiry for all deposits belonging to an address.

The important invariant is:

> A third party should not be able to make an existing user’s funds become claimable later merely by depositing a negligible amount into that user’s address.

---

<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 4, 2026, 5:46pm UTC](https://ghostforum.org/t/ghost-e02-third-party-dust-stake-can-extend-an-existing-users-warmup-expiry/131/2 "2026-10-04T17:46:43Z")

</div>

Thank you for your report!

Your report is precisely right. Let me add some context:

> At any point in time, a user can `ghost` - that is bridge-transfer - right away, without needing to wait for the expiry.

So if someone attacks an honest user, the user can `ghost` at any point without any issues. That said, you’re right that this behavior could be unexpected from the user’s perspective. To fully mitigate it, we’d need to flip the logic of how it’s checked.

But here’s the catch: we don’t know which route is preferable for the majority of users:

1. Accumulate tokens from different addresses into one, indefinitely, regardless of expiry, because bridging is available at any point in time.
2. Require active permission for that kind of behavior - exactly what you’re highlighting in your report - so it becomes a deliberate action.

I don’t think this is a technical question (the fix is dead simple) - it’s more about what the community decides. Any thoughts?

---

<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 4, 2026, 11:52pm UTC](https://ghostforum.org/t/ghost-e02-third-party-dust-stake-can-extend-an-existing-users-warmup-expiry/131/3 "2026-10-04T23:52:22Z")

</div>

Thanks for the clarification. Good to know `ghost()` is always available as an alternative way to move the funds.

I think the core issue still stands, though. The impact here isn’t that the funds are permanently locked; it’s that an unrelated third party can change when an existing position becomes claimable without any action from the owner. From the user’s perspective, someone else’s deposit shouldn’t silently change the timing of their existing position.

On the fix, I agree it can be narrower than introducing a new permission flow. If a third-party deposit simply cannot move the expiry of an existing position forward, the existing accumulation behavior and staking for other addresses can remain unchanged. The only behavior removed would be the ability to extend someone else’s existing warmup position through a dust deposit.

That seems like a relatively small behavioral change while preserving the existing use cases. I’d be interested in your thoughts on whether that would be preferable.

---

<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 5, 2026, 10:38am UTC](https://ghostforum.org/t/ghost-e02-third-party-dust-stake-can-extend-an-existing-users-warmup-expiry/131/4 "2026-10-05T10:38:57Z")

</div>

Actually, this could be gamified while fully eliminating any behavior reported in your post. For example, the warmup could be increased in accordance with the amount that is locked.

I have no idea how or what exactly - that’s just a fresh idea I had while reading your response. Any ideas on this?

---

<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 6, 2026, 12:17am UTC](https://ghostforum.org/t/ghost-e02-third-party-dust-stake-can-extend-an-existing-users-warmup-expiry/131/5 "2026-10-06T00:17:14Z")

</div>

Yeah, I think that could work.

The main thing I’d want to avoid is a small deposit being able to move the expiry of a much larger existing position.

A weighted expiry seems like one way to handle that. For example, a 1 wei deposit into a 1000-token position shouldn’t meaningfully move the existing expiry, while a much larger deposit could have a larger effect.

That would keep accumulation and stake-for-others working, while making the amount of influence on the expiry depend on the amount actually added.

I don’t think the exact formula needs to be decided yet. The main property I’d preserve is that a small unrelated deposit shouldn’t be able to materially extend someone else’s existing warmup.

---

<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:46pm UTC](https://ghostforum.org/t/ghost-e02-third-party-dust-stake-can-extend-an-existing-users-warmup-expiry/131/6 "2026-10-11T17:46:15Z")

</div>

Could you confirm whether the reported issue is partially [fixed here](https://git.ghostchain.io/ghostchain/ghost-dao-contracts/commit/44636659e3412050f41bdc70791fc70fb41ef4c4) and [here](https://git.ghostchain.io/ghostchain/ghost-dao-contracts/commit/d7977a5ddcb298d07fb16f1411a36db9cfa487a5)?

One more thing - while @daffh makes solid arguments that the reports are different:

> Why this is not a duplicate of GHOST-E02 (topic 131)
> 
> Topic 131 reports that a third-party dust deposit can extend a recipient’s warmup expiry. I read that report in full before filing, because it is the closest existing report to this one.  
> …

Could you confirm whether they are indeed different, or whether they are variations of the same issue?

---

<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 12, 2026, 8:57am UTC](https://ghostforum.org/t/ghost-e02-third-party-dust-stake-can-extend-an-existing-users-warmup-expiry/131/7 "2026-10-12T08:57:16Z")

</div>

I checked both commits, and I think they address different parts of the problem.

The first commit fixes the lock check on the direct-claim path, which is the issue daffh reported. It doesn’t change the expiry overwrite I reported in E02.

The second commit changes external deposits to opt-in. That blocks the dust-deposit attack against recipients who haven’t opted in, but `addToWarmup()` still overwrites `info.expiry` when an external deposit is allowed. So the original attack against passive recipients is blocked, but the expiry behavior remains for recipients who opt in.

I don’t think the two reports are duplicates. They both involve staking for another address, but daffh’s report is about bypassing the external-deposit lock on the direct-claim path, while my E02 report is about changing an existing warmup position’s expiry.

---

<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 12, 2026, 10:33am UTC](https://ghostforum.org/t/ghost-e02-third-party-dust-stake-can-extend-an-existing-users-warmup-expiry/131/8 "2026-10-12T10:33:12Z")

</div>

Thank you for your clarifications! Please confirm whether I’ve finally got the difference right.

You’re saying that if an address has external deposits turned on, then warmup would be extended for the whole amount. If that’s what you’re describing, we should acknowledge that this is part of the protocol’s design.

While the extension is needed in most cases to remove _near-instant_ selling pressure - whether after a bond purchase or after taking rebase rewards and converting them back to sell - a user will, in any case, always have the ability to bridge in via the `ghost` function.

For example, if N rebases have accumulated over some period of time, an attacker could use `stake + rebase + unstake + state + rebase + ...` with `isTrigger = false` in a single transaction and end up with extra funds that could be sold on a DEX right away.

What are your thoughts on this? Do you think the reported issue persists in any case?
