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

Commit

Permalink
Merge pull request #631 from keep-network/keep-rewards-distributor-be…
Browse files Browse the repository at this point in the history
…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
nkuba committed Dec 10, 2020
2 parents bb9abbc + 0e64044 commit decaa4a
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 3 deletions.
42 changes: 42 additions & 0 deletions solidity/contracts/ECDSARewardsDistributorEscrow.sol
@@ -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);
}
}
6 changes: 3 additions & 3 deletions solidity/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

144 changes: 144 additions & 0 deletions solidity/test/rewards/TestECDSARewardsDistributorEscrow.js
@@ -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)
)
})
})
})

0 comments on commit decaa4a

Please sign in to comment.