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

PIP-26: Adjust Emission Rate #58

Merged
merged 11 commits into from
Jun 14, 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
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ jobs:
run: |
forge --version
forge build --sizes
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
id: build

- name: Run Forge tests
# ! revert back to FOUNDRY_PROFILE=intense forge test -vvv
run: forge test -vvv
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
id: test
23 changes: 16 additions & 7 deletions src/DefaultEmissionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import {PowUtil} from "./lib/PowUtil.sol";
/// @title Default Emission Manager
/// @author Polygon Labs (@DhairyaSethi, @gretzke, @qedk, @simonDos)
/// @notice A default emission manager implementation for the Polygon ERC20 token contract on Ethereum L1
/// @dev The contract allows for a 3% mint per year (compounded). 2% staking layer and 1% treasury
/// @dev The contract allows for a 2.5% mint per year (compounded). 1.5% staking layer and 1% treasury
web3security marked this conversation as resolved.
Show resolved Hide resolved
/// @custom:security-contact [email protected]
contract DefaultEmissionManager is Ownable2StepUpgradeable, IDefaultEmissionManager {
using SafeERC20 for IPolygonEcosystemToken;

uint256 public constant INTEREST_PER_YEAR_LOG2 = 0.04264433740849372e18;
uint256 public constant INTEREST_PER_YEAR_LOG2 = 0.03562390973072122e18; // log2(1.025)

Choose a reason for hiding this comment

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

Just a comment about accuracy, it seems log2(1.025) = 0,0356239097307213452956518780335185248288369925056043873955

so maybe we can use 0.03562390973072135e18 wdyt? How the number was calculated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

calculated it in python, you? :D

uint256 public constant START_SUPPLY = 10_000_000_000e18;
address private immutable DEPLOYER;

Expand All @@ -27,6 +27,9 @@ contract DefaultEmissionManager is Ownable2StepUpgradeable, IDefaultEmissionMana
IPolygonEcosystemToken public token;
uint256 public startTimestamp;

// NEW STORAGE 1.2.0
uint256 public START_SUPPLY_1_2_0;
web3security marked this conversation as resolved.
Show resolved Hide resolved

constructor(address migration_, address stakeManager_, address treasury_) {
if (migration_ == address(0) || stakeManager_ == address(0) || treasury_ == address(0)) revert InvalidAddress();
DEPLOYER = msg.sender;
Expand All @@ -38,6 +41,11 @@ contract DefaultEmissionManager is Ownable2StepUpgradeable, IDefaultEmissionMana
_disableInitializers();
}

function reinitialize() external reinitializer(2) {
web3security marked this conversation as resolved.
Show resolved Hide resolved
START_SUPPLY_1_2_0 = token.totalSupply();
startTimestamp = block.timestamp;
}

function initialize(address token_, address owner_) external initializer {
// prevent front-running since we can't initialize on proxy deployment
if (DEPLOYER != msg.sender) revert();
Expand All @@ -62,7 +70,8 @@ contract DefaultEmissionManager is Ownable2StepUpgradeable, IDefaultEmissionMana
uint256 amountToMint = newSupply - currentSupply;
if (amountToMint == 0) return; // no minting required

uint256 treasuryAmt = amountToMint / 3;
// 2/5 of 2.5% is 1% going to the treasury
uint256 treasuryAmt = amountToMint * 2 / 5;
web3security marked this conversation as resolved.
Show resolved Hide resolved
uint256 stakeManagerAmt = amountToMint - treasuryAmt;

emit TokenMint(amountToMint, msg.sender);
Expand All @@ -75,15 +84,15 @@ contract DefaultEmissionManager is Ownable2StepUpgradeable, IDefaultEmissionMana
}

/// @inheritdoc IDefaultEmissionManager
function inflatedSupplyAfter(uint256 timeElapsed) public pure returns (uint256 supply) {
function inflatedSupplyAfter(uint256 timeElapsed) public view returns (uint256 supply) {
uint256 supplyFactor = PowUtil.exp2((INTEREST_PER_YEAR_LOG2 * timeElapsed) / 365 days);
supply = (supplyFactor * START_SUPPLY) / 1e18;
supply = (supplyFactor * START_SUPPLY_1_2_0) / 1e18;
}

/// @inheritdoc IDefaultEmissionManager
function version() external pure returns (string memory) {
return "1.1.0";
return "1.2.0";
}

uint256[48] private __gap;
uint256[47] private __gap;
}
2 changes: 1 addition & 1 deletion src/interfaces/IDefaultEmissionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface IDefaultEmissionManager {
/// approximate the compounded interest rate using x^y = 2^(log2(x)*y)
/// where x is the interest rate per year and y is the number of seconds elapsed since deployment divided by 365 days in seconds
/// log2(interestRatePerYear) = 0.04264433740849372 with 18 decimals, as the interest rate does not change, hard code the value
function inflatedSupplyAfter(uint256 timeElapsedInSeconds) external pure returns (uint256 inflatedSupply);
function inflatedSupplyAfter(uint256 timeElapsedInSeconds) external view returns (uint256 inflatedSupply);

/// @notice returns the version of the contract
/// @return version version string
Expand Down
30 changes: 16 additions & 14 deletions test/DefaultEmissionManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ contract DefaultEmissionManagerTest is Test {
vm.prank(governance);
migration.acceptOwnership();
emissionManager.initialize(address(polygon), governance);
emissionManager.reinitialize();

// POL being emissionary, while MATIC having a constant supply,
// the requirement of unmigrating POL to MATIC for StakeManager on each mint
// is satisfied by a one-time transfer of MATIC to the migration contract
Expand Down Expand Up @@ -144,16 +146,17 @@ contract DefaultEmissionManagerTest is Test {

assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA);
uint256 totalAmtMinted = polygon.totalSupply() - initialTotalSupply;
uint256 totalAmtMintedOneThird = totalAmtMinted / 3;
assertEq(matic.balanceOf(stakeManager), totalAmtMinted - totalAmtMintedOneThird);
uint256 totalAmtMintedTwoFifth = totalAmtMinted * 2 / 5;
assertEq(matic.balanceOf(stakeManager), totalAmtMinted - totalAmtMintedTwoFifth);
assertEq(matic.balanceOf(treasury), 0);
assertEq(polygon.balanceOf(stakeManager), 0);
assertEq(polygon.balanceOf(treasury), totalAmtMintedOneThird);
assertEq(polygon.balanceOf(treasury), totalAmtMintedTwoFifth);
}

function test_MintDelayTwice(uint128 delay) external {
vm.assume(delay <= 5 * 365 days && delay > 0);

// now that we actually pass this to calc.js, we only need to set it once.
uint256 initialTotalSupply = polygon.totalSupply();

skip(delay);
Expand All @@ -164,13 +167,12 @@ contract DefaultEmissionManagerTest is Test {
uint256 newSupply = abi.decode(vm.ffi(inputs), (uint256));

assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA);
uint256 balance = (polygon.totalSupply() - initialTotalSupply) / 3;
uint256 balance = (polygon.totalSupply() - initialTotalSupply) * 2 / 5;
uint256 stakeManagerBalance = (polygon.totalSupply() - initialTotalSupply) - balance;
assertEq(matic.balanceOf(stakeManager), stakeManagerBalance);
assertEq(polygon.balanceOf(stakeManager), 0);
assertEq(polygon.balanceOf(treasury), balance);

initialTotalSupply = polygon.totalSupply(); // for the new run
skip(delay);
emissionManager.mint();

Expand All @@ -180,10 +182,10 @@ contract DefaultEmissionManagerTest is Test {

assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA);
uint256 totalAmtMinted = polygon.totalSupply() - initialTotalSupply;
uint256 totalAmtMintedOneThird = totalAmtMinted / 3;
uint256 totalAmtMintedTwoFifth = totalAmtMinted * 2 / 5;

balance += totalAmtMintedOneThird;
stakeManagerBalance += totalAmtMinted - totalAmtMintedOneThird;
balance = totalAmtMintedTwoFifth;
stakeManagerBalance = totalAmtMinted - totalAmtMintedTwoFifth;

assertEq(matic.balanceOf(stakeManager), stakeManagerBalance);
assertEq(polygon.balanceOf(stakeManager), 0);
Expand All @@ -195,10 +197,10 @@ contract DefaultEmissionManagerTest is Test {

uint256 balance;
uint256 stakeManagerBalance;
// now that we actually pass this to calc.js, we only need to set it once.
uint256 initialTotalSupply = polygon.totalSupply();

for (uint256 cycle; cycle < cycles; cycle++) {
uint256 initialTotalSupply = polygon.totalSupply();

skip(delay);
emissionManager.mint();

Expand All @@ -208,10 +210,10 @@ contract DefaultEmissionManagerTest is Test {

assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA);
uint256 totalAmtMinted = polygon.totalSupply() - initialTotalSupply;
uint256 totalAmtMintedOneThird = totalAmtMinted / 3;
uint256 totalAmtMintedTwoFifth = totalAmtMinted * 2 / 5;

balance += totalAmtMintedOneThird;
stakeManagerBalance += totalAmtMinted - totalAmtMintedOneThird;
balance = totalAmtMintedTwoFifth;
stakeManagerBalance = totalAmtMinted - totalAmtMintedTwoFifth;

assertEq(matic.balanceOf(stakeManager), stakeManagerBalance);
assertEq(polygon.balanceOf(stakeManager), 0);
Expand All @@ -224,6 +226,6 @@ contract DefaultEmissionManagerTest is Test {
inputs[2] = vm.toString(delay);
inputs[3] = vm.toString(polygon.totalSupply());
uint256 newSupply = abi.decode(vm.ffi(inputs), (uint256));
assertApproxEqAbs(newSupply, emissionManager.inflatedSupplyAfter(block.timestamp + delay), 1e20);

Choose a reason for hiding this comment

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

Some assertions have been removed without further explanation like removing += by = as well as block.timestamp dependencies which is not good as we are just testing within a block now (not recommended for critical production env)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added this comment in line 200:

// now that we actually pass this to calc.js, we only need to set it once.
        uint256 initialTotalSupply = polygon.totalSupply();

before, calc.js had startSupply hardcoded at 10_000_000_000e18
now that our value actually gets parsed, we only set initialTotalSupply one time at the beginning.
This leads to us also not having to increment the balance and stakeManagerBalance anymore, but simply setting them to the new value, because when we calculate totalAmtMinted, initialTotalSupply now stays the same. Hope that makes sense :)

Choose a reason for hiding this comment

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

it makes sense, thanks Simon.

assertApproxEqAbs(newSupply, emissionManager.inflatedSupplyAfter(delay), 1e20);
}
}
80 changes: 80 additions & 0 deletions test/upgrade/DefaultEmissionManager.1.2.0.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import {PolygonEcosystemToken} from "src/PolygonEcosystemToken.sol";
import {DefaultEmissionManager} from "src/DefaultEmissionManager.sol";
import {PolygonMigration} from "src/PolygonMigration.sol";
import {ERC20PresetMinterPauser} from "openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";
import {
ProxyAdmin,
TransparentUpgradeableProxy,
ITransparentUpgradeableProxy
} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol";
import {Test} from "forge-std/Test.sol";

// this test forks mainnet and tests the upgradeability of DefaultEmissionManagerProxy

contract DefaultEmissionManagerTest is Test {
uint256 mainnetFork;

address POLYGON_PROTOCOL_COUNCIL = 0x37D085ca4a24f6b29214204E8A8666f12cf19516;
address EM_PROXY = 0xbC9f74b3b14f460a6c47dCdDFd17411cBc7b6c53;
address COMMUNITY_TREASURY = 0x2ff25495d77f380d5F65B95F103181aE8b1cf898;
address EM_PROXY_ADMIN = 0xEBea33f2c92D03556b417F4F572B2FbbE62C39c3;
PolygonEcosystemToken pol = PolygonEcosystemToken(0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6);

uint256 NEW_INTEREST_PER_YEAR_LOG2 = 0.03562390973072122e18; // log2(1.025)

string[] internal inputs = new string[](5);

function setUp() public {
string memory MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
mainnetFork = vm.createFork(MAINNET_RPC_URL);
}

function testUpgrade() external {
vm.selectFork(mainnetFork);

address newTreasury = makeAddr("newTreasury");

DefaultEmissionManager emProxy = DefaultEmissionManager(EM_PROXY);

assertEq(emProxy.treasury(), COMMUNITY_TREASURY);

address migration = address(emProxy.migration());
address stakeManager = emProxy.stakeManager();

DefaultEmissionManager newEmImpl = new DefaultEmissionManager(migration, stakeManager, newTreasury);

ProxyAdmin admin = ProxyAdmin(EM_PROXY_ADMIN);

vm.prank(POLYGON_PROTOCOL_COUNCIL);

admin.upgradeAndCall(
ITransparentUpgradeableProxy(address(emProxy)),
address(newEmImpl),
abi.encodeWithSelector(DefaultEmissionManager.reinitialize.selector)
);

// initialize can still not be called
vm.expectRevert("Initializable: contract is already initialized");
emProxy.initialize(makeAddr("token"), msg.sender);

assertEq(pol.totalSupply(), emProxy.START_SUPPLY_1_2_0());
assertEq(block.timestamp, emProxy.startTimestamp());

// emission is now 2.5%
inputs[0] = "node";
inputs[1] = "test/util/calc.js";
inputs[2] = vm.toString(uint256(365 days));
inputs[3] = vm.toString(pol.totalSupply());
// vm.ffi executes the js script which contains the new emission rate
uint256 newSupply = abi.decode(vm.ffi(inputs), (uint256));
assertApproxEqAbs(newSupply, emProxy.inflatedSupplyAfter(365 days), 1e20);

// treasury has been updated
assertEq(emProxy.treasury(), newTreasury);
// emission has been updated
assertEq(emProxy.INTEREST_PER_YEAR_LOG2(), NEW_INTEREST_PER_YEAR_LOG2);
}
}
7 changes: 4 additions & 3 deletions test/util/calc.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const interestRatePerYear = 1.03;
const startSupply = 10_000_000_000e18;
const emissionRatePerYear = 1.025;

function main() {
const [timeElapsedInSeconds] = process.argv.slice(2);
const [startSupply] = process.argv.slice(3);

const supplyFactor = Math.pow(interestRatePerYear, timeElapsedInSeconds / (365 * 24 * 60 * 60));
const supplyFactor = Math.pow(emissionRatePerYear, timeElapsedInSeconds / (365 * 24 * 60 * 60));
const newSupply = BigInt(startSupply * supplyFactor);

console.log("0x" + newSupply.toString(16).padStart(64, "0")); // abi.encode(toMint)
Expand Down
Loading