diff --git a/distribute-with-merkl/before-you-start.md b/distribute-with-merkl/before-you-start.md index 5601d27..fca6194 100644 --- a/distribute-with-merkl/before-you-start.md +++ b/distribute-with-merkl/before-you-start.md @@ -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**. diff --git a/earn-with-merkl/earning-with-merkl.md b/earn-with-merkl/earning-with-merkl.md index af650f5..542768e 100644 --- a/earn-with-merkl/earning-with-merkl.md +++ b/earn-with-merkl/earning-with-merkl.md @@ -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. -* 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. + +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. @@ -108,6 +123,179 @@ To structure the claim:

Claiming Merkl rewards using a block explorer

+## 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). diff --git a/merkl-mechanisms/campaign-types/airdrop.md b/merkl-mechanisms/campaign-types/airdrop.md index 59ef84c..66bd71d 100644 --- a/merkl-mechanisms/campaign-types/airdrop.md +++ b/merkl-mechanisms/campaign-types/airdrop.md @@ -108,3 +108,7 @@ For this type of campaign, in addition to the reward amount, campaign creators m * Snapshot Date – The balance snapshot is taken at the last block before this date. * Distribution Date – The date on which rewards should be distributed. * Snapshot Name – A label for your snapshot. + +## Automatic claim implementation + +In certain cases, it may be relevant to implement automatic claiming for your airdrop recipients. If you're interested in implementing such a solution, please contact us.