diff --git a/.gitignore b/.gitignore index fdadd9e..9c739de 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ /coverage lcov.info .DS_Store +broadcast/ +.env + +output/31337.json \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..6d80269 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.16.0 diff --git a/README.md b/README.md index 5091341..0ade691 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,27 @@ A default implementation is included and this contract will be proxy upgradable [Source Code](https://github.com/0xPolygon/pol-token/tree/main/src/DefaultEmissionManager.sol) +## Development + +# Setup + +- Install foundry: https://book.getfoundry.sh/getting-started/installation +- Install Dependencies: `forge install` +- Build: `forge build` +- Test: `forge test` + +# Deployment + +1. Ensure .env file is set, `cp .env.example` +2. Populate Enviornment variables: `source .env` + +3. We use a forge script to deploy the contracts, and have an additional extract.js script to store a JSON file with + +- (mainnet): `forge script script/Deploy.s.sol --broadcast --verify --rpc-url $RPC_URL --private-key $PRIVATE_KEY --etherscan-api-key $ETHERSCAN_API_KEY` +- (testnet, goerli for example): `forge script script/Deploy.s.sol --broadcast --verify --rpc-url $RPC_URL --private-key $PRIVATE_KEY --verifier-url https://api-goerli.etherscan.io/api --chain-id 5` + +4. Run `node script/util/extract.js` to extract deployment information from forge broadcast output (broadcast/latest-run.json specifically). + --- Copyright (C) 2023 PT Services DMCC diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..4e42a1b --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +book/ \ No newline at end of file diff --git a/docs/book.css b/docs/book.css new file mode 100644 index 0000000..b5ce903 --- /dev/null +++ b/docs/book.css @@ -0,0 +1,13 @@ +table { + margin: 0 auto; + border-collapse: collapse; + width: 100%; +} + +table td:first-child { + width: 15%; +} + +table td:nth-child(2) { + width: 25%; +} \ No newline at end of file diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000..404c19e --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,12 @@ +[book] +src = "src" +title = "" + +[output.html] +no-section-label = true +additional-js = ["solidity.min.js"] +additional-css = ["book.css"] +git-repository-url = "https://github.com/0xPolygon/pol-token" + +[output.html.fold] +enable = true diff --git a/docs/solidity.min.js b/docs/solidity.min.js new file mode 100644 index 0000000..1924932 --- /dev/null +++ b/docs/solidity.min.js @@ -0,0 +1,74 @@ +hljs.registerLanguage("solidity",(()=>{"use strict";function e(){try{return!0 +}catch(e){return!1}} +var a=/-?(\b0[xX]([a-fA-F0-9]_?)*[a-fA-F0-9]|(\b[1-9](_?\d)*(\.((\d_?)*\d)?)?|\.\d(_?\d)*)([eE][-+]?\d(_?\d)*)?|\b0)(?!\w|\$)/ +;e()&&(a=a.source.replace(/\\b/g,"(?{ +var a=r(e),o=l(e),c=/[A-Za-z_$][A-Za-z_$0-9.]*/,d=e.inherit(e.TITLE_MODE,{ +begin:/[A-Za-z$_][0-9A-Za-z$_]*/,lexemes:c,keywords:n}),u={className:"params", +begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,lexemes:c,keywords:n, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,o,s]},_={ +className:"operator",begin:/:=|->/};return{keywords:n,lexemes:c, +contains:[a,o,i,t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,_,{ +className:"function",lexemes:c,beginKeywords:"function",end:"{",excludeEnd:!0, +contains:[d,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,_]}]}}, +solAposStringMode:r,solQuoteStringMode:l,HEX_APOS_STRING_MODE:i, +HEX_QUOTE_STRING_MODE:t,SOL_NUMBER:s,isNegativeLookbehindAvailable:e} +;const{baseAssembly:c,solAposStringMode:d,solQuoteStringMode:u,HEX_APOS_STRING_MODE:_,HEX_QUOTE_STRING_MODE:m,SOL_NUMBER:b,isNegativeLookbehindAvailable:E}=o +;return e=>{for(var a=d(e),s=u(e),n=[],i=0;i<32;i++)n[i]=i+1 +;var t=n.map((e=>8*e)),r=[];for(i=0;i<=80;i++)r[i]=i +;var l=n.map((e=>"bytes"+e)).join(" ")+" ",o=t.map((e=>"uint"+e)).join(" ")+" ",g=t.map((e=>"int"+e)).join(" ")+" ",M=[].concat.apply([],t.map((e=>r.map((a=>e+"x"+a))))),p={ +keyword:"var bool string int uint "+g+o+"byte bytes "+l+"fixed ufixed "+M.map((e=>"fixed"+e)).join(" ")+" "+M.map((e=>"ufixed"+e)).join(" ")+" enum struct mapping address new delete if else for while continue break return throw emit try catch revert unchecked _ function modifier event constructor fallback receive error virtual override constant immutable anonymous indexed storage memory calldata external public internal payable pure view private returns import from as using pragma contract interface library is abstract type assembly", +literal:"true false wei gwei szabo finney ether seconds minutes hours days weeks years", +built_in:"self this super selfdestruct suicide now msg block tx abi blockhash gasleft assert require Error Panic sha3 sha256 keccak256 ripemd160 ecrecover addmod mulmod log0 log1 log2 log3 log4" +},O={className:"operator",begin:/[+\-!~*\/%<>&^|=]/ +},C=/[A-Za-z_$][A-Za-z_$0-9]*/,N={className:"params",begin:/\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,lexemes:C,keywords:p, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,s,b,"self"]},f={ +begin:/\.\s*/,end:/[^A-Za-z0-9$_\.]/,excludeBegin:!0,excludeEnd:!0,keywords:{ +built_in:"gas value selector address length push pop send transfer call callcode delegatecall staticcall balance code codehash wrap unwrap name creationCode runtimeCode interfaceId min max" +},relevance:2},y=e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/, +lexemes:C,keywords:p}),w={className:"built_in", +begin:(E()?"(?`|`string`|Version string| + + diff --git a/docs/src/src/PolygonEcosystemToken.sol/contract.PolygonEcosystemToken.md b/docs/src/src/PolygonEcosystemToken.sol/contract.PolygonEcosystemToken.md new file mode 100644 index 0000000..93f8d12 --- /dev/null +++ b/docs/src/src/PolygonEcosystemToken.sol/contract.PolygonEcosystemToken.md @@ -0,0 +1,138 @@ +# PolygonEcosystemToken +[Git Source](https://github.com/0xPolygon/pol-token/blob/c05c8984ac856501829862c1f6d199208aa77a8e/src/PolygonEcosystemToken.sol) + +**Inherits:** +ERC20Permit, AccessControlEnumerable, [IPolygonEcosystemToken](/src/interfaces/IPolygonEcosystemToken.sol/interface.IPolygonEcosystemToken.md) + +**Author:** +Polygon Labs (@DhairyaSethi, @gretzke, @qedk) + +This is the Polygon ERC20 token contract on Ethereum L1 + +*The contract allows for a 1-to-1 representation between $POL and $MATIC and allows for additional emission based on hub and treasury requirements* + + +## State Variables +### EMISSION_ROLE + +```solidity +bytes32 public constant EMISSION_ROLE = keccak256("EMISSION_ROLE"); +``` + + +### CAP_MANAGER_ROLE + +```solidity +bytes32 public constant CAP_MANAGER_ROLE = keccak256("CAP_MANAGER_ROLE"); +``` + + +### PERMIT2_REVOKER_ROLE + +```solidity +bytes32 public constant PERMIT2_REVOKER_ROLE = keccak256("PERMIT2_REVOKER_ROLE"); +``` + + +### PERMIT2 + +```solidity +address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; +``` + + +### mintPerSecondCap + +```solidity +uint256 public mintPerSecondCap = 10e18; +``` + + +### lastMint + +```solidity +uint256 public lastMint; +``` + + +### permit2Enabled + +```solidity +bool public permit2Enabled; +``` + + +## Functions +### constructor + + +```solidity +constructor(address migration, address emissionManager, address governance, address permit2Revoker) + ERC20("Polygon Ecosystem Token", "POL") + ERC20Permit("Polygon Ecosystem Token"); +``` + +### mint + +Mint token entrypoint for the emission manager contract + +*The function only validates the sender, the emission manager is responsible for correctness* + + +```solidity +function mint(address to, uint256 amount) external onlyRole(EMISSION_ROLE); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`to`|`address`|Address to mint to| +|`amount`|`uint256`|Amount to mint| + + +### updateMintCap + +Update the limit of tokens that can be minted per second + + +```solidity +function updateMintCap(uint256 newCap) external onlyRole(CAP_MANAGER_ROLE); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newCap`|`uint256`|the amount of tokens in 18 decimals as an absolute value| + + +### updatePermit2Allowance + +Manages the default max approval to the permit2 contract + + +```solidity +function updatePermit2Allowance(bool enabled) external onlyRole(PERMIT2_REVOKER_ROLE); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`enabled`|`bool`|If true, the permit2 contract has full approval by default, if false, it has no approval by default| + + +### allowance + +The permit2 contract has full approval by default. If the approval is revoked, it can still be manually approved. + + +```solidity +function allowance(address owner, address spender) public view override(ERC20, IERC20) returns (uint256); +``` + +### _updatePermit2Allowance + + +```solidity +function _updatePermit2Allowance(bool enabled) private; +``` + diff --git a/docs/src/src/PolygonMigration.sol/contract.PolygonMigration.md b/docs/src/src/PolygonMigration.sol/contract.PolygonMigration.md new file mode 100644 index 0000000..7f33eaf --- /dev/null +++ b/docs/src/src/PolygonMigration.sol/contract.PolygonMigration.md @@ -0,0 +1,216 @@ +# PolygonMigration +[Git Source](https://github.com/0xPolygon/pol-token/blob/c05c8984ac856501829862c1f6d199208aa77a8e/src/PolygonMigration.sol) + +**Inherits:** +Ownable2StepUpgradeable, [IPolygonMigration](/src/interfaces/IPolygonMigration.sol/interface.IPolygonMigration.md) + +**Author:** +Polygon Labs (@DhairyaSethi, @gretzke, @qedk) + +This is the migration contract for Matic <-> Polygon ERC20 token on Ethereum L1 + +*The contract allows for a 1-to-1 conversion from $MATIC into $POL and vice-versa* + + +## State Variables +### matic + +```solidity +IERC20 public immutable matic; +``` + + +### polygon + +```solidity +IERC20 public polygon; +``` + + +### unmigrationLocked + +```solidity +bool public unmigrationLocked; +``` + + +### __gap +*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* + + +```solidity +uint256[50] private __gap; +``` + + +## Functions +### onlyUnmigrationUnlocked + + +```solidity +modifier onlyUnmigrationUnlocked(); +``` + +### constructor + + +```solidity +constructor(address matic_); +``` + +### initialize + + +```solidity +function initialize() external initializer; +``` + +### setPolygonToken + +This function allows owner/governance to set POL token address *only once* + + +```solidity +function setPolygonToken(address polygon_) external onlyOwner; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`polygon_`|`address`|Address of deployed POL token| + + +### migrate + +This function allows for migrating MATIC tokens to POL tokens + +*The function does not do any validation since the migration is a one-way process* + + +```solidity +function migrate(uint256 amount) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`amount`|`uint256`|Amount of MATIC to migrate| + + +### unmigrate + +This function allows for unmigrating from POL tokens to MATIC tokens + +*The function can only be called when unmigration is unlocked (lock updatable by governance)* + +*The function does not do any further validation, also note the unmigration is a reversible process* + + +```solidity +function unmigrate(uint256 amount) external onlyUnmigrationUnlocked; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`amount`|`uint256`|Amount of POL to migrate| + + +### unmigrateTo + +This function allows for unmigrating POL tokens (from msg.sender) to MATIC tokens (to account) + +*The function can only be called when unmigration is unlocked (lock updatable by governance)* + +*The function does not do any further validation, also note the unmigration is a reversible process* + + +```solidity +function unmigrateTo(address recipient, uint256 amount) external onlyUnmigrationUnlocked; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`recipient`|`address`|Address to receive MATIC tokens| +|`amount`|`uint256`|Amount of POL to migrate| + + +### unmigrateWithPermit + +This function allows for unmigrating from POL tokens to MATIC tokens using an EIP-2612 permit + +*The function can only be called when unmigration is unlocked (lock updatable by governance)* + +*The function does not do any further validation, also note the unmigration is a reversible process* + + +```solidity +function unmigrateWithPermit(uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + onlyUnmigrationUnlocked; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`amount`|`uint256`|Amount of POL to migrate| +|`deadline`|`uint256`|Deadline for the permit| +|`v`|`uint8`|v value of the permit signature| +|`r`|`bytes32`|r value of the permit signature| +|`s`|`bytes32`|s value of the permit signature| + + +### updateUnmigrationLock + +Allows governance to lock or unlock the unmigration process + +*The function does not do any validation since governance can update the unmigration process if required* + + +```solidity +function updateUnmigrationLock(bool unmigrationLocked_) external onlyOwner; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`unmigrationLocked_`|`bool`|New unmigration lock status| + + +### getVersion + +Returns the implementation version + + +```solidity +function getVersion() external pure returns (string memory); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`string`|Version string| + + +### burn + +Allows governance to burn `amount` of POL tokens + +*This functions burns POL by sending to dead address* + +*does not change totalSupply in the internal accounting of POL* + + +```solidity +function burn(uint256 amount) external onlyOwner; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`amount`|`uint256`|Amount of POL to burn| + + diff --git a/docs/src/src/README.md b/docs/src/src/README.md new file mode 100644 index 0000000..2914a31 --- /dev/null +++ b/docs/src/src/README.md @@ -0,0 +1,8 @@ + + +# Contents +- [interfaces](/src/interfaces) +- [lib](/src/lib) +- [DefaultEmissionManager](DefaultEmissionManager.sol/contract.DefaultEmissionManager.md) +- [PolygonEcosystemToken](PolygonEcosystemToken.sol/contract.PolygonEcosystemToken.md) +- [PolygonMigration](PolygonMigration.sol/contract.PolygonMigration.md) diff --git a/docs/src/src/interfaces/IDefaultEmissionManager.sol/interface.IDefaultEmissionManager.md b/docs/src/src/interfaces/IDefaultEmissionManager.sol/interface.IDefaultEmissionManager.md new file mode 100644 index 0000000..380237f --- /dev/null +++ b/docs/src/src/interfaces/IDefaultEmissionManager.sol/interface.IDefaultEmissionManager.md @@ -0,0 +1,47 @@ +# IDefaultEmissionManager +[Git Source](https://github.com/0xPolygon/pol-token/blob/c05c8984ac856501829862c1f6d199208aa77a8e/src/interfaces/IDefaultEmissionManager.sol) + + +## Functions +### getVersion + + +```solidity +function getVersion() external pure returns (string memory version); +``` + +### startTimestamp + + +```solidity +function startTimestamp() external view returns (uint256 timestamp); +``` + +### mint + + +```solidity +function mint() external; +``` + +### inflatedSupplyAfter + + +```solidity +function inflatedSupplyAfter(uint256 timeElapsedInSeconds) external pure returns (uint256 inflatedSupply); +``` + +## Events +### TokenMint + +```solidity +event TokenMint(uint256 amount, address caller); +``` + +## Errors +### InvalidAddress + +```solidity +error InvalidAddress(); +``` + diff --git a/docs/src/src/interfaces/IPolygonEcosystemToken.sol/interface.IPolygonEcosystemToken.md b/docs/src/src/interfaces/IPolygonEcosystemToken.sol/interface.IPolygonEcosystemToken.md new file mode 100644 index 0000000..74931f8 --- /dev/null +++ b/docs/src/src/interfaces/IPolygonEcosystemToken.sol/interface.IPolygonEcosystemToken.md @@ -0,0 +1,76 @@ +# IPolygonEcosystemToken +[Git Source](https://github.com/0xPolygon/pol-token/blob/c05c8984ac856501829862c1f6d199208aa77a8e/src/interfaces/IPolygonEcosystemToken.sol) + +**Inherits:** +IERC20, IERC20Permit, IAccessControlEnumerable + + +## Functions +### mintPerSecondCap + + +```solidity +function mintPerSecondCap() external view returns (uint256 currentMintPerSecondCap); +``` + +### lastMint + + +```solidity +function lastMint() external view returns (uint256 lastMintTimestamp); +``` + +### permit2Enabled + + +```solidity +function permit2Enabled() external view returns (bool isPermit2Enabled); +``` + +### mint + + +```solidity +function mint(address to, uint256 amount) external; +``` + +### updateMintCap + + +```solidity +function updateMintCap(uint256 newCap) external; +``` + +### updatePermit2Allowance + + +```solidity +function updatePermit2Allowance(bool enabled) external; +``` + +## Events +### MintCapUpdated + +```solidity +event MintCapUpdated(uint256 oldCap, uint256 newCap); +``` + +### Permit2AllowanceUpdated + +```solidity +event Permit2AllowanceUpdated(bool enabled); +``` + +## Errors +### InvalidAddress + +```solidity +error InvalidAddress(); +``` + +### MaxMintExceeded + +```solidity +error MaxMintExceeded(uint256 maxMint, uint256 mintRequested); +``` + diff --git a/docs/src/src/interfaces/IPolygonMigration.sol/interface.IPolygonMigration.md b/docs/src/src/interfaces/IPolygonMigration.sol/interface.IPolygonMigration.md new file mode 100644 index 0000000..3c3a693 --- /dev/null +++ b/docs/src/src/interfaces/IPolygonMigration.sol/interface.IPolygonMigration.md @@ -0,0 +1,99 @@ +# IPolygonMigration +[Git Source](https://github.com/0xPolygon/pol-token/blob/c05c8984ac856501829862c1f6d199208aa77a8e/src/interfaces/IPolygonMigration.sol) + + +## Functions +### unmigrationLocked + + +```solidity +function unmigrationLocked() external view returns (bool isUnmigrationLocked); +``` + +### getVersion + + +```solidity +function getVersion() external pure returns (string memory version); +``` + +### migrate + + +```solidity +function migrate(uint256 amount) external; +``` + +### unmigrate + + +```solidity +function unmigrate(uint256 amount) external; +``` + +### unmigrateTo + + +```solidity +function unmigrateTo(address recipient, uint256 amount) external; +``` + +### unmigrateWithPermit + + +```solidity +function unmigrateWithPermit(uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; +``` + +### updateUnmigrationLock + + +```solidity +function updateUnmigrationLock(bool unmigrationLocked_) external; +``` + +### burn + + +```solidity +function burn(uint256 amount) external; +``` + +## Events +### Migrated + +```solidity +event Migrated(address indexed account, uint256 amount); +``` + +### Unmigrated + +```solidity +event Unmigrated(address indexed account, address indexed recipient, uint256 amount); +``` + +### UnmigrationLockUpdated + +```solidity +event UnmigrationLockUpdated(bool lock); +``` + +## Errors +### UnmigrationLocked + +```solidity +error UnmigrationLocked(); +``` + +### InvalidAddressOrAlreadySet + +```solidity +error InvalidAddressOrAlreadySet(); +``` + +### InvalidAddress + +```solidity +error InvalidAddress(); +``` + diff --git a/docs/src/src/interfaces/README.md b/docs/src/src/interfaces/README.md new file mode 100644 index 0000000..46aba7a --- /dev/null +++ b/docs/src/src/interfaces/README.md @@ -0,0 +1,6 @@ + + +# Contents +- [IDefaultEmissionManager](IDefaultEmissionManager.sol/interface.IDefaultEmissionManager.md) +- [IPolygonEcosystemToken](IPolygonEcosystemToken.sol/interface.IPolygonEcosystemToken.md) +- [IPolygonMigration](IPolygonMigration.sol/interface.IPolygonMigration.md) diff --git a/docs/src/src/lib/PowUtil.sol/library.PowUtil.md b/docs/src/src/lib/PowUtil.sol/library.PowUtil.md new file mode 100644 index 0000000..1b51a82 --- /dev/null +++ b/docs/src/src/lib/PowUtil.sol/library.PowUtil.md @@ -0,0 +1,12 @@ +# PowUtil +[Git Source](https://github.com/0xPolygon/pol-token/blob/c05c8984ac856501829862c1f6d199208aa77a8e/src/lib/PowUtil.sol) + + +## Functions +### exp2 + + +```solidity +function exp2(uint256 x) internal pure returns (uint256 result); +``` + diff --git a/docs/src/src/lib/README.md b/docs/src/src/lib/README.md new file mode 100644 index 0000000..c65ab09 --- /dev/null +++ b/docs/src/src/lib/README.md @@ -0,0 +1,4 @@ + + +# Contents +- [PowUtil](PowUtil.sol/library.PowUtil.md) diff --git a/foundry.toml b/foundry.toml index b7e9539..90f017c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,10 @@ optimize_runs = 999999 via_ir = true solc = '0.8.21' ffi = true +fs_permissions = [ + { access = "read", path = "script/config.json" }, + { access = "read-write", path = "output/deploy.json" }, +] [profile.intense.fuzz] runs = 10000 diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index b0bff57..804a814 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.21; -import {Script} from "forge-std/Script.sol"; +import {Script, stdJson, console2 as console} from "forge-std/Script.sol"; import {ProxyAdmin, TransparentUpgradeableProxy} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import {PolygonEcosystemToken} from "../src/PolygonEcosystemToken.sol"; @@ -9,19 +9,35 @@ import {DefaultEmissionManager} from "../src/DefaultEmissionManager.sol"; import {PolygonMigration} from "../src/PolygonMigration.sol"; contract Deploy is Script { + using stdJson for string; + + string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk"; uint256 public deployerPrivateKey; constructor() { - deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + deployerPrivateKey = vm.envOr({name: "PRIVATE_KEY", defaultValue: uint256(0)}); + if (deployerPrivateKey == 0) { + (, deployerPrivateKey) = deriveRememberKey({mnemonic: TEST_MNEMONIC, index: 0}); + } + } + + function _getLatestCommitHash() internal returns (string memory ret) { + string[] memory input = new string[](3); + input[0] = "git"; + input[1] = "rev-parse"; + input[2] = "HEAD"; + ret = vm.toString(vm.ffi(input)); } - function run( - address matic, - address governance, - address treasury, - address stakeManager, - address permit2revoker - ) public { + function run() public { + string memory config = vm.readFile("script/config.json"); + string memory chainIdSlug = string(abi.encodePacked('["', vm.toString(block.chainid), '"]')); + address matic = config.readAddress(string.concat(chainIdSlug, ".matic")); + address governance = config.readAddress(string.concat(chainIdSlug, ".governance")); + address treasury = config.readAddress(string.concat(chainIdSlug, ".treasury")); + address stakeManager = config.readAddress(string.concat(chainIdSlug, ".stakeManager")); + address permit2revoker = config.readAddress(string.concat(chainIdSlug, ".permit2revoker")); + vm.startBroadcast(deployerPrivateKey); ProxyAdmin admin = new ProxyAdmin(); diff --git a/script/config.json b/script/config.json new file mode 100644 index 0000000..494a454 --- /dev/null +++ b/script/config.json @@ -0,0 +1,28 @@ +{ + "31337": { + "matic": "0x0000000000000000000000000000000000000001", + "governance": "0x0000000000000000000000000000000000000002", + "treasury": "0x0000000000000000000000000000000000000001", + "stakeManager": "0x0000000000000000000000000000000000000001", + "permit2revoker": "0x0000000000000000000000000000000000000001" + }, + "1": { + "matic": "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", + "governance": "0x6e7a5820baD6cebA8Ef5ea69c0C92EbbDAc9CE48", + "treasury": "0x2ff25495d77f380d5F65B95F103181aE8b1cf898", + "stakeManager": "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908", + "permit2revoker": "0x6e7a5820baD6cebA8Ef5ea69c0C92EbbDAc9CE48" + }, + "5": { + "matic": "0x499d11E0b6eAC7c0593d8Fb292DCBbF815Fb29Ae", + "governance": "0x531c7Befe78B6496e5753815ab3d3Cc024c1E842", + "treasury": "0x531c7Befe78B6496e5753815ab3d3Cc024c1E842", + "stakeManager": "0x00200eA4Ee292E253E6Ca07dBA5EdC07c8Aa37A3", + "permit2revoker": "0x531c7Befe78B6496e5753815ab3d3Cc024c1E842" + }, + "defaultRpc": { + "31337": "http://127.0.0.1:8545", + "1": "https://eth.llamarpc.com", + "5": "https://ethereum-goerli.publicnode.com" + } +} diff --git a/script/utils/extract.js b/script/utils/extract.js new file mode 100644 index 0000000..cab7609 --- /dev/null +++ b/script/utils/extract.js @@ -0,0 +1,122 @@ +const { readFileSync, existsSync, writeFileSync, mkdirSync } = require("fs"); +const { execSync } = require("child_process"); +const { join } = require("path"); + +/** + * @description Extracts contract deployment data from run-latest.json (foundry broadcast output) and writes to output/{chainId}.json + * @usage node script/utils/extract.js {chainId} + * @dev + * currently only supports TransparentUpgradeableProxy pattern + * uses sha256 hash of init code to determine if contract has changed, + * uses `node:crypto` module, update if not using nodejs + */ +async function main() { + const [chainId] = process.argv.slice(2); + const commitHash = getCommitHash(); + const data = JSON.parse( + readFileSync(join(__dirname, `../../broadcast/Deploy.s.sol/${chainId}/run-latest.json`), "utf-8") + ); + const config = JSON.parse(readFileSync(join(__dirname, "../config.json"), "utf-8")); + const rpcUrl = config.defaultRpc[chainId] || process.env.RPC_URL || "http://127.0.0.1:8545"; + const deployments = data.transactions.filter(({ transactionType }) => transactionType === "CREATE"); // CREATE2? + const deployedContractsMap = new Map( + [...deployments].map(({ contractName, contractAddress }) => [contractAddress, contractName]) + ); + + // todo(future): add support for other proxy patterns + const proxies = await Promise.all( + deployments + .filter(({ contractName }) => contractName === "TransparentUpgradeableProxy") + .map(async ({ arguments, contractAddress, transaction: { data } }) => ({ + implementation: arguments[0], + proxyAdmin: arguments[1], + address: contractAddress, + contractName: deployedContractsMap.get(arguments[0]), + proxy: true, + ...(await getVersion(contractAddress, rpcUrl)), + proxyType: "TransparentUpgradeableProxy", + })) + ); + const nonProxies = await Promise.all( + deployments + .filter( + ({ contractName }) => + contractName !== "TransparentUpgradeableProxy" && + !proxies.find((p) => p.contractName === contractName) + ) + .map(async ({ contractName, contractAddress, transaction: { data } }) => ({ + address: contractAddress, + contractName, + proxy: false, + ...(await getVersion(contractAddress, rpcUrl)), + })) + ); + const contracts = [...proxies, ...nonProxies].reduce((obj, { contractName, ...rest }) => { + obj[contractName] = rest; + return obj; + }, {}); + + const outPath = join(__dirname, `../../output/${chainId}.json`); + if (!existsSync(join(__dirname, "../../output/"))) mkdirSync(join(__dirname, "../../output/")); + const out = JSON.parse( + (existsSync(outPath) && readFileSync(outPath, "utf-8")) || JSON.stringify({ chainId, latest: {}, history: [] }) + ); + + // only update if there are changes to specific contracts from history + if (Object.keys(out.latest).length != 0) { + if (out.history.find((h) => h.commitHash === commitHash) || out.latest.commitHash === commitHash) + return console.log("warn: commitHash already deployed"); // if commitHash already exists in history, return + out.history.unshift(out.latest); // add latest to history + } + // overwrite latest with changed contracts + out.latest = { + ...out.latest, + contracts, + input: config[chainId], + commitHash, + timestamp: data.timestamp, + }; + + writeFileSync(outPath, JSON.stringify(out, null, 2)); +} + +function getCommitHash() { + return execSync("git rev-parse HEAD").toString().trim(); // note: update if not using git +} + +function sha256(data) { + const { createHash } = require("node:crypto"); // note: update if not using nodejs + return createHash("sha256").update(data).digest("hex"); +} + +async function getVersion(contractAddress, rpcUrl) { + try { + const res = await ( + await fetch(rpcUrl, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: Date.now(), + method: "eth_call", + params: [{ to: contractAddress, data: "0x0d8e6e2c" }, "latest"], // getVersion()(string) + }), + }) + ).json(); + if (res.error) throw new Error(res.error.message); + return { version: hexToAscii(res.result)?.trim() || res.result }; + } catch (e) { + if (e.message === "execution reverted") return null; // contract does implement getVersion() + console.log("getVersion error:", rpcUrl, e.message); + return { version: undefined }; + } +} + +const hexToAscii = (str) => hexToUtf8(str).replace(/[\u0000-\u0008,\u000A-\u001F,\u007F-\u00A0]+/g, ""); // remove non-ascii chars +const hexToUtf8 = (str) => new TextDecoder().decode(hexToUint8Array(str)); // note: TextDecoder present in node, update if not using nodejs +function hexToUint8Array(hex) { + const value = hex.toLowerCase().startsWith("0x") ? hex.slice(2) : hex; + return new Uint8Array(Math.ceil(value.length / 2)).map((_, i) => parseInt(value.substring(i * 2, i * 2 + 2), 16)); +} + +main(); diff --git a/src/DefaultEmissionManager.sol b/src/DefaultEmissionManager.sol index e09b893..a93f7a9 100644 --- a/src/DefaultEmissionManager.sol +++ b/src/DefaultEmissionManager.sol @@ -100,5 +100,5 @@ contract DefaultEmissionManager is Ownable2StepUpgradeable, IDefaultEmissionMana * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[50] private __gap; + uint256[48] private __gap; } diff --git a/src/PolygonMigration.sol b/src/PolygonMigration.sol index eff80b7..5fabc9b 100644 --- a/src/PolygonMigration.sol +++ b/src/PolygonMigration.sol @@ -125,5 +125,5 @@ contract PolygonMigration is Ownable2StepUpgradeable, IPolygonMigration { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[50] private __gap; + uint256[48] private __gap; }