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

Reduce rewards every X blocks #3

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
63 changes: 51 additions & 12 deletions contracts/Minter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,26 @@ contract SPSMinter {
uint256 constant public poolsCap = 100;
/// @notice Maximum amount per block to each pool
uint256 constant public maxToPoolPerBlock = 50 ether;
// @notice minimum payout before it's rounded to 0
uint256 constant public minimumPayout = 0.1 ether;

/// @notice Struct to store information about each pool
struct Pool {
address receiver;
uint256 amountPerBlock;
uint256 reductionBlocks;
uint256 reductionBps;
uint256 lastUpdate;
}
/// @notice Array to store all pools
Pool[] public pools;

/// @notice Emitted when mint() is called
event Mint(address indexed receiver, uint256 amount);
/// @notice Emitted when pool is added
event PoolAdded(address indexed newReceiver, uint256 newAmount);
event PoolAdded(address indexed newReceiver, uint256 newAmount, uint256 newReductionBlocks, uint256 newReductionPercentage, uint256 newLastUpdate);
apbendi marked this conversation as resolved.
Show resolved Hide resolved
/// @notice Emitted when pool is updated
event PoolUpdated(uint256 index, address indexed newReceiver, uint256 newAmount);
event PoolUpdated(uint256 index, address indexed newReceiver, uint256 newAmount, uint256 newReductionBlocks, uint256 newReductionBps, uint256 newLastUpdate);
/// @notice Emitted when pool is removed
event PoolRemoved(uint256 index, address indexed receiver, uint256 amount);
/// @notice Emitted when admin address is updated
Expand Down Expand Up @@ -76,7 +81,7 @@ contract SPSMinter {
function mint() public {
require(totalMinted < cap, "SPSMinter: Cap reached");
require(block.number > lastMintBlock, "SPSMinter: Mint block not yet reached");

updateAllEmissions();

uint256 mintDifference;
unchecked {
Expand All @@ -85,7 +90,8 @@ contract SPSMinter {

lastMintBlock = block.number;

for (uint256 i = 0; i < pools.length; i++){
uint256 poolsLength = pools.length;
for (uint256 i = 0; i < poolsLength;){
uint256 amount = pools[i].amountPerBlock * mintDifference;

if(totalMinted + amount >= cap){
Expand All @@ -97,35 +103,68 @@ contract SPSMinter {
unchecked {
totalMinted = totalMinted + amount;
}
token.mint(pools[i].receiver, amount);

emit Mint(pools[i].receiver, amount);
if (amount > 0){
token.mint(pools[i].receiver, amount);
emit Mint(pools[i].receiver, amount);
}

unchecked { ++i; }
}
}

/**
* @notice Add new pool, can be called by admin
* @param newReceiver Address of the receiver
* @param newAmount Amount of tokens per block
* @param newReductionBlocks Number of blocks between emission reduction
* @param newReductionBps Number of basis points to reduce emission
*/
function addPool(address newReceiver, uint256 newAmount) external onlyAdmin {
function addPool(address newReceiver, uint256 newAmount, uint256 newReductionBlocks, uint256 newReductionBps) external onlyAdmin {
require(pools.length < poolsCap, 'SPSMinter: Pools cap reached');
require(newAmount <= maxToPoolPerBlock, 'SPSMinter: Maximum amount per block reached');
pools.push(Pool(newReceiver, newAmount));
emit PoolAdded(newReceiver, newAmount);
require(newReductionBps <= 10000, "SPSMinter: newReductionBps cannot be larger than 10000");
apbendi marked this conversation as resolved.
Show resolved Hide resolved
pools.push(Pool(newReceiver, newAmount, newReductionBlocks, newReductionBps, block.number));
emit PoolAdded(newReceiver, newAmount, newReductionBlocks, newReductionBps, block.number);
}

/**
* @notice Update pool, can be called by admin
* @param index Index in the array of the pool
* @param newReceiver Address of the receiver
* @param newAmount Amount of tokens per block
* @param newReductionBlocks Number of blocks between emission reduction
* @param newReductionBps Number of basis points (1 bps = 1/100th of 1%)to reduce emission
*/
function updatePool(uint256 index, address newReceiver, uint256 newAmount) external onlyAdmin {
function updatePool(uint256 index, address newReceiver, uint256 newAmount, uint256 newReductionBlocks, uint256 newReductionBps) external onlyAdmin {
require(newAmount <= maxToPoolPerBlock, 'SPSMinter: Maximum amount per block reached');
require(newReductionBps <= 10000, "SPSMinter: newReductionBps cannot be larger than 10000");
mint();
pools[index] = Pool(newReceiver, newAmount);
emit PoolUpdated(index, newReceiver, newAmount);
pools[index] = Pool(newReceiver, newAmount, newReductionBlocks, newReductionBps, block.number);
emit PoolUpdated(index, newReceiver, newAmount, newReductionBlocks, newReductionBps, block.number);
}

/**
* @notice Update emissions for one pool
* @param index Index in the array of the pool
*/
function updateEmissions(uint256 index) public {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know why this didn't occur to me earlier, but I think we can remove a lot of complexity and gas cost by calculating the current emission rather than actually updating the state like this. If you replace lastUpdate in the Pool struct with something like baseBlock:

  struct Pool {
    address receiver;
    uint256 amountPerBlock;
    uint256 reductionBlocks;
    uint256 reductionBps;
    uint256 baseBlock;
  }

Then you should be able to do something like this (untested):

function getPoolEmission(uint256 index) public view returns (uint256) {
    uint256 _blocksElapsed = block.number - pools[index].baseBlock;
    uint256 _reductions = _blocksElapsed / pools[index].reductionBlocks;
    uint256 _reductionBps = _reductions * pools[index].reductionBps;
    return (pools[index].amountPerBlock * (BPS - _reductionBps)) / BPS;
 }

Inside the mint function you can then use the getter above to calculate the amount

uint256 amount = getPoolEmission(i) * mintDifference;

Copy link
Contributor Author

@fbslo fbslo Apr 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about the same approach, but the requirements I was given were to use exponential decay ("...after the first day (28800 blocks) it would drop to 0.99 SPS / block, and then the next day it would drop to 0.9801 (1% reduction from 0.99), then 0.9702, etc..."), I tried implementing it, but the standard x*(1-y)**z) formula doesn't work in solidity.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yes, great point! My version is not exponential. Missed that.

I tried implementing it, but the standard x*(1-y)**z) formula doesn't work in solidity.

Just out of curiosity, what kind of issue did you hit implementing this formula?

If it's not possible, we should still be able to do something iteratively. Here's a quick attempt (not tested):

  function getPoolEmission(uint256 index) public view returns (uint256) {
    uint256 _blocksElapsed = block.number - pools[index].baseBlock;
    uint256 _reductions = _blocksElapsed / pools[index].reductionBlocks;

    uint256 _reductionBps = _reductions * pools[index].reductionBps;
    uint256 _emission = pools[index].amountPerBlock;

    for (uint256 i = 0; i < _reductions; i++) {
      _emission = _emission * (BPS - _reductionBps) / BPS;

      if (minimumPayout > _emission) {
        return 0;
      }
    }

    return _emission;
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with using x*(1-y)**z) formula was that 1-z should be less than 1, which doesn't work with solidity.

I was thinking about an iterative approach but wasn't sure if gas savings are worth it, did some testing now, and while it's cheaper (~1k gas), I don't think it's worth changing it considering it will be used on BSC anyway, so gas cost is not an issue.

While using a non-iterative approach requires calling updateEmissions every reductionBlocks, minter will be integrated with a yield farming contract where users will call it, so I don't think that will be an issue either.

if (block.number - pools[index].lastUpdate > pools[index].reductionBlocks){
pools[index].amountPerBlock = (pools[index].amountPerBlock * (10000 - pools[index].reductionBps)) / 10000;
if (minimumPayout > pools[index].amountPerBlock) pools[index].amountPerBlock = 0;
pools[index].lastUpdate = block.number;
}
}

/**
* @notice Update emissions for all pools
*/
function updateAllEmissions() public {
uint256 length = pools.length;
for (uint256 i = 0; i < length;){
updateEmissions(i);
unchecked { ++i; }
}
}

/**
Expand Down
169 changes: 158 additions & 11 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ describe("Minter", async function () {
it("should add pool", async function () {
await init()

let add = await minter.addPool(accounts[0].address, 1);
let add = await minter.addPool(accounts[0].address, '100000000000', 50, 100);
await add.wait();

let getPool = await minter.getPool(0);
let getPoolLength = await minter.getPoolLength();

expect(getPool.receiver).to.equal(accounts[0].address)
expect(getPool.amountPerBlock.toNumber()).to.equal(1)
expect(getPool.amountPerBlock.toNumber()).to.equal(100000000000)
expect(getPoolLength.toNumber()).to.equal(1)
});

Expand All @@ -50,17 +50,34 @@ describe("Minter", async function () {
});

it("should update pool", async function () {
let update = await minter.updatePool(0, accounts[0].address, 10);
let update = await minter.updatePool(0, accounts[0].address, '100000000000000000', 50, 100);
await update.wait();

let getPool = await minter.getPool(0);
let getPoolLength = await minter.getPoolLength();

expect(getPool.receiver).to.equal(accounts[0].address)
expect(getPool.amountPerBlock.toNumber()).to.equal(10)
expect(getPool.amountPerBlock.toString()).to.equal('100000000000000000')
expect(getPoolLength.toNumber()).to.equal(1)
});

it("should mint tokens after reduction", async function () {
let startLastMintBlock = await minter.lastMintBlock()
await mineBlocks(60)

let balanceBefore = await testToken.balanceOf(accounts[0].address)

let mint = await minter.mint();
await mint.wait();

let endLastMintBlock = await minter.lastMintBlock()

let balance = await testToken.balanceOf(accounts[0].address)
let getPool = await minter.getPool(0);

expect(balance.toNumber() - balanceBefore.toNumber()).to.equal(getPool.amountPerBlock.toNumber() * (endLastMintBlock - startLastMintBlock))
});

it("should fail when removing invalid pool if there are pools", async function () {
try {
let remove = await minter.removePool(1);
Expand Down Expand Up @@ -139,7 +156,7 @@ describe("Minter", async function () {

let lastBlock = await minter.lastMintBlock()

await minter.addPool(accounts[0].address, '1000');
await minter.addPool(accounts[0].address, '10000000000', 50, 100);
await minter.mint(); //mine for the first time
await minter.mint(); //should mine 0 tokens, since it's in the same block

Expand All @@ -149,16 +166,16 @@ describe("Minter", async function () {

await network.provider.send("evm_setAutomine", [true]);

expect(getSupply.toString()).to.equal("1000")
expect(getSupply.toString()).to.equal("10000000000")
});

it("should add multiple pools", async function () {
await init()

let add = await minter.addPool(accounts[0].address, 1);
let add = await minter.addPool(accounts[0].address, '10000000000', 50, 100);
await add.wait();

let add_2 = await minter.addPool(accounts[1].address, 5);
let add_2 = await minter.addPool(accounts[1].address, '50000000000', 50, 100);
await add_2.wait();

let getPool_1 = await minter.getPool(0);
Expand All @@ -167,9 +184,9 @@ describe("Minter", async function () {
let getPoolLength = await minter.getPoolLength();

expect(getPool_1.receiver).to.equal(accounts[0].address)
expect(getPool_1.amountPerBlock.toNumber()).to.equal(1)
expect(getPool_1.amountPerBlock.toString()).to.equal('10000000000')
expect(getPool_2.receiver).to.equal(accounts[1].address)
expect(getPool_2.amountPerBlock.toNumber()).to.equal(5)
expect(getPool_2.amountPerBlock.toString()).to.equal('50000000000')
expect(getPoolLength.toNumber()).to.equal(2)
});

Expand All @@ -193,7 +210,7 @@ describe("Minter", async function () {
});

it("should add pool with 0 emission and mint 0 tokens to it after 10 blocks", async function () {
let add = await minter.addPool(accounts[2].address, 0);
let add = await minter.addPool(accounts[2].address, 0, 50, 100);
await add.wait();

let getPool = await minter.getPool(2);
Expand All @@ -213,6 +230,136 @@ describe("Minter", async function () {

expect(balance.toNumber()).to.equal(0)
});

it("should update emission of one pool by index", async function () {
await init()

let add = await minter.addPool(accounts[0].address, '1000000000000000000', 50, 100);
await add.wait();

await mineBlocks(60);

await (await minter.updateEmissions(0)).wait()

let pool = await minter.getPool(0)

expect(pool.amountPerBlock.toString()).to.equal('990000000000000000')
});

it("should not update emission of one pool by index if it's not the time yet", async function () {
await init()

let add = await minter.addPool(accounts[0].address, '1000000000000000000', 50, 100);
await add.wait();

await mineBlocks(60);

await (await minter.updateEmissions(0)).wait()
await (await minter.updateEmissions(0)).wait()
await (await minter.updateEmissions(0)).wait()

let pool = await minter.getPool(0)

expect(pool.amountPerBlock.toString()).to.equal('990000000000000000')
});

it("should update emission of one pool by index if enough time has passed", async function () {
await init()

let add = await minter.addPool(accounts[0].address, '1000000000000000000', 50, 100);
await add.wait();

await mineBlocks(60);

await (await minter.updateEmissions(0)).wait()
await mineBlocks(60);
await (await minter.updateEmissions(0)).wait()

let pool = await minter.getPool(0)

expect(pool.amountPerBlock.toString()).to.equal('980100000000000000')
});

it("should update emission of all pools", async function () {
await init()

let add = await minter.addPool(accounts[0].address, '1000000000000000000', 50, 100);
await add.wait();

let add2 = await minter.addPool(accounts[0].address, '2000000000000000000', 50, 100);
await add2.wait();

await mineBlocks(60);

await (await minter.updateAllEmissions()).wait()

let pool = await minter.getPool(0)
let pool2 = await minter.getPool(1)

expect(pool.amountPerBlock.toString()).to.equal('990000000000000000')
expect(pool2.amountPerBlock.toString()).to.equal('1980000000000000000')
});

it("should not update emission of all pools if it's not the time yet", async function () {
await init()

let add = await minter.addPool(accounts[0].address, '1000000000000000000', 50, 100);
await add.wait();

let add2 = await minter.addPool(accounts[0].address, '2000000000000000000', 50, 100);
await add2.wait();

await mineBlocks(60);

await (await minter.updateAllEmissions()).wait()
await (await minter.updateAllEmissions()).wait()
await (await minter.updateAllEmissions()).wait()

let pool = await minter.getPool(0)
let pool2 = await minter.getPool(1)

expect(pool.amountPerBlock.toString()).to.equal('990000000000000000')
expect(pool2.amountPerBlock.toString()).to.equal('1980000000000000000')
});

it("should update emission of all pools if enough time has passed", async function () {
await init()

let add = await minter.addPool(accounts[0].address, '1000000000000000000', 50, 100);
await add.wait();

let add2 = await minter.addPool(accounts[0].address, '200000000000000000', 50, 100);
await add2.wait();

await mineBlocks(60);

await (await minter.updateAllEmissions()).wait()
await mineBlocks(60);
await (await minter.updateAllEmissions()).wait()

let pool = await minter.getPool(0)
let pool2 = await minter.getPool(1)

expect(pool.amountPerBlock.toString()).to.equal('980100000000000000')
expect(pool2.amountPerBlock.toString()).to.equal('196020000000000000')
});

it("should update amountPerBlock to 0 tokens if amount is under 0.1 token", async function () {
await init()

let add = await minter.addPool(accounts[0].address, '10000000000000000', 50, 100); //0.01 token per block
await add.wait();

await mineBlocks(60);

await (await minter.updateAllEmissions()).wait()
await mineBlocks(60);
await (await minter.updateAllEmissions()).wait()

let pool = await minter.getPool(0)

expect(pool.amountPerBlock.toString()).to.equal('0')
});
});


Expand Down