Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
927f9e3
Update testnet ISMP host and handler addresses
Wizdave97 May 18, 2026
dd85dc5
Remove TokenGateway code and migration scripts from the indexer
Wizdave97 May 18, 2026
80b8f64
Point indexer fee token to redeployment, skip IntentGateway tests
Wizdave97 May 18, 2026
371a8e4
Rename UserActivity entity to UserActivityV2
Wizdave97 May 18, 2026
8f607b1
Update HFT test addresses for the redeployment, skip the suite
Wizdave97 May 19, 2026
c234ce0
align indexer + simplex ABIs with IntentGatewayV2 #897; fix HFT SDK q…
royvardhan May 25, 2026
c86a86b
[indexer]: drop PostResponse handling
royvardhan May 25, 2026
95af0cc
[sdk]: HFT quote returns native cost; drop nonexistent quoteNative call
royvardhan May 25, 2026
a58e384
Merge remote-tracking branch 'origin/update-registry' into roy/hft-an…
royvardhan May 25, 2026
db0364d
Merge branch 'main' of github.com:polytope-labs/hyperbridge into roy/…
Wizdave97 May 25, 2026
774d42f
Update mainnet host/handler addresses, add Polkadot Hub, drop Unichain
Wizdave97 May 25, 2026
7f9067d
[docs]: bandwidth subscription system
royvardhan May 26, 2026
ee8caef
[sdk]: drop removed perByteFee from EvmHost quote and HostParams
royvardhan May 26, 2026
4f1ebf4
[simplex]: drop dead perByteFee cache and initCache fetch
royvardhan May 26, 2026
40895ce
[sdk]: source BSC Chapel host from chainConfigs in stateCalls test
royvardhan May 26, 2026
e206239
[indexer]: zero out computeProtocolFeeFromHexData after perByteFee re…
royvardhan May 26, 2026
2f9f29a
[indexer]: drop protocol-fee indexing and rename HyperBridgeChainStat…
royvardhan May 26, 2026
0d4d28b
[indexer]: refresh EvmHost ABI and fix GetRequestEvent topic for new …
royvardhan May 26, 2026
1d264b5
[indexer]: remove gnosis-chiado, no longer supported on testnet
royvardhan May 26, 2026
ddaae4a
enable hft test
Wizdave97 May 26, 2026
2437cbe
nit
Wizdave97 May 26, 2026
2b4111c
nit
Wizdave97 May 26, 2026
979326a
fix tests
Wizdave97 May 26, 2026
93bd077
Hash post/get-request commitments with abi.encode to match V3 host
Wizdave97 May 27, 2026
2f824ef
Pass tuple values as named objects to encodeAbiParameters
Wizdave97 May 27, 2026
bf4c9a8
[docs]: address PR review on bandwidth overview — protocol-fee termin…
royvardhan May 27, 2026
5068dcb
[docs]: drop vs PAYG column from bandwidth tier table
royvardhan May 27, 2026
553734c
Use Polygon Amoy's actual consensusStateId in the HFT test
Wizdave97 May 27, 2026
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
2 changes: 0 additions & 2 deletions .github/workflows/test-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ on:

jobs:
test:
# Disabled during protocol rewrite — re-enable once the SDK is stable.
if: false
runs-on: ubuntu-latest
defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,6 @@ package-lock.json
parachain/simtests/gargantua-runtime-new.wasm
parachain/simtests/hyperbridge-main-binary
**/config.example.toml
**/config.mainnet.toml
evm/script/UpdateConsensusClient.s.sol
evm/script/update-consensus-client.sh
127 changes: 127 additions & 0 deletions docs/content/developers/evm/bandwidth/configuration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
title: Configuration
description: Per-chain bring-up checklist — deploy BandwidthManager, bind it to the ISMP host, register on pallet-bandwidth, and configure tiers on both sides.
---

# Deployment & Configuration

Wiring a new source chain into the bandwidth system is a five-step checklist: deploy the manager, bind it to the local ISMP host, register it on the pallet, set the byte/duration side of each tier, push the prices to the manager. Until every step lands, purchases from that chain are rejected — usually with `UnknownManager` on the pallet or `UnknownTier()` on the manager.

## Constants

These values are baked into both sides of the system and must match exactly:

| Constant | Value | Notes |
|----------|-------|-------|
| `PALLET_BANDWIDTH_MODULE_ID` | `"BWMARKET"` (8 bytes) | The `to` field on every purchase dispatch; equal to `pallet_bandwidth::PALLET_BANDWIDTH.0`. Changing it on either side breaks the round-trip. |
| `OnAcceptActions.SetTiers` | discriminant `0` | Mirrors `ACTION_SET_TIERS` on the pallet. |
| `OnAcceptActions.Withdraw` | discriminant `1` | Mirrors `ACTION_WITHDRAW` on the pallet. |
| `TierIndex` | enum `{1, 2, 3, 4}` | Closed enum on both sides; adding a tier needs a coordinated upgrade. |
| `MAX_SUBSCRIPTIONS` | `1024` | FIFO cap per `(chain, app)`. |

## Deployment Matrix

One `BandwidthManager` is deployed per source chain. The pallet's `BandwidthManager<T>` storage holds a `(source_chain → manager_address)` mapping, so adding a chain is purely additive — existing chains are unaffected.

Tier *prices* are per-chain (set on each manager). Tier *byte budgets and durations* are global (set once on the pallet). The five-step checklist below covers both.

## Step 1 — Deploy `BandwidthManager`

```solidity
import {BandwidthManager} from "@hyperbridge/core/apps/BandwidthManager.sol";

address owner = 0x...; // governance multisig or admin
BandwidthManager mgr = new BandwidthManager(owner);
```

The constructor only sets the `Ownable` owner. The host address and tier prices are bound *after* deploy.

The owner's only responsibility is calling `setHost` exactly once (Step 2). Beyond that, ownership is dormant — governance operations (`SetTiers`, `Withdraw`) arrive over ISMP, not through `onlyOwner`.

## Step 2 — Bind the ISMP host

```solidity
mgr.setHost(hostAddr);
```

This call is **one-shot** — once `_host != address(0)`, subsequent calls revert with `UnauthorizedAction`. The host address is what the contract uses to authenticate inbound governance (`request.source == IDispatcher(_host).hyperbridge()`), so making it mutable would weaken the trust model.

If the local ISMP host is ever redeployed, the manager must be redeployed alongside it (and re-registered via Step 3).

## Step 3 — Register the manager on Hyperbridge

Governance binds the deployed contract address to the source chain on the pallet side:

```rust
BandwidthPallet::set_manager(
RawOrigin::Root.into(),
StateMachine::Evm(8453), // source chain id
H160::from_str("0xMANAGER...").unwrap(), // address from Step 1
);
```

Emits `ManagerRegistered { source, manager }`. From this point the pallet accepts purchases dispatched from `source` *if and only if* `request.from` matches the registered address — any other sender on that chain is rejected.

Overwriting an existing registration is allowed (useful for redeployments); in-flight purchases from the old contract will fail after the swap.

## Step 4 — Configure tier byte budgets

```rust
BandwidthPallet::set_tier(
RawOrigin::Root.into(),
TierIndex::TierOne,
Some(TierConfig {
bytes: 10_000_000, // 10 MB per month
duration_secs: 30 * 24 * 3600, // 30-day window
}),
);
```

The pallet rejects configs with zero `bytes` or zero `duration_secs` — use `None` to revoke instead. A tier without a `TierConfig` is unpurchasable; `on_accept` returns `tier X is not configured`.

This step is **per-tier, not per-chain** — the byte/duration side is global on Hyperbridge. Repeat for each `TierIndex` you want to offer.

## Step 5 — Push tier prices to the manager

```rust
BandwidthPallet::dispatch_set_tiers(
RawOrigin::Root.into(),
StateMachine::Evm(8453),
vec![
(TierIndex::TierOne, U256::from(50e18 as u128)),
(TierIndex::TierTwo, U256::from(200e18 as u128)),
(TierIndex::TierThree, U256::from(500e18 as u128)),
(TierIndex::TierFour, U256::from(2_000e18 as u128)),
],
);
```

This dispatches a `SetTiers` message to the registered manager on `target`. The manager's `onAccept` writes `tierPrice[tier] = price18d` for each row and emits `TierSet(tier, price18d)`. Tiers not in the batch are left untouched.

Prices are 18-decimal regardless of the local fee token's decimals — the contract scales at purchase time. See [Purchasing → Decimal scaling](/developers/evm/bandwidth/purchasing#decimal-scaling) for the rules and the `PriceNotRepresentable()` failure mode.

Repeat for every chain that should sell the same tiers — tier prices are per-chain because the manager contract is per-chain.

<Callout type="warn" title="Keep prices in sync">
If you change a tier's price, dispatch the update to every chain where a manager is deployed — otherwise the same SKU costs different amounts depending on which chain a buyer is on. The pallet emits `TiersDispatched { target, count, commitment }` for each push so you can audit.
</Callout>

## Verification

Before announcing a chain as live, verify the round-trip end-to-end:

1. `BandwidthManager.host()` returns the local ISMP host (not zero).
2. `BandwidthManager.tierPrice(tier)` returns the expected non-zero price on every configured tier.
3. Pallet storage `BandwidthManager::get(source)` returns the deployed contract address.
4. Pallet storage `Tiers::get(tier)` returns the expected `TierConfig` for every tier.
5. A test purchase from a funded account succeeds end-to-end: `BandwidthPurchased` emits on the source chain, `BandwidthCredited` emits on Hyperbridge, `Pallet::remaining(chain, app)` reflects the bytes credited.

## Upgrades

| Change | Procedure |
|--------|-----------|
| Update a tier's price on one chain | `dispatch_set_tiers(target, [(tier, new_price18d)])` — single-chain push. |
| Update a tier's byte budget or duration | `set_tier(tier, Some(new_config))` — global; affects every chain since this lives on the pallet. |
| Revoke a tier on the pallet | `set_tier(tier, None)` — new purchases fail with `UnknownTier`. Existing subscriptions continue draining normally. |
| Revoke a tier on one manager | `dispatch_set_tiers(target, [(tier, 0)])` — new purchases on that chain fail with `UnknownTier()`. |
| Redeploy `BandwidthManager` | Deploy → `setHost` → `set_manager` → `dispatch_set_tiers` (every tier). The old address is now orphaned; any approve allowance against it is dead weight. |
122 changes: 122 additions & 0 deletions docs/content/developers/evm/bandwidth/governance.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
title: Governance
description: Ongoing operations on pallet-bandwidth — tier updates, treasury withdrawals, allowlists, force-credits, and the events you can watch.
---

# Governance

Initial bring-up of a chain (deploy, bind host, register, set tiers) lives in [Configuration](/developers/evm/bandwidth/configuration). This page covers the *ongoing* operations: tier updates, treasury withdrawals, allowlist toggles, and admin force-credits.

Every privileged call on the pallet uses `pallet_ismp::Config::AdminOrigin` — the same origin that admins the ISMP host on Hyperbridge.

## Authentication

Two flows, two trust models:

- **Pallet → manager** (outbound governance). `BandwidthManager.onAccept` checks `request.source == IDispatcher(_host).hyperbridge()`. Only messages dispatched from Hyperbridge are honored. The `to` field is the manager's address — no module-id lookup.
- **Manager → pallet** (inbound purchases). The pallet rejects any purchase whose `request.from` doesn't equal the address stored under `BandwidthManager<T>::get(request.source)`. An attacker who deploys their own contract on a source chain cannot mint subscriptions.

## Pallet Calls

All six are gated by `AdminOrigin`:

| Call | Effect | Emits |
|------|--------|-------|
| `set_manager(source, manager)` | Register or overwrite the `BandwidthManager` authorised to send purchases from `source`. | `ManagerRegistered` |
| `set_tier(tier, config)` | Create, update, or revoke (`config: None`) a tier's `(bytes, duration_secs)`. Pallet-side only. | `TierSet` |
| `dispatch_set_tiers(target, updates)` | Push a `SetTiers` message to the `BandwidthManager` on `target`. Updates EVM-side prices. | `TiersDispatched` |
| `dispatch_withdraw(target, token, beneficiary, amount)` | Push a `Withdraw` message to the `BandwidthManager` on `target`. Drains the manager's treasury. | `WithdrawalDispatched` |
| `set_allowlist(source, app, on)` | Toggle gate bypass for an `(source, app)` pair. | `AllowlistChanged` |
| `force_credit(params)` | Append a subscription to a `(chain, app)` bucket without a purchase. | `ForceCredited` |

`set_manager`, `set_tier`, and `dispatch_set_tiers` are covered in [Configuration](/developers/evm/bandwidth/configuration). The remaining three are detailed below.

## Treasury Withdrawals

The fee tokens collected by `BandwidthManager.purchase()` accumulate in the manager contract. Governance drains them with `dispatch_withdraw`:

```rust
BandwidthPallet::dispatch_withdraw(
RawOrigin::Root.into(),
StateMachine::Evm(8453),
fee_token_address, // ERC-20 to withdraw. Use H160::zero() for native ETH.
treasury_address, // Beneficiary.
U256::from(1_000_000_000u128), // Amount, in fee-token decimals.
);
```

The pallet dispatches `[ACTION_WITHDRAW, abi.encode(Withdrawal)]` where `Withdrawal { token, beneficiary, amount }`. The manager's `onAccept` handler:

- If `token != address(0)`: `IERC20(token).safeTransfer(beneficiary, amount)`.
- If `token == address(0)`: `beneficiary.call{value: amount}("")` — reverts with `InsufficientNativeToken` if the call fails (insufficient balance, beneficiary rejects ETH, etc.).

The `token` field is named explicitly because the host occasionally swaps fee tokens (e.g. accepting native ETH on dispatch and routing through Uniswap). Stale balances of an old fee token still sit in the manager and are recoverable by specifying the old token's address.

Emits `Withdrawn(token, beneficiary, amount)` on the manager and `WithdrawalDispatched { target, token, beneficiary, amount, commitment }` on the pallet.

## Allowlist Management

The `Allowlist` storage map flips an `(source, app)` pair into bypass mode. While set, `BandwidthGate::try_consume` returns `Ok` for that pair without touching its subscriptions:

```rust
// Allow protocol-sponsored TokenGateway on Ethereum to bypass the gate.
BandwidthPallet::set_allowlist(
RawOrigin::Root.into(),
StateMachine::Evm(1),
AppKey::truncate_from(token_gateway_address.to_vec()),
true,
);

// Later, revoke.
BandwidthPallet::set_allowlist(
RawOrigin::Root.into(),
StateMachine::Evm(1),
AppKey::truncate_from(token_gateway_address.to_vec()),
false,
);
```

Intended for:

- **Phased rollout.** Apps that haven't yet migrated to bandwidth pricing are allowlisted while their integration is in flight.
- **Upgrade windows.** An app being redeployed can be allowlisted for the duration of the migration and removed once it's back on the meter.

Not intended as a long-term subsidy mechanism — every entry is a permanent gate bypass until governance revokes it.

## Force-Credit (Migrations & Refunds)

`force_credit` appends a subscription to any `(chain, app)` bucket without going through a purchase flow:

```rust
BandwidthPallet::force_credit(
RawOrigin::Root.into(),
ForceCreditParams {
app_chain: StateMachine::Evm(8453),
app: AppKey::truncate_from(app_address.to_vec()),
tier: TierIndex::TierOne, // label only — doesn't need to be configured
bytes: 5_000_000,
duration_secs: 30 * 24 * 3600,
},
);
```

Unlike a real purchase, the `tier` field here is **just a label** — it doesn't have to match a configured `TierConfig`. This is the admin escape hatch: refunds for bad purchases, credits during a runtime migration, one-off grants. The same FIFO/cap rules apply — pushing onto a full list evicts the oldest entry with `SubscriptionEvicted`.

## Events Cheat-Sheet

Subscribe for audit and monitoring:

| Side | Event | Fires on |
|------|-------|----------|
| Pallet | `ManagerRegistered { source, manager }` | `set_manager` |
| Pallet | `TierSet { tier, config }` | `set_tier` |
| Pallet | `TiersDispatched { target, count, commitment }` | `dispatch_set_tiers` |
| Pallet | `WithdrawalDispatched { target, token, beneficiary, amount, commitment }` | `dispatch_withdraw` |
| Pallet | `AllowlistChanged { source, app, on }` | `set_allowlist` |
| Pallet | `ForceCredited { app_chain, app, tier, bytes, expires_at }` | `force_credit` |
| Pallet | `BandwidthCredited { app_chain, app, paid_from, tier, bytes, expires_at }` | Purchase received via `on_accept` |
| Pallet | `BandwidthConsumed { source, app, bytes, remaining }` | Gate consumes bytes |
| Pallet | `SubscriptionEvicted { app_chain, app, tier, lost_bytes }` | FIFO cap hit, oldest entry dropped |
| Manager | `BandwidthPurchased(payer, feeToken, tier, months, amountPaid, app, chain, commitment)` | `purchase()` |
| Manager | `TierSet(tier, price18d)` | `onAccept` `SetTiers` row applied |
| Manager | `Withdrawn(token, beneficiary, amount)` | `onAccept` `Withdraw` transfer succeeded |
5 changes: 5 additions & 0 deletions docs/content/developers/evm/bandwidth/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Bandwidth",
"pages": ["overview", "purchasing", "governance", "configuration"],
"defaultOpen": false
}
Loading
Loading