xMaquina needs a capital distribution system that rewards veToken stakers based on:
- Their voting power (stake amount)
- Their stake duration (time locked)
- Continuous participation over a 30-day distribution period
The system should distribute rewards proportionally, with longer-term stakers receiving higher rewards through the veToken multiplier system.
The simplest approach - take a single snapshot at a specific timestamp. Calculate each user's reward share by calling
votingPowerAt(tokenId, timestamp) for their token and dividing by totalVotingPowerAt(timestamp). This gives their
proportional share of the reward pool.
- Pros: Simple, gas-efficient (single votingPowerAt call per user)
- Cons: Not gameable per se, but has a fairness issue - since veToken doesn't track historical ownership, users must own their tokens at snapshot time. If someone unstakes before the snapshot, they lose all accumulated rewards despite having staked for the period.
- Implementation: One timestamp, one calculation
While technically feasible, implementing an additional time-weighting function on top of veToken's existing mechanics would be unnecessarily complex. The veToken system already incorporates time-based rewards through its voting power multiplier - users who lock for longer periods receive up to 12x their base amount after 1 year.
Adding another layer of time-based calculations would:
- Require fetching individual token creation timestamps
- Add computational complexity for marginal benefit
- Create confusion since time-based incentives are already built into the voting power calculation
- Make the reward distribution harder to verify and reason about
The existing veToken multiplier elegantly solves the "reward long-term stakers" requirement without additional complexity.
Recommendation: Single snapshot approach - simple, gas-efficient, and aligns with veToken's ownership model.
Aragon's Capital Distributor includes a complete Merkle tree distribution implementation that allows complex reward calculations to be performed off-chain while maintaining verifiable on-chain claims.
How it works:
- Off-chain calculation: Calculate each user's veToken voting power share and rewards
- Tree generation: Create merkle tree with leaves as
keccak256(abi.encodePacked(address, amount)) - Campaign creation: Deploy campaign with merkle root as initialization data
- User claims: Users provide merkle proof to claim their allocation
Implementation Steps:
-
Generate Merkle Tree (using provided script):
# Create recipients.json with veToken calculations forge script GenerateMerkleTree --sig "generateTreeFromFile(string)" recipients.json
-
Create Campaign with merkle root:
bytes memory strategyAuxData = abi.encode(merkleRoot); ICapitalDistributorPlugin.StrategyConfig({ strategyId: "merkle-distributor-strategy", strategyParams: "", initData: strategyAuxData })
-
Users Claim with proof:
bytes memory claimData = abi.encode(merkleProof, claimAmount); plugin.claimCampaignPayout(campaignId, recipient, claimData, "");
Advantages for xMaquina:
- Supports complex veToken calculations off-chain (multiple snapshots, custom weighting)
- Gas-efficient for large user bases (O(log n) verification)
- Can integrate with existing xMaquina infrastructure
- Flexible updates via campaign pausing and root updates
Trade-offs:
- Requires off-chain computation infrastructure
- Users need merkle proofs (typically provided by frontend)
- Trust in calculation process (mitigated by open-source scripts)
-
Custom Allocation Strategy
contract VeTokenAllocationStrategy implements IAllocationStrategy { function getTotalClaimableAmount( uint256 _campaignId, address _recipient, bytes memory _strategyAuxData ) external view returns (uint256) }
-
Key Functions:
getTotalClaimableAmount: Calculate user's reward based on VP snapshotsdecodeAuxData: Extract snapshot timestamps and campaign parameterscalculateAverageShare: Sum VP across snapshots and divide by total
-
Multi-Token Support:
- To reward users with > 1 token (i.e. USDC,DEUS) just setup multiple campaigns
- Each reward token = separate campaign
- Reuse same strategy across campaigns
- Different budgets per token type
-
Optional Action Encoder:
- By default, rewards are transferred to the user's wallet. Optionally, you can define advanced behaviours.
- Enables alternative reward mechanisms, e.g., automatically stake DEUS rewards as xDEUS instead of direct transfer
- Not required in the base case, for simple ERC20 transfers
// not production code, just an idea
contract VeTokenAllocationStrategy is IAllocationStrategy {
IVotingEscrow public veToken;
struct SnapshotData {
uint256 timestamp;
uint256 totalBudget;
uint256[] tokenIds; // User's veToken IDs
}
function getTotalClaimableAmount(
uint256 _campaignId,
address _recipient,
bytes memory _strategyAuxData
) external view returns (uint256) {
// Decode the auxiliary data to get snapshot timestamp and total budget
SnapshotData memory data = abi.decode(_strategyAuxData, (SnapshotData));
uint256 userVP = 0;
// Sum voting power across all user's tokens at the snapshot
for (uint i = 0; i < data.tokenIds.length; i++) {
// Verify user still owns this token
if (veToken.ownerOf(data.tokenIds[i]) == _recipient) {
userVP += veToken.votingPowerAt(data.tokenIds[i], data.timestamp);
}
}
uint256 totalVP = veToken.totalVotingPowerAt(data.timestamp);
if (totalVP == 0) return 0;
// Calculate user's share of the total voting power
uint256 userShare = (userVP * 1e18) / totalVP;
// Apply share to total budget to get user's reward
return (data.totalBudget * userShare) / 1e18;
}
}