# Bounty of 100% encodes as 0 in EVM calldata (as u32 wrap)

**URL:** https://ghostforum.org/t/bounty-of-100-encodes-as-0-in-evm-calldata-as-u32-wrap/133
**Category:** Resolved
**Tags:** exodus-pallet
**Created:** [October 4, 2026, 2:47am UTC](https://ghostforum.org/t/bounty-of-100-encodes-as-0-in-evm-calldata-as-u32-wrap/133 "2026-10-04T02:47:14Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![Rusdiwireng](https://ghostforum.org/letter_avatar_proxy/v4/letter/r/9de0a6/32.png) [@Rusdiwireng](https://ghostforum.org/u/Rusdiwireng)
#### Post date: [October 4, 2026, 2:47am UTC](https://ghostforum.org/t/bounty-of-100-encodes-as-0-in-evm-calldata-as-u32-wrap/133/1 "2026-10-04T02:47:14Z")

</div>

**Component:** `pallets/exodus` @ `0162da46`, `src/types/exodus_request.rs:383-391`.

**Description:** The `Perbill` bounty is scaled to `uint32` for the `recall(uint256,uint256,uint256)` metadata slot:

```rust
let scaled_bounty = multiply_by_rational_with_rounding(
    bounty.deconstruct() as u128, 1u128 << 32, 1_000_000_000, Rounding::Down)?;
let bounty_uint32 = scaled_bounty as u32; // <-- wrap!

```

`Perbill::from_percent(100)` → `deconstruct()` = 1\_000\_000\_000 → `scaled_bounty` = exactly 2^32 → `as u32` truncates to **0**. The existing tests only cover 1%.

**Impact:** A user setting a 100% bounty encodes 0 instead: the EVM relayer gets nothing. Self-inflicted; no other party can exploit it (the value is written by the submitter).

**Reproduction:** `ExodusRequest::evm_bridge_out(..., Perbill::from_percent(100), ...)` → `get_message` → the last 4 bytes of the metadata slot are `0x00000000` instead of `0xFFFFFFFF`.

**Severity: Low.** Reason: a real correctness bug at the upper bound, with no attacker path and no cross-party fund loss.

**Recommendation:** Clamp before casting (`scaled_bounty.min(u32::MAX as u128) as u32` or `unique_saturated_into::<u32>()`); add boundary tests for 0%/100%.

**Verification:** VERIFIED (static).

---

<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:31am UTC](https://ghostforum.org/t/bounty-of-100-encodes-as-0-in-evm-calldata-as-u32-wrap/133/2 "2026-10-05T10:31:28Z")

</div>

Thank you for your report!

Actually, you’re right. It looks like only 100% will be affected, correct?

One more thing - the fix I have at the top of my head is:

```rust
let scaled_bounty = multiply_by_rational_with_rounding(
    bounty.deconstruct() as u128,
    (1u128 << 32) - 1,
    1_000_000_000,
    sp_runtime::Rounding::Down
)?;

```

At the same time, do you think this way is _overengineering_? Maybe you could see a simpler way of doing it?

---

<div class="post-metadata">

### Author: ![Rusdiwireng](https://ghostforum.org/letter_avatar_proxy/v4/letter/r/9de0a6/32.png) [@Rusdiwireng](https://ghostforum.org/u/Rusdiwireng)
#### Post date: [October 6, 2026, 4:30am UTC](https://ghostforum.org/t/bounty-of-100-encodes-as-0-in-evm-calldata-as-u32-wrap/133/3 "2026-10-06T04:30:49Z")

</div>

Yes, correct: only 100% is affected. Perbill::deconstruct() maxes out at 1\_000\_000\_000 (100%), which scales to exactly 2^32; every smaller value scales to strictly less than 2^32 and survives the as u32 cast. So 100% is the single wrapping input.

On the fix: your (1u128 \<\< 32) - 1 multiplier works, but it rescales every input slightly (all values map to a marginally smaller range). The simpler change is clamping before the cast, which leaves all existing behavior untouched:

let bounty\_uint32 = scaled\_bounty.min(u32::MAX as u128) as u32;

One line, only the 100% case changes (2^32 saturates to u32::MAX instead of wrapping to 0). I would go with the clamp over the multiplier.

---

<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 10, 2026, 3:26pm UTC](https://ghostforum.org/t/bounty-of-100-encodes-as-0-in-evm-calldata-as-u32-wrap/133/4 "2026-10-10T15:26:53Z")

</div>

Hey @Rusdiwireng - it looks like the dev team has [fixed the issue](https://git.ghostchain.io/ghostchain/ghost-node/commit/e3a81e3902178a8bf008f5727c0f0fee6f666f86) you reported. Could you confirm whether it’s resolved on your end now, or continue the thread if it still persists?

We decided to simply cap the value, since the rest of the logic looks sound and we only needed to eliminate this edge case.

---

<div class="post-metadata">

### Author: ![Rusdiwireng](https://ghostforum.org/letter_avatar_proxy/v4/letter/r/9de0a6/32.png) [@Rusdiwireng](https://ghostforum.org/u/Rusdiwireng)
#### Post date: [October 11, 2026, 8:15am UTC](https://ghostforum.org/t/bounty-of-100-encodes-as-0-in-evm-calldata-as-u32-wrap/133/5 "2026-10-11T08:15:31Z")

</div>

Checked e3a81e3. The clamp is exactly what I suggested: scaled\_bounty.min(u32::MAX as u128) as u32. Only the 100% case changes (2^32 saturates instead of wrapping to 0); everything below is untouched by the min(). Confirmed resolved.
