Skip to content

Commit fb829af

Browse files
committed
feat(fsp): add claiming rewards guide
1 parent b50d5b8 commit fb829af

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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 GitHub or GitLab.
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+
5. **Recipient address:** The address that will receive the rewards (`recipient`).
37+
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.
38+
7. **Wrap option:** `wrapRewards` is `true` unless explicitly set to `false`.
39+
40+
## Step-by-step
41+
42+
### 1. Connect to the contracts
43+
44+
Instantiate contract clients (ABI + address) for:
45+
46+
- [`FlareSystemsManager`](/network/fsp/solidity-reference/IFlareSystemsManager)
47+
- [`RewardManager`](/network/fsp/solidity-reference/IRewardManager)
48+
49+
Example using [ethers v6](https://docs.ethers.org/v6/):
50+
51+
```ts
52+
import { JsonRpcProvider, Wallet, Contract } from "ethers";
53+
54+
// 1) Provider + signer
55+
const provider = new JsonRpcProvider(process.env.RPC_URL);
56+
const signer = new Wallet(process.env.CLAIM_EXECUTOR_PRIVATE_KEY!, provider);
57+
58+
// 2) Contract instances (ABI omitted here; use your generated ABI or interface)
59+
const flareSystemsManager = new Contract(
60+
process.env.FLARE_SYSTEMS_MANAGER_ADDRESS!,
61+
FLARE_SYSTEMS_MANAGER_ABI,
62+
provider,
63+
);
64+
65+
const rewardManager = new Contract(
66+
process.env.REWARD_MANAGER_ADDRESS!,
67+
REWARD_MANAGER_ABI,
68+
provider,
69+
);
70+
```
71+
72+
### 2. Read the claimable epoch range
73+
74+
Call:
75+
76+
1. `startRewardEpochId = RewardManager.getNextClaimableRewardEpochId(beneficiary)`
77+
2. `[_, endRewardEpochId] = RewardManager.getRewardEpochIdsWithClaimableRewards()`
78+
79+
If `endRewardEpochId < startRewardEpochId`, there is nothing to claim.
80+
81+
Example:
82+
83+
```ts
84+
const start = await rewardManager.getNextClaimableRewardEpochId(beneficiary);
85+
const [, end] = await rewardManager.getRewardEpochIdsWithClaimableRewards();
86+
87+
if (end < start) {
88+
console.log("Nothing claimable for this beneficiary.");
89+
}
90+
```
91+
92+
### 3. Keep only signed epochs
93+
94+
For each `epochId` in `[startRewardEpochId … endRewardEpochId]`, call:
95+
96+
- `rewardsHash = FlareSystemsManager.rewardsHash(epochId)`
97+
98+
Only proceed if `rewardsHash` is **not** `0x000...000` (`bytes32(0)`).
99+
100+
Example:
101+
102+
```ts
103+
const ZERO_BYTES32 =
104+
"0x0000000000000000000000000000000000000000000000000000000000000000";
105+
106+
const signedEpochs: bigint[] = [];
107+
for (let epochId = start; epochId <= end; epochId++) {
108+
const rewardsHash = await flareSystemsManager.rewardsHash(epochId);
109+
if (rewardsHash && rewardsHash !== ZERO_BYTES32) signedEpochs.push(epochId);
110+
}
111+
```
112+
113+
### 4. Fetch reward distribution tuples
114+
115+
For each signed epoch, fetch `reward-distribution-data-tuples.json`.
116+
You fetch **one JSON file per epoch**, using the epoch id in the path.
117+
If the JSON cannot be fetched or validated, you cannot build a valid Merkle proof for that epoch.
118+
119+
<Tabs block groupId="network">
120+
<TabItem value="flare" label="Flare Mainnet" default>
121+
Fetch `reward-distribution-data-tuples.json` from [flare-foundation/fsp-rewards](https://github.com/flare-foundation/fsp-rewards) on GitHub.
122+
123+
```bash
124+
EPOCH_ID=123
125+
BENEFICIARY="0xYourBeneficiaryAddressHere"
126+
CLAIM_TYPE=0
127+
128+
curl -fsSL \
129+
"https://raw.githubusercontent.com/flare-foundation/fsp-rewards/refs/heads/main/flare/${EPOCH_ID}/reward-distribution-data-tuples.json" \
130+
| jq --arg b "${BENEFICIARY}" --argjson ct "${CLAIM_TYPE}" -r '
131+
.rewardClaims[]
132+
| select(.[1][1] | ascii_downcase == ($b | ascii_downcase))
133+
| select(.[1][3] == $ct)
134+
'
135+
```
136+
137+
</TabItem>
138+
<TabItem value="coston2" label="Flare Testnet Coston2">
139+
Fetch `reward-distribution-data-tuples.json` from [timivesel/ftsov2-testnet-rewards](https://gitlab.com/timivesel/ftsov2-testnet-rewards) on GitLab.
140+
141+
```bash
142+
EPOCH_ID=123
143+
BENEFICIARY="0xYourBeneficiaryAddressHere"
144+
CLAIM_TYPE=0
145+
146+
curl -fsSL \
147+
"https://gitlab.com/timivesel/ftsov2-testnet-rewards/-/raw/main/rewards-data/coston2/${EPOCH_ID}/reward-distribution-data-tuples.json" \
148+
| jq --arg b "${BENEFICIARY}" --argjson ct "${CLAIM_TYPE}" -r '
149+
.rewardClaims[]
150+
| select(.[1][1] | ascii_downcase == ($b | ascii_downcase))
151+
| select(.[1][3] == $ct)
152+
'
153+
```
154+
155+
</TabItem>
156+
<TabItem value="songbird" label="Songbird Canary-Network">
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/songbird/${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="coston" label="Songbird Testnet Coston">
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/coston/${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+
</Tabs>
193+
194+
### 5. Extract your Merkle proof + claim tuple
195+
196+
In the fetched JSON, locate an entry in `rewardClaims` where:
197+
198+
- `address` matches your `beneficiary` (case-insensitive match offchain, exact value used onchain)
199+
- `claimType` equals your target `claimType` (`0` for DIRECT, `1` for FEE)
200+
201+
Each matching entry provides:
202+
203+
- `merkleProof`: array of hex strings
204+
- tuple `[id, address, sum, claimType]`
205+
206+
:::note
207+
208+
You do **not** submit the JSON onchain.
209+
You only extract your `merkleProof` and tuple from `rewardClaims` to build the `claims[]` argument passed to `RewardManager.claim(...)`.
210+
211+
:::
212+
213+
Build a [`RewardClaimWithProof`](/network/fsp/solidity-reference/IRewardManager#rewardclaimwithproof) struct from the JSON:
214+
215+
```ts
216+
{
217+
merkleProof: string[],
218+
body: {
219+
rewardEpochId: bigint, // BigInt(id)
220+
beneficiary: string, // address
221+
amount: bigint, // BigInt(sum)
222+
claimType: bigint // BigInt(claimType)
223+
}
224+
}
225+
```
226+
227+
**Important:** `rewardEpochId` is taken from the tuple's `id`.
228+
229+
Example:
230+
231+
```ts
232+
type ClaimType = 0 | 1; // DIRECT=0, FEE=1
233+
type RewardClaimTuple = [number, string, string, number]; // [id, address, sum, claimType]
234+
type RewardClaimEntry = [string[], RewardClaimTuple]; // [merkleProof[], tuple]
235+
236+
function extractClaim(
237+
rewardClaims: RewardClaimEntry[],
238+
beneficiary: string,
239+
claimType: ClaimType,
240+
) {
241+
const entry = rewardClaims.find(([, tuple]) => {
242+
const [, address, , ct] = tuple;
243+
return (
244+
address.toLowerCase() === beneficiary.toLowerCase() && ct === claimType
245+
);
246+
});
247+
248+
if (!entry) return null;
249+
250+
const [merkleProof, [id, address, sum, ct]] = entry;
251+
return {
252+
merkleProof,
253+
body: {
254+
rewardEpochId: BigInt(id),
255+
beneficiary: address,
256+
amount: BigInt(sum),
257+
claimType: BigInt(ct),
258+
},
259+
};
260+
}
261+
```
262+
263+
### 6. Submit the claim transaction
264+
265+
Call:
266+
267+
`RewardManager.claim(beneficiary, recipient, lastEpochIdToClaim, wrapRewards, claims)`
268+
269+
Where:
270+
271+
- `beneficiary`: signing policy address (DIRECT) **or** identity address (FEE)
272+
- `recipient`: the address that should receive rewards
273+
- `claims`: array of the structs from Step 5
274+
- `lastEpochIdToClaim`: set to the **maximum** `body.rewardEpochId` included in `claims`
275+
- `wrapRewards`: boolean (`true` unless explicitly set to `false`)
276+
277+
Example:
278+
279+
```ts
280+
if (claims.length === 0) throw new Error("No claims to submit.");
281+
282+
const lastEpochIdToClaim = claims
283+
.map((c) => c.body.rewardEpochId)
284+
.reduce((max, v) => (v > max ? v : max));
285+
286+
const tx = await rewardManager
287+
.connect(signer)
288+
.claim(beneficiary, recipient, lastEpochIdToClaim, wrapRewards, claims);
289+
290+
console.log("Submitted:", tx.hash);
291+
await tx.wait();
292+
console.log("Confirmed:", tx.hash);
293+
```
294+
295+
## Troubleshooting
296+
297+
<details>
298+
<summary>T1. No matching tuple found for my address</summary>
299+
300+
- Double-check:
301+
- Beneficiary address selection (DIRECT uses signing policy address; FEE uses identity address).
302+
- `claimType` value (DIRECT=0, FEE=1).
303+
304+
</details>
305+
306+
<details>
307+
<summary>T2. Claim transaction reverts</summary>
308+
309+
- Ensure the Merkle proof and tuple fields (`rewardEpochId`, `beneficiary`, `amount`, `claimType`) are copied exactly from the JSON.
310+
- Ensure `lastEpochIdToClaim` is `>=` the maximum `rewardEpochId` included in your `claims` array.
311+
- Ensure the epoch is signed (non-zero `rewardsHash`) and within the claimable range you read onchain.
312+
313+
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.
314+
315+
</details>

0 commit comments

Comments
 (0)