Skip to content

Commit b0d8b8f

Browse files
committed
docs(x2earn-rewards-pool): V9 - separate sustainable from bonus rewards
vechain/vebetterdao#3398 - Document distributeNonProofReward (V9) and the NonProofRewardCategory enum (Endorser / Leaderboard / Streak / Cashback / Referral / Other). This entrypoint does NOT register a passport action and emits the dedicated NonProofRewardDistributed event so indexers can exclude these payouts from personhood signals. - Mark the proof arrays as mandatory on every distributeRewardWithProof* entrypoint (V9): empty arrays revert with 'X2EarnRewardsPool: proof is mandatory'. The pre-V9 'at least proof OR impact' relaxation no longer applies. - Mark distributeReward as DEPRECATED in V9 (kept for backward compatibility) and add distributeRewardDeprecatedForRound to the distribution-function tables. - Refresh the x-2-earn-apps Solidity example to call distributeRewardWithProof with typed arrays and add a distributeNonProofReward leaderboard-prize example. - Refresh the vebetterdao backend / multi-clause examples to use the V9-compliant entrypoints.
1 parent a9fc7ef commit b0d8b8f

4 files changed

Lines changed: 195 additions & 50 deletions

File tree

skills/vebetterdao/references/contracts-tokens-apps.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -127,21 +127,28 @@ Unallocated amounts (apps that didn't claim, rounding remainders) are sent to Tr
127127

128128
## X2EarnRewardsPool
129129

130-
Reward pool for X2Earn apps to distribute B3TR to users for sustainable actions.
131-
132-
| Function | Description |
133-
|----------|-------------|
134-
| `deposit(amount, appId)` | Fund rewards pool for an app |
135-
| `withdraw(amount, appId, reason)` | Admin withdrawal to team wallet |
136-
| `distributeReward(appId, amount, receiver)` | Basic reward distribution |
137-
| `distributeRewardWithProof(appId, amount, receiver, proofTypes, proofValues, impactCodes, impactValues, description)` | Distribution with impact tracking |
138-
| `distributeRewardWithProofAndMetadata(...)` | Distribution with additional JSON metadata |
139-
| `distributeRewardForRound(appId, amount, receiver, proof, actionRound)` | Basic distribution attributed to a specific round |
140-
| `distributeRewardWithProofForRound(..., actionRound)` | Distribution with proof attributed to a specific round |
141-
| `distributeRewardWithProofAndMetadataForRound(..., actionRound)` | Distribution with proof + metadata attributed to a specific round |
142-
| `toggleRewardsPoolBalance(appId, enable)` | Enable/disable rewards pool for app |
143-
| `pauseDistribution()` / `unpauseDistribution()` | Admin pause |
144-
| `addImpactKey()` / `removeImpactKey()` | Manage allowed impact categories |
130+
Reward pool for X2Earn apps. **V9** separates the two reward flows so passport personhood reflects sustainable activity only:
131+
132+
- **Sustainable rewards**`distributeRewardWithProof*` family. Proof arrays (`proofTypes` / `proofValues`) are now **mandatory** (empty arrays revert with `"X2EarnRewardsPool: proof is mandatory"`). Registers a passport action via `veBetterPassport.registerAction[ForRound]`.
133+
- **Bonus / secondary rewards**`distributeNonProofReward(appId, amount, receiver, NonProofRewardCategory category, description)`. Does **NOT** register a passport action. Emits the dedicated `NonProofRewardDistributed` event with a typed category (`Endorser` / `Leaderboard` / `Streak` / `Cashback` / `Referral` / `Other`).
134+
- **Legacy**`distributeReward` is marked deprecated in V9 but kept for backward compatibility. `distributeRewardDeprecated*` remain available for the legacy JSON-string proof flow.
135+
136+
| Function | Description | V9 status |
137+
|----------|-------------|-----------|
138+
| `deposit(amount, appId)` | Fund rewards pool for an app ||
139+
| `withdraw(amount, appId, reason)` | Admin withdrawal to team wallet ||
140+
| `distributeRewardWithProof(appId, amount, receiver, proofTypes, proofValues, impactCodes, impactValues, description)` | Sustainable distribution with impact tracking | **Proof mandatory** — registers passport action |
141+
| `distributeRewardWithProofAndMetadata(...)` | Sustainable distribution with additional JSON metadata | **Proof mandatory** — registers passport action |
142+
| `distributeRewardWithProofForRound(..., actionRound)` | Sustainable distribution attributed to a specific round | **Proof mandatory** — registers passport action |
143+
| `distributeRewardWithProofAndMetadataForRound(..., actionRound)` | Sustainable distribution with proof + metadata attributed to a round | **Proof mandatory** — registers passport action |
144+
| `distributeNonProofReward(appId, amount, receiver, NonProofRewardCategory category, description)` | **V9** — bonus / non-sustainable payout | Does **NOT** register a passport action. Emits `NonProofRewardDistributed`. No `ForRound` variant. |
145+
| `distributeReward(appId, amount, receiver, proof)` | Legacy no-proof distribution | **DEPRECATED V9** (kept for backward compatibility) |
146+
| `distributeRewardForRound(appId, amount, receiver, proof, actionRound)` | Legacy no-proof distribution attributed to a round | Same migration path as `distributeReward` |
147+
| `distributeRewardDeprecated(appId, amount, receiver, proof)` | Legacy JSON-string proof | Pre-existing deprecation |
148+
| `distributeRewardDeprecatedForRound(appId, amount, receiver, proof, actionRound)` | **V9** — round-attribution counterpart of `distributeRewardDeprecated` | Pre-existing deprecation scheme |
149+
| `toggleRewardsPoolBalance(appId, enable)` | Enable/disable rewards pool for app ||
150+
| `pauseDistribution()` / `unpauseDistribution()` | Admin pause ||
151+
| `addImpactKey()` / `removeImpactKey()` | Manage allowed impact categories ||
145152

146153
| Role | Can |
147154
|------|-----|

skills/vebetterdao/references/vebetterdao.md

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,26 @@ All logic in a traditional backend using `@vechain/sdk-network` to interact with
6969
pragma solidity ^0.8.20;
7070
7171
interface IX2EarnRewardsPool {
72+
enum NonProofRewardCategory {
73+
Endorser,
74+
Leaderboard,
75+
Streak,
76+
Cashback,
77+
Referral,
78+
Other
79+
}
80+
81+
// DEPRECATED in V9 — kept for backward compatibility.
82+
// Registers a passport action without a typed proof. New integrations MUST use
83+
// distributeRewardWithProof (sustainable) or distributeNonProofReward (bonus).
7284
function distributeReward(
7385
bytes32 appId,
7486
uint256 amount,
7587
address receiver,
7688
string memory proof
7789
) external;
7890
91+
// V9: proofTypes / proofValues are MANDATORY (non-empty) — empty arrays revert.
7992
function distributeRewardWithProof(
8093
bytes32 appId,
8194
uint256 amount,
@@ -86,6 +99,16 @@ interface IX2EarnRewardsPool {
8699
uint256[] memory impactValues,
87100
string memory description
88101
) external;
102+
103+
// NEW in V9 — bonus / non-sustainable payouts.
104+
// Does NOT register a passport action. Emits NonProofRewardDistributed.
105+
function distributeNonProofReward(
106+
bytes32 appId,
107+
uint256 amount,
108+
address receiver,
109+
NonProofRewardCategory category,
110+
string memory description
111+
) external;
89112
}
90113
91114
contract MyRewardDistributor {
@@ -97,10 +120,7 @@ contract MyRewardDistributor {
97120
APP_ID = _appId;
98121
}
99122
100-
function rewardUser(address receiver, uint256 rewardAmount) external {
101-
x2EarnRewardsPool.distributeReward(APP_ID, rewardAmount, receiver, "");
102-
}
103-
123+
// V9: proof arrays are mandatory.
104124
function rewardUserWithProof(
105125
address receiver,
106126
uint256 rewardAmount,
@@ -115,6 +135,16 @@ contract MyRewardDistributor {
115135
proofTypes, proofValues, impactCodes, impactValues, description
116136
);
117137
}
138+
139+
// V9 bonus reward — does NOT register a passport action.
140+
function payBonus(
141+
address receiver,
142+
uint256 amount,
143+
IX2EarnRewardsPool.NonProofRewardCategory category,
144+
string memory description
145+
) external {
146+
x2EarnRewardsPool.distributeNonProofReward(APP_ID, amount, receiver, category, description);
147+
}
118148
}
119149
```
120150

@@ -146,15 +176,7 @@ const rewardsPool = thorClient.contracts.load(
146176
signer
147177
);
148178

149-
// Distribute reward without proofs
150-
await rewardsPool.transact.distributeReward(
151-
APP_ID,
152-
ethers.parseEther("10"), // 10 B3TR
153-
receiverAddress,
154-
""
155-
);
156-
157-
// Distribute reward with sustainability proofs
179+
// Sustainable reward — V9 requires non-empty proofTypes / proofValues
158180
await rewardsPool.transact.distributeRewardWithProof(
159181
APP_ID,
160182
ethers.parseEther("10"),
@@ -165,6 +187,16 @@ await rewardsPool.transact.distributeRewardWithProof(
165187
[100, 50], // impact values
166188
"Recycled 100g of plastic waste"
167189
);
190+
191+
// Bonus reward (V9) — does NOT register a passport action
192+
// NonProofRewardCategory: 0=Endorser, 1=Leaderboard, 2=Streak, 3=Cashback, 4=Referral, 5=Other
193+
await rewardsPool.transact.distributeNonProofReward(
194+
APP_ID,
195+
ethers.parseEther("5"),
196+
receiverAddress,
197+
1, // Leaderboard
198+
"Week 12 leaderboard - 3rd place",
199+
);
168200
```
169201

170202
### Batch Distribution with Multi-Clause
@@ -177,8 +209,17 @@ const clauses = users.map(user => ({
177209
value: '0x0',
178210
data: ABIContract.encodeFunctionInput(
179211
x2EarnRewardsPoolABI,
180-
'distributeReward',
181-
[APP_ID, ethers.parseEther(user.amount), user.address, ""]
212+
'distributeRewardWithProof',
213+
[
214+
APP_ID,
215+
ethers.parseEther(user.amount),
216+
user.address,
217+
user.proofTypes,
218+
user.proofValues,
219+
user.impactCodes,
220+
user.impactValues,
221+
user.description,
222+
]
182223
),
183224
}));
184225

@@ -195,7 +236,7 @@ const clauses = users.map(user => ({
195236

196237
`carbon`, `water`, `energy`, `waste_mass`, `education_time`, `timber`, `plastic`, `trees_planted`, `calories_burned`, `sleep_quality_percentage`, `clean_energy_production_wh`
197238

198-
**Mandatory rule**: At least proof OR impact must be provided when using `distributeRewardWithProof`; if neither is provided, the transaction reverts.
239+
**Mandatory rule (V9)**: `proofTypes` AND `proofValues` MUST be non-empty on every `distributeRewardWithProof*` entrypoint — empty arrays revert with `"X2EarnRewardsPool: proof is mandatory"`. For non-sustainable / bonus payouts (endorser, leaderboard, streak, cashback, referral, other) use `distributeNonProofReward(appId, amount, receiver, category, description)` which does NOT register a passport action and emits `NonProofRewardDistributed`.
199240

200241
### Distribution with Metadata
201242

skills/x-2-earn-apps/references/getting-started.md

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,12 @@ async function rewardUser(address: string) {
158158
}
159159
```
160160

161-
**Note**: the JS examples above use `distributeRewardDeprecated` (legacy JSON string format). For new apps, prefer `distributeRewardWithProof` with typed arrays — see the **vebetterdao** skill for the typed API.
161+
**Note**: the JS example above uses `distributeRewardDeprecated` (legacy JSON-string format) and is kept for reference only. For new apps, prefer:
162162

163-
**Critical**: the PUBLIC ADDRESS of the wallet calling `distributeReward` must be registered as a Reward Distributor for your app via the governance dApp settings.
163+
- `distributeRewardWithProof` with typed arrays for **sustainable** rewards (registers a passport action; **proof arrays are mandatory in V9**). See [sustainability-proofs.md](sustainability-proofs.md) for the typed API.
164+
- `distributeNonProofReward(appId, amount, receiver, category, description)` for **bonus / non-sustainable** rewards (endorser, leaderboard, streak, cashback, referral, other). Added in V9 — does **not** register a passport action and emits `NonProofRewardDistributed`.
165+
166+
**Critical**: the PUBLIC ADDRESS of the wallet calling `distributeReward*` / `distributeNonProofReward` must be registered as a Reward Distributor for your app via the governance dApp settings.
164167

165168
### Solidity (On-chain)
166169

@@ -181,26 +184,54 @@ contract MySustainableAppContract {
181184
VBD_APP_ID = _appId;
182185
}
183186
184-
/// @notice Claim reward for a validated sustainable action
185-
/// @dev Contract address must be registered as Reward Distributor
187+
/// @notice Claim reward for a validated sustainable action.
188+
/// @dev Contract address must be registered as Reward Distributor.
189+
/// V9: proofTypes / proofValues are mandatory — empty arrays revert.
186190
function claimReward(uint256 _actionId) external {
187191
// ... validate action is approved and unclaimed
188192
189-
x2EarnRewardsPool.distributeReward(
193+
string[] memory proofTypes = new string[](1);
194+
proofTypes[0] = "link";
195+
196+
string[] memory proofValues = new string[](1);
197+
proofValues[0] = actions[_actionId].proofUrl;
198+
199+
string[] memory impactCodes = new string[](1);
200+
impactCodes[0] = "waste_mass";
201+
202+
uint256[] memory impactValues = new uint256[](1);
203+
impactValues[0] = actions[_actionId].impact;
204+
205+
x2EarnRewardsPool.distributeRewardWithProof(
190206
VBD_APP_ID,
191207
actions[_actionId].rewardAmount,
192208
msg.sender,
193-
"" // proof can be empty or JSON string
209+
proofTypes,
210+
proofValues,
211+
impactCodes,
212+
impactValues,
213+
"User performed a sustainable action on my app"
194214
);
195215
196216
rewardClaimed[_actionId] = true;
197217
}
218+
219+
/// @notice Pay a leaderboard prize — V9 bonus reward, no passport action.
220+
function payLeaderboardPrize(address winner, uint256 amount, uint256 week) external onlyAdmin {
221+
x2EarnRewardsPool.distributeNonProofReward(
222+
VBD_APP_ID,
223+
amount,
224+
winner,
225+
IX2EarnRewardsPool.NonProofRewardCategory.Leaderboard,
226+
string.concat("Week ", Strings.toString(week), " leaderboard")
227+
);
228+
}
198229
}
199230
```
200231

201-
**Critical**: the contract address must be set as a **Reward Distributor** on the governance dApp before it can call `distributeReward`.
232+
**Critical**: the contract address must be set as a **Reward Distributor** on the governance dApp before it can call any `distribute*` entrypoint on `X2EarnRewardsPool`.
202233

203-
**Round attribution**: if your app lets users accumulate actions and claim later, use the `ForRound` variants (`distributeRewardForRound`, `distributeRewardWithProofForRound`, `distributeRewardWithProofAndMetadataForRound`) to attribute actions to the round they were performed in. These accept an additional `actionRound` parameter (must be > 0) representing the round ID when the action happened. This prevents users from stacking actions across rounds.
234+
**Round attribution**: if your app lets users accumulate actions and claim later, use the `ForRound` variants (`distributeRewardWithProofForRound`, `distributeRewardWithProofAndMetadataForRound`) to attribute actions to the round they were performed in. These accept an additional `actionRound` parameter (must be > 0) representing the round ID when the action happened. This prevents users from stacking actions across rounds. `distributeNonProofReward` has **no** `ForRound` variant — bonus rewards do not register a passport action so round attribution is not applicable.
204235

205236
---
206237

0 commit comments

Comments
 (0)