-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
89 changed files
with
24,752 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
This is a suite of contracts and scrips allowing Sonne Finance to distribute rewards on BASE to `uSonne` and `sSonne` stakers on Optimism. | ||
|
||
# 🗄️ Main contracts | ||
- [`SonneMerkleDistributor.sol`](https://github.com/0xjaki/Sonne-Cross-Chain-Reward-Distribution/blob/main/contracts/SonneMerkleDistributor.sol) Contract to be deployed on BASE to distribute rewards | ||
- [`Delegator.sol`](https://github.com/0xjaki/Sonne-Cross-Chain-Reward-Distribution/blob/main/contracts/Delegator.sol) Contract to be deployed on Optimism to allow user to delegate their claim | ||
- [`getAccountRoot.ts`](https://github.com/0xjaki/Sonne-Cross-Chain-Reward-Distribution/blob/main/scripts/getAccountRoot.ts) Script to generate snapshot, including `accountRoot` | ||
- [`getProofForUser.ts`](https://github.com/0xjaki/Sonne-Cross-Chain-Reward-Distribution/blob/main/scripts/getProofForUser.ts) Script to generate merkle proof for a given user | ||
|
||
# ▶️ Sonne Merkle Distributor | ||
SonneMerkleDistributor is a smart contract that allows the distribution of ERC20 rewards tokens to addresses based on a Merkle tree of staking balances. | ||
|
||
## Features | ||
|
||
- Deposit reward tokens and associate with a Merkle root | ||
- Users can claim rewards by providing Merkle proofs | ||
- Support for delegated reward claims | ||
- Withdrawal of unclaimed rewards by the owner (after a specified period) | ||
- Check claim status for addresses | ||
|
||
## Usage | ||
### Adding Rewards | ||
The contract owner can add new rewards distributions using the `addReward` function: | ||
|
||
```solidity | ||
function addReward( | ||
uint256 amount, | ||
bytes32 merkleRoot, | ||
uint256 blockNumber, | ||
uint256 withdrawUnlockTime, | ||
uint256 totalStakedBalance | ||
) external onlyOwner; | ||
``` | ||
|
||
This deposits the reward tokens into the contract and associates them with a Merkle root. | ||
- `amount` - Total reward tokens to distribute | ||
- `merkleRoot` - Root of the Merkle tree containing staking balances | ||
- `blockNumber` - Block number on Optimism where balances were fetched | ||
- `withdrawUnlockTime` - Timestamp after which the owner can withdraw remaining rewards | ||
- `totalStakedBalance` - Total staked balance across all accounts | ||
|
||
### Claiming Rewards | ||
Users can claim their rewards by calling the claim function: | ||
|
||
```solidity | ||
function claim( | ||
uint256 blockNumber, | ||
bytes[] calldata proof, | ||
) external; | ||
``` | ||
|
||
This verifies the Merkle proof and transfers the reward tokens to the user's address. | ||
- `blockNumber` - Reward distribution to claim from | ||
- `proof` - Merkle proof of the user's staking balance | ||
|
||
### Withdrawing Remaining Rewards | ||
The owner can withdraw unclaimed rewards after the `withdrawUnlockTime` by calling: | ||
|
||
```solidity | ||
function withdrawFunds( | ||
uint256 blockNumber, | ||
uint256 amount | ||
) external onlyOwner; | ||
``` | ||
|
||
- blockNumber - Reward distribution to withdraw from | ||
- amount - Amount of remaining rewards to withdraw | ||
|
||
### Delegated Claims | ||
The contract owner can set a delegated address for users with `setDelegator`: | ||
|
||
```solidity | ||
function setDelegator( | ||
address recipient, | ||
address delegator | ||
) external onlyOwner; | ||
``` | ||
|
||
This allows the delegator to claim rewards on behalf of the recipient. | ||
|
||
### Checking Claim Status | ||
To verify if an account can claim rewards from a distribution: | ||
|
||
```solidity | ||
function isClaimable( | ||
uint256 blockNumber, | ||
address account, | ||
bytes[] calldata proof | ||
) external view returns (bool); | ||
``` | ||
Returns true if the account can claim rewards from the distribution based on the Merkle proof. | ||
|
||
### Security | ||
The contract uses OpenZeppelin's `ReentrancyGuard` to prevent reentrancy attacks. All external functions that alter state use the `nonReentrant` modifier. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity 0.8.19; | ||
|
||
contract Delegate { | ||
|
||
mapping(address => address) public delegates; | ||
|
||
event DelegateChanged(address indexed addr, address newDelegate); | ||
|
||
function setDelegate(address delegate) public { | ||
delegates[msg.sender] = delegate; | ||
emit DelegateChanged(msg.sender, delegate); | ||
} | ||
|
||
function getDelegate(address addr) public view returns (address) { | ||
return delegates[addr]; | ||
} | ||
} |
226 changes: 226 additions & 0 deletions
226
crosschain-rewards/contracts/SonneMerkleDistributor.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity 0.8.19; | ||
|
||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import {SecureMerkleTrie} from "@eth-optimism/contracts-bedrock/contracts/libraries/trie/SecureMerkleTrie.sol"; | ||
import {RLPReader} from "@eth-optimism/contracts-bedrock/contracts/libraries/rlp/RLPReader.sol"; | ||
import {ISonneMerkleDistributor} from "./interfaces/ISonneMerkleDistributor.sol"; | ||
|
||
/// @title Sonne Finance Merkle tree-based rewards distributor | ||
/// @notice Contract to distribute rewards on BASE network to Sonne Finance Optimism stakers | ||
contract SonneMerkleDistributor is | ||
ReentrancyGuard, | ||
Ownable, | ||
ISonneMerkleDistributor | ||
{ | ||
using SafeERC20 for IERC20; | ||
|
||
struct Reward { | ||
uint256 balance; // amount of reward tokens held in this reward | ||
bytes32 merkleRoot; // root of claims merkle tree | ||
uint256 withdrawUnlockTime; // time after which owner can withdraw remaining rewards | ||
uint256 ratio; // ratio of rewards to be distributed per one staked token on OP | ||
mapping(bytes32 => bool) leafClaimed; // mapping of leafes that already claimed | ||
} | ||
|
||
IERC20 public immutable rewardToken; | ||
uint256[] public rewards; // a list of all rewards | ||
|
||
mapping(uint256 => Reward) public Rewards; // mapping between blockNumber => Reward | ||
mapping(address => address) public delegatorAddresses; | ||
|
||
/// mapping to allow msg.sender to claim on behalf of a delegators address | ||
|
||
/// @notice Contract constructor to initialize rewardToken | ||
/// @param _rewardToken The reward token to be distributed | ||
constructor(IERC20 _rewardToken) { | ||
require(address(_rewardToken) != address(0), "Token cannot be zero"); | ||
rewardToken = _rewardToken; | ||
} | ||
|
||
/// @notice Sets a delegator address for a given recipient | ||
/// @param _recipient original eligible recipient address | ||
/// @param _delegator The address that sould claim on behalf of the owner | ||
function setDelegator( | ||
address _recipient, | ||
address _delegator | ||
) external onlyOwner { | ||
require( | ||
_recipient != address(0) && _delegator != address(0), | ||
"Invalid address provided" | ||
); | ||
delegatorAddresses[_delegator] = _recipient; | ||
} | ||
|
||
/// @notice Creates a new Reward struct for a rewards distribution | ||
/// @param amount The amount of reward tokens to deposit | ||
/// @param merkleRoot The merkle root of the distribution tree | ||
/// @param blockNumber The block number for the Reward | ||
/// @param withdrawUnlockTime The timestamp after which withdrawals by owner are allowed | ||
/// @param totalStakedBalance Total staked balance of the merkleRoot (computed off-chain) | ||
function addReward( | ||
uint256 amount, | ||
bytes32 merkleRoot, | ||
uint256 blockNumber, | ||
uint256 withdrawUnlockTime, | ||
uint256 totalStakedBalance | ||
) external onlyOwner { | ||
require(merkleRoot != bytes32(0), "Merkle root cannot be zero"); | ||
|
||
// creates a new reward struct tied to the blocknumber the merkleProof was created at | ||
Reward storage reward = Rewards[blockNumber]; | ||
|
||
require( | ||
reward.merkleRoot == bytes32(0), | ||
"Merkle root was already posted" | ||
); | ||
uint256 balance = rewardToken.balanceOf(msg.sender); | ||
require( | ||
amount > 0 && amount <= balance, | ||
"Invalid amount or insufficient balance" | ||
); | ||
|
||
// transfer rewardToken from the distributor to the contract | ||
rewardToken.safeTransferFrom(msg.sender, address(this), amount); | ||
|
||
// record Reward in stable storage | ||
reward.balance = amount; | ||
reward.merkleRoot = merkleRoot; | ||
reward.withdrawUnlockTime = withdrawUnlockTime; | ||
reward.ratio = (amount * 1e18) / (totalStakedBalance); | ||
rewards.push(blockNumber); | ||
emit NewMerkle( | ||
msg.sender, | ||
address(rewardToken), | ||
amount, | ||
merkleRoot, | ||
blockNumber, | ||
withdrawUnlockTime | ||
); | ||
} | ||
|
||
/// @notice Allows to withdraw available funds to owner after unlock time | ||
/// @param blockNumber The block number for the Reward | ||
/// @param amount The amount to withdraw | ||
function withdrawFunds( | ||
uint256 blockNumber, | ||
uint256 amount | ||
) external onlyOwner { | ||
Reward storage reward = Rewards[blockNumber]; | ||
require( | ||
block.timestamp >= reward.withdrawUnlockTime, | ||
"Rewards may not be withdrawn" | ||
); | ||
require(amount <= reward.balance, "Insufficient balance"); | ||
|
||
// update Rewards record | ||
reward.balance = reward.balance -= amount; | ||
|
||
// transfer rewardToken back to owner | ||
rewardToken.safeTransfer(msg.sender, amount); | ||
emit MerkleFundUpdate( | ||
msg.sender, | ||
reward.merkleRoot, | ||
blockNumber, | ||
amount, | ||
true | ||
); | ||
} | ||
|
||
/// @notice Claims the specified amount for an account if valid | ||
/// @dev Checks proofs and claims tracking before transferring rewardTokens | ||
/// @param blockNumber The block number for the Reward | ||
/// @param proof The merkle proof for the claim | ||
function claim( | ||
uint256 blockNumber, | ||
bytes[] calldata proof | ||
) external nonReentrant { | ||
Reward storage reward = Rewards[blockNumber]; | ||
require(reward.merkleRoot != bytes32(0), "Reward not found"); | ||
|
||
// Check if the delegatorAddresses includes the account | ||
// The delegatorAddresses mapping allows for an account to delegate its claim ability to another address | ||
// This can be useful in scenarios where the target recipient might not have the ability to directly interact | ||
// with the BASE network contract (e.g. a smart contract with a different address) | ||
address recipient = delegatorAddresses[msg.sender] != address(0) | ||
? delegatorAddresses[msg.sender] | ||
: msg.sender; | ||
|
||
// Assuming slotNr is 2 as per your previous function | ||
bytes32 key = keccak256(abi.encode(recipient, uint256(2))); | ||
|
||
//Get the amount of the key from the merkel tree | ||
uint256 amount = _getValueFromMerkleTree(reward.merkleRoot, key, proof); | ||
|
||
// calculate the reward based on the ratio | ||
uint256 rewardAmount = (amount * reward.ratio) / 1e18; // TODO check if there is a loss of precision possible here | ||
|
||
require( | ||
reward.balance >= rewardAmount, | ||
"Claim under-funded by funder." | ||
); | ||
require( | ||
Rewards[blockNumber].leafClaimed[key] == false, | ||
"Already claimed" | ||
); | ||
|
||
// marks the leaf as claimed | ||
reward.leafClaimed[key] = true; | ||
|
||
// Subtract the rewardAmount, not the amount | ||
reward.balance = reward.balance - rewardAmount; | ||
|
||
//Send reward tokens to the recipient | ||
rewardToken.safeTransfer(recipient, rewardAmount); | ||
|
||
emit MerkleClaim( | ||
recipient, | ||
address(rewardToken), | ||
blockNumber, | ||
rewardAmount | ||
); | ||
} | ||
|
||
/// @notice Checks if a claim is valid and claimable | ||
/// @param blockNumber The block number for the Reward | ||
/// @param account The address of the account claiming | ||
/// @param proof The merkle proof for the claim | ||
/// @return A bool indicating if the claim is valid and claimable | ||
function isClaimable( | ||
uint256 blockNumber, | ||
address account, | ||
bytes[] calldata proof | ||
) external view returns (bool) { | ||
bytes32 merkleRoot = Rewards[blockNumber].merkleRoot; | ||
|
||
// At the staking contract, the balances are stored in a mapping (address => uint256) at storage slot 2 | ||
bytes32 leaf = keccak256(abi.encode(account, uint256(2))); | ||
|
||
if (merkleRoot == 0) return false; | ||
return | ||
!Rewards[blockNumber].leafClaimed[leaf] && | ||
_getValueFromMerkleTree(merkleRoot, leaf, proof) > 0; | ||
} | ||
|
||
/// @dev Uses SecureMerkleTrie Library to extract the value from the Merkle proof provided by the user | ||
/// @param merkleRoot the merkle root | ||
/// @param key the key of the leaf => keccak256(address,2) | ||
/// @return result The converted uint256 value as stored in the slot on OP | ||
function _getValueFromMerkleTree( | ||
bytes32 merkleRoot, | ||
bytes32 key, | ||
bytes[] calldata proof | ||
) internal pure returns (uint256 result) { | ||
// Uses SecureMerkleTrie Library to extract the value from the Merkle proof provided by the user | ||
// Reverts if Merkle proof verification fails | ||
bytes memory data = RLPReader.readBytes( | ||
SecureMerkleTrie.get(abi.encodePacked(key), proof, merkleRoot) | ||
); | ||
|
||
for (uint256 i = 0; i < data.length; i++) { | ||
result = result * 256 + uint8(data[i]); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity 0.8.19; | ||
|
||
contract TestContract { | ||
address public owner; | ||
//Fake vars to move shares mapping at slot number 2 | ||
uint256 private bar; | ||
|
||
mapping(address => uint256) public shares; | ||
|
||
function mint(address account, uint256 amount) public { | ||
require(msg.sender == owner, "only owner"); | ||
shares[account] = amount; | ||
} | ||
|
||
constructor() { | ||
owner = msg.sender; | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
crosschain-rewards/contracts/interfaces/IERC20WithPermit.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity 0.8.19; | ||
|
||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
|
||
interface IERC20WithPermit is IERC20 { | ||
function permit( | ||
address, | ||
address, | ||
uint256, | ||
uint256, | ||
uint8, | ||
bytes32, | ||
bytes32 | ||
) external; | ||
} |
Oops, something went wrong.