Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Threshold rewards distribution contract #149

Merged
merged 88 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from 78 commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
584e57a
Add Threshold IApplication interface
cygnusv May 16, 2024
ad1311d
Add IApplication reference in constructor
cygnusv May 16, 2024
3bd7087
Add methods that trigger withdraw in application
cygnusv May 16, 2024
32e4131
Add TACo app to deployment script
cygnusv May 16, 2024
ee547c3
Fix taco app mainnet address
manumonti Jun 11, 2024
a4f6b71
Add TACo app address for Lynx testnet
manumonti Jun 11, 2024
63d1651
Replace Merkle verify function for OZ one
manumonti Jun 12, 2024
29a80a2
Format code
manumonti Jun 12, 2024
b1840d2
Add commit to .git-blame-ignore-revs
manumonti Jun 12, 2024
e4ed12b
Rename Merkle Distribution contract
manumonti Jun 12, 2024
b8add39
Move mock contracts to test folder
manumonti Jun 12, 2024
a2cdc77
Remove test function syntax errors
manumonti Jun 12, 2024
9f737df
Add ignore-warnings support to compilations
manumonti Jun 12, 2024
0a47f40
Add Application mock contract
manumonti Jun 12, 2024
d7b6cc6
Adapt test to new MerkleDistributor contract
manumonti Jun 12, 2024
0cb969f
Use imported IApplication instead of local copy
manumonti Jun 17, 2024
f71afb0
Add token transfer to ApplicationMock
manumonti Jun 17, 2024
64ff146
Add MerkleDistributor deployment tests
manumonti Jun 17, 2024
62d28d6
Change test file name
manumonti Jun 17, 2024
11b5af1
Add constructor parameter for application in tests
manumonti Jun 17, 2024
a2355aa
Introduce cumulativeClaimed view function in the IMerkleDistributor i…
cygnusv Jun 17, 2024
1f29f4a
Consider claimed balance in old MerkleDistribution contract
cygnusv Jun 17, 2024
53ccbf8
Rename claiming functions (withoutApps instead of withApps angle)
cygnusv Jun 17, 2024
092580a
Fix compilation errors
manumonti Jun 19, 2024
e61887c
Add efficiency to tests not deploying repetitively
manumonti Jun 20, 2024
f4236f9
Add old Merkle Distribution contract to tests
manumonti Jun 20, 2024
3a9b5c1
Fix some tests failing with new contract
manumonti Jun 20, 2024
d34c44a
Fix typo on event name
manumonti Jun 21, 2024
fd03263
Add comments to the claim functions
manumonti Jun 21, 2024
43ecbff
Fix tests failing with new contract
manumonti Jun 21, 2024
40b2db6
Add tests for Merkle distributor contract deployment
manumonti Jun 25, 2024
5e8178b
Fix typo on contract's comment
manumonti Jun 25, 2024
e4b4d83
Add fixtures to tests
manumonti Jun 25, 2024
a55ba0c
Rename main contract and add claimApps functions
manumonti Jun 26, 2024
c8fa562
Rename main contract to RewardsAggregator
manumonti Jun 26, 2024
962946e
Add conditional sentence to claim function
manumonti Jun 27, 2024
e3198d9
Rename event emitted when claimMerkle is executed
manumonti Jun 27, 2024
599bcda
Rename Merkle claim structure
manumonti Jun 27, 2024
8e227bc
Rename the Merkle cumulative already claimed func
manumonti Jun 27, 2024
c19523d
Remove TODO
manumonti Jun 28, 2024
73630dc
Add TODO
manumonti Jul 2, 2024
8456e51
Fix README typo
manumonti Jul 3, 2024
f206374
Add functions to RewardsAggregator contract
manumonti Jul 3, 2024
59774fc
Rewrite contract comments to add clarity
manumonti Jul 3, 2024
ae12768
Rename var for Merkle rewards already claimed
manumonti Jul 3, 2024
81d597c
Add TODOs to tests
manumonti Jul 3, 2024
b790bf1
Add TODO
manumonti Jul 4, 2024
1a8f7cd
Rename parameter to make it more comprehensive
manumonti Aug 29, 2024
842500e
Separate utility functions from tests file
manumonti Aug 29, 2024
a74b53c
Rename test file for MerkleDistribution
manumonti Aug 29, 2024
c5751d9
Move fixture function to utils file
manumonti Aug 29, 2024
9bb2587
Rename Old Merkle contract
manumonti Aug 29, 2024
1d535b3
Rename old Merkle contract references
manumonti Aug 29, 2024
5e4e37d
Fix error message on constructor
manumonti Aug 29, 2024
e9cf8cc
Fix deployment tests
manumonti Aug 29, 2024
402a943
Rename test file
manumonti Aug 29, 2024
b093a7f
Fix setting Merkler Root tests
manumonti Aug 29, 2024
0a6adc7
Fix typo on revert cause
manumonti Aug 29, 2024
608004b
Add set rewards holder tests
manumonti Aug 29, 2024
6d4c8a2
Fix verifyMerkleProof tests
manumonti Aug 29, 2024
f4787e2
Add test to verify past distributions
manumonti Aug 29, 2024
044a37c
Replace old Merkle dist contract for the original
manumonti Sep 3, 2024
6b059d8
Adapt test to the use of original dist contract
manumonti Sep 3, 2024
8b5479c
Add tests for Merkle distribution functions
manumonti Sep 3, 2024
23608a7
Move old Merkle distribution contract path
manumonti Sep 3, 2024
177d19c
Recover tests for old Merkle distribution contract
manumonti Sep 3, 2024
7216e24
Rename Rewards Aggregator tests
manumonti Sep 3, 2024
74a0d18
Add test for claiming invalid amount of tokens
manumonti Sep 4, 2024
a51f0b8
Make comment more verbose on ApplicationMock
manumonti Sep 4, 2024
68f1f7f
Add tests for claiming rewards from Threshold apps
manumonti Sep 4, 2024
3f0f6df
Add test for batch-claiming Merkle rewards
manumonti Sep 4, 2024
69d9626
Add tests for claiming from both apps and Merkle
manumonti Sep 5, 2024
3508f55
Remove outdated tests
manumonti Sep 5, 2024
b6d8db5
Add instructions to README
manumonti Sep 5, 2024
48d5295
Format code of contracts and tests files
manumonti Sep 5, 2024
687da6d
Add format commit to git-blame-ingore-revs
manumonti Sep 5, 2024
41a1c92
Add inline comment to ignore warning
manumonti Sep 5, 2024
8846ab7
Use functions on utils instead of redeclaring them
manumonti Sep 5, 2024
6aa0a8e
Set a fixed version for Solidity compiler
manumonti Sep 6, 2024
28df6ba
Fix comment typo
manumonti Sep 6, 2024
7694f23
Add Merkle word to avoid confusions
manumonti Sep 10, 2024
86c1ecc
Unify notation
manumonti Sep 10, 2024
a906b84
Add explanatory comment to function
manumonti Sep 11, 2024
69f3da9
Remove unused dependencies
manumonti Sep 11, 2024
747b3fa
Add IApplication file instead of Node dependency
manumonti Sep 11, 2024
2f30fc2
Use NPM package instead of github ref for TokenMock
manumonti Sep 11, 2024
dd53240
Add NPM package for OpenZeppelin contracts
manumonti Sep 11, 2024
a390c28
Unify prefix notation
manumonti Sep 11, 2024
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
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 @@ -116,10 +116,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
262 changes: 262 additions & 0 deletions contracts/RewardsAggregator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
Copy link
Member Author

Choose a reason for hiding this comment

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

@vzotova Do you think we should increase the version here?

Copy link

Choose a reason for hiding this comment

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

good question, I'd use same as in threshold staking (which is 0.8.9 not ^0.8.9) or latest available

Copy link
Member

Choose a reason for hiding this comment

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

✅ addressed on 6aa0a8e


import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see the open zeppelin contract dependency in package.json.

Copy link
Member

Choose a reason for hiding this comment

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

✅ addressed on dd53240

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@threshold-network/solidity-contracts/contracts/staking/IApplication.sol";
Copy link
Member Author

Choose a reason for hiding this comment

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

If this is the only thing we're using from this repo, I'd suggest we just copy the interface here and remove the dependency, so we don't pollute ours with their subdependencies. In fact, that would allow us to use OZ v5, otherwise we'd be stuck with v4.

Copy link
Member

@manumonti manumonti Sep 11, 2024

Choose a reason for hiding this comment

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

✅ Addressed on 747b3fa

Note that we are using v4 of OpenZeppelin contracts because v5 is not compatible with CumulativeMerkleDrop contract.


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

/**
* @title Rewards Aggregator
* @notice RewardsAggregator is the contract responsible for the distribution
* of the Threshold Network staking rewards. The rewards are generated
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
* of the Threshold Network staking rewards. The rewards are generated
* of Threshold Network staking rewards, which are generated

Copy link
Member

Choose a reason for hiding this comment

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

✅ addressed on 28df6ba

* 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,
cygnusv marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Member

Choose a reason for hiding this comment

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

Did you want to keep the _ prefix consistent here, and in the function below as well i.e. _merkleRoot , and _rewardsHolder?

Copy link
Member

Choose a reason for hiding this comment

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

Good catch. Done in a390c28

emit MerkleRootUpdated(merkleRoot, merkleRoot_);
merkleRoot = merkleRoot_;
}

function setRewardsHolder(address rewardsHolder_) external onlyOwner {
cygnusv marked this conversation as resolved.
Show resolved Hide resolved
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,
cygnusv marked this conversation as resolved.
Show resolved Hide resolved
uint256 cumulativeAmount,
cygnusv marked this conversation as resolved.
Show resolved Hide resolved
bytes32 expectedMerkleRoot,
bytes32[] calldata merkleProof
) public {
require(merkleRoot == expectedMerkleRoot, "Merkle root was updated");

// Verify the merkle proof
bytes32 leaf = keccak256(
abi.encodePacked(stakingProvider, beneficiary, cumulativeAmount)
);
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 < cumulativeAmount, "Nothing to claim");
cumulativeClaimed[stakingProvider] = cumulativeAmount;

// Send the tokens
unchecked {
uint256 amount = cumulativeAmount - 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;
cygnusv marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @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