Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions distribute-with-merkl/before-you-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,177 @@ You can check whether your token is already whitelisted by setting it as the rew
Make sure you have all the tokens you want to distribute in your wallet when creating a campaign
{% endhint %}

## 💳 Token Predeposit System

Merkl offers a **predeposit system** that allows campaign creators to pre-fund their campaign tokens directly into the `DistributionCreator` contract. This system provides several advantages:

* **Gas optimization**: Pre-fund tokens once and use them for multiple campaigns without repeated approvals
* **Better token management**: Centralize your campaign tokens in one place
* **Operator delegation**: Enable operators to manage campaigns on your behalf using predeposited balances

### How it works

The predeposit system uses two key mechanisms:

1. **Creator Balance** (`creatorBalance`): Stores tokens predeposited by each campaign creator
2. **Creator Allowance** (`creatorAllowance`): Allows creators to grant spending permissions to operators

{% hint style="info" %}
**Finding the DistributionCreator contract address**: You can find the `DistributionCreator` contract address for your chain on the [Merkl Status page](https://app.merkl.xyz/status). Select your chain and click on **Creator** to view the contract address and access it directly in your chain's block explorer.
{% endhint %}

### How to deposit tokens

The most common workflow is for a Safe (multisig) or individual wallet to deposit tokens for themselves and then grant permissions to operators. Here's how it works:

#### Step 1: Approve the contract

First, approve the `DistributionCreator` contract to spend your tokens:

```solidity
IERC20(token).approve(distributionCreatorAddress, amount)
```

{% hint style="tip" %}
**Tip**: You can approve `type(uint256).max` for unlimited approval if you plan to deposit multiple times—this is more gas efficient.
{% endhint %}

#### Step 2: Deposit tokens into your creator balance

Call `increaseTokenBalance()` to transfer tokens from your wallet into your creator balance:

```solidity
increaseTokenBalance(yourAddress, rewardToken, amount)
```

**Parameters:**

* `yourAddress`: the Safe or wallet address
* `rewardToken`: The token address you want to deposit
* `amount`: The amount of tokens to deposit

**Example:**

```solidity
// Your Safe wants to deposit 10,000 aglaMerkl
IERC20(aglaMerkl).approve(distributionCreator, type(uint256).max);
distributionCreator.increaseTokenBalance(safeAddress, aglaMerkl, 10000e18);
// ✅ 10,000 aglaMerkl transferred from Safe wallet
// ✅ creatorBalance[safeAddress][aglaMerkl] = 10000e18
```

#### Withdrawing tokens

To withdraw tokens from your creator balance back to your wallet:

```solidity
decreaseTokenBalance(yourAddress, rewardToken, recipientAddress, amount)
```

**Example:**

```solidity
// Withdraw 5,000 aglaMerkl from your creator balance to your Safe
distributionCreator.decreaseTokenBalance(safeAddress, aglaMerkl, safeAddress, 5000e18);
// ✅ creatorBalance[safeAddress][aglaMerkl] decreased by 5000e18
// ✅ 5,000 aglaMerkl transferred back to Safe wallet
```

#### Depositing for others

Regular users can deposit tokens on behalf of other addresses, but the tokens will always come from the caller's wallet:

```solidity
// Alice deposits tokens but credits Bob's balance
distributionCreator.increaseTokenBalance(Bob, aglaMerkl, 1000e18);
// ⚠️ Tokens are taken from Alice's wallet, not Bob's
// ✅ creatorBalance[Bob][aglaMerkl] = 1000e18
```

**Note**: Once tokens are credited to Bob's balance, Bob becomes the owner and only Bob (or a governor) can withdraw them.

#### Step 3: Grant allowances to operators

Once you have tokens in your creator balance, you can grant spending permissions to operators:

```solidity
increaseTokenAllowance(yourAddress, operator, rewardToken, amount)
```

**Parameters:**

* `yourAddress`: Your address (the owner of the balance)
* `operator`: The operator address you want to authorize
* `rewardToken`: The token for which to grant allowance
* `amount`: Maximum amount the operator can spend from your balance

**Example:**

```solidity
// Grant operator permission to spend up to 5,000 aglaMerkl
distributionCreator.increaseTokenAllowance(safeAddress, operatorAddress, aglaMerkl, 5000e18);
// ✅ creatorAllowance[safeAddress][operatorAddress][aglaMerkl] = 5000e18
// ✅ Operator can now create campaigns using up to 5,000 aglaMerkl from your balance
```

### Setting up operators

To delegate campaign management to operators, you need to grant **two separate permissions**:

1. **Campaign management permission**: `toggleCampaignOperator(user, operator)` - Authorizes operators to create and manage campaigns (toggle on/off)
2. **Token spending allowance**: `increaseTokenAllowance(user, operator, rewardToken, amount)` - Grants operators permission to spend your predeposited tokens

Both permissions are required for an operator to fully manage campaigns using your predeposited balance. See the [Campaign Operators section](campaign-management.md#campaign-operators) for more details.

### Creating campaigns on behalf of a creator

When an operator creates a campaign on behalf of a creator, it's **critical** to set the correct `creator` parameter in the campaign parameters. The system will look for the predeposited balance and allowance based on this `creator` address.

**Operator creates campaign:**

```solidity
CampaignParameters memory campaign = CampaignParameters({
creator: creatorAddress, // ✅ MUST be the address that granted the allowance and has the predeposited balance!
// ... other parameters
});

distributionCreator.createCampaign(campaign);
```

**⚠️ Common mistake:**

If the operator sets `creator = address(0)` or `creator = operatorAddress`:

* ❌ System looks for `creatorBalance[operatorAddress][token]` = 0
* ❌ Fallback → takes tokens from the operator's wallet instead
* ⚠️ If the operator doesn't have sufficient tokens in their wallet, the transaction will revert

{% hint style="warning" %}
**Important**: Always set the `creator` parameter to the address that granted the allowance and has the predeposited balance. Otherwise, the system will fall back to the operator's wallet, and the transaction will revert if the operator has insufficient tokens.
{% endhint %}

**⚠️ Insufficient predeposited balance or allowance:**

If the operator correctly sets the `creator` parameter but encounters one of these issues:

* **Insufficient predeposited balance**: The creator's `creatorBalance[creatorAddress][token]` is insufficient or zero
* **Allowance exceeded**: The operator's allowance has been exceeded or was never granted

The system will attempt a **fallback**: it tries to take tokens from the operator's wallet instead.

* ✅ If the operator has sufficient tokens in their wallet → the transaction succeeds (using the operator's tokens), and **the operator becomes the creator** of the campaign
* ❌ If the operator's wallet balance is insufficient → the transaction will **revert**

⚠️ The creator should ensure:
* Sufficient tokens are predeposited before an operator attempts to create a campaign
* The operator has been granted sufficient allowance to spend the predeposited tokens

Otherwise, the campaign creation will fall back to using the operator's wallet balance, and **the operator will become the campaign creator** instead of the intended creator address, which may not be the intended behavior.

{% hint style="info" %}
Before creating a campaign, verify that the creator has predeposited enough tokens to cover the campaign amount plus fees, and that the operator has sufficient allowance. If multiple campaigns are created in sequence, track both the remaining predeposited balance and remaining allowance to avoid unintended fallback behavior or failed transactions.
{% endhint %}

## 🧪 Test campaigns

You may want to start testing the flow and integrating our data before your point program starts. Merkl is not deployed on testnets, but you can still run test campaigns using our test token: **aglaMerkl**.
Expand Down
196 changes: 192 additions & 4 deletions earn-with-merkl/earning-with-merkl.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,30 @@ You can claim all your rewards per chain at once to optimize gas costs!

Rewards on Merkl do not increase block by block, but can be claimed at a frequency which depends on the chain. You can check the claim frequency on the [Status page](https://app.merkl.xyz/status).

Note that, by default, rewards can only be claimed by the address that earned them. You can however approve an operator to claim on your behalf by calling the function `toggleOperator` on the [distributor smart contract](https://app.merkl.xyz/status). However, rewards will still be sent to the original address that earned them.
### Operator System: Delegating Claim Rights

So to sum up, assuming Alice earned the rewards:
By default, rewards can only be claimed by the address that earned them. However, Merkl provides a flexible operator system that allows you to delegate the right to claim rewards to another address, while still receiving the rewards yourself.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can specify that some addresses may have in some cases admin rights to claim for everyone -> on a case by case basis (cf indirect reference to main operator)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would say:

However, Merkl provides a flexible operator system that lets you delegate the right to execute the claim transaction on your behalf, while still receiving the rewards yourself.


* by default only Alice can claim and rewards are sent to Alice.
* by calling `toggleOperator`, Alice can allow Bob to claim on her behalf. Then, Bob can claim for Alice by sending Alice's proof to the contract, and rewards are then sent to Alice.
**How regular operators work:**

You can approve an operator to claim on your behalf by calling the function `toggleOperator` on the [distributor smart contract](https://app.merkl.xyz/status). When an operator claims on your behalf, the rewards are still sent to your original address—the operator only facilitates the transaction.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add in this section that if you toggleOperator address(0) then anyone can claim on your behalf

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be even more explicit in the last sentence (+ I would remove the em dash):

"the rewards are still sent to your original address. The operator’s role is limited to sign the claim transaction."


If you call `toggleOperator` with `address(0)`, then anyone can claim on your behalf without requiring individual operator approvals.

**Example:**

Assuming Alice earned the rewards:

- **Default behavior**: Only Alice can claim, and rewards are sent to Alice.
- **With operator**: By calling `toggleOperator`, Alice can allow Bob to claim on her behalf. Bob can then claim for Alice by submitting Alice's proof to the contract, and rewards are still sent to Alice's address.
- **With address(0)**: By calling `toggleOperator(address(0))`, Alice allows anyone to claim on her behalf. Any address can then claim for Alice by submitting Alice's proof to the contract, and rewards are still sent to Alice's address.

If you can't call `toggleOperator` and are stuck, please [open a tech ticket in our Discord ](https://discord.com/channels/1209830388726243369/1210212731047776357), the team may be able to call it on your behalf.

**Admin rights to claim for everyone:**

In some cases, certain addresses may be granted admin rights that allow them to claim rewards on behalf of all users without requiring individual operator approvals. These admin rights are granted on a case-by-case basis and enable streamlined reward distribution for specific use cases, such as protocol-level reward forwarding or automated claim processes.

### Claiming from a multisig

When claiming rewards from a multisig address, we recommend delegating the claim process to an operator. This is because Merkl reward proofs are only valid for on average four hours (varies depending on the chain). If you fail to gather the necessary signatures and execute the claim transaction before a Merkle root is updated, your claim transaction will fail.
Expand All @@ -108,6 +123,179 @@ To structure the claim:

<figure><img src=".gitbook/assets/DistributorClaim.png" alt=""><figcaption><p>Claiming Merkl rewards using a block explorer</p></figcaption></figure>

## Claim Recipient System

The claim recipient system allows you to redirect where claimed rewards are sent, instead of always sending to the user who earned them.

### Function: `setClaimRecipient()`

**Signature:**
```solidity
function setClaimRecipient(address recipient, address token) external
```

**Parameters:**

- `recipient`: Address that will receive the claimed tokens. Use `address(0)` to remove the custom recipient (revert to default)
- `token`: Token for which to set the recipient. Use `address(0)` to set a **global recipient** that applies to all tokens without a specific recipient

**Access:** Callable by any user (sets recipient for `msg.sender`)

**How it works:**

- Sets a custom recipient for your own claims
- Setting `recipient = address(0)` removes the custom recipient for that token
- When `token = address(0)`, it sets `claimRecipient[user][address(0)]` as a global fallback

**Priority order when claiming:**

When claiming rewards, the system follows this priority order:

1. **Token-specific recipient**: `claimRecipient[user][token]` (if set and not `address(0)`)
2. **Global recipient**: `claimRecipient[user][address(0)]` (if set)
3. **Default**: User's own address

Token-specific recipients take precedence over the global recipient.

**Examples:**

```solidity
// Define a global recipient for all tokens
setClaimRecipient(vaultAddress, address(0));
// ✅ claimRecipient[user][address(0)] = vaultAddress
// ✅ All tokens without a specific recipient will go to vaultAddress

// Define a specific recipient for aglaMerkl (takes priority over global)
setClaimRecipient(aglaMerklVaultAddress, aglaMerkl);
// ✅ claimRecipient[user][aglaMerkl] = aglaMerklVaultAddress
// ✅ aglaMerkl goes to aglaMerklVaultAddress (not the global vault)

// Remove the specific recipient for aglaMerkl
setClaimRecipient(address(0), aglaMerkl);
// ✅ claimRecipient[user][aglaMerkl] = address(0)
// ✅ aglaMerkl will now use the global recipient (vaultAddress)
```

**Example flow:**

```solidity
// Setup
setClaimRecipient(vaultAglaMerkl, aglaMerkl); // Specific for aglaMerkl
setClaimRecipient(defaultVault, address(0)); // Global fallback

// When claiming:
// - aglaMerkl → goes to vaultAglaMerkl (token-specific)
// - WETH → goes to defaultVault (global fallback)
// - DAI → goes to defaultVault (global fallback)
```

**Use cases:**

- Routing rewards to smart contracts that cannot directly claim
- Redirecting rewards to user-controlled addresses
- Protocol-level reward forwarding mechanisms

---

## Claim Callback System (Automatic Actions)

The `claimWithRecipient()` function supports passing arbitrary data (`bytes`) to recipient contracts, enabling automatic actions like swaps via aggregators when rewards are claimed.

### How it works

When you call `claimWithRecipient()` with non-empty `datas`, the system:

1. Transfers tokens to the recipient address
2. Automatically calls `onClaim()` on the recipient contract (if it implements `IClaimRecipient`)
3. Passes the custom data to the callback, allowing the recipient to perform actions like swaps, deposits, or conversions

### Function: `claimWithRecipient()`

**Signature:**
```solidity
function claimWithRecipient(
address[] calldata users,
address[] calldata tokens,
uint256[] calldata amounts,
bytes32[][] calldata proofs,
address[] calldata recipients,
bytes[] memory datas // Custom data for callback
) external
```

**Parameters:**

- `datas`: Array of arbitrary `bytes` data, one per claim. This data is passed to the recipient's `onClaim()` callback.

### IClaimRecipient Interface

Recipient contracts must implement this interface:

```solidity
interface IClaimRecipient {
function onClaim(address user, address token, uint256 amount, bytes memory data)
external returns (bytes32);
}
```

The callback must return `CALLBACK_SUCCESS` (keccak256("IClaimRecipient.onClaim")) or the claim will revert.

**Important notes:**

- If `data.length == 0`, no callback is made (standard transfer only)
- If the callback reverts, the token transfer still occurred (uses `try/catch`)
- The `datas` array must have the same length as other arrays
- Data encoding is flexible - you can encode any parameters your recipient contract needs

### Use cases

- **Automatic swaps**: Swap rewards immediately via DEX aggregators (1inch, 0x, etc.)
- **Auto-deposits**: Deposit rewards into liquidity pools or yield farms
- **Token conversions**: Convert rewards to a different token automatically
- **Complex routing**: Perform multiple actions based on encoded data

### Example: Automatic Swap

```solidity
// 1. Set a swap vault as recipient
setClaimRecipient(swapVaultAddress, aglaMerkl);

// 2. Claim with swap parameters encoded in data
bytes memory swapData = abi.encode(
targetToken, // Token to receive after swap
minAmountOut, // Minimum amount expected
swapRouter // Aggregator router address
);

claimWithRecipient(
[userAddress],
[aglaMerkl],
[amount],
[proof],
[swapVaultAddress],
[swapData] // Data for automatic swap
);

// The swap vault receives aglaMerkl, automatically swaps it, and sends the result to the user
```

---

## Support for `address(0)` as Wildcard

The `Distributor` contract uses `address(0)` as a wildcard parameter in several functions, representing **"all tokens"** or **"default"**.

### Where `address(0)` is used:

1. **`setClaimRecipient(recipient, address(0))`**
- Sets a **global recipient** that applies to all tokens without specific recipients

### How it works:

- When `address(0)` is used as the token parameter, it acts as a fallback/default
- This simplifies operations that need to work across multiple tokens
- Token-specific recipients take precedence over the global recipient (see priority order above)

### Address Remapping

If a smart contract you use can’t claim rewards, ask the Merkl team to remap those rewards to a claimable wallet by reaching out on [Discord](https://discord.com/channels/1209830388726243369/1210212731047776357) with the campaign ID, source address, and destination address. The full walkthrough lives in the [Reward Forwarding guide](../merkl-mechanisms/reward-forwarding.md#address-remapping).
Loading