Skip to content

Commit

Permalink
Merge pull request #47 from jagerman/use-block-timestamp
Browse files Browse the repository at this point in the history
Remove timestamp argument from RewardRatePool query functions
  • Loading branch information
Doy-lee authored Jul 4, 2024
2 parents 2382076 + 5e464cf commit fb139e3
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 60 deletions.
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%
});
});

0 comments on commit fb139e3

Please sign in to comment.