This repository has been archived by the owner on May 22, 2023. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #631 from keep-network/keep-rewards-distributor-be…
…neficiary Keep rewards distributor beneficiary Introducing a new token holder contract allowing the contract owner to allocate rewards to `ECDSARewardsDistributor` in phases. Each phase corresponds to a new reward interval allocated with its own Merkle root used by stakers to withdraw their rewards. The contract can receive funds from any other `PhasedEscrow` with `ECDSARewardsEscrowBeneficiary`. ECDSA staker rewards have been withdrawn from `ECDSARewards` contract to a `PhasedEscrow` (https://etherscan.io/address/0x973005c57872bd7bffb2157e88a6408d428a0f0a). The team initiated the upgrade in interval 2 and finalized it after interval 2 ended to implement [updates to the rewards mechanism](https://blog.keep.network/a-new-rewards-mechanism-deef3412c3e1). `ECDSARewardsDistributor` introduced in #627 will be allocated each interval with an allocation and Merkle root corresponding to that interval's rewards. The missing building block is `ECDSARewardsDistributorEscrow` introduced in this PR. `ECDSARewardsDistributorEscrow` should receive all funds from `PhasedEscrow` contract that holds now ECDSA staker KEEP rewards. Then, for each new reward interval, the contract owner will call `allocateInterval(bytes32 merkleRoot, uint256 amount)` function to allocate a new reward interval on `ECDSARewardsDistributor` from the tokens held by the contract. To withdraw funds from `PhasedEscrow` to `ECDSARewardsDistributorEscrow`, it is enough to deploy `ECDSARewardsEscrowBeneficiary` pointing to `ECDSARewardsDistributorEscrow` and set it as a beneficiary of `PhasedEscrow`. This scenario is exercised in `"funding"` describe in unit tests.
- Loading branch information
Showing
3 changed files
with
189 additions
and
3 deletions.
There are no files selected for viewing
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,42 @@ | ||
/** | ||
▓▓▌ ▓▓ ▐▓▓ ▓▓▓▓▓▓▓▓▓▓▌▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▄ | ||
▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▌▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ | ||
▓▓▓▓▓▓ ▓▓▓▓▓▓▓▀ ▐▓▓▓▓▓▓ ▐▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓ ▐▓▓▓▓▓▌ ▐▓▓▓▓▓▓ | ||
▓▓▓▓▓▓▄▄▓▓▓▓▓▓▓▀ ▐▓▓▓▓▓▓▄▄▄▄ ▓▓▓▓▓▓▄▄▄▄ ▐▓▓▓▓▓▌ ▐▓▓▓▓▓▓ | ||
▓▓▓▓▓▓▓▓▓▓▓▓▓▀ ▐▓▓▓▓▓▓▓▓▓▓▌ ▓▓▓▓▓▓▓▓▓▓▌ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ | ||
▓▓▓▓▓▓▀▀▓▓▓▓▓▓▄ ▐▓▓▓▓▓▓▀▀▀▀ ▓▓▓▓▓▓▀▀▀▀ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▀ | ||
▓▓▓▓▓▓ ▀▓▓▓▓▓▓▄ ▐▓▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓ ▐▓▓▓▓▓▌ | ||
▓▓▓▓▓▓▓▓▓▓ █▓▓▓▓▓▓▓▓▓ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ | ||
▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ | ||
Trust math, not hardware. | ||
*/ | ||
|
||
pragma solidity 0.5.17; | ||
|
||
import "@keep-network/keep-core/contracts/PhasedEscrow.sol"; | ||
import "./ECDSARewardsDistributor.sol"; | ||
|
||
/// @title ECDSARewardsDistributorEscrow | ||
/// @notice A token holder contract allowing contract owner to allocate rewards | ||
/// to ECDSARewardsDistributor in phases. Each phase corresponds to | ||
/// a new reward interval allocated with its own merkle root used by | ||
/// stakers to withdraw their rewards. | ||
contract ECDSARewardsDistributorEscrow is PhasedEscrow { | ||
ECDSARewardsDistributor public ecdsaRewardsDistributor; | ||
|
||
constructor(IERC20 _token, ECDSARewardsDistributor _ecdsaRewardsDistributor) | ||
public | ||
PhasedEscrow(_token) | ||
{ | ||
ecdsaRewardsDistributor = _ecdsaRewardsDistributor; | ||
} | ||
|
||
function allocateInterval(bytes32 merkleRoot, uint256 amount) | ||
external | ||
onlyOwner | ||
{ | ||
token.approve(address(ecdsaRewardsDistributor), amount); | ||
ecdsaRewardsDistributor.allocate(merkleRoot, amount); | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
144 changes: 144 additions & 0 deletions
144
solidity/test/rewards/TestECDSARewardsDistributorEscrow.js
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,144 @@ | ||
const {accounts, contract, web3} = require("@openzeppelin/test-environment") | ||
const {createSnapshot, restoreSnapshot} = require("../helpers/snapshot") | ||
const {expectRevert} = require("@openzeppelin/test-helpers") | ||
const {expect} = require("chai") | ||
|
||
const KeepToken = contract.fromArtifact("KeepToken") | ||
const PhasedEscrow = contract.fromArtifact("PhasedEscrow") | ||
const ECDSARewardsEscrowBeneficiary = contract.fromArtifact( | ||
"ECDSARewardsEscrowBeneficiary" | ||
) | ||
const ECDSARewardsDistributor = contract.fromArtifact("ECDSARewardsDistributor") | ||
const ECDSARewardsDistributorEscrow = contract.fromArtifact( | ||
"ECDSARewardsDistributorEscrow" | ||
) | ||
|
||
describe("ECDSARewardsDistributorEscrow", () => { | ||
const owner = accounts[1] | ||
const thirdParty = accounts[2] | ||
|
||
const tokenDecimalMultiplier = web3.utils.toBN(10).pow(web3.utils.toBN(18)) | ||
const totalRewards = web3.utils.toBN(178200000).mul(tokenDecimalMultiplier) | ||
|
||
let token | ||
let rewardsDistributor | ||
let escrow | ||
|
||
before(async () => { | ||
token = await KeepToken.new({from: owner}) | ||
rewardsDistributor = await ECDSARewardsDistributor.new(token.address, { | ||
from: owner, | ||
}) | ||
escrow = await ECDSARewardsDistributorEscrow.new( | ||
token.address, | ||
rewardsDistributor.address, | ||
{from: owner} | ||
) | ||
|
||
await rewardsDistributor.transferOwnership(escrow.address, {from: owner}) | ||
}) | ||
|
||
beforeEach(async () => { | ||
await createSnapshot() | ||
}) | ||
|
||
afterEach(async () => { | ||
await restoreSnapshot() | ||
}) | ||
|
||
describe("funding", async () => { | ||
it("can be done from phased escrow", async () => { | ||
const fundingEscrow = await PhasedEscrow.new(token.address, {from: owner}) | ||
await token.approveAndCall(fundingEscrow.address, totalRewards, "0x0", { | ||
from: owner, | ||
}) | ||
|
||
const beneficiary = await ECDSARewardsEscrowBeneficiary.new( | ||
token.address, | ||
escrow.address, | ||
{from: owner} | ||
) | ||
await beneficiary.transferOwnership(fundingEscrow.address, {from: owner}) | ||
await fundingEscrow.setBeneficiary(beneficiary.address, {from: owner}) | ||
|
||
await fundingEscrow.withdraw(totalRewards, {from: owner}) | ||
expect(await token.balanceOf(escrow.address)).to.eq.BN(totalRewards) | ||
}) | ||
}) | ||
|
||
describe("allocateInterval", async () => { | ||
const merkleRoot = | ||
"0x65b315f4565a40f738cbaaef7dbab4ddefa14620407507d0f2d5cdbd1d8063f6" | ||
const amount = web3.utils.toBN(999998997) | ||
|
||
beforeEach(async () => { | ||
// The initial state set up with approveAndCall and confirmed with the | ||
// assertion below is the escrow state after getting funded from | ||
// another PhasedEscrow, as demonstrated in "funding" describe. | ||
// This reflects the flow of funds on mainnet for the updated ECDSA | ||
// staker rewards deployment. | ||
await token.approveAndCall(escrow.address, totalRewards, "0x0", { | ||
from: owner, | ||
}) | ||
expect(await token.balanceOf(escrow.address)).to.eq.BN(totalRewards) | ||
}) | ||
|
||
it("can not be called by non-owner", async () => { | ||
await expectRevert( | ||
escrow.allocateInterval(merkleRoot, amount, {from: thirdParty}), | ||
"Ownable: caller is not the owner" | ||
) | ||
}) | ||
|
||
it("can be called by owner", async () => { | ||
await escrow.allocateInterval(merkleRoot, amount, {from: owner}) | ||
// ok, no reverts | ||
}) | ||
|
||
it("allocates reward distribution", async () => { | ||
await escrow.allocateInterval(merkleRoot, amount, {from: owner}) | ||
|
||
const eventList = await rewardsDistributor.getPastEvents( | ||
"RewardsAllocated", | ||
{ | ||
fromBlock: 0, | ||
toBlock: "latest", | ||
} | ||
) | ||
|
||
expect(eventList.length).to.equal(1, "incorrect number of emitted events") | ||
const event = eventList[0].returnValues | ||
expect(event.merkleRoot).to.equal(merkleRoot, "unexpected merkle root") | ||
expect(event.amount).to.eq.BN(amount) | ||
}) | ||
|
||
it("allocates multiple reward distributions", async () => { | ||
const merkleRoot2 = | ||
"0xa7418520411d369b511eabb10ffb214c72b521ca0f6bd021fa83d9c47e65227e" | ||
const amount2 = web3.utils.toBN(1337) | ||
|
||
await escrow.allocateInterval(merkleRoot, amount, {from: owner}) | ||
await escrow.allocateInterval(merkleRoot2, amount2, {from: owner}) | ||
|
||
const eventList = await rewardsDistributor.getPastEvents( | ||
"RewardsAllocated", | ||
{ | ||
fromBlock: 0, | ||
toBlock: "latest", | ||
} | ||
) | ||
|
||
expect(eventList.length).to.equal(2, "incorrect number of emitted events") | ||
const event1 = eventList[0].returnValues | ||
expect(event1.merkleRoot).to.equal(merkleRoot, "unexpected merkle root") | ||
expect(event1.amount).to.eq.BN(amount) | ||
const event2 = eventList[1].returnValues | ||
expect(event2.merkleRoot).to.equal(merkleRoot2, "unexpected merkle root") | ||
expect(event2.amount).to.eq.BN(amount2) | ||
|
||
expect(await token.balanceOf(rewardsDistributor.address)).to.eq.BN( | ||
amount.add(amount2) | ||
) | ||
}) | ||
}) | ||
}) |