Skip to content

Commit cfad301

Browse files
authored
feat(fsp): add claiming rewards guide (#1208)
1 parent b640dcb commit cfad301

File tree

2 files changed

+369
-0
lines changed

2 files changed

+369
-0
lines changed

docs/network/fsp/2-rewarding.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ We refer to the [Weights and Signing](/network/fsp/weights-and-signing) page for
7575

7676
### Calculation process
7777

78+
:::tip[Calculating or verifying rewards data]
79+
80+
All rewards scripts are publicly available, and data providers are encouraged to calculate verify the rewards data for themselves.
81+
The data for the rewards is published on [flare-foundation/fsp-rewards](https://github.com/flare-foundation/fsp-rewards) and the instructions for how to calculate them are [here](https://github.com/flare-foundation/FTSO-Scaling/blob/main/scripts/rewards/README.md#public-reward-data).
82+
83+
:::
84+
7885
Each sub-protocol is responsible for calculating its **partial reward claims** for each reward epoch by:
7986

8087
1. **Data Input**: Determining the relevant data sources (indexers).
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
---
2+
title: Claiming rewards
3+
description: Instructions for data providers to claim rewards.
4+
keywords: [flare-network, reward, claim, data-provider, ftso, fdc, validator]
5+
sidebar_position: 1
6+
---
7+
8+
import Tabs from "@theme/Tabs";
9+
import TabItem from "@theme/TabItem";
10+
11+
Claiming rewards directly via smart contracts follows this flow:
12+
13+
1. **Discover claimable reward epochs** (onchain reads via `RewardManager` + `FlareSystemsManager`).
14+
2. **Fetch the reward distribution data** for each claimable epoch (`reward-distribution-data-tuples.json`) from public repositories.
15+
3. **Extract your claim tuple + Merkle proof** for the correct `beneficiary` and `claimType`.
16+
4. **Submit a single onchain transaction** calling `RewardManager.claim(...)` with one or more claim structs.
17+
18+
:::warning[Reward claim period]
19+
20+
Delegation rewards expire after 25 reward epochs. Make sure to claim before then.
21+
22+
Learn more about how [signing](/network/fsp/weights-and-signing) and [rewards](/network/fsp/rewarding) work in the FSP.
23+
24+
:::
25+
26+
## Prerequisites
27+
28+
1. **Wallet + gas:** A wallet that can sign transactions on the target network, funded with enough native token to pay gas.
29+
2. **RPC access:** An RPC endpoint for the chosen network (you can use any RPC listed on the [Network](/network/overview) page).
30+
3. **Beneficiary address (depends on claim type):**
31+
- **DIRECT:** `beneficiary` must be the **signing policy address**.
32+
- **FEE:** `beneficiary` must be the **identity address**.
33+
4. **Contracts:** Addresses can be found in the [Flare Contract Registry](/network/guides/flare-contracts-registry):
34+
- [`FlareSystemsManager`](/network/fsp/solidity-reference/IFlareSystemsManager)
35+
- [`RewardManager`](/network/fsp/solidity-reference/IRewardManager)
36+
- [`ClaimSetupManager`](/network/solidity-reference/IClaimSetupManager)
37+
5. **Recipient address:** The address that will receive the rewards (`recipient`).
38+
6. **Reward distribution data access:** You must be able to fetch per-epoch `reward-distribution-data-tuples.json` from the official distribution locations on GitHub or GitLab.
39+
7. **Wrap option:** `wrapRewards` is `true` unless explicitly set to `false`.
40+
41+
### Recommended: Configure a claim executor and claim recipient
42+
43+
For improved security, set up a **claim executor** (an account authorized to claim on your behalf) and an **allowed claim recipient** (an address permitted to receive rewards).
44+
45+
1. Create and fund a new account with sufficient native tokens
46+
2. Authorize this account as a claim executor, which can be done using the [`setClaimExecutors`](/network/solidity-reference/IClaimSetupManager#setclaimexecutors) method.
47+
- For **DIRECT (0)** claims: Use your signing policy account to authorize the executor.
48+
- For **FEE (1)** claims: Use your identity account to authorize the executor.
49+
3. Setup a **Claim Recipient** account, this can be done through the [`setAllowedClaimRecipient`](/network/solidity-reference/IClaimSetupManager#setallowedclaimrecipients) method.
50+
51+
```ts
52+
import { Contract } from "ethers";
53+
54+
const claimSetupManager = new Contract(
55+
process.env.CLAIM_SETUP_MANAGER_ADDRESS!, // from contract registry
56+
CLAIM_SETUP_MANAGER_ABI,
57+
beneficiarySigner, // IMPORTANT: signer must be the signing policy address (`DIRECT`) or identity address (`FEE`)
58+
);
59+
60+
// 1) Authorize the executor that will submit RewardManager.claim(...)
61+
await (await claimSetupManager.setClaimExecutors([executorAddress])).wait();
62+
63+
// 2) Allowlist a recipient address that is permitted to receive rewards
64+
await (
65+
await claimSetupManager.setAllowedClaimRecipients([recipientAddress])
66+
).wait();
67+
```
68+
69+
## Step-by-step
70+
71+
### 1. Connect to the contracts
72+
73+
Instantiate contract clients (ABI + address) for:
74+
75+
- [`FlareSystemsManager`](/network/fsp/solidity-reference/IFlareSystemsManager)
76+
- [`RewardManager`](/network/fsp/solidity-reference/IRewardManager)
77+
78+
Example using [ethers v6](https://docs.ethers.org/v6/):
79+
80+
```ts
81+
import { JsonRpcProvider, Wallet, Contract } from "ethers";
82+
83+
// 1) Provider + signer
84+
const provider = new JsonRpcProvider(process.env.RPC_URL);
85+
const signer = new Wallet(process.env.CLAIM_EXECUTOR_PRIVATE_KEY!, provider);
86+
87+
// 2) Contract instances (ABI omitted here; use your generated ABI or interface)
88+
const flareSystemsManager = new Contract(
89+
process.env.FLARE_SYSTEMS_MANAGER_ADDRESS!,
90+
FLARE_SYSTEMS_MANAGER_ABI,
91+
provider,
92+
);
93+
94+
const rewardManager = new Contract(
95+
process.env.REWARD_MANAGER_ADDRESS!,
96+
REWARD_MANAGER_ABI,
97+
provider,
98+
);
99+
```
100+
101+
### 2. Read the claimable epoch range
102+
103+
Call:
104+
105+
1. `startRewardEpochId = RewardManager.getNextClaimableRewardEpochId(beneficiary)`
106+
2. `[_, endRewardEpochId] = RewardManager.getRewardEpochIdsWithClaimableRewards()`
107+
108+
If `endRewardEpochId < startRewardEpochId`, there is nothing to claim.
109+
110+
Example:
111+
112+
```ts
113+
const start = await rewardManager.getNextClaimableRewardEpochId(beneficiary);
114+
const [, end] = await rewardManager.getRewardEpochIdsWithClaimableRewards();
115+
116+
if (end < start) {
117+
console.log("Nothing claimable for this beneficiary.");
118+
}
119+
```
120+
121+
### 3. Keep only signed epochs
122+
123+
For each `epochId` in `[startRewardEpochId … endRewardEpochId]`, call:
124+
125+
- `rewardsHash = FlareSystemsManager.rewardsHash(epochId)`
126+
127+
Only proceed if `rewardsHash` is **not** `0x000...000` (`bytes32(0)`).
128+
129+
Example:
130+
131+
```ts
132+
const ZERO_BYTES32 =
133+
"0x0000000000000000000000000000000000000000000000000000000000000000";
134+
135+
const signedEpochs: bigint[] = [];
136+
for (let epochId = start; epochId <= end; epochId++) {
137+
const rewardsHash = await flareSystemsManager.rewardsHash(epochId);
138+
if (rewardsHash && rewardsHash !== ZERO_BYTES32) signedEpochs.push(epochId);
139+
}
140+
```
141+
142+
### 4. Fetch reward distribution tuples
143+
144+
:::tip[Calculating or verifying rewards data]
145+
146+
All rewards scripts are publicly available, and you are encouraged to calculate verify the rewards data yourself.
147+
The data for the rewards is published on [flare-foundation/fsp-rewards](https://github.com/flare-foundation/fsp-rewards) and the instructions for how to calculate them are [here](https://github.com/flare-foundation/FTSO-Scaling/blob/main/scripts/rewards/README.md#public-reward-data).
148+
149+
:::
150+
151+
For each signed epoch, fetch `reward-distribution-data-tuples.json`.
152+
You fetch **one JSON file per epoch**, using the epoch id in the path.
153+
If the JSON cannot be fetched or validated, you cannot build a valid Merkle proof for that epoch.
154+
155+
<Tabs block groupId="network">
156+
<TabItem value="flare" label="Flare Mainnet" default>
157+
Fetch `reward-distribution-data-tuples.json` from [flare-foundation/fsp-rewards](https://github.com/flare-foundation/fsp-rewards) on GitHub.
158+
159+
```bash
160+
EPOCH_ID=123
161+
BENEFICIARY="0xYourBeneficiaryAddressHere"
162+
CLAIM_TYPE=0
163+
164+
curl -fsSL \
165+
"https://raw.githubusercontent.com/flare-foundation/fsp-rewards/refs/heads/main/flare/${EPOCH_ID}/reward-distribution-data-tuples.json" \
166+
| jq --arg b "${BENEFICIARY}" --argjson ct "${CLAIM_TYPE}" -r '
167+
.rewardClaims[]
168+
| select(.[1][1] | ascii_downcase == ($b | ascii_downcase))
169+
| select(.[1][3] == $ct)
170+
'
171+
```
172+
173+
</TabItem>
174+
<TabItem value="coston2" label="Flare Testnet Coston2">
175+
Fetch `reward-distribution-data-tuples.json` from [timivesel/ftsov2-testnet-rewards](https://gitlab.com/timivesel/ftsov2-testnet-rewards) on GitLab.
176+
177+
```bash
178+
EPOCH_ID=123
179+
BENEFICIARY="0xYourBeneficiaryAddressHere"
180+
CLAIM_TYPE=0
181+
182+
curl -fsSL \
183+
"https://gitlab.com/timivesel/ftsov2-testnet-rewards/-/raw/main/rewards-data/coston2/${EPOCH_ID}/reward-distribution-data-tuples.json" \
184+
| jq --arg b "${BENEFICIARY}" --argjson ct "${CLAIM_TYPE}" -r '
185+
.rewardClaims[]
186+
| select(.[1][1] | ascii_downcase == ($b | ascii_downcase))
187+
| select(.[1][3] == $ct)
188+
'
189+
```
190+
191+
</TabItem>
192+
<TabItem value="songbird" label="Songbird Canary-Network">
193+
Fetch `reward-distribution-data-tuples.json` from [flare-foundation/fsp-rewards](https://github.com/flare-foundation/fsp-rewards) on GitHub.
194+
195+
```bash
196+
EPOCH_ID=123
197+
BENEFICIARY="0xYourBeneficiaryAddressHere"
198+
CLAIM_TYPE=0
199+
200+
curl -fsSL \
201+
"https://raw.githubusercontent.com/flare-foundation/fsp-rewards/refs/heads/main/songbird/${EPOCH_ID}/reward-distribution-data-tuples.json" \
202+
| jq --arg b "${BENEFICIARY}" --argjson ct "${CLAIM_TYPE}" -r '
203+
.rewardClaims[]
204+
| select(.[1][1] | ascii_downcase == ($b | ascii_downcase))
205+
| select(.[1][3] == $ct)
206+
'
207+
```
208+
209+
</TabItem>
210+
<TabItem value="coston" label="Songbird Testnet Coston">
211+
Fetch `reward-distribution-data-tuples.json` from [timivesel/ftsov2-testnet-rewards](https://gitlab.com/timivesel/ftsov2-testnet-rewards) on GitLab.
212+
213+
```bash
214+
EPOCH_ID=123
215+
BENEFICIARY="0xYourBeneficiaryAddressHere"
216+
CLAIM_TYPE=0
217+
218+
curl -fsSL \
219+
"https://gitlab.com/timivesel/ftsov2-testnet-rewards/-/raw/main/rewards-data/coston/${EPOCH_ID}/reward-distribution-data-tuples.json" \
220+
| jq --arg b "${BENEFICIARY}" --argjson ct "${CLAIM_TYPE}" -r '
221+
.rewardClaims[]
222+
| select(.[1][1] | ascii_downcase == ($b | ascii_downcase))
223+
| select(.[1][3] == $ct)
224+
'
225+
```
226+
227+
</TabItem>
228+
</Tabs>
229+
230+
### 5. Extract your Merkle proof + claim tuple
231+
232+
In the fetched JSON, locate an entry in `rewardClaims` where:
233+
234+
- `address` matches your `beneficiary` (case-insensitive match offchain, exact value used onchain)
235+
- `claimType` equals your target `claimType` (`0` for DIRECT, `1` for FEE)
236+
237+
Each matching entry provides:
238+
239+
- `merkleProof`: array of hex strings
240+
- tuple `[id, address, sum, claimType]`
241+
242+
:::info
243+
244+
You do **not** submit the JSON onchain.
245+
You only extract your `merkleProof` and tuple from `rewardClaims` to build the `claims[]` argument passed to `RewardManager.claim(...)`.
246+
247+
:::
248+
249+
Build a [`RewardClaimWithProof`](/network/fsp/solidity-reference/IRewardManager#rewardclaimwithproof) struct from the JSON:
250+
251+
```ts
252+
{
253+
merkleProof: string[],
254+
body: {
255+
rewardEpochId: bigint, // BigInt(id)
256+
beneficiary: string, // address
257+
amount: bigint, // BigInt(sum)
258+
claimType: bigint // BigInt(claimType)
259+
}
260+
}
261+
```
262+
263+
**Important:** `rewardEpochId` is taken from the tuple's `id`.
264+
265+
Example:
266+
267+
```ts
268+
type ClaimType = 0 | 1; // DIRECT=0, FEE=1
269+
type RewardClaimTuple = [number, string, string, number]; // [id, address, sum, claimType]
270+
type RewardClaimEntry = [string[], RewardClaimTuple]; // [merkleProof[], tuple]
271+
272+
function extractClaim(
273+
rewardClaims: RewardClaimEntry[],
274+
beneficiary: string,
275+
claimType: ClaimType,
276+
) {
277+
const entry = rewardClaims.find(([, tuple]) => {
278+
const [, address, , ct] = tuple;
279+
return (
280+
address.toLowerCase() === beneficiary.toLowerCase() && ct === claimType
281+
);
282+
});
283+
284+
if (!entry) return null;
285+
286+
const [merkleProof, [id, address, sum, ct]] = entry;
287+
return {
288+
merkleProof,
289+
body: {
290+
rewardEpochId: BigInt(id),
291+
beneficiary: address,
292+
amount: BigInt(sum),
293+
claimType: BigInt(ct),
294+
},
295+
};
296+
}
297+
```
298+
299+
### 6. Submit the claim transaction
300+
301+
Call:
302+
303+
`RewardManager.claim(beneficiary, recipient, lastEpochIdToClaim, wrapRewards, claims)`
304+
305+
Where:
306+
307+
- `beneficiary`: signing policy address (DIRECT) **or** identity address (FEE)
308+
- `recipient`: the address that should receive rewards
309+
- `claims`: array of the structs from Step 5
310+
- `lastEpochIdToClaim`: set to the **maximum** `body.rewardEpochId` included in `claims`
311+
- `wrapRewards`: boolean (`true` unless explicitly set to `false`)
312+
313+
Example:
314+
315+
```ts
316+
if (claims.length === 0) throw new Error("No claims to submit.");
317+
318+
const lastEpochIdToClaim = claims
319+
.map((c) => c.body.rewardEpochId)
320+
.reduce((max, v) => (v > max ? v : max));
321+
322+
const tx = await rewardManager
323+
.connect(signer)
324+
.claim(beneficiary, recipient, lastEpochIdToClaim, wrapRewards, claims);
325+
326+
console.log("Submitted:", tx.hash);
327+
await tx.wait();
328+
console.log("Confirmed:", tx.hash);
329+
```
330+
331+
## Troubleshooting
332+
333+
<details>
334+
<summary>T0. Authorization failure: executor not allowed or recipient not allowlisted</summary>
335+
336+
- If the signer sending `RewardManager.claim(...)` is not the beneficiary, ensure it is configured as a **claim executor** via `setClaimExecutors(...)`.
337+
- If the transaction reverts when using a `recipient` address, ensure the recipient is allowlisted via `setAllowedClaimRecipients(...)`.
338+
- Ensure you set these using the correct controlling account:
339+
- **DIRECT**: signing policy account
340+
- **FEE**: identity account
341+
342+
</details>
343+
344+
<details>
345+
<summary>T1. No matching tuple found for my address</summary>
346+
347+
- Double-check:
348+
- Beneficiary address selection (DIRECT uses signing policy address; FEE uses identity address).
349+
- `claimType` value (DIRECT=0, FEE=1).
350+
351+
</details>
352+
353+
<details>
354+
<summary>T2. Claim transaction reverts</summary>
355+
356+
- Ensure the Merkle proof and tuple fields (`rewardEpochId`, `beneficiary`, `amount`, `claimType`) are copied exactly from the JSON.
357+
- Ensure `lastEpochIdToClaim` is `>=` the maximum `rewardEpochId` included in your `claims` array.
358+
- Ensure the epoch is signed (non-zero `rewardsHash`) and within the claimable range you read onchain.
359+
360+
If a revert reason indicates additional authorization rules (e.g., the caller must be a specific executor), those rules are enforced by the contract and must be satisfied as written in the revert reason; they are not inferable from the claim flow alone.
361+
362+
</details>

0 commit comments

Comments
 (0)