Skip to content
This repository has been archived by the owner on May 22, 2023. It is now read-only.

Keep rewards distributor #627

Merged
merged 45 commits into from Dec 4, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
d027006
Very early draft for merkle rewards distributor
dimpar Nov 26, 2020
df6da9b
Update KEEP amount for the give merkle root
dimpar Nov 26, 2020
9f2316d
Drafting merkle root and proofs generation
dimpar Nov 27, 2020
ef4ffa4
Renaming event after rewards have been allocated
dimpar Nov 27, 2020
d361c5a
Improving docs, error messages and renames
dimpar Nov 27, 2020
ae08a82
Adding additional validation for receiveApproval()
dimpar Nov 27, 2020
2af367a
Removing KEEP balance check
dimpar Nov 30, 2020
99bd634
Adding a function to get total KEEP amount allocated for a given root
dimpar Nov 30, 2020
76bdfbc
Using SafeMath for uint256 operations
dimpar Nov 30, 2020
004f910
Rewards -> Reward
dimpar Nov 30, 2020
fb4ed1b
Improving docs
dimpar Nov 30, 2020
bb3cb61
Cleanup and linting
dimpar Nov 30, 2020
31d9bcb
Renaming staker-rewards-input.json -> example-rewards-input.json
dimpar Nov 30, 2020
33126b5
Adding onlyOwner modifier for receiveApproval function
dimpar Nov 30, 2020
733e67d
Adding validation if a merkle root has allocated rewards
dimpar Nov 30, 2020
e801f10
Updating scripts for merkle object generation
dimpar Nov 30, 2020
b2eb519
Adding package.json for merkle generator
dimpar Nov 30, 2020
2ac380b
Merge remote-tracking branch 'origin' into keep-rewards-distributor
dimpar Dec 1, 2020
20f7d19
Replacing receiveApproval function with a seperate allocate function
dimpar Dec 1, 2020
5c7d564
Adding merkleRoot param for RewardsClaimed event
dimpar Dec 1, 2020
110cc5d
Removed submodule from include/ dir
dimpar Dec 1, 2020
e3e63e6
Moving merkle-distributor submodule to /staker-rewards dir
dimpar Dec 1, 2020
7cbe29f
Using npm instead of yarn
dimpar Dec 1, 2020
af04479
Placing merkleRoot as a first parameter in events and functions
dimpar Dec 2, 2020
58fddbd
Adding indexed modifier to RewardsClaimed event
dimpar Dec 2, 2020
04bf914
Simplifying merkleRoots map to just indicate if it was allocated
dimpar Dec 2, 2020
0c14bb2
Adding tests for ECDSARewardsDistributor contract
dimpar Dec 2, 2020
83223a5
Improving docs for the rewards distributor contract
dimpar Dec 3, 2020
0ed6e72
Flipping the order of validation when claiming rewards
dimpar Dec 3, 2020
9bbddb9
Cleaning up leftovers
dimpar Dec 3, 2020
eef3df5
Merge remote-tracking branch 'origin' into keep-rewards-distributor
dimpar Dec 3, 2020
9bb2eef
Linting MerkleDistributorTest.js
dimpar Dec 3, 2020
ffff169
Updating package-lock.js
dimpar Dec 3, 2020
44c86e4
Removing submodule depended on Uniswap/merkle-distributor
dimpar Dec 3, 2020
bfc5e2e
Adding fork as submodule of a merkle-distributor project
dimpar Dec 3, 2020
85de99e
Moving assertion of RewardsAllocated event to a seperate test
dimpar Dec 3, 2020
707037e
Changing:
dimpar Dec 3, 2020
f317a2d
Adding assertion of transfered tokens to a staker account
dimpar Dec 3, 2020
cb3007f
Using addn for adding a number to BN
dimpar Dec 3, 2020
f49013f
Adding more tests:
dimpar Dec 4, 2020
fcf2244
Adding indexed in RewardsClaimed event for index param
dimpar Dec 4, 2020
a282c05
Adding additional assertion for token receiver
dimpar Dec 4, 2020
bd1ebce
Merge remote-tracking branch 'origin' into keep-rewards-distributor
dimpar Dec 4, 2020
5164015
Adding package-lock.json
dimpar Dec 4, 2020
39e1a10
Adding additional test for isClaimed()
dimpar Dec 4, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -29,3 +29,4 @@ build/
# Configuration files
configs/config.toml
external-contracts.js-e
include/
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions .gitmodules
@@ -0,0 +1,3 @@
[submodule "include/merkle-distributor"]
path = include/merkle-distributor
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
url = https://github.com/Uniswap/merkle-distributor.git
1 change: 1 addition & 0 deletions include/merkle-distributor
Submodule merkle-distributor added at c3255b
30 changes: 30 additions & 0 deletions rewards-merkle-generator.sh
@@ -0,0 +1,30 @@
#!/bin/bash
pdyraga marked this conversation as resolved.
Show resolved Hide resolved

set -e

LOG_START='\n\e[1;36m' # new line + bold + color
LOG_END='\n\e[0m' # new line + reset color

WORKDIR=$PWD

printf "${LOG_START}Initializing merkle-distributor submodule...${LOG_END}"

git submodule update --init --recursive --remote --rebase --force

printf "${LOG_START}Installing dependencies...${LOG_END}"

cd "$WORKDIR/include/merkle-distributor"
yarn
pdyraga marked this conversation as resolved.
Show resolved Hide resolved

cd "$WORKDIR/staker-rewards"
yarn

printf "${LOG_START}Generating merkle output object...${LOG_END}"

REWARDS_INPUT_PATH="staker-rewards/example-rewards-input.json"
if [[ $1 == *"--input"* ]]; then
v="${1/--/}"
declare REWARDS_INPUT_PATH="$2"
fi

yarn ts-node generate-merkle-root.ts --input "$WORKDIR/$REWARDS_INPUT_PATH"
134 changes: 134 additions & 0 deletions solidity/contracts/ECDSARewardsDistributor.sol
@@ -0,0 +1,134 @@
/**
▓▓▌ ▓▓ ▐▓▓ ▓▓▓▓▓▓▓▓▓▓▌▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▄
▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▌▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓ ▓▓▓▓▓▓▓▀ ▐▓▓▓▓▓▓ ▐▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓ ▐▓▓▓▓▓▌ ▐▓▓▓▓▓▓
▓▓▓▓▓▓▄▄▓▓▓▓▓▓▓▀ ▐▓▓▓▓▓▓▄▄▄▄ ▓▓▓▓▓▓▄▄▄▄ ▐▓▓▓▓▓▌ ▐▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▀ ▐▓▓▓▓▓▓▓▓▓▓▌ ▓▓▓▓▓▓▓▓▓▓▌ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▀▀▓▓▓▓▓▓▄ ▐▓▓▓▓▓▓▀▀▀▀ ▓▓▓▓▓▓▀▀▀▀ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▀
▓▓▓▓▓▓ ▀▓▓▓▓▓▓▄ ▐▓▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓ ▐▓▓▓▓▓▌
▓▓▓▓▓▓▓▓▓▓ █▓▓▓▓▓▓▓▓▓ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓

Trust math, not hardware.
*/

pragma solidity 0.5.17;

import "@keep-network/keep-core/contracts/utils/BytesLib.sol";
import "@keep-network/keep-core/contracts/KeepToken.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol";
import "openzeppelin-solidity/contracts/cryptography/MerkleProof.sol";

/// @title ECDSA Rewards distributor
/// @notice This contract can be used by stakers to claim their rewards for
/// participation in the keep network for operating ECDSA nodes.
/// @dev This contract is based on the Uniswap's Merkle Distributor
/// https://github.com/Uniswap/merkle-distributor with some modifications:
/// - added a map of merkle root keys with the amount of KEEP (value) that will
/// be allocated for those merkle roots
/// - added receiveApproval() function that will be called each time to allocate
/// new KEEP rewards for a given merkle root. Merkle root is going to be generated
/// regulary (ex. every week) and it is also means that an interval for that
/// merkle root has passed
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
/// - changed code accordingly to process claimed rewards using a map of merkle
/// roots
contract ECDSARewardsDistributor is Ownable {
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
using SafeERC20 for KeepToken;
using BytesLib for bytes;
using SafeMath for uint256;
pdyraga marked this conversation as resolved.
Show resolved Hide resolved

KeepToken public token;

// This event is triggered whenever a call to #claim succeeds.
event RewardsClaimed(uint256 index, address account, uint256 amount);
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
// This event is triggered whenever rewards are allocated.
event RewardsAllocated(bytes32 merkleRoot, uint256 amount);

// Merkle root -> total amount for distribution for a given timeframe.
mapping(bytes32 => uint256) private merkleRoots;
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
// Bytes32 key is a merkle root and the value is a packed array of booleans.
mapping(bytes32 => mapping(uint256 => uint256)) private claimedBitMap;

constructor(address _token) public {
token = KeepToken(_token);
}

function claim(
uint256 index,
address account,
uint256 amount,
bytes32 merkleRoot,
bytes32[] calldata merkleProof
) external {
require(!isClaimed(index, merkleRoot), "Reward already claimed");
require(
merkleRoots[merkleRoot] > 0,
"Rewards must be allocated for a given merkle root"
);

// Verify the merkle proof.
bytes32 node = keccak256(abi.encodePacked(index, account, amount));

pdyraga marked this conversation as resolved.
Show resolved Hide resolved
require(
MerkleProof.verify(merkleProof, merkleRoot, node),
"Invalid proof"
);

// Mark it claimed and send the token.
_setClaimed(index, merkleRoot);
require(IERC20(token).transfer(account, amount), "Transfer failed");

// Update KEEP amount for the given merkleRoot
merkleRoots[merkleRoot] = merkleRoots[merkleRoot].sub(amount);

emit RewardsClaimed(index, account, amount);
}

/// Call receiveApproval to allocate amount of KEEP for a given merkle root.
/// @param _from The original sender of the tokens.
/// @param _amount The amount of KEEP tokens to fund.
/// @param _token The KEEP token to fund the rewards in.
/// @param _extraData Merkle root (32 bytes).
function receiveApproval(
address _from,
uint256 _amount,
address _token,
bytes memory _extraData
) public onlyOwner {
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
require(IERC20(_token) == token, "Unsupported token");
require(_extraData.length == 32, "Wrong length of merkle root");

token.safeTransferFrom(_from, address(this), _amount);

bytes32 merkleRoot = _extraData.toBytes32();

merkleRoots[merkleRoot] = _amount;

emit RewardsAllocated(merkleRoot, _amount);
}

function isClaimed(uint256 index, bytes32 merkleRoot)
public
view
returns (bool)
{
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
uint256 claimedWord = claimedBitMap[merkleRoot][claimedWordIndex];
uint256 mask = (1 << claimedBitIndex);
return claimedWord & mask == mask;
}

function getAllocation(bytes32 merkleRoot) public view returns (uint256) {
return merkleRoots[merkleRoot];
}

function _setClaimed(uint256 index, bytes32 merkleRoot) private {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
claimedBitMap[merkleRoot][claimedWordIndex] =
claimedBitMap[merkleRoot][claimedWordIndex] |
(1 << claimedBitIndex);
}
}
102 changes: 102 additions & 0 deletions staker-rewards/example-rewards-input.json
@@ -0,0 +1,102 @@
{
"0xF3c6F5F265F503f53EAD8aae90FC257A5aa49AC1": 1,
"0xB9CcDD7Bedb7157798e10Ff06C7F10e0F37C6BdD": 2,
"0xf94DbB18cc2a7852C9CEd052393d517408E8C20C": 3,
"0xf0591a60b8dBa2420408Acc5eDFA4f8A15d87308": 4,
"0x6A2dE67981CbE91209c1046D67eF7a45631d0666": 5,
"0x7C262baf13794f54e3514539c411f92716996C38": 6,
"0x57E7c6B647C004CFB7A38E08fDDef09Af5Ea55eD": 7,
"0x05fc93DeFFFe436822100E795F376228470FB514": 8,
"0x6b6C7139B48156d7EC90eD4c55C56bDFCB1C19D2": 9,
"0x7D13f07889F04a593a3E12f5d3f8Bf850d07465B": 10,
"0xb86739476a4820FcC39Ff1C413d9af0b96c1589F": 11,
"0xf66705E0Ae4e5DfC02b2633356f5305662F00d3b": 12,
"0xC7AA922f0823DeE2eD721E61ebCCF2F9596017Fb": 13,
"0x6E9Ead46916950088E236A77bb7b6309170827CA": 14,
"0x656231095A6700620062B308C900E124461C48B6": 15,
"0xCb73bE1851f2133895C05D408666475bA8Da351e": 16,
"0x6FCBCB45deE6649450932f7FF142C7c434CED9a6": 17,
"0xB34bf945E5a5698087820812e9CCBA0686D2a783": 18,
"0x31f161a781a30AB4bF4Bf11175e3098204FB5235": 19,
"0xde03a8041B40FF95F7F6b6ca0d1Da80fbBD07925": 20,
"0xdc7B752019AC5eFA067Bd3dE17Fc2D2c7C8d881e": 21,
"0xcB4A9ae3d5C4c9BF3c688d387230559018FB90C3": 22,
"0x97Ac383e64d5a1A2A08c646C87B6e0546F7c164B": 23,
"0xc0B7C64d370A9ffcFb9ef675809126c5cAfA9619": 24,
"0xC910240362A5dda9e6cE8fAc86C329864d9Da15d": 25,
"0x7e72833a9D8Da5458470f2B226f1c095ef335e86": 26,
"0x40DeF14b2793e99f1f453FbD98A0f251a1D19f4f": 27,
"0xBC61c73CFc191321DA837def848784c002279a01": 28,
"0x32Cc3F29cde7ac9c000FEbf0D8F28B94F1A34441": 29,
"0x538F43872aC14d3130721Df4F02a3Ff05053A2d9": 30,
"0xDE78e3462c9F976257E5E4Ed821BE7B306B23450": 31,
"0xF1079DD1048A65cA9f9153246164758203d1aEd5": 32,
"0xAb7Fb5958785b20bdccd2A65d15F139B60080fAc": 33,
"0xc6b467aCAa5B07b8182749524385B79DBb909B14": 34,
"0xE7fA80757FeAb870E0bF3b3dc8d4647f403A65ac": 35,
"0x72381936D8e22a52F9a6ea62e23628084085D05b": 36,
"0x3fC98BAD7384a354f8b083Fc5A7D621DF5fB9F41": 37,
"0x5e471D67A610f541B63a8789A9BE1F0fAcd9E244": 38,
"0x5a693Fc88b80Bd7e57f676Bd5e0945995f68bC47": 39,
"0x69eA0b9B0b489B87E061e3e85886D668b24157Ad": 40,
"0x7D0Fe663D9488F6793D813e51EC1DC600F289ad3": 41,
"0x162F49fE6F365d04Db07F77377699aeFE2E8A2cf": 42,
"0x8A1F2B46A35D10F0EafbE6c7f0671d8DB847dcA2": 43,
"0x4aA6E2Fe3f306CB777dFeA344daaAd33eB50f972": 44,
"0x1fCBa490902B2BD44ba98359C7075e2C8a2b9F15": 45,
"0xdd1f7Ea709BD594D834411AE22D81a5B6a91008F": 46,
"0x406b7968735b79688C6694634f2Ef5CF01c386F5": 47,
"0x7152dc7a0eC646A7bCD3b00EF4Ca984E337da2B3": 48,
"0xdf9424b7563A00386217471cfAC8944185505c56": 49,
"0xaDF30D969b396DFC5035Cb3921034Bfb86CC055d": 50,
"0xf970e1f7e89a57485E139F9EB4652181Ef270515": 51,
"0xBd8BcBdF78205590FD576acaf110d70069eE7125": 52,
"0xd7663Ca75082939012A9b5DCaFaDABEA51352F70": 53,
"0x75662678a74C6aD63501519F656CE4Db04e1EF49": 54,
"0xC1f94BCA2146B462685FC04Bf10f8b8CB7a305a3": 55,
"0x7DD3A4cCf156475AE927E9aedc91E9f33AABc79d": 56,
"0x85c5EE48A6687c9D903052a22f5764Bed2B4A6A8": 57,
"0x73f24B3cB7FDAf629d2DC44f67ADaA99005719B0": 58,
"0x9Cce64165E28dEA01a8b9c977F4dbD9D791EbcFf": 59,
"0xcB667d9F540E721858e77E6667e281Aa6fFD5C17": 60,
"0x0f39bceBE74751D89c37a2671DE0c750b71cA152": 61,
"0xC0CDEE637cd0Ef7Ef7ab2696ffADc9C78F4daa0B": 62,
"0x0cF605Ad65B1A541Ca6390606F944D176D5B9950": 63,
"0x614de94D2E18c174bc0155EFef55bAa9cB55bAf2": 64,
"0xa47A8fa265bf540184fA3499566761A608Be84EE": 65,
"0xD2ED9f212c6f5d127757fA700cc55235F5cBc167": 66,
"0xfA4563612C9De62302364ee8042635e44c8327fF": 67,
"0x0350D208F3D94Af84724e437fAa7ebe5A3C35aC7": 68,
"0x31de7522f31322081516703F78ce8eA128d9D6f7": 69,
"0x3e51a90d40F8dC43d2b8720B3671aa208b0316ac": 70,
"0x9BDFE65726326c104a302B172e49c4946E481306": 71,
"0xe7B6bdA3990D0F6892cB1c37F4f2867a8Df4Fe5e": 72,
"0x42B6cC78074eF1C5eC4AEe844B4B9c27b199831F": 73,
"0x1E5eF4320142B6721C27846c9Ab4D6F0a0aFD2CE": 74,
"0xA998c01B7c9674490480ec68bCf27C836CF9B495": 75,
"0xa087344Bc4A05D2885aa9531ae6694e0C5dEb728": 76,
"0x8874a8B06bd074953a9b22CaaC0Ce3bCf1260fB4": 77,
"0x8943b759EaAa0e51b93ddB12B19e9EB71A361c69": 78,
"0x4540e3Ef6dC7a420cd44767F98EF15BEEA28606D": 79,
"0x4d9366B189AA78B9764Bd50B22F9398ABB4AcFbD": 80,
"0x250fC1677986e6c3CEb348a378919f5b0Eb487ea": 81,
"0x1668395E2FDEC223E175111ff8bDce4180A3B680": 82,
"0x904dac1641aC5DAB76B7b2B2AB2779E98ac6BC74": 83,
"0x169D0Cc3e36D3C9253Dd8418421Af5F84a75EC76": 84,
"0x012ed55a0876Ea9e58277197DC14CbA47571CE28": 85,
"0xc07b1400fB950253fbfC5484601036f18c8A91CC": 86,
"0x3741c4751bBff7ba5D58BDA8F1c25Cb71b6b95D2": 87,
"0x01dC7F8C928CeA27D8fF928363111c291bEB20b1": 88,
"0x41560a0CC92C4267614721140a031aE20051Bb65": 89,
"0xF1d322d48E47eb4A806bAB843B5C79Af641bb8cD": 90,
"0x332BC77780057942cAa2c7bad21a04E91B5Ed687": 91,
"0x8519E69FfaF870479534b362ce34F666533aE758": 92,
"0xBF134F1BD442c77F01d4784D991F3c191ce700cf": 93,
"0xb7649E4000AD4748dc2907eCdcAC4ae3d59da0D5": 94,
"0x84664986ad6D1237010be3BFC0F88555edc6987f": 95,
"0x983a9ed0e4A274314231BFce58Ec973f0D298c9d": 96,
"0x9F0A64c6956D7205E883ae8A3C19577f1cadD78F": 97,
"0xB96cE59522314ACB1502Dc8d3e192995e36439c1": 98,
"0x5A553d59435Df0688fd5dEa1aa66C7430541ffB3": 99
}

19 changes: 19 additions & 0 deletions staker-rewards/generate-merkle-root.ts
@@ -0,0 +1,19 @@
import { program } from 'commander'

import * as fs from 'fs'
import { parseBalanceMap } from '../include/merkle-distributor/src/parse-balance-map'

program
.version('0.0.0')
.requiredOption(
'-i, --input <path>',
'input JSON file location containing a map of account addresses to string balances'
)

program.parse(process.argv)

const json = JSON.parse(fs.readFileSync(program.input, { encoding: 'utf8' }))

if (typeof json !== 'object') throw new Error('Invalid JSON')

fs.writeFileSync('./output-merkle-object.json', JSON.stringify(parseBalanceMap(json), null, 2))
24 changes: 24 additions & 0 deletions staker-rewards/package.json
@@ -0,0 +1,24 @@
{
"name": "@keep-network/keep-ecdsa",
"author": {
"name": "Moody Salem"
},
"description": "Script for merkle object generation",
"version": "1.0.0",
"keywords": [
"keep-network",
"rewards"
],
"repository": {
"type": "git",
"url": "ssh://git@github.com/keep-network/keep-ecdsa.git"
},
"engines": {
"node": ">=10"
},
"devDependencies": {
"commander": "^6.1.0",
"ts-node": "^8.5.4",
"typescript": "^3.7.3"
}
}