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

Remove timestamp argument from RewardRatePool query functions #47

Merged
merged 4 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 41 additions & 21 deletions contracts/RewardRatePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

/**
* @title Reward Rate Pool Contract
* @dev Implements reward distribution based on a fixed annual interest rate.
* @dev Implements reward distribution based on a fixed simple annual payout rate.
*/
contract RewardRatePool is Initializable, Ownable2StepUpgradeable {
using SafeERC20 for IERC20;
Expand All @@ -18,12 +18,34 @@ contract RewardRatePool is Initializable, Ownable2StepUpgradeable {
address public beneficiary;
uint256 public totalPaidOut;
uint256 public lastPaidOutTime;
uint64 public constant ANNUAL_INTEREST_RATE = 145; // 14.5% in tenths of a percent
// The simple annual payout rate used for reward calculations. This 15.1% value is chosen so
// that, with daily payouts computed at this simple rate, the total (compounded) payout over a
// year will equal 14% of the amount that was in the pool at the beginning of the year.
//
// To elaborate where this comes from, with daily payout r (=R/365, that is, the annual payout
// divided by 365 days per year), with starting balance P, the payout on day 1 equals:
// rP
// leaving P-rP = (1-r)P in the pool, and so the day 2 payout equals:
// r(1-r)P
// leaving (1-r)P - r(1-r)P = (1-r)(1-r)P = (1-r)^2 P in the pool. And so on, so that
// after 365 days there will be (1-r)^365 P left in the pool.
//
// To hit a target of 14% over a year, then, we want to find r to solve:
// (1-r)^{365} P = (1-.14) P
// i.e.
// (1-r)^{365} = 0.86
// and then we multiply the `r` solution by 365 to get the simple annual rate with daily
// payouts. Rounded to the nearest 10th of a percent, that value equals 0.151, i.e. 15.1%.
//
// There is, of course, some slight imprecision here from the rounding and because the precise
// payout frequency depends on the times between calling this smart contract, but the errors are
// expected to be small, keeping this close to the 14% target.
uint64 public constant ANNUAL_SIMPLE_PAYOUT_RATE = 151; // 15.1% in tenths of a percent
uint64 public constant BASIS_POINTS = 1000; // Basis points for percentage calculation

/**
* @dev Sets the initial beneficiary and SENT token address.
* @param _beneficiary Address that will receive the interest payouts.
* @param _beneficiary Address that will receive the payouts.
* @param _sent Address of the SENT ERC20 token contract.
*/
function initialize(address _beneficiary, address _sent) public initializer {
Expand All @@ -44,11 +66,11 @@ contract RewardRatePool is Initializable, Ownable2StepUpgradeable {
//////////////////////////////////////////////////////////////

/**
* @dev Calculates and releases the due interest payout to the beneficiary.
* @dev Calculates and releases the due payout to the beneficiary.
* Updates the total paid out and the last payout time.
*/
function payoutReleased() public {
uint256 newTotalPaidOut = calculateReleasedAmount(block.timestamp);
uint256 newTotalPaidOut = calculateReleasedAmount();
uint256 released = newTotalPaidOut - totalPaidOut;
totalPaidOut = newTotalPaidOut;
lastPaidOutTime = block.timestamp;
Expand All @@ -70,14 +92,13 @@ contract RewardRatePool is Initializable, Ownable2StepUpgradeable {
//////////////////////////////////////////////////////////////

/**
* @dev Returns the block reward for a 2 minutes block at a certain timestamp.
* @param timestamp The timestamp of the block.
* @dev Returns the current 2-minute block reward.
* @return The calculated block reward.
*/
function rewardRate(uint256 timestamp) public view returns (uint256) {
uint256 alreadyReleased = calculateReleasedAmount(timestamp);
function rewardRate() public view returns (uint256) {
uint256 alreadyReleased = calculateReleasedAmount();
uint256 totalDeposited = calculateTotalDeposited();
return calculateInterestAmount(totalDeposited - alreadyReleased, 2 minutes);
return calculatePayoutAmount(totalDeposited - alreadyReleased, 2 minutes);
}

/**
Expand All @@ -89,22 +110,21 @@ contract RewardRatePool is Initializable, Ownable2StepUpgradeable {
}

/**
* @dev Calculates the amount of SENT tokens released up to a specific timestamp.
* @param timestamp The timestamp until which to calculate the released amount.
* @dev Calculates the amount of SENT tokens released up to the current time.
* @return The calculated amount of SENT tokens released.
*/
function calculateReleasedAmount(uint256 timestamp) public view returns (uint256) {
uint256 timeElapsed = timestamp - lastPaidOutTime;
return totalPaidOut + calculateInterestAmount(SENT.balanceOf(address(this)), timeElapsed);
function calculateReleasedAmount() public view returns (uint256) {
uint256 timeElapsed = block.timestamp - lastPaidOutTime;
return totalPaidOut + calculatePayoutAmount(SENT.balanceOf(address(this)), timeElapsed);
}

/**
* @dev Calculates 14.5% annual interest for a given balance and time period.
* @param balance The principal balance to calculate interest on.
* @param timeElapsed The time period over which to calculate interest.
* @return The calculated interest amount.
* @dev Calculates payout amount for a given balance and time period.
* @param balance The principal balance to calculate payout from.
* @param timeElapsed The time period over which to calculate payout.
* @return The calculated payout amount.
*/
function calculateInterestAmount(uint256 balance, uint256 timeElapsed) public pure returns (uint256) {
return (balance * ANNUAL_INTEREST_RATE * timeElapsed) / (BASIS_POINTS * 365 days);
function calculatePayoutAmount(uint256 balance, uint256 timeElapsed) public pure returns (uint256) {
return (balance * ANNUAL_SIMPLE_PAYOUT_RATE * timeElapsed) / (BASIS_POINTS * 365 days);
}
}
89 changes: 50 additions & 39 deletions test/unit-js/RewardRatePool.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ describe("RewardRatePool Contract Tests", function () {
let serviceNodeRewards;
let RewardRatePool;
let rewardRatePool;
let principal = 100000;
let bigAtomicPrincipal = ethers.parseUnits(principal.toString(), 9);
let seconds_in_year = 365*24*60*60;
let seconds_in_2_minutes = 2*60;
const principal = 100000;
const bigAtomicPrincipal = ethers.parseUnits(principal.toString(), 9);
const seconds_in_day = 24*60*60;
const seconds_in_year = 365 * seconds_in_day;
const seconds_in_2_minutes = 2*60;

beforeEach(async function () {
// Deploy a mock ERC20 token
Expand All @@ -34,65 +35,75 @@ describe("RewardRatePool Contract Tests", function () {
rewardRatePool = await upgrades.deployProxy(RewardRatePool, [await serviceNodeRewards.getAddress(), await mockERC20.getAddress()]);
});

it("Should have the correct interest rate", async function () {
await expect(await rewardRatePool.ANNUAL_INTEREST_RATE())
.to.equal(145);
it("Should have the correct payout rate", async function () {
await expect(await rewardRatePool.ANNUAL_SIMPLE_PAYOUT_RATE())
.to.equal(151);
});

it("should calculate 14.5% interest correctly", async function () {
await expect(await rewardRatePool.calculateInterestAmount(principal, seconds_in_year))
.to.equal((principal * 0.145).toFixed(0));
it("should calculate 15.1% payout correctly", async function () {
await expect(await rewardRatePool.calculatePayoutAmount(principal, seconds_in_year))
.to.equal((principal * 0.151).toFixed(0));
});

it("should calculate 14.5% released correctly", async function () {
it("should calculate 15.1% released correctly", async function () {
await time.setNextBlockTimestamp(await time.latest() + 42)
await mockERC20.transfer(rewardRatePool, bigAtomicPrincipal);
let last_paid = await rewardRatePool.lastPaidOutTime();
await expect(await rewardRatePool.calculateReleasedAmount(last_paid + BigInt(seconds_in_year)))
.to.equal(ethers.parseUnits((principal * 0.145).toFixed(0).toString(), 9));
await expect(await rewardRatePool.calculateReleasedAmount())
// Block timestamp has advanced by 42 seconds, so there will be a corresponding released amount:
.to.equal(ethers.parseUnits((principal * 0.151).toFixed().toString(), 9) * BigInt(42) / BigInt(seconds_in_year));
});

it("should calculate 14.5% released correctly", async function () {

it("should calculate reward rate", async function () {
await time.setNextBlockTimestamp(await time.latest() + 1)
await mockERC20.transfer(rewardRatePool, bigAtomicPrincipal);
let last_paid = await rewardRatePool.lastPaidOutTime();
await expect(await rewardRatePool.calculateReleasedAmount(last_paid))
.to.equal(0);
// The -1 here is because the block time advances by at least 1, and that's enough to just
// slightly reduce our reward by one atomic unit with the specific values we use here.
await expect(await rewardRatePool.rewardRate())
.to.equal(ethers.parseUnits((principal * 0.151).toFixed().toString(), 9) * BigInt(seconds_in_2_minutes) / BigInt(seconds_in_year) - BigInt(1));
});

it("should calculate reward rate", async function () {
it("should should be ~14.017% with daily withdrawals", async function () {
await mockERC20.transfer(rewardRatePool, bigAtomicPrincipal);
let last_paid = await rewardRatePool.lastPaidOutTime();
await expect(await rewardRatePool.rewardRate(last_paid))
.to.equal(ethers.parseUnits((principal * 0.145).toFixed().toString(), 9) * BigInt(seconds_in_2_minutes) / BigInt(seconds_in_year));
let t = await rewardRatePool.lastPaidOutTime();
let total_paid = 0;
for (let i = 0; i < 365; i++) {
t += BigInt(seconds_in_day);
await time.setNextBlockTimestamp(t);
await rewardRatePool.payoutReleased();
}
await expect(await rewardRatePool.calculateReleasedAmount())
.to.equal(bigAtomicPrincipal * BigInt("14017916502388") / BigInt("100000000000000"));
});

it("should should be ~14.098% with monthly withdrawals", async function () {
await mockERC20.transfer(rewardRatePool, bigAtomicPrincipal);
let t = await rewardRatePool.lastPaidOutTime();
let total_paid = 0;
for (let i = 0; i < 12; i++) {
t += BigInt(seconds_in_year / 12);
await time.setNextBlockTimestamp(t);
await rewardRatePool.payoutReleased();
}
await expect(await rewardRatePool.calculateReleasedAmount())
.to.equal(bigAtomicPrincipal * BigInt("14097571610714") / BigInt("100000000000000"));
});

it("should be able to release funds to the rewards contract", async function () {
await mockERC20.transfer(rewardRatePool, bigAtomicPrincipal);
expect(await mockERC20.balanceOf(rewardRatePool)).to.equal(bigAtomicPrincipal);

// NOTE: Advance time and test the payout release
let last_paid = await rewardRatePool.lastPaidOutTime();
let total_payout = await rewardRatePool.calculateReleasedAmount(last_paid + BigInt(seconds_in_year));

let last_paid = await rewardRatePool.lastPaidOutTime();
await time.setNextBlockTimestamp(last_paid + BigInt(seconds_in_year));
await expect(await rewardRatePool.payoutReleased()).to
.emit(rewardRatePool, 'FundsReleased')
.withArgs(total_payout);

expect(await mockERC20.balanceOf(serviceNodeRewards)).to
.equal(total_payout);

.withArgs(15100000000000);

// NOTE: Advance time again and test the payout release
last_paid = await rewardRatePool.lastPaidOutTime();
let next_total_payout = await rewardRatePool.calculateReleasedAmount(last_paid + BigInt(seconds_in_year));
total_payout = next_total_payout - total_payout;

last_paid = await rewardRatePool.lastPaidOutTime();
await time.setNextBlockTimestamp(last_paid + BigInt(seconds_in_year));
await expect(await rewardRatePool.payoutReleased()).to
.emit(rewardRatePool, 'FundsReleased')
.withArgs(total_payout);

expect(await mockERC20.balanceOf(serviceNodeRewards)).to
.equal(next_total_payout);
.withArgs(12819900000000); // (10000 - 15.1%) * 15.1%
});
});
Loading