From 01bc269bb86c1a0083fabd172fd5cf3bc0457cad Mon Sep 17 00:00:00 2001 From: Simon Dosch Date: Fri, 14 Jun 2024 17:14:15 +0200 Subject: [PATCH] PIP-17: Adjust Emission Rate (#58) --- .github/workflows/test.yml | 4 + src/DefaultEmissionManager.sol | 23 ++++-- src/interfaces/IDefaultEmissionManager.sol | 2 +- test/DefaultEmissionManager.t.sol | 30 +++---- .../DefaultEmissionManager.1.2.0.t.sol | 80 +++++++++++++++++++ test/util/calc.js | 7 +- 6 files changed, 121 insertions(+), 25 deletions(-) create mode 100644 test/upgrade/DefaultEmissionManager.1.2.0.t.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ea46b3..879320b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/src/DefaultEmissionManager.sol b/src/DefaultEmissionManager.sol index 1c3884a..de18ff2 100644 --- a/src/DefaultEmissionManager.sol +++ b/src/DefaultEmissionManager.sol @@ -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 /// @custom:security-contact security@polygon.technology 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) uint256 public constant START_SUPPLY = 10_000_000_000e18; address private immutable DEPLOYER; @@ -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; + constructor(address migration_, address stakeManager_, address treasury_) { if (migration_ == address(0) || stakeManager_ == address(0) || treasury_ == address(0)) revert InvalidAddress(); DEPLOYER = msg.sender; @@ -38,6 +41,11 @@ contract DefaultEmissionManager is Ownable2StepUpgradeable, IDefaultEmissionMana _disableInitializers(); } + function reinitialize() external reinitializer(2) { + 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(); @@ -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; uint256 stakeManagerAmt = amountToMint - treasuryAmt; emit TokenMint(amountToMint, msg.sender); @@ -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; } diff --git a/src/interfaces/IDefaultEmissionManager.sol b/src/interfaces/IDefaultEmissionManager.sol index f779b40..03ae50b 100644 --- a/src/interfaces/IDefaultEmissionManager.sol +++ b/src/interfaces/IDefaultEmissionManager.sol @@ -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 diff --git a/test/DefaultEmissionManager.t.sol b/test/DefaultEmissionManager.t.sol index 7e07ee7..c1267db 100644 --- a/test/DefaultEmissionManager.t.sol +++ b/test/DefaultEmissionManager.t.sol @@ -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 @@ -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); @@ -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(); @@ -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); @@ -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(); @@ -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); @@ -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); + assertApproxEqAbs(newSupply, emissionManager.inflatedSupplyAfter(delay), 1e20); } } diff --git a/test/upgrade/DefaultEmissionManager.1.2.0.t.sol b/test/upgrade/DefaultEmissionManager.1.2.0.t.sol new file mode 100644 index 0000000..ba4ecf4 --- /dev/null +++ b/test/upgrade/DefaultEmissionManager.1.2.0.t.sol @@ -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); + } +} diff --git a/test/util/calc.js b/test/util/calc.js index 7b96dbd..73593a5 100644 --- a/test/util/calc.js +++ b/test/util/calc.js @@ -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)