From ee23a674148ddea3f94fddc97f3f382e254788dd Mon Sep 17 00:00:00 2001 From: Simon Dosch Date: Thu, 25 Apr 2024 12:00:56 +0200 Subject: [PATCH 1/5] staking layer emission 2 --> 1.5% --- src/DefaultEmissionManager.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/DefaultEmissionManager.sol b/src/DefaultEmissionManager.sol index 1c3884a..e3644aa 100644 --- a/src/DefaultEmissionManager.sol +++ b/src/DefaultEmissionManager.sol @@ -16,7 +16,7 @@ import {PowUtil} from "./lib/PowUtil.sol"; 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; @@ -62,7 +62,7 @@ contract DefaultEmissionManager is Ownable2StepUpgradeable, IDefaultEmissionMana uint256 amountToMint = newSupply - currentSupply; if (amountToMint == 0) return; // no minting required - uint256 treasuryAmt = amountToMint / 3; + uint256 treasuryAmt = amountToMint * 2 / 5; uint256 stakeManagerAmt = amountToMint - treasuryAmt; emit TokenMint(amountToMint, msg.sender); @@ -82,7 +82,7 @@ contract DefaultEmissionManager is Ownable2StepUpgradeable, IDefaultEmissionMana /// @inheritdoc IDefaultEmissionManager function version() external pure returns (string memory) { - return "1.1.0"; + return "1.2.0"; } uint256[48] private __gap; From 9e4329ea07a17c6b6b92eaf1fee9e914e3917f8b Mon Sep 17 00:00:00 2001 From: Simon Dosch Date: Fri, 3 May 2024 13:38:32 +0200 Subject: [PATCH 2/5] update doc --- src/DefaultEmissionManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DefaultEmissionManager.sol b/src/DefaultEmissionManager.sol index e3644aa..f7e07c8 100644 --- a/src/DefaultEmissionManager.sol +++ b/src/DefaultEmissionManager.sol @@ -11,7 +11,7 @@ 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; From 5c807396073f46f8615a3de6d0a7589ef8f8424c Mon Sep 17 00:00:00 2001 From: Simon Dosch Date: Tue, 7 May 2024 21:52:57 +0200 Subject: [PATCH 3/5] reinit startTime & startSupply, tests --- src/DefaultEmissionManager.sol | 12 ++++++++++-- src/interfaces/IDefaultEmissionManager.sol | 2 +- test/DefaultEmissionManager.t.sol | 10 ++++++---- test/util/calc.js | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/DefaultEmissionManager.sol b/src/DefaultEmissionManager.sol index f7e07c8..8739a28 100644 --- a/src/DefaultEmissionManager.sol +++ b/src/DefaultEmissionManager.sol @@ -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(); @@ -75,9 +83,9 @@ 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 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..a13bdb1 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,7 +146,7 @@ contract DefaultEmissionManagerTest is Test { assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); uint256 totalAmtMinted = polygon.totalSupply() - initialTotalSupply; - uint256 totalAmtMintedOneThird = totalAmtMinted / 3; + uint256 totalAmtMintedOneThird = totalAmtMinted * 2 / 5; assertEq(matic.balanceOf(stakeManager), totalAmtMinted - totalAmtMintedOneThird); assertEq(matic.balanceOf(treasury), 0); assertEq(polygon.balanceOf(stakeManager), 0); @@ -164,7 +166,7 @@ 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); @@ -180,7 +182,7 @@ contract DefaultEmissionManagerTest is Test { assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); uint256 totalAmtMinted = polygon.totalSupply() - initialTotalSupply; - uint256 totalAmtMintedOneThird = totalAmtMinted / 3; + uint256 totalAmtMintedOneThird = totalAmtMinted * 2 / 5; balance += totalAmtMintedOneThird; stakeManagerBalance += totalAmtMinted - totalAmtMintedOneThird; @@ -208,7 +210,7 @@ contract DefaultEmissionManagerTest is Test { assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); uint256 totalAmtMinted = polygon.totalSupply() - initialTotalSupply; - uint256 totalAmtMintedOneThird = totalAmtMinted / 3; + uint256 totalAmtMintedOneThird = totalAmtMinted * 2/ 5; balance += totalAmtMintedOneThird; stakeManagerBalance += totalAmtMinted - totalAmtMintedOneThird; diff --git a/test/util/calc.js b/test/util/calc.js index 7b96dbd..6a1a1d1 100644 --- a/test/util/calc.js +++ b/test/util/calc.js @@ -1,4 +1,4 @@ -const interestRatePerYear = 1.03; +const interestRatePerYear = 1.025; const startSupply = 10_000_000_000e18; function main() { const [timeElapsedInSeconds] = process.argv.slice(2); From f31c5dcc39ff637b68eba4bc3eea5aea1ecc07a4 Mon Sep 17 00:00:00 2001 From: Simon Dosch Date: Wed, 8 May 2024 10:56:44 +0200 Subject: [PATCH 4/5] test EM upgrade, fix tests --- test/DefaultEmissionManager.t.sol | 28 +++---- .../DefaultEmissionManager.1.2.0.t.sol | 79 +++++++++++++++++++ test/util/calc.js | 7 +- 3 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 test/upgrade/DefaultEmissionManager.1.2.0.t.sol diff --git a/test/DefaultEmissionManager.t.sol b/test/DefaultEmissionManager.t.sol index a13bdb1..c1267db 100644 --- a/test/DefaultEmissionManager.t.sol +++ b/test/DefaultEmissionManager.t.sol @@ -56,7 +56,7 @@ contract DefaultEmissionManagerTest is Test { 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 @@ -146,16 +146,17 @@ contract DefaultEmissionManagerTest is Test { assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); uint256 totalAmtMinted = polygon.totalSupply() - initialTotalSupply; - uint256 totalAmtMintedOneThird = totalAmtMinted * 2 / 5; - 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); @@ -172,7 +173,6 @@ contract DefaultEmissionManagerTest is Test { assertEq(polygon.balanceOf(stakeManager), 0); assertEq(polygon.balanceOf(treasury), balance); - initialTotalSupply = polygon.totalSupply(); // for the new run skip(delay); emissionManager.mint(); @@ -182,10 +182,10 @@ contract DefaultEmissionManagerTest is Test { assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); uint256 totalAmtMinted = polygon.totalSupply() - initialTotalSupply; - uint256 totalAmtMintedOneThird = totalAmtMinted * 2 / 5; + 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); @@ -197,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(); @@ -210,10 +210,10 @@ contract DefaultEmissionManagerTest is Test { assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); uint256 totalAmtMinted = polygon.totalSupply() - initialTotalSupply; - uint256 totalAmtMintedOneThird = totalAmtMinted * 2/ 5; + 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); @@ -226,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..a8be54f --- /dev/null +++ b/test/upgrade/DefaultEmissionManager.1.2.0.t.sol @@ -0,0 +1,79 @@ +// 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_BOARD = 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_BOARD); + + 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.upgrade(ITransparentUpgradeableProxy(address(emProxy)), address(newEmImpl)); + + // initialize can still not be called + vm.expectRevert("Initializable: contract is already initialized"); + emProxy.initialize(makeAddr("token"), msg.sender); + + // reinitialize to reset startTimestamp and start supply + emProxy.reinitialize(); + + 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 6a1a1d1..73593a5 100644 --- a/test/util/calc.js +++ b/test/util/calc.js @@ -1,9 +1,10 @@ -const interestRatePerYear = 1.025; -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) From 90d25738c7306053eb4b9944f7ef4779119fe585 Mon Sep 17 00:00:00 2001 From: Simon Dosch Date: Wed, 8 May 2024 11:20:37 +0200 Subject: [PATCH 5/5] send POL to StakeManager --- src/DefaultEmissionManager.sol | 6 ++++-- test/DefaultEmissionManager.t.sol | 14 ++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/DefaultEmissionManager.sol b/src/DefaultEmissionManager.sol index 8739a28..16fb351 100644 --- a/src/DefaultEmissionManager.sol +++ b/src/DefaultEmissionManager.sol @@ -76,10 +76,12 @@ contract DefaultEmissionManager is Ownable2StepUpgradeable, IDefaultEmissionMana emit TokenMint(amountToMint, msg.sender); IPolygonEcosystemToken _token = token; + _token.mint(address(this), amountToMint); + _token.safeTransfer(treasury, treasuryAmt); - // backconvert POL to MATIC before sending to StakeManager - migration.unmigrateTo(stakeManager, stakeManagerAmt); + + _token.safeTransfer(stakeManager, stakeManagerAmt); } /// @inheritdoc IDefaultEmissionManager diff --git a/test/DefaultEmissionManager.t.sol b/test/DefaultEmissionManager.t.sol index c1267db..20081ac 100644 --- a/test/DefaultEmissionManager.t.sol +++ b/test/DefaultEmissionManager.t.sol @@ -127,7 +127,6 @@ contract DefaultEmissionManagerTest is Test { emissionManager.mint(); // timeElapsed is zero, so no minting assertEq(polygon.balanceOf(stakeManager), 0); - assertEq(matic.balanceOf(stakeManager), 0); assertEq(polygon.balanceOf(treasury), 0); } @@ -147,9 +146,7 @@ contract DefaultEmissionManagerTest is Test { assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); uint256 totalAmtMinted = polygon.totalSupply() - initialTotalSupply; uint256 totalAmtMintedTwoFifth = totalAmtMinted * 2 / 5; - assertEq(matic.balanceOf(stakeManager), totalAmtMinted - totalAmtMintedTwoFifth); - assertEq(matic.balanceOf(treasury), 0); - assertEq(polygon.balanceOf(stakeManager), 0); + assertEq(polygon.balanceOf(stakeManager), totalAmtMinted - totalAmtMintedTwoFifth); assertEq(polygon.balanceOf(treasury), totalAmtMintedTwoFifth); } @@ -169,8 +166,7 @@ contract DefaultEmissionManagerTest is Test { assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); 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(stakeManager), stakeManagerBalance); assertEq(polygon.balanceOf(treasury), balance); skip(delay); @@ -187,8 +183,7 @@ contract DefaultEmissionManagerTest is Test { balance = totalAmtMintedTwoFifth; stakeManagerBalance = totalAmtMinted - totalAmtMintedTwoFifth; - assertEq(matic.balanceOf(stakeManager), stakeManagerBalance); - assertEq(polygon.balanceOf(stakeManager), 0); + assertEq(polygon.balanceOf(stakeManager), stakeManagerBalance); assertEq(polygon.balanceOf(treasury), balance); } @@ -215,8 +210,7 @@ contract DefaultEmissionManagerTest is Test { balance = totalAmtMintedTwoFifth; stakeManagerBalance = totalAmtMinted - totalAmtMintedTwoFifth; - assertEq(matic.balanceOf(stakeManager), stakeManagerBalance); - assertEq(polygon.balanceOf(stakeManager), 0); + assertEq(polygon.balanceOf(stakeManager), stakeManagerBalance); assertEq(polygon.balanceOf(treasury), balance); } }