Skip to content

Commit

Permalink
Merge pull request #149 from threshold-network/with-taco
Browse files Browse the repository at this point in the history
New Threshold rewards distribution contract
  • Loading branch information
cygnusv authored Sep 16, 2024
2 parents 3027eb8 + a390c28 commit 3f1f33a
Show file tree
Hide file tree
Showing 17 changed files with 11,421 additions and 25,508 deletions.
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Start using a common code style with Prettier and ESLint
ca739bb30b6c4f251cac9948d94c75abbbc7401d
88189677d296d8b832b248bc24f4cd2478c3fcc1
48d52958a80fda49f9b92d6887140f23119d7ce8
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,26 @@ calculating the appropriate rewards. These subgraphs are queried using
supports auto-pagination, retry, fallback, etc.

Modification or addition of new subgraphs must be done in `.graphclientrc.yml`. Also, new queries
must be added to this file in addition to `src/script/graphql` folder.
must be added to this file in addition to the `src/script/graphql` folder.

Every time the subgraph queries are modified, these must be recompiled:

```bash
yarn graphclient build --fileType json
```

## Contracts development

### Testing

To compile the contracts, just run:

```bash
npx hardhat compile
```

To run the tests just run:

```bash
npx hardhat test
```
45 changes: 36 additions & 9 deletions contracts/CumulativeMerkleDrop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import "./interfaces/ICumulativeMerkleDrop.sol";


contract CumulativeMerkleDrop is Ownable, ICumulativeMerkleDrop {
using SafeERC20 for IERC20;
using MerkleProof for bytes32[];
Expand All @@ -27,7 +26,10 @@ contract CumulativeMerkleDrop is Ownable, ICumulativeMerkleDrop {

constructor(address token_, address rewardsHolder_, address newOwner) {
require(IERC20(token_).totalSupply() > 0, "Token contract must be set");
require(rewardsHolder_ != address(0), "Rewards Holder must be an address");
require(
rewardsHolder_ != address(0),
"Rewards Holder must be an address"
);
transferOwnership(newOwner);
token = token_;
rewardsHolder = rewardsHolder_;
Expand All @@ -39,7 +41,10 @@ contract CumulativeMerkleDrop is Ownable, ICumulativeMerkleDrop {
}

function setRewardsHolder(address rewardsHolder_) external onlyOwner {
require(rewardsHolder_ != address(0), "Rewards holder must be an address");
require(
rewardsHolder_ != address(0),
"Rewards holder must be an address"
);
emit RewardsHolderUpdated(rewardsHolder, rewardsHolder_);
rewardsHolder = rewardsHolder_;
}
Expand All @@ -54,8 +59,13 @@ contract CumulativeMerkleDrop is Ownable, ICumulativeMerkleDrop {
require(merkleRoot == expectedMerkleRoot, "Merkle root was updated");

// Verify the merkle proof
bytes32 leaf = keccak256(abi.encodePacked(stakingProvider, beneficiary, cumulativeAmount));
require(_verifyAsm(merkleProof, expectedMerkleRoot, leaf), "Invalid proof");
bytes32 leaf = keccak256(
abi.encodePacked(stakingProvider, beneficiary, cumulativeAmount)
);
require(
_verifyAsm(merkleProof, expectedMerkleRoot, leaf),
"Invalid proof"
);

// Mark it claimed
uint256 preclaimed = cumulativeClaimed[stakingProvider];
Expand All @@ -66,7 +76,12 @@ contract CumulativeMerkleDrop is Ownable, ICumulativeMerkleDrop {
unchecked {
uint256 amount = cumulativeAmount - preclaimed;
IERC20(token).safeTransferFrom(rewardsHolder, beneficiary, amount);
emit Claimed(stakingProvider, amount, beneficiary, expectedMerkleRoot);
emit Claimed(
stakingProvider,
amount,
beneficiary,
expectedMerkleRoot
);
}
}

Expand All @@ -85,18 +100,30 @@ contract CumulativeMerkleDrop is Ownable, ICumulativeMerkleDrop {
}
}

function verify(bytes32[] calldata merkleProof, bytes32 root, bytes32 leaf) public pure returns (bool) {
function verify(
bytes32[] calldata merkleProof,
bytes32 root,
bytes32 leaf
) public pure returns (bool) {
return merkleProof.verify(root, leaf);
}

function _verifyAsm(bytes32[] calldata proof, bytes32 root, bytes32 leaf) private pure returns (bool valid) {
function _verifyAsm(
bytes32[] calldata proof,
bytes32 root,
bytes32 leaf
) private pure returns (bool valid) {
// solhint-disable-next-line no-inline-assembly
assembly {
let mem1 := mload(0x40)
let mem2 := add(mem1, 0x20)
let ptr := proof.offset

for { let end := add(ptr, mul(0x20, proof.length)) } lt(ptr, end) { ptr := add(ptr, 0x20) } {
for {
let end := add(ptr, mul(0x20, proof.length))
} lt(ptr, end) {
ptr := add(ptr, 0x20)
} {
let node := calldataload(ptr)

switch lt(leaf, node)
Expand Down
265 changes: 265 additions & 0 deletions contracts/RewardsAggregator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import "./interfaces/IRewardsAggregator.sol";
import "./interfaces/ICumulativeMerkleDrop.sol";
import "./interfaces/IApplication.sol";

/**
* @title Rewards Aggregator
* @notice RewardsAggregator is the contract responsible for the distribution
* of Threshold Network staking rewards, which are generated by two
* different sources: a Merkle tree distribution and the Threshold
* Network applications.
*/
contract RewardsAggregator is Ownable, IRewardsAggregator {
using SafeERC20 for IERC20;
using MerkleProof for bytes32[];

address public immutable override token;
address public rewardsHolder;

bytes32 public override merkleRoot;
mapping(address => uint256) internal cumulativeClaimed;

// TODO: Generalize to an array of IApplication in the future.
// For the moment, it will only be used for TACo app.
IApplication public immutable application;

ICumulativeMerkleDrop public immutable oldCumulativeMerkleDrop;

struct MerkleClaim {
address stakingProvider;
address beneficiary;
uint256 amount;
bytes32[] proof;
}

constructor(
address _token,
IApplication _application,
ICumulativeMerkleDrop _oldCumulativeMerkleDrop,
address _rewardsHolder,
address newOwner
) {
require(IERC20(_token).totalSupply() > 0, "Token contract must be set");
require(
_rewardsHolder != address(0),
"Rewards Holder must be an address"
);
require(
address(_application) != address(0),
"Application must be an address"
);
require(
_token == _oldCumulativeMerkleDrop.token(),
"Incompatible old Merkle Distribution contract"
);

transferOwnership(newOwner);
token = _token;
application = _application;
rewardsHolder = _rewardsHolder;
oldCumulativeMerkleDrop = _oldCumulativeMerkleDrop;
}

function setMerkleRoot(bytes32 _merkleRoot) external override onlyOwner {
emit MerkleRootUpdated(merkleRoot, _merkleRoot);
merkleRoot = _merkleRoot;
}

/**
* @notice Sets the address from where Merkle rewards are being pulled.
*/
function setMerkleRewardsHolder(address _rewardsHolder) external onlyOwner {
require(
_rewardsHolder != address(0),
"Rewards Holder must be an address"
);
emit RewardsHolderUpdated(rewardsHolder, _rewardsHolder);
rewardsHolder = _rewardsHolder;
}

/**
* @notice Returns the amount of rewards that a given stake has already
* claimed from the Merkle distribution, including the old Merkle
* distribution contract. The returned amount does not include the
* claimed rewards generated by the applications.
*/
function cumulativeMerkleClaimed(
address stakingProvider
) public view returns (uint256) {
uint256 newAmount = cumulativeClaimed[stakingProvider];
if (newAmount > 0) {
return newAmount;
} else {
return oldCumulativeMerkleDrop.cumulativeClaimed(stakingProvider);
}
}

/**
* @notice Claim the rewards that have been generated by the Merkle
* distribution mechanism.
*/
function claimMerkle(
address stakingProvider,
address beneficiary,
uint256 merkleCumulativeAmount,
bytes32 expectedMerkleRoot,
bytes32[] calldata merkleProof
) public {
require(merkleRoot == expectedMerkleRoot, "Merkle root was updated");

// Verify the merkle proof
bytes32 leaf = keccak256(
abi.encodePacked(stakingProvider, beneficiary, merkleCumulativeAmount)
);
require(
verifyMerkleProof(merkleProof, expectedMerkleRoot, leaf),
"Invalid proof"
);

// Mark it claimed (potentially taking into consideration state in old
// MerkleDistribution contract)
uint256 preclaimed = cumulativeMerkleClaimed(stakingProvider);
require(preclaimed < merkleCumulativeAmount, "Nothing to claim");
cumulativeClaimed[stakingProvider] = merkleCumulativeAmount;

// Send the tokens
unchecked {
uint256 amount = merkleCumulativeAmount - preclaimed;
IERC20(token).safeTransferFrom(rewardsHolder, beneficiary, amount);
emit MerkleClaimed(
stakingProvider,
amount,
beneficiary,
expectedMerkleRoot
);
}
}

/**
* @notice Check if a particular stake has available rewards to claim.
*/
function canClaimApps(address stakingProvider) public view returns (bool) {
return application.availableRewards(stakingProvider) > 0;
}

/**
* @notice Claim the rewards generated by the Threshold Network
* applications.
*/
function claimApps(address stakingProvider) public {
application.withdrawRewards(stakingProvider);
}

/**
* @notice Claim the rewards generated by both the Merkle distributions
* and the Threshold Network applications.
*/
function claim(
address stakingProvider,
address beneficiary,
uint256 merkleCumulativeAmount,
bytes32 expectedMerkleRoot,
bytes32[] calldata merkleProof
) public {
if (
merkleCumulativeAmount != 0 &&
expectedMerkleRoot != bytes32(0) &&
merkleProof.length != 0
) {
claimMerkle(
stakingProvider,
beneficiary,
merkleCumulativeAmount,
expectedMerkleRoot,
merkleProof
);
}
if (canClaimApps(stakingProvider)) {
claimApps(stakingProvider);
}
}

/**
* @notice Claim the rewards generated by the Threshold Network
* applications.
*/
function claim(address stakingProvider) public {
claimApps(stakingProvider);
}

/**
* @notice Claim a batch of rewards generated by the Merkle distributions.
*/
function batchClaimMerkle(
bytes32 expectedMerkleRoot,
MerkleClaim[] calldata Claims
) external {
for (uint i; i < Claims.length; i++) {
claimMerkle(
Claims[i].stakingProvider,
Claims[i].beneficiary,
Claims[i].amount,
expectedMerkleRoot,
Claims[i].proof
);
}
}

/**
* @notice Claim a batch of rewards generated by the Threshold Network
* applications.
*/
function batchClaimApps(address[] calldata stakingProviders) external {
for (uint i; i < stakingProviders.length; i++) {
claimApps(stakingProviders[i]);
}
}

/**
* @notice Claim a batch of rewards generated by both the Merkle
* distribution and the Threshold Network applications.
*/
function batchClaim(
bytes32 expectedMerkleRoot,
MerkleClaim[] calldata Claims
) external {
for (uint i; i < Claims.length; i++) {
claim(
Claims[i].stakingProvider,
Claims[i].beneficiary,
Claims[i].amount,
expectedMerkleRoot,
Claims[i].proof
);
}
}

/**
* @notice Claim a batch of rewards generated by the Threshold Network
* applications.
*/
function batchClaim(address[] calldata stakingProviders) external {
for (uint i; i < stakingProviders.length; i++) {
claim(stakingProviders[i]);
}
}

/**
* @notice Check if a merkle proof is valid.
*/
function verifyMerkleProof(
bytes32[] calldata merkleProof,
bytes32 root,
bytes32 leaf
) public pure returns (bool) {
return merkleProof.verify(root, leaf);
}
}
Loading

0 comments on commit 3f1f33a

Please sign in to comment.