> For the complete documentation index, see [llms.txt](https://docs.kpk.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kpk.io/funds/infrastructure/onchain-accounting/attested-balance-adapters/merkl.md).

# Merkl Rewards

Reports an account's unclaimed Merkl rewards — held by the Distributor, valued from a proof-verified cache, per reward token and campaign share.

Reports an account's **unclaimed Merkl rewards**, per reward token.

The rewards are real and already on-chain: campaign funders transfer the reward tokens to Merkl's **Distributor**, which custodies them until each account claims its share. What is not on-chain is the **split** — Merkl's off-chain engine derives each account's cumulative entitlement per token from campaign rules and off-chain observation, publishes the resulting reward file, and pushes only a **merkle root over it** to the Distributor at intervals. The contract therefore holds the tokens, the current root, and `claimed` — how much each account has already withdrawn — but nothing that yields an account's entitlement on its own.

That is precisely the [attested](/funds/infrastructure/onchain-accounting/attested-balance-adapters.md) shape: the value is on-chain **verifiable but not derivable**. The family page covers the model, the trust boundaries, the introspection reads and the error taxonomy; this page covers only what is specific to Merkl.

* **Type:** Attested adapter — the family's first member
* **protocolBrand / protocolId / protocolSubId:** all three are `ProtocolIds.MERKL`; Merkl has no product split to distinguish
* **positionId:** `abi.encode(DISTRIBUTOR)` — **identical for every leg of every token**
* **positionInstanceId:** the campaign coordinate, where a breakdown was submitted
* **positionKind:** `Rewards` · **protocolName():** `"Merkl"` · **labels:** `["Claimable Rewards"]`
* **Distributor:** `0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae` — Angle Labs, the same address on every supported chain
* **Source:** [`MerklRewardsAttestedBalanceAdapter.sol`](https://github.com/karpatkey/onchain-accounting/blob/main/src/balances/AttestedBalanceAdapters/MerklRewardsAttestedBalanceAdapter.sol)

***

## What it reports

A token's unclaimed total is its **proof-verified cumulative minus the amount already claimed on-chain**, with `claimed` read live from the Distributor on every read. One leg per cached reward token, or one per campaign share where a breakdown was submitted.

The live netting is what keeps the position from being counted twice. A claim does not destroy value — it **moves** it out of the Distributor and into the account's wallet, where the [default adapter](/funds/infrastructure/onchain-accounting/balance-adapters.md) already reports it as an ordinary ERC-20 balance. Because Merkl's leaves carry a *cumulative* figure rather than an incremental one, subtracting `claimed` hands off exactly the settled portion and leaves the entitlement reported only while it is genuinely still outstanding.

| Leg               | PositionKind | isDebt  | Amount                                           |
| ----------------- | ------------ | ------- | ------------------------------------------------ |
| Claimable Rewards | `Rewards`    | `false` | Proven cumulative − claimed, in the reward token |

***

## Reading the legs — sum them, never de-duplicate

{% hint style="danger" %}
**Every Merkl leg of one token shares an identical reconciliation key.**

`positionId` is `abi.encode(DISTRIBUTOR)` — the same value for every leg of every token — and the campaign coordinate rides in `positionInstanceId`, which is **excluded** from the stable key. Legs of *different* tokens still separate, because the key includes `balanceAsset.asset`. Legs of the *same* token do not.

So a consumer that treats a repeated key as duplicate data and keeps one row **under-reports that token by up to the per-campaign cap**. Sum the legs.

Campaign attribution is **display-grade, not audit-grade**: the proof binds only the per-token total and the contract checks only that the shares sum to it. **The per-token total — the sum of a token's legs — is the proof-backed figure.** Treat the split as presentation.
{% endhint %}

`campaignsOf(user, token)` returns the cached `CampaignShare[]` directly, for a consumer that wants the breakdown without reassembling it from legs. Same standing as the legs' campaign coordinates: display and debugging, with the shares summing to the proof-verified per-token cumulative. Empty where the token was submitted untagged.

***

## The proof shape

Merkl decodes the family's opaque `bytes` into `Allocation[]` — the same shape for the write path and the cache-free read:

```solidity
struct CampaignShare {
    bytes32 campaignId;
    bytes32 subCampaignId;
    uint256 cumulativeAmount;
}

struct Allocation {
    address        token;             // reward token
    uint256        cumulativeAmount;  // proof-bound total for (account, token)
    bytes32[]      proof;             // merkle proof against the Distributor's live root
    CampaignShare[] campaigns;        // optional breakdown; must SUM to cumulativeAmount
}
```

The leaf proved is `keccak256(abi.encode(account, token, cumulativeAmount))`. Typed convenience wrappers exist for keeper ergonomics — `submitProofs(user, allocations)` and `submitProofsBatch(users, allocations)` — carrying the same atomicity as the family's `submit` and `submitBatch`.

***

## Reading a position without the cache

`previewPositions(account, abi.encode(allocations))` values an account straight from caller-supplied proof data — the same payload the write path takes. See [reading without the cache](/funds/infrastructure/onchain-accounting/attested-balance-adapters.md#reading-without-the-cache) for the semantics; the Merkl-specific part is only the payload shape above. As on the write path, the **campaign split is not proof-bound** — a preview built from fabricated shares that happen to sum correctly returns those shares.

***

## Staleness and the dispute window

Each new root is published subject to a short dispute window — a window in which watchers verify the reward file and can post collateral to challenge it, and a challenged tree can be resolved back to its predecessor or revoked outright. `getMerkleRoot()` reflects that: during the window, or while a disputer is set, it returns the **previous** root. The root is therefore **not monotonic** — it can return to a value it held before, which is why the family's staleness check is a straight equality test and never an extrapolation.

Two operational consequences:

* **`isAttestationLive` can flap** for the same element — `false` during a rotation, `true` again if the root rolls back. A monitor that pages on a single `false` will page on dispute windows.
* **A clear performed while the root is rolled back is not undone when the root returns.** `clearAttestation` is legitimate the moment an element reads stale, so an element cleared inside the window stays gone until the keeper resubmits. The reading is an under-count that self-heals on the next feed cycle.

{% hint style="info" %}
**`AmountRegression` should never fire.** For a well-formed Merkl tree it is unreachable — the amount is committed inside the leaf, so exactly one value verifies per `(user, token)` under one root. It is a tripwire, not a protection: alert on any occurrence rather than retrying.
{% endhint %}

***

## Bounds

Two caps bound the per-account leg count, and it is their **product** a read budget must size against — not the token cap alone, because the campaign split is not proof-bound and so is influenceable by whoever submits. Read `MAX_TOKENS_PER_USER` and `MAX_CAMPAIGNS_PER_TOKEN` off the adapter rather than from a figure here; the product has already been tightened once. The family read `maxAssetsPerAccount()` returns the token cap, so a protocol-agnostic consumer needs no Merkl symbol to watch it.

Two consequences of the token cap that garbage collection, not configuration, resolves:

* Under upsert semantics the cache **accumulates every token ever proven**. Stale entries go only via the permissionless, stale-gated clears, so an account farming Merkl across many root epochs drifts toward the cap unless the keeper also collects garbage.
* **At the cap, a submit carrying a new token reverts** — and because a submit is atomic over its whole list, that reverts the refresh of the account's *other* tokens too. A keeper at the cap stops updating everything, not just the new token.

Per-token isolation does not make an under-budgeted read survivable: with a full cache and an expensive token valued last, the read can revert outright. Size the budget off the product.

***

## Failure behaviour

Writes fail closed with named errors — `InvalidProof` (proof does not verify against the live root), `AmountRegression` (a cumulative below the cached value for the *same* root), `CampaignSumMismatch` (shares do not sum to the proven total), `TooManyCampaigns` (breakdown over the cap). A rejected submit leaves the previous cached value intact rather than a partial one.

Reads never revert on a bad cache entry: a stale epoch reads `0`, and a token whose asset is unregistered on the NAV contributes nothing.

{% hint style="warning" %}
**One silent failure mode worth monitoring.** The live netting reads `claimed` off the Distributor unguarded and ABI-decodes the result, so if the Distributor — an upgradeable proxy under Angle governance — ever returns something undecodable, the read reverts. On the NAV path that means this adapter's whole leg set drops out for the account: an under-report, the safe direction, but with no error surfaced. The [family diagnostic](/funds/infrastructure/onchain-accounting/attested-balance-adapters.md#error-taxonomy) applies.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kpk.io/funds/infrastructure/onchain-accounting/attested-balance-adapters/merkl.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
