diff --git a/contracts/Minter.sol b/contracts/Minter.sol index 16b9c6a..89a9e32 100644 --- a/contracts/Minter.sol +++ b/contracts/Minter.sol @@ -24,11 +24,19 @@ 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 basis points + uint256 constant BPS = 1e4; /// @notice Struct to store information about each pool struct Pool { address receiver; uint256 amountPerBlock; + uint256 reductionBlocks; + uint256 reductionBps; + uint256 lastUpdate; + address callAddress; } /// @notice Array to store all pools Pool[] public pools; @@ -36,9 +44,9 @@ contract SPSMinter { /// @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 newReductionBps, uint256 newLastUpdate, address newCallAddress); /// @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, address newCallAddress); /// @notice Emitted when pool is removed event PoolRemoved(uint256 index, address indexed receiver, uint256 amount); /// @notice Emitted when admin address is updated @@ -76,7 +84,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 { @@ -85,7 +93,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){ @@ -97,9 +106,13 @@ 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; } } } @@ -107,12 +120,15 @@ contract SPSMinter { * @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, address newCallAddress) 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 <= BPS, "SPSMinter: newReductionBps cannot be larger than max allowed"); + pools.push(Pool(newReceiver, newAmount, newReductionBlocks, newReductionBps, block.number, newCallAddress)); + emit PoolAdded(newReceiver, newAmount, newReductionBlocks, newReductionBps, block.number, newCallAddress); } /** @@ -120,12 +136,43 @@ contract SPSMinter { * @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, address newCallAddress) external onlyAdmin { require(newAmount <= maxToPoolPerBlock, 'SPSMinter: Maximum amount per block reached'); + require(newReductionBps <= BPS, "SPSMinter: newReductionBps cannot be larger than max allowed"); mint(); - pools[index] = Pool(newReceiver, newAmount); - emit PoolUpdated(index, newReceiver, newAmount); + pools[index] = Pool(newReceiver, newAmount, newReductionBlocks, newReductionBps, block.number, newCallAddress); + emit PoolUpdated(index, newReceiver, newAmount, newReductionBlocks, newReductionBps, block.number, newCallAddress); + } + + /** + * @notice Update emissions for one pool + * @param index Index in the array of the pool + */ + function updateEmissions(uint256 index) public { + if (block.number - pools[index].lastUpdate > pools[index].reductionBlocks){ + pools[index].amountPerBlock = (pools[index].amountPerBlock * (BPS - pools[index].reductionBps)) / BPS; + if (minimumPayout > pools[index].amountPerBlock) pools[index].amountPerBlock = 0; + pools[index].lastUpdate = block.number; + + if (pools[index].callAddress != address(0)){ + // Call external contract, won't revert on failure. Used to "notify" other contract that there was a change + pools[index].callAddress.call{value: 0}(abi.encodeWithSignature("minterCall()")); + } + } + } + + /** + * @notice Update emissions for all pools + */ + function updateAllEmissions() public { + uint256 length = pools.length; + for (uint256 i = 0; i < length;){ + updateEmissions(i); + unchecked { ++i; } + } } /** diff --git a/scripts/deploy.js b/scripts/deploy.js index f86d4a2..7d67a81 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -1,8 +1,8 @@ const hre = require("hardhat"); -let newToken = '' -let startBlock = '' -let newAdmin = '' +let newToken = '0x44883053bfcaf90af0787618173dd56e8c2deb36' +let startBlock = '17511000' +let newAdmin = '0xdf5fd6b21e0e7ac559b41cf2597126b3714f432c' async function main() { const Minter = await hre.ethers.getContractFactory("Minter"); diff --git a/test/test.js b/test/test.js index 6aa80e5..6ed97f6 100644 --- a/test/test.js +++ b/test/test.js @@ -4,6 +4,7 @@ const { ethers } = require("hardhat"); let accounts; let testToken; let minter; +let zeroAddres = "0x0000000000000000000000000000000000000000" describe("Minter", async function () { @@ -23,14 +24,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, zeroAddres); 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) }); @@ -50,17 +51,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, zeroAddres); 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); @@ -139,7 +157,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, zeroAddres); await minter.mint(); //mine for the first time await minter.mint(); //should mine 0 tokens, since it's in the same block @@ -149,16 +167,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, zeroAddres); 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, zeroAddres); await add_2.wait(); let getPool_1 = await minter.getPool(0); @@ -167,9 +185,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) }); @@ -193,7 +211,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, zeroAddres); await add.wait(); let getPool = await minter.getPool(2); @@ -213,6 +231,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, zeroAddres); + 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, zeroAddres); + 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, zeroAddres); + 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, zeroAddres); + await add.wait(); + + let add2 = await minter.addPool(accounts[0].address, '2000000000000000000', 50, 100, zeroAddres); + 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, zeroAddres); + await add.wait(); + + let add2 = await minter.addPool(accounts[0].address, '2000000000000000000', 50, 100, zeroAddres); + 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, zeroAddres); + await add.wait(); + + let add2 = await minter.addPool(accounts[0].address, '200000000000000000', 50, 100, zeroAddres); + 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, zeroAddres); //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') + }); });