Skip to content

Commit

Permalink
add crosschain-rewards
Browse files Browse the repository at this point in the history
  • Loading branch information
0xjaki committed Sep 11, 2023
1 parent eaefb37 commit 8a950a2
Show file tree
Hide file tree
Showing 89 changed files with 24,752 additions and 0 deletions.
Binary file added .DS_Store
Binary file not shown.
Binary file added crosschain-rewards/.DS_Store
Binary file not shown.
93 changes: 93 additions & 0 deletions crosschain-rewards/README.md
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.
18 changes: 18 additions & 0 deletions crosschain-rewards/contracts/Delegator.sol
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 crosschain-rewards/contracts/SonneMerkleDistributor.sol
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]);
}
}
}
19 changes: 19 additions & 0 deletions crosschain-rewards/contracts/TestContract.sol
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 crosschain-rewards/contracts/interfaces/IERC20WithPermit.sol
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;
}
Loading

0 comments on commit 8a950a2

Please sign in to comment.