Merkl Protocol 是一个 DeFi 奖励分发协议,通过链上合约 + 链下计算引擎 + Merkle 树验证,实现项目方自助创建奖励活动、用户一键领取多协议奖励。
Bonus 类似于 Web2 的大促打折,通过额外奖励吸引用户参与 DeFi 协议。对于小众协议尤其重要——用户不了解也没有意愿参与,Bonus 是引流的有效手段。
| 维度 | Bonus 2.0 | Bonus 3.0(Merkl) |
|---|---|---|
| 技术架构 | 链上金库 + 链下计算 | 链上金库 + 链下计算 |
| 领取方式 | 用户主动 claim | 用户主动 claim |
| 活动创建 | 手动申请 → 部署合约 → 配置活动 | 项目方一键创建 |
| 领取验证 | 私钥验签 | Merkle 树验证 |
| 领取范围 | 一次只能领单一协议 | 一次领取一条链上所有奖励 |
Merkl Platform 面向两类用户:
graph TB
subgraph 项目方
C1["Create 页面"] --> C2["选择活动类型<br/>设置奖励参数<br/>存入奖励资金"]
end
subgraph 普通用户
U1["Opportunities 页面<br/>发现感兴趣的活动"]
U2["Dashboard 页面<br/>查看奖励 & 一键领取"]
end
C2 -->|"活动上线"| U1
U1 -->|"参与活动"| U2
只向项目方收费,用户不收费(领取时需自付 Gas):
| 链 | 排名 | 累计发放金额 |
|---|---|---|
| Arbitrum | 1 | $15.32M |
| Ethereum | 2 | $14.47M |
| Polygon | 3 | $5.098M |
| Optimism | 4 | $2.776M |
| Immutable zkEVM | 5 | $1.395M |
| Base | 6 | $1.314M |
| Blast | 7 | $713.7K |
| Manta Pacific | 8 | $504.5K |
| Mode | 9 | $334.8K |
| Polygon zkEVM | 10 | $246.1K |
Dune 数据看板:https://dune.com/clarkcui/merkl-angle
graph TB
subgraph 链上合约
CREATOR["Creator Contract<br/>创建活动 & 存储活动信息"]
DIST["Distributor Contract<br/>存储奖励资金 & Merkle Root<br/>用户 claim 交互"]
ACM["AccessControlManager<br/>多签管理合约"]
end
subgraph 链下服务
ENGINE["Merkl Engine<br/>(非公开)<br/>计算奖励 & 生成 Merkle Root"]
API["Merkl Public API<br/>(公开)<br/>供前端查询奖励和 Proof"]
BOT["Dispute Bot<br/>验证 Merkle Root 正确性"]
BUCKET["Merkl Rewards Bucket<br/>存储历史 Merkle Root"]
end
CREATOR -->|"活动信息"| ENGINE
ENGINE -->|"查询链上数据"| RPC["RPC / Subgraph"]
ENGINE -->|"提交 Merkle Root"| DIST
ENGINE -->|"存储历史"| BUCKET
API -->|"读取"| ENGINE
BOT -->|"验证"| BUCKET
BOT -->|"发现错误则回滚"| DIST
sequenceDiagram
participant Project as 项目方
participant Creator as Creator 合约
participant Engine as Merkl Engine
participant Chain as 链上数据
participant Dist as Distributor 合约
participant Bot as Dispute Bot
participant User as 用户
Project->>Creator: 创建活动 & 存入奖励
Note over Engine: 每 3-12 小时执行一次
Engine->>Creator: 获取活动信息
Engine->>Chain: 查询 LP 持仓等数据
Engine->>Engine: 计算每个地址的奖励
Engine->>Engine: 聚合生成 Merkle Root
Engine->>Dist: 提交新的 Merkle Root
Note over Bot: 争议期 1-2 小时
Bot->>Bot: 验证 Merkle Root 正确性
alt 发现错误
Bot->>Dist: 回滚到上一个 Merkle Root
end
Note over User: 争议期结束后
User->>Dist: 调用 claim 方法
Dist->>Dist: Merkle 证明验证
Dist->>User: 发送奖励代币 💰
Merkl Engine 统计和计算收益的时间周期,一般 3-12 小时(不同链可能不同)。这决定了用户每天最多可以领取几次新奖励。例如 8 小时周期意味着每天最多 3 次。
新 Merkle Root 生成后的 1-2 小时窗口期。在此期间:
任何人都可以运行争议机器人:https://github.com/AngleProtocol/merkl-dispute
活动数据结构:
struct CampaignParameters {
bytes32 campaignId;
address creator;
address rewardToken;
uint256 amount;
uint32 campaignType;
uint32 startTimestamp;
uint32 duration;
bytes campaignData;
}创建活动时自动扣除手续费,将奖励资金转入 Distributor 合约,并将活动信息存入 campaignList。
1. Merkle Root 存储与更新
MerkleTree public tree; // 当前 Merkle Root
MerkleTree public lastTree; // 上一个 Merkle Root
function updateTree(MerkleTree calldata _tree) external {
// 检查:无争议 & 有更新权限 & 争议期已过
MerkleTree memory _lastTree = tree;
tree = _tree;
lastTree = _lastTree;
}合约只存储 Merkle Root(而非整棵树),因为验证只需要 Root + Proof 路径即可,完整的树数据存储在链下。
2. Merkle 证明验证
function getMerkleRoot() public view returns (bytes32) {
// 争议期内返回上一个有效 Root,争议期后返回最新 Root
if (block.timestamp >= endOfDisputePeriod && disputer == address(0))
return tree.merkleRoot;
else
return lastTree.merkleRoot;
}
function _verifyProof(bytes32 leaf, bytes32[] memory proof) internal view returns (bool) {
bytes32 currentHash = leaf;
for (uint256 i; i < proof.length; ) {
// 从叶子节点逐层向上计算 Root
if (currentHash < proof[i]) {
currentHash = keccak256(abi.encode(currentHash, proof[i]));
} else {
currentHash = keccak256(abi.encode(proof[i], currentHash));
}
unchecked { ++i; }
}
return currentHash == getMerkleRoot(); // 与链上存储的 Root 比对
}3. 用户 Claim
function claim(
address[] calldata users,
address[] calldata tokens,
uint256[] calldata amounts,
bytes32[][] calldata proofs
) external {
for (uint256 i; i < users.length; ) {
// 权限检查:只有用户自己或被授权者可以 claim
// 构造叶子节点:leaf = keccak256(user, token, amount)
// 验证 Merkle 证明
// 计算应发金额 = 累计金额 - 已领金额
// 转账代币给用户
unchecked { ++i; }
}
}Claim 的最小维度是币种(token)级别——用户可以一次性领取同一条链上所有协议的所有币种奖励。
getMerkleRoot() 在争议期内返回的是 lastTree.merkleRoot(上一个已验证的 Root)。新周期的奖励数据只存在于新 Root 的 Merkle 树中,用旧 Root 验证新奖励的 Proof 会失败,所以争议期内新奖励无法被领取。
graph TB
LEAF["叶子节点<br/>keccak256(user, token, amount)"]
LEAF -->|"+ proof[0]"| N1["中间节点 1"]
N1 -->|"+ proof[1]"| N2["中间节点 2"]
N2 -->|"+ proof[n]"| ROOT["计算得到的 Root"]
STORED["链上存储的<br/>Merkle Root"]
ROOT -->|"比对"| CHECK{"相等?"}
STORED --> CHECK
CHECK -->|"Yes"| OK["✅ 验证通过<br/>发放奖励"]
CHECK -->|"No"| FAIL["❌ 验证失败<br/>拒绝领取"]
通过 Merkl Public API 集成,前端可以独立构建 claim 页面:
GET https://api.merkl.xyz/v3/userRewards?user={address}&chainId={chainId}&proof=true返回数据包含每个代币的累计奖励、未领取金额和 Merkle Proof:
{
"0x代币地址": {
"accumulated": "1474003470054576877800",
"symbol": "ANGLE",
"unclaimed": "1474003470054576877800",
"reasons": {
"Arrakis": { "accumulated": "996...", "unclaimed": "996..." },
"UniswapV3": { "accumulated": "349...", "unclaimed": "349..." }
},
"proof": ["0xf1c7...", "0x7192...", "..."]
}
}import { Distributor__factory } from "@angleprotocol/sdk";
import axios from "axios";
export const claim = async (chainId, signer) => {
// 1. 从 API 获取用户奖励数据(含 proof)
const { data } = await axios.get(
`https://api.merkl.xyz/v3/userRewards?chainId=${chainId}&user=${signer._address}&proof=true`
);
// 2. 筛选有 proof 的代币
const tokens = Object.keys(data).filter(k => data[k].proof?.length > 0);
const claims = tokens.map(t => data[t].accumulated);
const proofs = tokens.map(t => data[t].proof);
// 3. 调用 Distributor 合约的 claim 方法
const contract = Distributor__factory.connect(distributorAddress, signer);
await (await contract.claim(
tokens.map(() => signer._address), // users
tokens, // tokens
claims, // amounts
proofs // proofs
)).wait();
};