diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eda48cf..5ea46b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,43 +1,43 @@ name: test on: - push: - branches: [main, dev] - pull_request: - branches: [main, dev, feat/stakeManager] + push: + branches: [main, dev] + pull_request: + branches: [main, dev, feat/stakeManager] env: - FOUNDRY_PROFILE: ci + FOUNDRY_PROFILE: ci jobs: - check: - strategy: - fail-fast: true - - name: Foundry project - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly - - - name: Get node.js - uses: actions/setup-node@v3 - with: - node-version: 18.x - - - name: Run Forge build - run: | - forge --version - forge build --sizes - id: build - - - name: Run Forge tests - # ! revert back to FOUNDRY_PROFILE=intense forge test -vvv - run: forge test -vvv - id: test + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Get node.js + uses: actions/setup-node@v3 + with: + node-version: 18.x + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + # ! revert back to FOUNDRY_PROFILE=intense forge test -vvv + run: forge test -vvv + id: test diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ae8848a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +.prettierrc +foundry.toml +out +lib/ +cache/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..315f7cf --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "useTabs": false, + "singleQuote": false, + "bracketSpacing": false +} diff --git a/README.md b/README.md index 6c55880..2e28a56 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# indicia \ No newline at end of file +# indicia diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index a492c23..b3ae428 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -15,43 +15,37 @@ contract Deploy is Script { deployerPrivateKey = vm.envUint("PRIVATE_KEY"); } - function run( - address matic, - address governance, - address treasury, - address stakeManager - ) public { + function run(address matic, address governance, address treasury, address stakeManager) public { vm.startBroadcast(deployerPrivateKey); - PolygonMigration migration = new PolygonMigration(matic, governance); + address migrationImplementation = address(new PolygonMigration()); - address inflationManagerImplementation = address( - new DefaultInflationManager() - ); - address inflationManagerProxy = address( + address migrationProxy = address( new TransparentUpgradeableProxy( - address(inflationManagerImplementation), + migrationImplementation, governance, - "" + abi.encodeCall(PolygonMigration.initialize, matic) ) ); - Polygon polygonToken = new Polygon( - address(migration), - address(inflationManagerProxy) + address inflationManagerImplementation = address(new DefaultInflationManager()); + address inflationManagerProxy = address( + new TransparentUpgradeableProxy(address(inflationManagerImplementation), governance, "") ); + Polygon polygonToken = new Polygon(migrationProxy, inflationManagerProxy); + DefaultInflationManager(inflationManagerProxy).initialize( address(polygonToken), - address(migration), + migrationProxy, stakeManager, treasury, governance ); - migration.setPolygonToken(address(polygonToken)); + PolygonMigration(migrationProxy).setPolygonToken(address(polygonToken)); - migration.transferOwnership(governance); // governance needs to accept the ownership transfer + PolygonMigration(migrationProxy).transferOwnership(governance); // governance needs to accept the ownership transfer vm.stopBroadcast(); } diff --git a/slither.config.json b/slither.config.json new file mode 100644 index 0000000..3583960 --- /dev/null +++ b/slither.config.json @@ -0,0 +1,3 @@ +{ + "filter_paths": "(lib/|test/|script/)" +} diff --git a/src/DefaultInflationManager.sol b/src/DefaultInflationManager.sol index 1a265a9..a4a3d39 100644 --- a/src/DefaultInflationManager.sol +++ b/src/DefaultInflationManager.sol @@ -10,19 +10,15 @@ import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/Safe import {PowUtil} from "./lib/PowUtil.sol"; /// @title Default Inflation Manager -/// @author QEDK (https://polygon.technology) +/// @author Polygon Labs (@DhairyaSethi, @gretzke, @qedk) /// @notice A default inflation manager implementation for the Polygon ERC20 token contract on Ethereum L1 -/// @dev The contract allows for a 1% mint *each* per year (compounded every second) to the stakeManager and treasury contracts +/// @dev The contract allows for a 1% mint *each* per year (compounded every year) to the stakeManager and treasury contracts /// @custom:security-contact security@polygon.technology -contract DefaultInflationManager is - Initializable, - Ownable2StepUpgradeable, - IDefaultInflationManager -{ +contract DefaultInflationManager is Initializable, Ownable2StepUpgradeable, IDefaultInflationManager { using SafeERC20 for IPolygon; - // log2(2%pa continuously compounded inflation per second) in 18 decimals(Wad), see _inflatedSupplyAfter - uint256 public constant INTEREST_PER_SECOND_LOG2 = 0.000000000914951192e18; + // log2(2%pa continuously compounded inflation per year) in 18 decimals, see _inflatedSupplyAfter + uint256 public constant INTEREST_PER_YEAR_LOG2 = 0.028569152196770894e18; uint256 public constant START_SUPPLY = 10_000_000_000e18; IPolygon public token; @@ -61,7 +57,7 @@ contract DefaultInflationManager is assert(START_SUPPLY == token.totalSupply()); token.safeApprove(migration_, type(uint256).max); - + // initial ownership setup bypassing 2 step ownership transfer process _transferOwnership(owner_); } @@ -79,27 +75,23 @@ contract DefaultInflationManager is uint256 treasuryAmt = amountToMint / 2; uint256 stakeManagerAmt = amountToMint - treasuryAmt; + emit TokenMint(amountToMint, msg.sender); + token.mint(address(this), amountToMint); - token.transfer(treasury, treasuryAmt); + token.safeTransfer(treasury, treasuryAmt); // backconvert POL to MATIC before sending to StakeManager - migration.unmigrateTo(stakeManagerAmt, stakeManager); - - emit TokenMint(amountToMint, msg.sender); + migration.unmigrateTo(stakeManager, stakeManagerAmt); } /// @notice Returns total supply from compounded inflation after timeElapsed from startTimestamp (deployment) /// @param timeElapsed The time elapsed since startTimestamp - /// @dev interestRatePerSecond = 1.000000000634195839; 2% per year in seconds with 18 decimals - /// approximate the compounded interest rate per second using x^y = 2^(log2(x)*y) - /// where x is the interest rate per second and y is the number of seconds elapsed since deployment - /// log2(interestRatePerSecond) = 0.000000000914951192 with 18 decimals, as the interest rate does not change, hard code the value + /// @dev interestRatePerYear = 1.02; 2% per year + /// 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.028569152196770894 with 18 decimals, as the interest rate does not change, hard code the value /// @return supply total supply from compounded inflation after timeElapsed - function _inflatedSupplyAfter( - uint256 timeElapsed - ) private pure returns (uint256 supply) { - uint256 supplyFactor = PowUtil.exp2( - INTEREST_PER_SECOND_LOG2 * timeElapsed - ); + function _inflatedSupplyAfter(uint256 timeElapsed) private pure returns (uint256 supply) { + uint256 supplyFactor = PowUtil.exp2((INTEREST_PER_YEAR_LOG2 * timeElapsed) / 365 days); supply = (supplyFactor * START_SUPPLY) / 1e18; } @@ -108,5 +100,5 @@ contract DefaultInflationManager is * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[49] private __gap; + uint256[50] private __gap; } diff --git a/src/Polygon.sol b/src/Polygon.sol index 8a081f6..bedcfa4 100644 --- a/src/Polygon.sol +++ b/src/Polygon.sol @@ -6,22 +6,18 @@ import {IPolygon} from "./interfaces/IPolygon.sol"; import {IDefaultInflationManager} from "./interfaces/IDefaultInflationManager.sol"; /// @title Polygon ERC20 token -/// @author QEDK (https://polygon.technology) +/// @author Polygon Labs (@DhairyaSethi, @gretzke, @qedk) /// @notice This is the Polygon ERC20 token contract on Ethereum L1 /// @dev The contract allows for a 1-to-1 representation between $POL and $MATIC and allows for additional inflation based /// on hub and treasury requirements /// @custom:security-contact security@polygon.technology contract Polygon is ERC20Permit, IPolygon { + uint256 public constant MINT_PER_SECOND_CAP = 0.0000000420e18; // 0.0000042% of POL Supply per second, in 18 decimals address public immutable inflationManager; - uint256 public constant mintPerSecondCap = 0.00000001e18; // 0.00000001% of POL Supply per second, in 18 deciamls uint256 public lastMint; - constructor( - address migration_, - address inflationManager_ - ) ERC20("Polygon", "POL") ERC20Permit("Polygon") { - if (migration_ == address(0) || inflationManager_ == address(0)) - revert InvalidAddress(); + constructor(address migration_, address inflationManager_) ERC20("Polygon", "POL") ERC20Permit("Polygon") { + if (migration_ == address(0) || inflationManager_ == address(0)) revert InvalidAddress(); inflationManager = inflationManager_; _mint(migration_, 10_000_000_000e18); @@ -33,14 +29,11 @@ contract Polygon is ERC20Permit, IPolygon { /// @param amount Amount to mint function mint(address to, uint256 amount) external { if (msg.sender != inflationManager) revert OnlyInflationManager(); - if (lastMint == 0) - lastMint = IDefaultInflationManager(inflationManager) - .startTimestamp(); + uint256 lastMintCache = lastMint; + if (lastMintCache == 0) lastMintCache = IDefaultInflationManager(inflationManager).startTimestamp(); - uint256 timeElapsedSinceLastMint = block.timestamp - lastMint; - uint256 maxMint = (timeElapsedSinceLastMint * - mintPerSecondCap * - totalSupply()) / 1e18; + uint256 timeElapsedSinceLastMint = block.timestamp - lastMintCache; + uint256 maxMint = (timeElapsedSinceLastMint * MINT_PER_SECOND_CAP * totalSupply()) / 1e18; if (amount > maxMint) revert MaxMintExceeded(maxMint, amount); lastMint = block.timestamp; diff --git a/src/PolygonMigration.sol b/src/PolygonMigration.sol index bed2e74..87043ed 100644 --- a/src/PolygonMigration.sol +++ b/src/PolygonMigration.sol @@ -4,41 +4,42 @@ pragma solidity 0.8.21; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Ownable2Step} from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; +import {Ownable2StepUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; import {IPolygonMigration} from "./interfaces/IPolygonMigration.sol"; /// @title Polygon Migration -/// @author QEDK (https://polygon.technology) +/// @author Polygon Labs (@DhairyaSethi, @gretzke, @qedk) /// @notice This is the migration contract for Matic <-> Polygon ERC20 token on Ethereum L1 /// @dev The contract allows for a 1-to-1 conversion from $MATIC into $POL and vice-versa /// @custom:security-contact security@polygon.technology -contract PolygonMigration is Ownable2Step, IPolygonMigration { +contract PolygonMigration is Ownable2StepUpgradeable, IPolygonMigration { using SafeERC20 for IERC20; using SafeERC20 for IERC20Permit; IERC20 public polygon; - IERC20 public immutable matic; - uint256 public releaseTimestamp; - uint256 public unmigrationLock; + IERC20 public matic; + bool public unmigrationLocked; - modifier ifUnmigrationUnlocked() { - if (unmigrationLock != 0) revert UnmigrationLocked(); + modifier onlyUnmigrationUnlocked() { + if (unmigrationLocked) revert UnmigrationLocked(); _; } - constructor(address matic_, address owner_) { - if (matic_ == address(0) || owner_ == address(0)) - revert InvalidAddress(); + constructor() { + // so that the implementation contract cannot be initialized + _disableInitializers(); + } + function initialize(address matic_) external initializer { + __Ownable_init(); + if (matic_ == address(0)) revert InvalidAddress(); matic = IERC20(matic_); - releaseTimestamp = block.timestamp + (365 days * 4); // 4 years } /// @notice This function allows owner/governance to set POL token address *only once* /// @param polygon_ Address of deployed POL token function setPolygonToken(address polygon_) external onlyOwner { - if (polygon_ == address(0) || address(polygon) != address(0)) - revert InvalidAddressOrAlreadySet(); + if (polygon_ == address(0) || address(polygon) != address(0)) revert InvalidAddressOrAlreadySet(); polygon = IERC20(polygon_); } @@ -54,7 +55,7 @@ contract PolygonMigration is Ownable2Step, IPolygonMigration { /// @notice This function allows for unmigrating from POL tokens to MATIC tokens /// @param amount Amount of POL to migrate - function unmigrate(uint256 amount) external ifUnmigrationUnlocked { + function unmigrate(uint256 amount) external onlyUnmigrationUnlocked { emit Unmigrated(msg.sender, amount); polygon.safeTransferFrom(msg.sender, address(this), amount); @@ -64,10 +65,7 @@ contract PolygonMigration is Ownable2Step, IPolygonMigration { /// @notice This function allows for unmigrating POL tokens (from msg.sender) to MATIC tokens (to account) /// @param amount Amount of POL to migrate /// @param account Address to receive MATIC tokens - function unmigrateTo( - uint256 amount, - address account - ) external ifUnmigrationUnlocked { + function unmigrateTo(address account, uint256 amount) external onlyUnmigrationUnlocked { emit Unmigrated(msg.sender, amount); polygon.safeTransferFrom(msg.sender, address(this), amount); @@ -82,52 +80,26 @@ contract PolygonMigration is Ownable2Step, IPolygonMigration { uint8 v, bytes32 r, bytes32 s - ) external ifUnmigrationUnlocked { + ) external onlyUnmigrationUnlocked { emit Unmigrated(msg.sender, amount); - IERC20Permit(address(polygon)).safePermit( - msg.sender, - address(this), - amount, - deadline, - v, - r, - s - ); + IERC20Permit(address(polygon)).safePermit(msg.sender, address(this), amount, deadline, v, r, s); polygon.safeTransferFrom(msg.sender, address(this), amount); matic.safeTransfer(msg.sender, amount); } - /// @notice Allows governance to update the release timestamp if required - /// @dev The function does not do any validation since governance can correct the timestamp if required - /// @param timestamp_ New release timestamp - function updateReleaseTimestamp(uint256 timestamp_) external onlyOwner { - if (timestamp_ < block.timestamp) revert InvalidTimestamp(); - releaseTimestamp = timestamp_; - emit ReleaseTimestampUpdated(timestamp_); - } - /// @notice Allows governance to lock or unlock the unmigration process /// @dev The function does not do any validation since governance can update the unmigration process if required - /// @param unmigrationLock_ New unmigration lock status - function updateUnmigrationLock( - uint256 unmigrationLock_ - ) external onlyOwner { - unmigrationLock = unmigrationLock_; - emit UnmigrationLockUpdated(unmigrationLock_); + /// @param unmigrationLocked_ New unmigration lock status + function updateUnmigrationLock(bool unmigrationLocked_) external onlyOwner { + unmigrationLocked = unmigrationLocked_; + emit UnmigrationLockUpdated(unmigrationLocked_); } - /// @notice Allows governance to release the remaining POL tokens after the migration period has elapsed - /// @dev In case any MATIC was sent out of process, it will be sent to the dead address - function release() external onlyOwner { - if (block.timestamp < releaseTimestamp) revert MigrationNotOver(); - uint256 polAmount = polygon.balanceOf(address(this)); - uint256 maticAmount = matic.balanceOf(address(this)); - polygon.safeTransfer(msg.sender, polAmount); - matic.safeTransfer( - 0x000000000000000000000000000000000000dEaD, - maticAmount - ); - emit Released(polAmount, maticAmount); - } + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; } diff --git a/src/interfaces/IPolygonMigration.sol b/src/interfaces/IPolygonMigration.sol index 0b4ee88..4e4c253 100644 --- a/src/interfaces/IPolygonMigration.sol +++ b/src/interfaces/IPolygonMigration.sol @@ -4,27 +4,17 @@ pragma solidity 0.8.21; interface IPolygonMigration { error UnmigrationLocked(); error InvalidAddressOrAlreadySet(); - error InvalidTimestamp(); - error MigrationNotOver(); error InvalidAddress(); event Migrated(address indexed account, uint256 amount); event Unmigrated(address indexed account, uint256 amount); - event UnmigrationLockUpdated(uint256 lock); - event ReleaseTimestampUpdated(uint256 timestamp); - event Released(uint256 polAmount, uint256 maticAmount); + event UnmigrationLockUpdated(bool lock); function migrate(uint256 amount) external; function unmigrate(uint256 amount) external; - function unmigrateTo(uint256 amount, address account) external; + function unmigrateTo(address account, uint256 amount) external; - function unmigrateWithPermit( - uint256 amount, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; + function unmigrateWithPermit(uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; } diff --git a/test/DefaultInflationManager.t.sol b/test/DefaultInflationManager.t.sol index d7c251f..23c4915 100644 --- a/test/DefaultInflationManager.t.sol +++ b/test/DefaultInflationManager.t.sol @@ -20,10 +20,8 @@ contract DefaultInflationManagerTest is Test { DefaultInflationManager public inflationManager; DefaultInflationManager public inflationManagerImplementation; - uint256 private constant _INTEREST_PER_SECOND_LOG2 = - 0.000000000914951192e18; - // precision accurary due to log2 approximation is upto the first 9 digits - uint256 private constant _MAX_PRECISION_DELTA = 1e21; + // precision accuracy due to log2 approximation is up to the first 5 digits + uint256 private constant _MAX_PRECISION_DELTA = 1e13; string[] internal inputs = new string[](4); @@ -33,28 +31,19 @@ contract DefaultInflationManagerTest is Test { governance = makeAddr("governance"); inflationManagerImplementation = new DefaultInflationManager(); inflationManager = DefaultInflationManager( - address( - new TransparentUpgradeableProxy( - address(inflationManagerImplementation), - msg.sender, - "" - ) - ) + address(new TransparentUpgradeableProxy(address(inflationManagerImplementation), msg.sender, "")) ); matic = new ERC20PresetMinterPauser("Matic Token", "MATIC"); - migration = new PolygonMigration(address(matic), governance); + migration = PolygonMigration( + address(new TransparentUpgradeableProxy(address(new PolygonMigration()), msg.sender, "")) + ); + migration.initialize(address(matic)); polygon = new Polygon(address(migration), address(inflationManager)); migration.setPolygonToken(address(polygon)); // deployer sets token migration.transferOwnership(governance); vm.prank(governance); migration.acceptOwnership(); - inflationManager.initialize( - address(polygon), - address(migration), - stakeManager, - treasury, - governance - ); + inflationManager.initialize(address(polygon), address(migration), stakeManager, treasury, governance); // POL being inflationary, 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 @@ -68,13 +57,7 @@ contract DefaultInflationManagerTest is Test { function testRevert_Initialize() external { vm.expectRevert("Initializable: contract is already initialized"); - inflationManager.initialize( - address(0), - address(0), - address(0), - address(0), - address(0) - ); + inflationManager.initialize(address(0), address(0), address(0), address(0), address(0)); } function test_Deployment() external { @@ -82,10 +65,7 @@ contract DefaultInflationManagerTest is Test { assertEq(inflationManager.stakeManager(), stakeManager); assertEq(inflationManager.treasury(), treasury); assertEq(inflationManager.owner(), governance); - assertEq( - polygon.allowance(address(inflationManager), address(migration)), - type(uint256).max - ); + assertEq(polygon.allowance(address(inflationManager), address(migration)), type(uint256).max); assertEq(inflationManager.START_SUPPLY(), 10_000_000_000e18); assertEq(polygon.totalSupply(), 10_000_000_000e18); } @@ -101,32 +81,21 @@ contract DefaultInflationManagerTest is Test { params[seed % params.length] = address(0); // any one is zero addr address proxy = address( - new TransparentUpgradeableProxy( - address(new DefaultInflationManager()), - msg.sender, - "" - ) + new TransparentUpgradeableProxy(address(new DefaultInflationManager()), msg.sender, "") ); vm.expectRevert(InvalidAddress.selector); - DefaultInflationManager(proxy).initialize( - params[0], - params[1], - params[2], - params[3], - params[4] - ); + DefaultInflationManager(proxy).initialize(params[0], params[1], params[2], params[3], params[4]); } function test_ImplementationCannotBeInitialized() external { vm.expectRevert("Initializable: contract is already initialized"); - DefaultInflationManager(address(inflationManagerImplementation)) - .initialize( - address(0), - address(0), - address(0), - address(0), - address(0) - ); + DefaultInflationManager(address(inflationManagerImplementation)).initialize( + address(0), + address(0), + address(0), + address(0), + address(0) + ); vm.expectRevert("Initializable: contract is already initialized"); DefaultInflationManager(address(inflationManager)).initialize( address(0), @@ -148,51 +117,35 @@ contract DefaultInflationManagerTest is Test { function test_MintDelay(uint128 delay) external { vm.assume(delay <= 10 * 365 days); - uint256 elapsedTime = block.timestamp + delay; uint256 initialTotalSupply = polygon.totalSupply(); skip(delay); inflationManager.mint(); - inputs[2] = vm.toString(elapsedTime); + inputs[2] = vm.toString(delay); inputs[3] = vm.toString(initialTotalSupply); uint256 newSupply = abi.decode(vm.ffi(inputs), (uint256)); - assertApproxEqAbs( - newSupply, - polygon.totalSupply(), - _MAX_PRECISION_DELTA - ); - assertEq( - matic.balanceOf(stakeManager), - (polygon.totalSupply() - initialTotalSupply) / 2 - ); + assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); + assertEq(matic.balanceOf(stakeManager), (polygon.totalSupply() - initialTotalSupply) / 2); assertEq(polygon.balanceOf(stakeManager), 0); - assertEq( - polygon.balanceOf(treasury), - (polygon.totalSupply() - initialTotalSupply) / 2 - ); + assertEq(polygon.balanceOf(treasury), (polygon.totalSupply() - initialTotalSupply) / 2); } function test_MintDelayTwice(uint128 delay) external { vm.assume(delay <= 5 * 365 days && delay > 0); - uint256 elapsedTime = block.timestamp + delay; uint256 initialTotalSupply = polygon.totalSupply(); skip(delay); inflationManager.mint(); - inputs[2] = vm.toString(elapsedTime); + inputs[2] = vm.toString(delay); inputs[3] = vm.toString(initialTotalSupply); uint256 newSupply = abi.decode(vm.ffi(inputs), (uint256)); - assertApproxEqAbs( - newSupply, - polygon.totalSupply(), - _MAX_PRECISION_DELTA - ); + assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); uint256 balance = (polygon.totalSupply() - initialTotalSupply) / 2; assertEq(matic.balanceOf(stakeManager), balance); assertEq(polygon.balanceOf(stakeManager), 0); @@ -202,15 +155,11 @@ contract DefaultInflationManagerTest is Test { skip(delay); inflationManager.mint(); - inputs[2] = vm.toString(elapsedTime + delay); + inputs[2] = vm.toString(delay * 2); inputs[3] = vm.toString(initialTotalSupply); newSupply = abi.decode(vm.ffi(inputs), (uint256)); - assertApproxEqAbs( - newSupply, - polygon.totalSupply(), - _MAX_PRECISION_DELTA - ); + assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); balance += (polygon.totalSupply() - initialTotalSupply) / 2; assertEq(matic.balanceOf(stakeManager), balance); assertEq(polygon.balanceOf(stakeManager), 0); @@ -218,28 +167,21 @@ contract DefaultInflationManagerTest is Test { } function test_MintDelayAfterNCycles(uint128 delay, uint8 cycles) external { - vm.assume( - delay * uint256(cycles) <= 10 * 365 days && delay > 0 && cycles < 30 - ); + vm.assume(delay * uint256(cycles) <= 10 * 365 days && delay > 0 && cycles < 30); uint256 balance; for (uint256 cycle; cycle < cycles; cycle++) { - uint256 elapsedTime = block.timestamp + delay; uint256 initialTotalSupply = polygon.totalSupply(); skip(delay); inflationManager.mint(); - inputs[2] = vm.toString(elapsedTime); + inputs[2] = vm.toString(delay * (cycle + 1)); inputs[3] = vm.toString(initialTotalSupply); uint256 newSupply = abi.decode(vm.ffi(inputs), (uint256)); - assertApproxEqAbs( - newSupply, - polygon.totalSupply(), - _MAX_PRECISION_DELTA - ); + assertApproxEqAbs(newSupply, polygon.totalSupply(), _MAX_PRECISION_DELTA); balance += (polygon.totalSupply() - initialTotalSupply) / 2; assertEq(matic.balanceOf(stakeManager), balance); assertEq(polygon.balanceOf(stakeManager), 0); diff --git a/test/Polygon.t.sol b/test/Polygon.t.sol index 1353e92..8ed78ba 100644 --- a/test/Polygon.t.sol +++ b/test/Polygon.t.sol @@ -14,6 +14,7 @@ contract PolygonTest is Test { address public treasury; address public stakeManager; DefaultInflationManager public inflationManager; + uint256 public constant mintPerSecondCap = 0.0000000420e18; // 0.0000042% of POL Supply per second, in 18 decimals function setUp() external { migration = makeAddr("migration"); @@ -21,22 +22,10 @@ contract PolygonTest is Test { stakeManager = makeAddr("stakeManager"); matic = makeAddr("matic"); inflationManager = DefaultInflationManager( - address( - new TransparentUpgradeableProxy( - address(new DefaultInflationManager()), - msg.sender, - "" - ) - ) + address(new TransparentUpgradeableProxy(address(new DefaultInflationManager()), msg.sender, "")) ); polygon = new Polygon(migration, address(inflationManager)); - inflationManager.initialize( - address(polygon), - migration, - stakeManager, - treasury, - msg.sender - ); + inflationManager.initialize(address(polygon), migration, stakeManager, treasury, msg.sender); } function test_Deployment() external { @@ -60,11 +49,7 @@ contract PolygonTest is Test { token = new Polygon(address(0), address(0)); } - function testRevert_Mint( - address user, - address to, - uint256 amount - ) external { + function testRevert_Mint(address user, address to, uint256 amount) external { vm.assume(user != address(inflationManager)); vm.startPrank(user); vm.expectRevert(IPolygon.OnlyInflationManager.selector); @@ -73,40 +58,20 @@ contract PolygonTest is Test { function test_Mint(address to, uint256 amount) external { skip(1e8); // delay needed for a max mint of 10B - vm.assume( - to != address(0) && - amount <= 10000000000 * 10 ** 18 && - to != migration - ); + vm.assume(to != address(0) && amount <= 10000000000 * 10 ** 18 && to != migration); vm.prank(address(inflationManager)); polygon.mint(to, amount); assertEq(polygon.balanceOf(to), amount); } - function test_MintMaxExceeded( - address to, - uint256 amount, - uint256 delay - ) external { - vm.assume( - to != address(0) && - amount <= 10000000000 * 10 ** 18 && - to != migration && - delay < 10 * 365 days - ); + function test_MintMaxExceeded(address to, uint256 amount, uint256 delay) external { + vm.assume(to != address(0) && amount <= 10000000000 * 10 ** 18 && to != migration && delay < 10 * 365 days); skip(++delay); // avoid delay == 0 - uint256 maxMint = (0.00000001e18 * delay * polygon.totalSupply()) / - 1e18; + uint256 maxMint = (mintPerSecondCap * delay * polygon.totalSupply()) / 1e18; if (amount > maxMint) - vm.expectRevert( - abi.encodeWithSelector( - IPolygon.MaxMintExceeded.selector, - maxMint, - amount - ) - ); + vm.expectRevert(abi.encodeWithSelector(IPolygon.MaxMintExceeded.selector, maxMint, amount)); vm.prank(address(inflationManager)); polygon.mint(to, amount); diff --git a/test/PolygonMigration.t.sol b/test/PolygonMigration.t.sol index 2897674..e5cb72b 100644 --- a/test/PolygonMigration.t.sol +++ b/test/PolygonMigration.t.sol @@ -5,6 +5,7 @@ import {Polygon} from "src/Polygon.sol"; import {PolygonMigration} from "src/PolygonMigration.sol"; import {IPolygonMigration} from "src/interfaces/IPolygonMigration.sol"; import {ERC20PresetMinterPauser} from "openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {TransparentUpgradeableProxy} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {SigUtils} from "test/SigUtils.t.sol"; import {Test} from "forge-std/Test.sol"; @@ -24,7 +25,15 @@ contract PolygonMigrationTest is Test { inflationManager = makeAddr("inflationManager"); governance = makeAddr("governance"); matic = new ERC20PresetMinterPauser("Matic Token", "MATIC"); - migration = new PolygonMigration(address(matic), governance); + migration = PolygonMigration( + address( + new TransparentUpgradeableProxy( + address(new PolygonMigration()), + msg.sender, + abi.encodeCall(PolygonMigration.initialize, address(matic)) + ) + ) + ); polygon = new Polygon(address(migration), address(inflationManager)); sigUtils = new SigUtils(polygon.DOMAIN_SEPARATOR()); @@ -37,28 +46,23 @@ contract PolygonMigrationTest is Test { function test_Deployment() external { assertEq(address(migration.polygon()), address(polygon)); assertEq(address(migration.matic()), address(matic)); - assertEq( - migration.releaseTimestamp(), - block.timestamp + (365 days * 4) - ); assertEq(migration.owner(), governance); } function test_InvalidDeployment() external { - PolygonMigration temp; - vm.expectRevert(IPolygonMigration.InvalidAddress.selector); - temp = new PolygonMigration(address(matic), address(0)); - vm.expectRevert(IPolygonMigration.InvalidAddress.selector); - temp = new PolygonMigration(address(0), governance); + PolygonMigration temp = PolygonMigration( + address(new TransparentUpgradeableProxy(address(new PolygonMigration()), msg.sender, "")) + ); vm.expectRevert(IPolygonMigration.InvalidAddress.selector); - temp = new PolygonMigration(address(0), address(0)); + temp.initialize(address(0)); + temp.initialize(address(matic)); + vm.expectRevert("Initializable: contract is already initialized"); + temp.initialize(address(1)); } function test_Migrate(address user, uint256 amount) external { vm.assume( - amount <= 10000000000 * 10 ** 18 && - user != address(0) && - user != address(migration) + amount <= 10000000000 * 10 ** 18 && user != address(0) && user != address(migration) && user != governance ); matic.mint(user, amount); vm.startPrank(user); @@ -85,16 +89,13 @@ contract PolygonMigrationTest is Test { vm.stopPrank(); } - function test_Unmigrate( - address user, - uint256 amount, - uint256 amount2 - ) external { + function test_Unmigrate(address user, uint256 amount, uint256 amount2) external { vm.assume( amount <= 10000000000 * 10 ** 18 && amount2 <= amount && user != address(0) && - user != address(migration) + user != address(migration) && + user != governance ); matic.mint(user, amount); vm.startPrank(user); @@ -113,16 +114,10 @@ contract PolygonMigrationTest is Test { assertEq(matic.balanceOf(user), amount2); } - function testRevert_Unmigrate( - address user, - uint256 amount, - uint256 unmigrationLock - ) external { + function testRevert_Unmigrate(address user, uint256 amount) external { + bool unmigrationLock = true; vm.assume( - amount <= 10000000000 * 10 ** 18 && - user != address(0) && - user != address(migration) && - unmigrationLock != 0 + amount <= 10000000000 * 10 ** 18 && user != address(0) && user != address(migration) && user != governance ); matic.mint(user, amount); vm.startPrank(user); @@ -142,17 +137,13 @@ contract PolygonMigrationTest is Test { vm.stopPrank(); } - function test_UnmigrateTo( - address user, - address migrateTo, - uint256 amount, - uint256 amount2 - ) external { + function test_UnmigrateTo(address user, address migrateTo, uint256 amount, uint256 amount2) external { vm.assume( amount <= 10000000000 * 10 ** 18 && amount2 <= amount && user != address(0) && user != address(migration) && + user != governance && user != migrateTo && migrateTo != address(0) && migrateTo != address(migration) @@ -167,7 +158,7 @@ contract PolygonMigrationTest is Test { assertEq(polygon.balanceOf(user), amount); polygon.approve(address(migration), amount2); - migration.unmigrateTo(amount2, migrateTo); + migration.unmigrateTo(migrateTo, amount2); assertEq(polygon.balanceOf(user), amount - amount2); assertEq(matic.balanceOf(address(migration)), amount - amount2); @@ -175,17 +166,13 @@ contract PolygonMigrationTest is Test { assertEq(matic.balanceOf(migrateTo), amount2); } - function test_UnmigrateWithPermit( - uint256 privKey, - uint256 amount, - uint256 amount2 - ) external { + function test_UnmigrateWithPermit(uint256 privKey, uint256 amount, uint256 amount2) external { address user; vm.assume( privKey != 0 && - privKey < - 115792089237316195423570985008687907852837564279074904382605163141518161494337 && + privKey < 115792089237316195423570985008687907852837564279074904382605163141518161494337 && (user = vm.addr(privKey)) != address(migration) && + user != governance && amount <= 10000000000 * 10 ** 18 && amount2 <= amount ); @@ -214,53 +201,4 @@ contract PolygonMigrationTest is Test { assertEq(matic.balanceOf(address(migration)), amount - amount2); assertEq(matic.balanceOf(user), amount2); } - - function testRevert_UpdateReleaseTimestampOnlyGovernance( - address user, - uint256 timestamp - ) external { - vm.assume(timestamp >= block.timestamp && user != governance); - vm.expectRevert("Ownable: caller is not the owner"); - migration.updateReleaseTimestamp(timestamp); - } - - function testRevert_UpdateReleaseTimestampTooEarly( - uint256 timestamp - ) external { - vm.assume(timestamp < block.timestamp); - vm.startPrank(governance); - vm.expectRevert(IPolygonMigration.InvalidTimestamp.selector); - migration.updateReleaseTimestamp(timestamp); - } - - function test_UpdateReleaseTimestamp(uint256 timestamp) external { - vm.assume(timestamp >= block.timestamp); - vm.startPrank(governance); - migration.updateReleaseTimestamp(timestamp); - - assertEq(migration.releaseTimestamp(), timestamp); - } - - function testRevert_ReleaseOnlyGovernance() external { - vm.expectRevert("Ownable: caller is not the owner"); - migration.release(); - } - - function testRevert_ReleaseTooEarly(uint256 timestamp) external { - vm.assume(timestamp < migration.releaseTimestamp()); - vm.startPrank(governance); - vm.expectRevert(IPolygonMigration.MigrationNotOver.selector); - migration.release(); - } - - function test_Release(uint256 timestamp) external { - vm.assume(timestamp >= migration.releaseTimestamp()); - vm.warp(timestamp); - uint256 balance = polygon.balanceOf(address(migration)); - vm.startPrank(governance); - migration.release(); - - assertEq(polygon.balanceOf(address(migration)), 0); - assertEq(polygon.balanceOf(governance), balance); - } } diff --git a/test/SigUtils.t.sol b/test/SigUtils.t.sol index a97165b..e93934f 100644 --- a/test/SigUtils.t.sol +++ b/test/SigUtils.t.sol @@ -21,9 +21,17 @@ contract SigUtils { // computes the hash of a permit function getStructHash(Permit memory _permit) internal pure returns (bytes32) { - return keccak256( - abi.encode(PERMIT_TYPEHASH, _permit.owner, _permit.spender, _permit.value, _permit.nonce, _permit.deadline) - ); + return + keccak256( + abi.encode( + PERMIT_TYPEHASH, + _permit.owner, + _permit.spender, + _permit.value, + _permit.nonce, + _permit.deadline + ) + ); } // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer diff --git a/test/util/calc.js b/test/util/calc.js index 386038a..b5c2f19 100644 --- a/test/util/calc.js +++ b/test/util/calc.js @@ -1,12 +1,12 @@ -const interestRatePerSecond = 1.000000000634195839 -const startSupply = 10_000_000_000e18 +const interestRatePerYear = 1.02; +const startSupply = 10_000_000_000e18; function main() { - const [timeElapsedInSeconds] = process.argv.slice(2) + const [timeElapsedInSeconds] = process.argv.slice(2); - const supplyFactor = Math.pow(interestRatePerSecond, timeElapsedInSeconds) - const newSupply = BigInt(startSupply * supplyFactor) + const supplyFactor = Math.pow(interestRatePerYear, timeElapsedInSeconds / (365 * 24 * 60 * 60)); + const newSupply = BigInt(startSupply * supplyFactor); - console.log('0x' + newSupply.toString(16).padStart(64, '0')) // abi.encode(toMint) + console.log("0x" + newSupply.toString(16).padStart(64, "0")); // abi.encode(toMint) } main();