diff --git a/README.md b/README.md index c27f9828..874cdec0 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ upgrade one at a time. Every wallet is a beacon proxy, so one beacon call moves | `Account4337` | The ERC-4337 `IAccount` and `IERC1271` base that `DeviceWallet` builds on | Inherited by `DeviceWallet` | | `WebAuthn` | Verifies WebAuthn authentication assertions. Tries the RIP-7212 precompile first and falls back to FreshCryptoLib | Library | | `P256Verifier` | One immutable address for accounts to verify through, wrapping the WebAuthn library | Plain contract | -| `ProtocolAdmin` | Timelock meant to own the four singletons. Adds a delay floor that `updateDelay` cannot go under, and a guardian role with exactly two powers. **Written, not deployed** | Plain contract | +| `ProtocolAdmin` | Timelock owning the four singletons. Adds a delay floor that `updateDelay` cannot go under, and a guardian role with exactly two powers | Plain contract | | `Errors` | Every custom error in the suite | Library | | `CustomStructs` | Structs shared across contracts | Types | | `interfaces/` | `IPausable` and `IOwnable2Step`, the two calls `ProtocolAdmin` makes back into the protocol | Interfaces | @@ -115,30 +115,39 @@ slither . --filter-paths "test/,script/,lib/,node_modules/" aderyn . ``` -**Trust model, as it stands.** One EOA owns all four UUPS proxies and both factories that own the -beacons, on both chains. A single key compromise reaches every wallet in one transaction, and admin -transactions go into the public mempool with no private relay in front of them. `ProtocolAdmin` -exists to replace that with a two day timelock and it is not deployed yet. Read the testnet -deployment below with that in mind. +**Trust model, as it stands.** On the v0.8 Base Sepolia deployment, `ProtocolAdmin` owns all four +UUPS proxies and both factories that own the beacons. Every owner gated call now waits out a two day +delay, proposing is 2-of-3 or a cold key, and a 3-of-3 guardian executes. The older deployments are +not on that footing: one EOA still owns everything on the v0.7 Base Sepolia and OP Sepolia +deployments, so a single key compromise reaches every wallet there in one transaction. Admin +transactions go into the public mempool on all three, with no private relay in front of them. ## Deployments -Testnet only. These were deployed from an earlier commit and bind the v0.7 EntryPoint, while the -current branch builds against v0.8, so the suite gets redeployed rather than upgraded. - -| Contract | Base Sepolia (EP v0.7) | OP Sepolia | -|---|---|---| -| `RegistryProxy` | `0xCa447f5C75C57f6C59027304A5Fb5A09F0E005c9` | `0x96dA9cE92D2C09f7b3ADE01260608e9079f16d12` | -| `LazyWalletRegistryProxy` | `0x8a1E53b903efcc6b252CE4bD3b255202318505Ef` | `0x3F14D060074B174B0784056bDe5e0f8970D25ff1` | -| `DeviceWalletFactoryProxy` | `0xB4473979ff8cE4e09161B08f74EEb66BD7718076` | `0x243cCdE6a56b0Ba740E067f39896772748E20fFD` | -| `ESIMWalletFactoryProxy` | `0x63005d8214533fC7209678Aa39F7b9b0b51a7bcB` | `0x8444bF9C39F01e4B092e42DC11695C61f8B93957` | -| `DeviceWalletImpl` | `0xde0dC03eF67317D4702e1d6Ef3f8cE246517e84e` | `0x22FCFa80868dc9F423873F9332817eDAe4483974` | -| `ESIMWalletImpl` | `0x59A78Cbb73e94a3fD6ada0136C89AE658BA16Dd9` | `0xf86FE9253b6ea9454abda657f47aE508B00c15C1` | -| `P256Verifier` | `0xF04f3b3935aD461D17d4a8a78E7ea21d4a61AEb1` | `0x3c15a78046838481788613A9F111F972B562623C` | -| `EntryPoint` (v0.7) | `0x0000000071727De22E5E9d8BAf0edAc6f37da032` | `0x0000000071727De22E5E9d8BAf0edAc6f37da032` | +Testnet only. The v0.8 column is the current deployment, from commit [`8e49dd9`](https://github.com/Blockchain-Powered-eSIM/smart-contract-suite/tree/8e49dd96eb8dfd09b95d584079e423b5ed350a7b), tagged +`deploy/base-sepolia-entrypoint-v8`. The two older columns bind the v0.7 EntryPoint and were built +from an earlier commit. They were redeployed rather than upgraded, because the EntryPoint address is +immutable in the wallets. + +| Contract | Base Sepolia (EP v0.8) | Base Sepolia (EP v0.7) | OP Sepolia | +|---|---|---|---| +| `RegistryProxy` | [`0x89e386E3251692F21a2E9048A46518AdC2A5Cb4A`](https://sepolia.basescan.org/address/0x89e386E3251692F21a2E9048A46518AdC2A5Cb4A) | `0xCa447f5C75C57f6C59027304A5Fb5A09F0E005c9` | `0x96dA9cE92D2C09f7b3ADE01260608e9079f16d12` | +| `LazyWalletRegistryProxy` | [`0x394177c5cc4762b897c37de1820259B75993e033`](https://sepolia.basescan.org/address/0x394177c5cc4762b897c37de1820259B75993e033) | `0x8a1E53b903efcc6b252CE4bD3b255202318505Ef` | `0x3F14D060074B174B0784056bDe5e0f8970D25ff1` | +| `DeviceWalletFactoryProxy` | [`0xB006c7066C89a5d7Bfc229e9fb0bADf96c8F979f`](https://sepolia.basescan.org/address/0xB006c7066C89a5d7Bfc229e9fb0bADf96c8F979f) | `0xB4473979ff8cE4e09161B08f74EEb66BD7718076` | `0x243cCdE6a56b0Ba740E067f39896772748E20fFD` | +| `ESIMWalletFactoryProxy` | [`0x13998C0bb7433c51cE5101922B12EE69F459699A`](https://sepolia.basescan.org/address/0x13998C0bb7433c51cE5101922B12EE69F459699A) | `0x63005d8214533fC7209678Aa39F7b9b0b51a7bcB` | `0x8444bF9C39F01e4B092e42DC11695C61f8B93957` | +| `DeviceWalletImpl` | [`0x8076aD3AdaeFb5A35a1ADFdE850F44A06C379DC8`](https://sepolia.basescan.org/address/0x8076aD3AdaeFb5A35a1ADFdE850F44A06C379DC8) | `0xde0dC03eF67317D4702e1d6Ef3f8cE246517e84e` | `0x22FCFa80868dc9F423873F9332817eDAe4483974` | +| `ESIMWalletImpl` | [`0xF77FE1da39501Bb1963f08e8778242F25Bc668C2`](https://sepolia.basescan.org/address/0xF77FE1da39501Bb1963f08e8778242F25Bc668C2) | `0x59A78Cbb73e94a3fD6ada0136C89AE658BA16Dd9` | `0xf86FE9253b6ea9454abda657f47aE508B00c15C1` | +| `DeviceWalletBeacon` | [`0x985519b60b39C630d9575911d62635A993383900`](https://sepolia.basescan.org/address/0x985519b60b39C630d9575911d62635A993383900) | not recorded | not recorded | +| `ESIMWalletBeacon` | [`0x7D0515286Ad92953665B6ED02D4e3b3901479c19`](https://sepolia.basescan.org/address/0x7D0515286Ad92953665B6ED02D4e3b3901479c19) | not recorded | not recorded | +| `P256Verifier` | [`0x625561429bD99d647956ccBCA4eBf762aaA142c5`](https://sepolia.basescan.org/address/0x625561429bD99d647956ccBCA4eBf762aaA142c5) | `0xF04f3b3935aD461D17d4a8a78E7ea21d4a61AEb1` | `0x3c15a78046838481788613A9F111F972B562623C` | +| `ProtocolAdmin` | [`0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F`](https://sepolia.basescan.org/address/0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F) | not deployed | not deployed | +| `EntryPoint` | [`0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108`](https://sepolia.basescan.org/address/0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108) | `0x0000000071727De22E5E9d8BAf0edAc6f37da032` | `0x0000000071727De22E5E9d8BAf0edAc6f37da032` | The full list, including the Ethereum Sepolia deployment, is in -[deployments/address.json](./deployments/address.json). +[deployments/address.json](./deployments/address.json). That file is addresses only. What the v0.8 +deploy captured beyond them, build provenance, every transaction, gas, constructor arguments and +verification status, is in +[deployments/base-sepolia-84532-entrypoint-v8.json](./deployments/base-sepolia-84532-entrypoint-v8.json). The two chains are not symmetric in one way that file does not record. The owner EOA carries an EIP-7702 delegation on Base Sepolia and none on OP Sepolia, so the same address is a smart account diff --git a/deployments/address.json b/deployments/address.json index 4593e23c..445fba61 100644 --- a/deployments/address.json +++ b/deployments/address.json @@ -38,5 +38,18 @@ "ESIMWalletFactoryProxy": "0xB4473979ff8cE4e09161B08f74EEb66BD7718076", "RegistryProxy": "0xCa447f5C75C57f6C59027304A5Fb5A09F0E005c9", "LazyWalletRegistryProxy": "0x8a1E53b903efcc6b252CE4bD3b255202318505Ef" + }, + "base-sepolia-84532-entrypoint-v8": { + "EntryPoint": "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108", + "P256Verifier": "0x625561429bD99d647956ccBCA4eBf762aaA142c5", + "DeviceWalletImpl": "0x8076aD3AdaeFb5A35a1ADFdE850F44A06C379DC8", + "ESIMWalletImpl": "0xF77FE1da39501Bb1963f08e8778242F25Bc668C2", + "DeviceWalletFactoryProxy": "0xB006c7066C89a5d7Bfc229e9fb0bADf96c8F979f", + "ESIMWalletFactoryProxy": "0x13998C0bb7433c51cE5101922B12EE69F459699A", + "RegistryProxy": "0x89e386E3251692F21a2E9048A46518AdC2A5Cb4A", + "LazyWalletRegistryProxy": "0x394177c5cc4762b897c37de1820259B75993e033", + "DeviceWalletBeacon": "0x985519b60b39C630d9575911d62635A993383900", + "ESIMWalletBeacon": "0x7D0515286Ad92953665B6ED02D4e3b3901479c19", + "ProtocolAdmin": "0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F" } } diff --git a/deployments/base-sepolia-84532-entrypoint-v8.json b/deployments/base-sepolia-84532-entrypoint-v8.json new file mode 100644 index 00000000..35725f87 --- /dev/null +++ b/deployments/base-sepolia-84532-entrypoint-v8.json @@ -0,0 +1,562 @@ +{ + "admin": { + "cancellers": [], + "guardians": [ + "0xA71daa87b7C653843b177Ef296B8a7aB90DebE1A" + ], + "initialDelay": 172800, + "minDelayFloor": 3600, + "proposers": [ + "0x97a2103118064820180fb3acbCBedDe6E4D9fCb9", + "0xC85Da397D15827d4F15c9D380AdA4e4Abe99227e" + ], + "protocolAdmin": "0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F" + }, + "build": { + "branch": "main", + "commit": "8e49dd96eb8dfd09b95d584079e423b5ed350a7b", + "compiler": { + "bytecodeHash": "none", + "evmVersion": "osaka", + "optimizer": true, + "optimizerRuns": 10000000, + "solc": "0.8.36", + "viaIR": true + }, + "dirty": false, + "storageLayoutHashes": { + "Account4337": "0x681ae11fe8b28d43f331d165b8ec0f468cf209f81c608d42af94cd759dd20e0d", + "DeviceWallet": "0xb759db7b201091025843654a09be1d7114c1f78bb811d33c310a5737f5bb185f", + "DeviceWalletFactory": "0x20ab4778cad28f268b1e26640af2ecc680229a0de6cf54b409af43184e8ff8f6", + "ESIMWallet": "0x184e6ec7c9b3017054ff618cfa3fa5a6374ef2e0fdd57e34f312bd8f2ef97288", + "ESIMWalletFactory": "0xef9535c77903c202700c327016e8bcec5b1b2f9963c309fd75b9e3ca40be12b0", + "LazyWalletRegistry": "0xcf05e7e437ee2ab9ff37b2ea4b05a146a1d3b5e58f86c3f3668de2ba15b02519", + "P256Verifier": "0xa2d556665824f74973bf5cb8ae6e2fc2b622f25a244bf0f9992c9136d98a7cd6", + "ProtocolAdmin": "0x8c94e923c664cadbd287c5afdb1e2110972f30283a9c80d698ed06d716ebeece", + "Registry": "0x491bf46f0f9fd90e57b57abc4e56da48f4e5a7b68456cf0660ff6010a829bc10", + "RegistryHelper": "0x5388d6eccdeadeb2145b4e213ebba4799820284043ed1114cf3c8cda5a985a79" + }, + "submodules": { + "lib/FreshCryptoLib": { + "atPin": true, + "commit": "76f3f135b7b27d2aa519f265b56bfc49a2573ab5" + }, + "lib/account-abstraction": { + "atPin": true, + "commit": "4cbc06072cdc19fd60f285c5997f4f7f57a588de" + }, + "lib/forge-std": { + "atPin": true, + "commit": "bf647bd6046f2f7da30d0c2bf435e5c76a780c1b" + }, + "lib/openzeppelin-contracts-upgradeable": { + "atPin": true, + "commit": "e725abddf1e01cf05ace496e950fc8e243cc7cab" + }, + "lib/openzeppelin-foundry-upgrades": { + "atPin": true, + "commit": "258e12e727bfe7f0ec30c51995d01ec88b82efc1" + }, + "lib/p256-verifier": { + "atPin": true, + "commit": "607d3ec8377a3f59d65eca60d87dee8485d2ebcc" + }, + "lib/solady": { + "atPin": true, + "commit": "acd959aa4bd04720d640bf4e6a5c71037510cc4b" + } + } + }, + "chain": { + "chainId": 84532, + "deployedAtBlock": 45640392, + "deployedAtTimestamp": 1787049072, + "deployer": "0x749e003F324cb13E59a2A102B49c78C1e1467F29", + "network": "base-sepolia", + "recordKey": "base-sepolia-84532-entrypoint-v8" + }, + "contracts": { + "DeviceWalletFactoryProxy": { + "address": "0xB006c7066C89a5d7Bfc229e9fb0bADf96c8F979f", + "beacon": "0x985519b60b39C630d9575911d62635A993383900", + "codehash": "0x382e8868194975b927c150c9c56765629a715255a014f27386fcb461295ae9bb", + "implementation": "0xb01733D9D9F97f1f437104B2a086Ce681544fEB7", + "constructorArgs": [ + "0xb01733D9D9F97f1f437104B2a086Ce681544fEB7", + "0x1459457a0000000000000000000000008076ad3adaefb5a35a1adfde850f44a06c379dc8000000000000000000000000749e003f324cb13e59a2a102b49c78c1e1467f2900000000000000000000000013998c0bb7433c51ce5101922b12ee69f459699a0000000000000000000000004337084d9e255ff0702461cf8895ce9e3b5ff108000000000000000000000000625561429bd99d647956ccbca4ebf762aaa142c5" + ] + }, + "DeviceWalletImplementation": { + "address": "0x8076aD3AdaeFb5A35a1ADFdE850F44A06C379DC8", + "codehash": "0x3b0cd966132e90a3f1681f187c8c1fbd6f70e1f913dd798b6df0d10cd98cdacf", + "constructorArgs": [ + "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108", + "0x625561429bD99d647956ccBCA4eBf762aaA142c5" + ] + }, + "ESIMWalletFactoryProxy": { + "address": "0x13998C0bb7433c51cE5101922B12EE69F459699A", + "beacon": "0x7D0515286Ad92953665B6ED02D4e3b3901479c19", + "codehash": "0x382e8868194975b927c150c9c56765629a715255a014f27386fcb461295ae9bb", + "implementation": "0x32613a37f4019F0613Df3fF8edDd4ca74754b477", + "constructorArgs": [ + "0x32613a37f4019F0613Df3fF8edDd4ca74754b477", + "0x485cc955000000000000000000000000f77fe1da39501bb1963f08e8778242f25bc668c2000000000000000000000000749e003f324cb13e59a2a102b49c78c1e1467f29" + ] + }, + "ESIMWalletImplementation": { + "address": "0xF77FE1da39501Bb1963f08e8778242F25Bc668C2", + "codehash": "0x4a495009dd2496b7b61809f607eaf7f2a56b9327e3b2a6d7b379d5ee42e523c1", + "constructorArgs": [] + }, + "LazyWalletRegistryProxy": { + "address": "0x394177c5cc4762b897c37de1820259B75993e033", + "codehash": "0x382e8868194975b927c150c9c56765629a715255a014f27386fcb461295ae9bb", + "implementation": "0xa5d5e1ea59aEDd553c09909A6A3cc6455B1FB286", + "constructorArgs": [ + "0xa5d5e1ea59aEDd553c09909A6A3cc6455B1FB286", + "0x485cc95500000000000000000000000089e386e3251692f21a2e9048a46518adc2a5cb4a000000000000000000000000749e003f324cb13e59a2a102b49c78c1e1467f29" + ] + }, + "P256Verifier": { + "address": "0x625561429bD99d647956ccBCA4eBf762aaA142c5", + "codehash": "0xebe592cf903ce040ce656d47af6a943644092dc7145d4006395126b676ea4be1", + "constructorArgs": [] + }, + "ProtocolAdmin": { + "address": "0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F", + "codehash": "0x9435a5f636a7fd53c25c7ce4ac145a0b3d361efa5cdd6b0c1fbf1b1420b6c241", + "constructorArgs": [ + "172800", + "3600", + "[0x97a2103118064820180fb3acbCBedDe6E4D9fCb9, 0xC85Da397D15827d4F15c9D380AdA4e4Abe99227e]", + "[]", + "[0xA71daa87b7C653843b177Ef296B8a7aB90DebE1A]" + ] + }, + "RegistryProxy": { + "address": "0x89e386E3251692F21a2E9048A46518AdC2A5Cb4A", + "codehash": "0x382e8868194975b927c150c9c56765629a715255a014f27386fcb461295ae9bb", + "implementation": "0x1d39bCa61E0d4E2a015C1370FcDFe35987243A14", + "constructorArgs": [ + "0x1d39bCa61E0d4E2a015C1370FcDFe35987243A14", + "0x1460e3900000000000000000000000009be9a586a2c8ee59504805f7491b1861e541fe3a0000000000000000000000009e60e1d876e0e47c410174dc4ea7f59d5e6c6d1d000000000000000000000000749e003f324cb13e59a2a102b49c78c1e1467f29000000000000000000000000b006c7066c89a5d7bfc229e9fb0badf96c8f979f00000000000000000000000013998c0bb7433c51ce5101922b12ee69f459699a0000000000000000000000004337084d9e255ff0702461cf8895ce9e3b5ff108000000000000000000000000000000000000000000000000016345785d8a0000" + ] + }, + "DeviceWalletBeacon": { + "address": "0x985519b60b39C630d9575911d62635A993383900", + "implementation": "0x8076aD3AdaeFb5A35a1ADFdE850F44A06C379DC8", + "createdBy": "DeviceWalletFactoryProxy initializer" + }, + "ESIMWalletBeacon": { + "address": "0x7D0515286Ad92953665B6ED02D4e3b3901479c19", + "implementation": "0xF77FE1da39501Bb1963f08e8778242F25Bc668C2", + "createdBy": "ESIMWalletFactoryProxy initializer" + } + }, + "external": { + "entryPoint": "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108", + "entryPointVersion": "0.8.0" + }, + "params": { + "dataBundlePriceCap": 100000000000000000, + "eSIMWalletAdmin": "0x9Be9a586A2C8Ee59504805F7491B1861E541fe3a", + "vault": "0x9E60E1d876e0E47c410174dC4Ea7F59D5E6c6D1d" + }, + "status": { + "configured": true, + "ownershipTransferred": true + }, + "transactions": { + "Deploy": [ + { + "hash": "0xbe11261526a2ca50cb79a4eec75fae5c71abe69d5353f9e1db24062f58a801bb", + "type": "CREATE", + "target": "ProtocolAdmin", + "address": "0x77a1d6f27462c34bf038832d9cff6b3e94a9fe6f", + "arguments": [ + "172800", + "3600", + "[0x97a2103118064820180fb3acbCBedDe6E4D9fCb9, 0xC85Da397D15827d4F15c9D380AdA4e4Abe99227e]", + "[]", + "[0xA71daa87b7C653843b177Ef296B8a7aB90DebE1A]" + ], + "blockNumber": 45640397, + "gasUsed": 2590646, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0xd5e9554700d6f7ea39c15b08f7d29c929b9cfd6d3840472ac16595233d8d201e", + "type": "CREATE", + "target": "P256Verifier", + "address": "0x625561429bd99d647956ccbca4ebf762aaa142c5", + "arguments": [], + "blockNumber": 45640397, + "gasUsed": 1511681, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0x9f76c9cb8d392ff79167425af2d5a7b1c4b67b27276778f5ddb27e5891009c2d", + "type": "CREATE", + "target": "ESIMWalletImplementation", + "address": "0xf77fe1da39501bb1963f08e8778242f25bc668c2", + "arguments": [], + "blockNumber": 45640397, + "gasUsed": 2137542, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0xc0851867fd2f42c47b7868cd5cb3b93a57bf7fd4714caba52918b09a8a62d4f4", + "type": "CREATE", + "target": "ESIMWalletFactory", + "address": "0x32613a37f4019f0613df3ff8eddd4ca74754b477", + "arguments": [], + "blockNumber": 45640397, + "gasUsed": 457962, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0x4a0abfb0f0c7fccdb192bb50b42c0e589d4337e0443944e10ca1cc1da432525b", + "type": "CREATE", + "target": "ESIMWalletFactoryProxy", + "address": "0x13998c0bb7433c51ce5101922b12ee69f459699a", + "arguments": [ + "0x32613a37f4019F0613Df3fF8edDd4ca74754b477", + "0x485cc955000000000000000000000000f77fe1da39501bb1963f08e8778242f25bc668c2000000000000000000000000749e003f324cb13e59a2a102b49c78c1e1467f29" + ], + "blockNumber": 45640397, + "gasUsed": 2288070, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0x2c31303973365bca9db1d8ff5eace2f99972ee15eabc0e521e177f21ed2452b1", + "type": "CREATE", + "target": "DeviceWalletImplementation", + "address": "0x8076ad3adaefb5a35a1adfde850f44a06c379dc8", + "arguments": [ + "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108", + "0x625561429bD99d647956ccBCA4eBf762aaA142c5" + ], + "blockNumber": 45640398, + "gasUsed": 300509, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0xaf9187ee9703e401750fc2dde442aec914c4383e2185dc35326dcaeaeb671833", + "type": "CREATE", + "target": "DeviceWalletFactory", + "address": "0xb01733d9d9f97f1f437104b2a086ce681544feb7", + "arguments": [], + "blockNumber": 45640398, + "gasUsed": 184242, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0x529d213f7484f58dc61178439a6d811d7b6f2469674375800ba0c3c35f08dc2b", + "type": "CREATE", + "target": "DeviceWalletFactoryProxy", + "address": "0xb006c7066c89a5d7bfc229e9fb0badf96c8f979f", + "arguments": [ + "0xb01733D9D9F97f1f437104B2a086Ce681544fEB7", + "0x1459457a0000000000000000000000008076ad3adaefb5a35a1adfde850f44a06c379dc8000000000000000000000000749e003f324cb13e59a2a102b49c78c1e1467f2900000000000000000000000013998c0bb7433c51ce5101922b12ee69f459699a0000000000000000000000004337084d9e255ff0702461cf8895ce9e3b5ff108000000000000000000000000625561429bd99d647956ccbca4ebf762aaa142c5" + ], + "blockNumber": 45640398, + "gasUsed": 526364, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0x1c42158230c5778c5604397956ef7921b8c28e6f588ee235603d2726da18990d", + "type": "CREATE", + "target": "Registry", + "address": "0x1d39bca61e0d4e2a015c1370fcdfe35987243a14", + "arguments": [], + "blockNumber": 45640398, + "gasUsed": 3281842, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0x5d51cf2e9d0ae7ad343b0c3742ddb07cb6dfb2323622ac87dddb98ebf0ec24b2", + "type": "CREATE", + "target": "RegistryProxy", + "address": "0x89e386e3251692f21a2e9048a46518adc2a5cb4a", + "arguments": [ + "0x1d39bCa61E0d4E2a015C1370FcDFe35987243A14", + "0x1460e3900000000000000000000000009be9a586a2c8ee59504805f7491b1861e541fe3a0000000000000000000000009e60e1d876e0e47c410174dc4ea7f59d5e6c6d1d000000000000000000000000749e003f324cb13e59a2a102b49c78c1e1467f29000000000000000000000000b006c7066c89a5d7bfc229e9fb0badf96c8f979f00000000000000000000000013998c0bb7433c51ce5101922b12ee69f459699a0000000000000000000000004337084d9e255ff0702461cf8895ce9e3b5ff108000000000000000000000000000000000000000000000000016345785d8a0000" + ], + "blockNumber": 45640398, + "gasUsed": 3030648, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0xed775a1f39ecd64b7f560083cdd041e5b663741cf92b0ab5eb10260658115681", + "type": "CREATE", + "target": "LazyWalletRegistry", + "address": "0xa5d5e1ea59aedd553c09909a6a3cc6455b1fb286", + "arguments": [], + "blockNumber": 45640398, + "gasUsed": 3954069, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0xffbba2624d38c07bd6b920dc04995701341f304ff94469586afbbb441fd76daa", + "type": "CREATE", + "target": "LazyWalletRegistryProxy", + "address": "0x394177c5cc4762b897c37de1820259b75993e033", + "arguments": [ + "0xa5d5e1ea59aEDd553c09909A6A3cc6455B1FB286", + "0x485cc95500000000000000000000000089e386e3251692f21a2e9048a46518adc2a5cb4a000000000000000000000000749e003f324cb13e59a2a102b49c78c1e1467f29" + ], + "blockNumber": 45640398, + "gasUsed": 3774922, + "effectiveGasPriceWei": 6000000, + "status": "success" + } + ], + "Configure": [ + { + "hash": "0x3f77b8fca1842122961d57949d2e9b478ea6f37f90c516f2c6096cc0129f6e99", + "type": "CALL", + "target": "DeviceWalletFactoryProxy", + "address": "0xb006c7066c89a5d7bfc229e9fb0badf96c8f979f", + "function": "addRegistryAddress(address)", + "arguments": [ + "0x89e386E3251692F21a2E9048A46518AdC2A5Cb4A" + ], + "blockNumber": 45640534, + "gasUsed": 52035, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0x86d1cb88d56d453747d5a1f2b2310fd090bc3ad5cd55701915b203f27d08f073", + "type": "CALL", + "target": "ESIMWalletFactoryProxy", + "address": "0x13998c0bb7433c51ce5101922b12ee69f459699a", + "function": "addRegistryAddress(address)", + "arguments": [ + "0x89e386E3251692F21a2E9048A46518AdC2A5Cb4A" + ], + "blockNumber": 45640534, + "gasUsed": 51979, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0x1221e14ae6c39b41d160999223c87c326fd68a581132496c0c5d9d18904d856a", + "type": "CALL", + "target": "RegistryProxy", + "address": "0x89e386e3251692f21a2e9048a46518adc2a5cb4a", + "function": "addOrUpdateLazyWalletRegistryAddress(address)", + "arguments": [ + "0x394177c5cc4762b897c37de1820259B75993e033" + ], + "blockNumber": 45640534, + "gasUsed": 51940, + "effectiveGasPriceWei": 6000000, + "status": "success" + } + ], + "TransferOwnership": [ + { + "hash": "0x3040dce20090ce21a578ced3c75f193c5043187a2cf23ec70e5929f15b4f5606", + "type": "CALL", + "target": "RegistryProxy", + "address": "0x89e386e3251692f21a2e9048a46518adc2a5cb4a", + "function": "transferOwnership(address)", + "arguments": [ + "0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F" + ], + "blockNumber": 45640547, + "gasUsed": 53327, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0x4dd9380cf313eacd09bbdf4893d11ac79db399383c9752a14bf5f0f418e6e442", + "type": "CALL", + "target": "LazyWalletRegistryProxy", + "address": "0x394177c5cc4762b897c37de1820259b75993e033", + "function": "transferOwnership(address)", + "arguments": [ + "0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F" + ], + "blockNumber": 45640547, + "gasUsed": 52862, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0xd90df431c47c11ab23d15805f0d4ee72d3c5ed1020fcd1586a81b106785ccff0", + "type": "CALL", + "target": "DeviceWalletFactoryProxy", + "address": "0xb006c7066c89a5d7bfc229e9fb0badf96c8f979f", + "function": "transferOwnership(address)", + "arguments": [ + "0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F" + ], + "blockNumber": 45640547, + "gasUsed": 52796, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0x26011070dc1230ff9b4aeb5a33f2d2b9714c872620d70e3985917d96a676a840", + "type": "CALL", + "target": "ESIMWalletFactoryProxy", + "address": "0x13998c0bb7433c51ce5101922b12ee69f459699a", + "function": "transferOwnership(address)", + "arguments": [ + "0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F" + ], + "blockNumber": 45640547, + "gasUsed": 52648, + "effectiveGasPriceWei": 6000000, + "status": "success" + }, + { + "hash": "0xbedeb14755f7f22d10af2c3e86a360b20f9e4ae3fd694896bc0459c887bc5019", + "type": "CALL", + "target": "ProtocolAdmin", + "address": "0x77a1d6f27462c34bf038832d9cff6b3e94a9fe6f", + "function": "acceptOwnershipBatch(address[])", + "arguments": [ + "[0x89e386E3251692F21a2E9048A46518AdC2A5Cb4A, 0x394177c5cc4762b897c37de1820259B75993e033, 0xB006c7066C89a5d7Bfc229e9fb0bADf96c8F979f, 0x13998C0bb7433c51cE5101922B12EE69F459699A]" + ], + "blockNumber": 45640547, + "gasUsed": 95153, + "effectiveGasPriceWei": 6000000, + "status": "success" + } + ] + }, + "cost": { + "totalGasUsed": 24501237, + "totalWeiSpent": 147007422000000, + "totalEthSpent": "0.000147007422" + }, + "create2": { + "deviceWalletDeployer": "0xB006c7066C89a5d7Bfc229e9fb0bADf96c8F979f", + "eSIMWalletDeployer": "0x13998C0bb7433c51cE5101922B12EE69F459699A", + "beaconProxyCreationCodeHash": "0xc571dd76379a732e12f1973fa9f4cbbaeb1702bb0ace06e5beb7e2b56cd03c6b", + "beaconProxyCreationCode": "0x60a0806040526104e480380380916100178285610292565b833981016040828203126101eb5761002e826102c9565b602083015190926001600160401b0382116101eb57019080601f830112156101eb57815161005b816102dd565b926100696040519485610292565b8184526020840192602083830101116101eb57815f926020809301855e84010152823b15610274577fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5080546001600160a01b0319166001600160a01b038516908117909155604051635c60da1b60e01b8152909190602081600481865afa9081156101f7575f9161023a575b50803b1561021a5750817f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e5f80a282511561020257602060049260405193848092635c60da1b60e01b82525afa9182156101f7575f926101ae575b505f809161018a945190845af43d156101a6573d9161016e836102dd565b9261017c6040519485610292565b83523d5f602085013e6102f8565b505b60805260405161018d908161035782396080518160460152f35b6060916102f8565b9291506020833d6020116101ef575b816101ca60209383610292565b810103126101eb575f80916101e161018a956102c9565b9394509150610150565b5f80fd5b3d91506101bd565b6040513d5f823e3d90fd5b505050341561018c5763b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f9081526001600160a01b0391909116600452602490fd5b90506020813d60201161026c575b8161025560209383610292565b810103126101eb57610266906102c9565b5f6100f5565b3d9150610248565b631933b43b60e21b5f9081526001600160a01b038416600452602490fd5b601f909101601f19168101906001600160401b038211908210176102b557604052565b634e487b7160e01b5f52604160045260245ffd5b51906001600160a01b03821682036101eb57565b6001600160401b0381116102b557601f01601f191660200190565b9061031c575080511561030d57602081519101fd5b63d6bda27560e01b5f5260045ffd5b8151158061034d575b61032d575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b1561032556fe60806040527f5c60da1b000000000000000000000000000000000000000000000000000000006080526020608060048173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa8015610107575f9015610163575060203d602011610100575b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f820116608001906080821067ffffffffffffffff8311176100d3576100ce91604052608001610112565b610163565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b503d610081565b6040513d5f823e3d90fd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80602091011261015f5760805173ffffffffffffffffffffffffffffffffffffffff8116810361015f5790565b5f80fd5b5f8091368280378136915af43d5f803e1561017c573d5ff35b3d5ffdfea164736f6c6343000824000a", + "deviceWalletInitSignature": "init(address,bytes32[2],string,address)", + "deviceWalletInitArgs": [ + "registry", + "ownerKey", + "deviceUniqueIdentifier", + "eSIMWalletFactory" + ], + "verifiedExample": { + "note": "Derived offchain and checked against getCounterFactualAddress on the deployed factory. Nothing was deployed at this address.", + "ownerKeyX": "0x1111111111111111111111111111111111111111111111111111111111111111", + "ownerKeyY": "0x2222222222222222222222222222222222222222222222222222222222222222", + "deviceUniqueIdentifier": "record-example-device-1", + "salt": 1, + "initCodeHash": "0x6730f936ed7451797a14568c94bdd1aa5cc926c4abec573b8e7899b7bd98d9e6", + "address": "0xAa593e776Be88863E31e27A0253d618110A0580C" + } + }, + "verification": { + "explorer": "https://sepolia.basescan.org", + "verifier": "etherscan v2, chainid 84532", + "checkedAt": "2026-08-18", + "method": "getsourcecode returned a contract name for every address below", + "contracts": { + "ProtocolAdmin": { + "address": "0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F", + "verifiedAs": "ProtocolAdmin", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0x77A1D6f27462c34BF038832d9Cff6b3E94a9Fe6F#code" + }, + "P256Verifier": { + "address": "0x625561429bD99d647956ccBCA4eBf762aaA142c5", + "verifiedAs": "P256Verifier", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0x625561429bD99d647956ccBCA4eBf762aaA142c5#code" + }, + "DeviceWalletImplementation": { + "address": "0x8076aD3AdaeFb5A35a1ADFdE850F44A06C379DC8", + "verifiedAs": "DeviceWallet", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0x8076aD3AdaeFb5A35a1ADFdE850F44A06C379DC8#code" + }, + "ESIMWalletImplementation": { + "address": "0xF77FE1da39501Bb1963f08e8778242F25Bc668C2", + "verifiedAs": "ESIMWallet", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0xF77FE1da39501Bb1963f08e8778242F25Bc668C2#code" + }, + "RegistryImplementation": { + "address": "0x1d39bCa61E0d4E2a015C1370FcDFe35987243A14", + "verifiedAs": "Registry", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0x1d39bCa61E0d4E2a015C1370FcDFe35987243A14#code" + }, + "RegistryProxy": { + "address": "0x89e386E3251692F21a2E9048A46518AdC2A5Cb4A", + "verifiedAs": "ERC1967Proxy", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0x89e386E3251692F21a2E9048A46518AdC2A5Cb4A#code" + }, + "LazyWalletRegistryImplementation": { + "address": "0xa5d5e1ea59aEDd553c09909A6A3cc6455B1FB286", + "verifiedAs": "LazyWalletRegistry", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0xa5d5e1ea59aEDd553c09909A6A3cc6455B1FB286#code" + }, + "LazyWalletRegistryProxy": { + "address": "0x394177c5cc4762b897c37de1820259B75993e033", + "verifiedAs": "ERC1967Proxy", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0x394177c5cc4762b897c37de1820259B75993e033#code" + }, + "DeviceWalletFactoryImplementation": { + "address": "0xb01733D9D9F97f1f437104B2a086Ce681544fEB7", + "verifiedAs": "DeviceWalletFactory", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0xb01733D9D9F97f1f437104B2a086Ce681544fEB7#code" + }, + "DeviceWalletFactoryProxy": { + "address": "0xB006c7066C89a5d7Bfc229e9fb0bADf96c8F979f", + "verifiedAs": "ERC1967Proxy", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0xB006c7066C89a5d7Bfc229e9fb0bADf96c8F979f#code" + }, + "ESIMWalletFactoryImplementation": { + "address": "0x32613a37f4019F0613Df3fF8edDd4ca74754b477", + "verifiedAs": "ESIMWalletFactory", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0x32613a37f4019F0613Df3fF8edDd4ca74754b477#code" + }, + "ESIMWalletFactoryProxy": { + "address": "0x13998C0bb7433c51cE5101922B12EE69F459699A", + "verifiedAs": "ERC1967Proxy", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0x13998C0bb7433c51cE5101922B12EE69F459699A#code" + }, + "DeviceWalletBeacon": { + "address": "0x985519b60b39C630d9575911d62635A993383900", + "verifiedAs": "UpgradeableBeacon", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0x985519b60b39C630d9575911d62635A993383900#code" + }, + "ESIMWalletBeacon": { + "address": "0x7D0515286Ad92953665B6ED02D4e3b3901479c19", + "verifiedAs": "UpgradeableBeacon", + "status": "verified", + "url": "https://sepolia.basescan.org/address/0x7D0515286Ad92953665B6ED02D4e3b3901479c19#code" + } + } + } +} \ No newline at end of file diff --git a/scripts/checks/build-provenance.py b/scripts/checks/build-provenance.py index 1aa4efa1..349d2853 100644 --- a/scripts/checks/build-provenance.py +++ b/scripts/checks/build-provenance.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 """Emit a JSON object describing the tree a deployment was built from. -The deploy script calls this through `vm.ffi` and nests the result under `build` in -`deployments/address.json`. Everything here answers the same question: given only that file, -can somebody rebuild byte-identical bytecode and check it against what is onchain? +The deploy script calls this through `vm.ffi` and nests the result under `build` in that +deployment's record, `deployments/.json`. Everything here answers the same question: +given only that file, can somebody rebuild byte-identical bytecode and check it against what is +onchain? That needs three things, and a commit hash alone is none of them. It needs the commit, the seven submodule commits (a `lib/` bump changes the output without changing this repo's diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index 2a41b41e..6a5023f3 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -76,9 +76,7 @@ contract Deploy is Script { // Refuse to write over a chain that already has a deployment. Overwriting the record is // how a live proxy stops being reachable by any script, since nothing else remembers it. - if(DeploymentRecord.has("contracts.RegistryProxy")) { - revert AlreadyDeployed(DeployConfig.recordKey()); - } + if(DeploymentRecord.isRecorded()) revert AlreadyDeployed(DeployConfig.recordKey()); _logPlan(config); @@ -207,9 +205,11 @@ contract Deploy is Script { } /// @notice Writes the deployment record for this chain + /// @dev Two files. The full record goes to its own file named after the record key, and the + /// flat name to address map goes into the shared address book under the same key. The + /// address book is what the README, the SDK and anyone reading by hand actually want; the + /// record file is what the configuration, handover and upgrade scripts read. function _record(DeployConfig.Config memory config, Deployed memory deployed) private { - string memory network = DeployConfig.recordKey(); - string memory record = vm.serializeString("record", "build", _buildProvenance()); record = vm.serializeString("record", "chain", _chainSection(config)); record = vm.serializeString("record", "external", _externalSection(config)); @@ -218,8 +218,42 @@ contract Deploy is Script { record = vm.serializeString("record", "contracts", _contractsSection(deployed)); record = vm.serializeString("record", "status", _statusSection()); - vm.writeJson(record, DeploymentRecord.PATH, string.concat(".", network)); - console.log("Record written to", DeploymentRecord.PATH, "under", network); + DeploymentRecord.writeRecord(record); + DeploymentRecord.writeAddressBook(_addressBook(config, deployed)); + + console.log("Record written to", DeploymentRecord.recordPath()); + console.log("Addresses written to", DeploymentRecord.ADDRESS_BOOK); + } + + /// @notice The flat name to address map for this deployment + /// @dev Names match the entries already in the address book, so one chain's entry reads the + /// same as the next. Forge sorts the keys on the way out, so the order here is for reading + /// rather than for the file. The EntryPoint is in the map despite not being deployed by + /// this script, because an address book that omits it cannot answer which EntryPoint these + /// wallets are bound to, and that is the one thing the key promises. + function _addressBook(DeployConfig.Config memory config, Deployed memory deployed) + private + returns (string memory entry) + { + vm.serializeAddress("book", "EntryPoint", address(config.entryPoint)); + vm.serializeAddress("book", "P256Verifier", deployed.p256Verifier); + vm.serializeAddress("book", "DeviceWalletImpl", deployed.deviceWalletImplementation); + vm.serializeAddress("book", "ESIMWalletImpl", deployed.eSIMWalletImplementation); + vm.serializeAddress("book", "DeviceWalletFactoryProxy", deployed.deviceWalletFactory); + vm.serializeAddress("book", "ESIMWalletFactoryProxy", deployed.eSIMWalletFactory); + vm.serializeAddress("book", "RegistryProxy", deployed.registry); + vm.serializeAddress("book", "LazyWalletRegistryProxy", deployed.lazyWalletRegistry); + vm.serializeAddress( + "book", + "DeviceWalletBeacon", + address(DeviceWalletFactory(deployed.deviceWalletFactory).beacon()) + ); + vm.serializeAddress( + "book", + "ESIMWalletBeacon", + address(ESIMWalletFactory(deployed.eSIMWalletFactory).beacon()) + ); + entry = vm.serializeAddress("book", "ProtocolAdmin", deployed.protocolAdmin); } /// @notice Everything needed to rebuild this bytecode later, collected offchain diff --git a/scripts/deploy/config/DeploymentRecord.sol b/scripts/deploy/config/DeploymentRecord.sol index af079d7f..98eb7c43 100644 --- a/scripts/deploy/config/DeploymentRecord.sol +++ b/scripts/deploy/config/DeploymentRecord.sol @@ -7,19 +7,27 @@ import {Vm} from "forge-std/Vm.sol"; // Config import {DeployConfig} from "./DeployConfig.sol"; -/// @notice Reads and writes `deployments/address.json`, the record every later script starts from +/// @notice Reads and writes the deployment record every later script starts from /// @dev The deploy, the configuration step, the ownership handover and both upgrade scripts all run /// separately and none of them takes an address as an argument. Passing addresses on the /// command line is how the wrong proxy gets upgraded, so each script looks its targets up here /// instead and fails loudly when an entry is missing. /// -/// Records are keyed by chain name and chain id together, `base-sepolia-84532`, so writing one -/// chain never touches another and the key cannot disagree with the chain it came from. -/// Everything under a key is written by the scripts and nothing is hand edited: a hand edited -/// address is indistinguishable from a deployed one to every reader of this file. +/// Two files, because they are read by different people. `deployments/address.json` is a flat +/// name to address map keyed by chain, which is what a reader wants and what the SDK and the +/// README quote. `deployments/.json` is the full record for one deployment: build +/// provenance, constructor arguments, role holders, codehashes, status. Keeping the detail out +/// of the address book is what stops the address book growing past the point where anyone +/// reads it. +/// +/// The record file is named after the record key, `base-sepolia-84532-entrypoint-v8.json`, so +/// the filename cannot disagree with the chain and EntryPoint version it describes. Nothing +/// here is hand edited: a hand edited address is indistinguishable from a deployed one to +/// every reader of this file. library DeploymentRecord { - string internal constant PATH = "deployments/address.json"; + /// @notice Flat name to address map, one entry per deployment + string internal constant ADDRESS_BOOK = "deployments/address.json"; Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); @@ -29,17 +37,28 @@ library DeploymentRecord { /// @notice A recorded address carries no code on chain error NoCodeAt(string key, address target); + /// @notice No deployment record exists for this chain + /// @dev Separate from `NotRecorded` because the fix is different. A missing entry means the + /// deploy wrote a record and left something out; a missing file means this chain was never + /// deployed by these scripts, or was deployed before the record was split in two. + error NoRecordFile(string network, string path); + + /// @notice Path to the full record for the chain the script is connected to + /// @return path Record file path, for example `deployments/base-sepolia-84532-entrypoint-v8.json` + function recordPath() internal view returns (string memory path) { + path = string.concat("deployments/", DeployConfig.recordKey(), ".json"); + } + /// @notice Reads a deployed address out of the record and checks it carries code /// @dev The code check is the point. A missing key and a key holding an address from another /// chain both read as a plain address, and only one of them fails at parse time. /// @param key Contract name as recorded, for example `RegistryProxy` /// @return target Address recorded for this contract on the current chain function readAddress(string memory key) internal view returns (address target) { - string memory network = DeployConfig.recordKey(); - string memory json = vm.readFile(PATH); - string memory pointer = string.concat(".", network, ".contracts.", key, ".address"); + string memory json = _record(); + string memory pointer = string.concat(".contracts.", key, ".address"); - if(!vm.keyExistsJson(json, pointer)) revert NotRecorded(network, key); + if(!vm.keyExistsJson(json, pointer)) revert NotRecorded(DeployConfig.recordKey(), key); target = vm.parseJsonAddress(json, pointer); if(target.code.length == 0) revert NoCodeAt(key, target); @@ -48,58 +67,67 @@ library DeploymentRecord { /// @notice Reads a recorded address without requiring it to carry code /// @dev For entries that name an account rather than a contract, and for reading a value back /// on a chain the script is not connected to. - /// @param path Dotted path below the network key, for example `admin.protocolAdmin` + /// @param path Dotted path into the record, for example `admin.protocolAdmin` /// @return value Address at that path function readRaw(string memory path) internal view returns (address value) { - string memory network = DeployConfig.recordKey(); - string memory json = vm.readFile(PATH); - string memory pointer = string.concat(".", network, ".", path); + string memory json = _record(); + string memory pointer = string.concat(".", path); - if(!vm.keyExistsJson(json, pointer)) revert NotRecorded(network, path); + if(!vm.keyExistsJson(json, pointer)) revert NotRecorded(DeployConfig.recordKey(), path); value = vm.parseJsonAddress(json, pointer); } /// @notice Reads a recorded number - /// @param path Dotted path below the network key + /// @param path Dotted path into the record /// @return value Number at that path function readUint(string memory path) internal view returns (uint256 value) { - string memory network = DeployConfig.recordKey(); - string memory json = vm.readFile(PATH); - string memory pointer = string.concat(".", network, ".", path); + string memory json = _record(); + string memory pointer = string.concat(".", path); - if(!vm.keyExistsJson(json, pointer)) revert NotRecorded(network, path); + if(!vm.keyExistsJson(json, pointer)) revert NotRecorded(DeployConfig.recordKey(), path); value = vm.parseJsonUint(json, pointer); } /// @notice Reads recorded bytes, used for a scheduled operation's payload - /// @param path Dotted path below the network key + /// @param path Dotted path into the record /// @return value Bytes at that path function readBytes(string memory path) internal view returns (bytes memory value) { - string memory network = DeployConfig.recordKey(); - string memory json = vm.readFile(PATH); - string memory pointer = string.concat(".", network, ".", path); + string memory json = _record(); + string memory pointer = string.concat(".", path); - if(!vm.keyExistsJson(json, pointer)) revert NotRecorded(network, path); + if(!vm.keyExistsJson(json, pointer)) revert NotRecorded(DeployConfig.recordKey(), path); value = vm.parseJsonBytes(json, pointer); } /// @notice Reads a recorded 32 byte value, used for salts and operation ids - /// @param path Dotted path below the network key + /// @param path Dotted path into the record /// @return value Word at that path function readBytes32(string memory path) internal view returns (bytes32 value) { - string memory network = DeployConfig.recordKey(); - string memory json = vm.readFile(PATH); - string memory pointer = string.concat(".", network, ".", path); + string memory json = _record(); + string memory pointer = string.concat(".", path); - if(!vm.keyExistsJson(json, pointer)) revert NotRecorded(network, path); + if(!vm.keyExistsJson(json, pointer)) revert NotRecorded(DeployConfig.recordKey(), path); value = vm.parseJsonBytes32(json, pointer); } - /// @notice Records a step as complete under the current network + /// @notice Creates this chain's record file + /// @dev Whole file at once, so a rerun leaves no field behind from the run before it. + /// @param json Serialized record + function writeRecord(string memory json) internal { + vm.writeJson(json, recordPath()); + } + + /// @notice Adds this chain's entry to the flat address book + /// @param json Serialized name to address map + function writeAddressBook(string memory json) internal { + vm.writeJson(json, ADDRESS_BOOK, string.concat(".", DeployConfig.recordKey())); + } + + /// @notice Records a step as complete /// @dev Written by the configuration and handover scripts so a partially finished deployment /// says so in the file rather than in somebody's terminal history. /// @param key Name of the step, for example `configured` @@ -107,32 +135,49 @@ library DeploymentRecord { function writeStatus(string memory key, bool done) internal { // `writeJson` takes the value as JSON text, so a bool is the literal word rather than a // serialized object. Serializing here would write `{"configured":true}` where the bool goes. - vm.writeJson( - done ? "true" : "false", - PATH, - string.concat(".", DeployConfig.recordKey(), ".status.", key) - ); + vm.writeJson(done ? "true" : "false", recordPath(), string.concat(".status.", key)); } - /// @notice Writes a pre-serialized JSON object under the current network + /// @notice Writes a pre-serialized JSON object into the record /// @dev Used for the pending upgrade entries, where a schedule and its later execution have to /// agree on an implementation address, a salt and a delay down to the byte. Recomputing /// those at execution time is how an operation id stops matching the one that was /// scheduled, and the timelock rejects it with nothing to show why. - /// @param path Dotted path below the network key + /// @param path Dotted path into the record /// @param json Serialized object to write there function writeObject(string memory path, string memory json) internal { - vm.writeJson(json, PATH, string.concat(".", DeployConfig.recordKey(), ".", path)); + vm.writeJson(json, recordPath(), string.concat(".", path)); } /// @notice True when the record already holds an entry at this path for the current chain - /// @param path Dotted path below the network key + /// @dev A chain with no record file has no entry at any path, so this answers false rather than + /// reverting. Callers use it to ask whether something has happened yet. + /// @param path Dotted path into the record /// @return present Whether the path resolves function has(string memory path) internal view returns (bool present) { - string memory json = vm.readFile(PATH); - present = vm.keyExistsJson( - json, - string.concat(".", DeployConfig.recordKey(), ".", path) + if(!vm.isFile(recordPath())) return false; + present = vm.keyExistsJson(vm.readFile(recordPath()), string.concat(".", path)); + } + + /// @notice True when this chain already carries a deployment, in either file + /// @dev Both files are checked because either one alone is enough to make a redeploy destructive. + /// A record file with no address book entry is a half-written deploy; an address book entry + /// with no record file is a live deployment whose detail was lost, and overwriting it would + /// leave the live proxies reachable by nothing. + /// @return deployed Whether anything is recorded for this chain + function isRecorded() internal view returns (bool deployed) { + if(vm.isFile(recordPath())) return true; + + deployed = vm.keyExistsJson( + vm.readFile(ADDRESS_BOOK), + string.concat(".", DeployConfig.recordKey()) ); } + + /// @notice Reads the record file, failing with the path when there is none + function _record() private view returns (string memory json) { + string memory path = recordPath(); + if(!vm.isFile(path)) revert NoRecordFile(DeployConfig.recordKey(), path); + json = vm.readFile(path); + } } diff --git a/scripts/fork/rehearse.sh b/scripts/fork/rehearse.sh index 9ec9c09c..b1bd5326 100755 --- a/scripts/fork/rehearse.sh +++ b/scripts/fork/rehearse.sh @@ -16,7 +16,8 @@ # domain, since v0.8 folds the chain id into userOpHash; EntryPointValidation.t.sol covers that at # the real chain id and this does not try to. # -# The anvil-31337-entrypoint-v8 record this writes is a rehearsal artifact. It is removed on exit. +# The anvil-31337-entrypoint-v8 record this writes is a rehearsal artifact, both the address book +# entry and the record file of the same name. Both are removed on exit. set -euo pipefail @@ -27,6 +28,7 @@ CHAIN_ID=31337 RPC="http://127.0.0.1:8545" RECORD="deployments/address.json" RECORD_KEY="anvil-${CHAIN_ID}-entrypoint-v8" +RECORD_FILE="deployments/${RECORD_KEY}.json" LOG_DIR="$(mktemp -d)" ANVIL_PID="" @@ -37,8 +39,10 @@ cleanup() { local status=$? [[ -n "$ANVIL_PID" ]] && kill "$ANVIL_PID" 2>/dev/null || true - # Drop the rehearsal's record entry however the run ended. Leaving it behind is how a fork - # address gets mistaken for a deployed one later. + # Drop the rehearsal's record however the run ended. Leaving it behind is how a fork address + # gets mistaken for a deployed one later. + rm -f "$RECORD_FILE" + if [[ -f "$RECORD" ]]; then python3 - "$RECORD" "$RECORD_KEY" <<'PY' || true import collections, json, pathlib, sys @@ -135,7 +139,12 @@ probe_p256() { probe_p256 "$RPC" "the fork" probe_p256 "$ALCHEMY_BASE_SEPOLIA_HTTPS" "Base Sepolia itself" -# Refuse to start on a record that already holds this key, the same way Deploy.s.sol does. +# Refuse to start on a record that already exists, the same way Deploy.s.sol does. Either file +# alone is enough: the deploy checks both. +if [[ -f "$RECORD_FILE" ]]; then + fail "$RECORD_FILE exists. Remove it before rehearsing again." +fi + if python3 -c "import json,sys; sys.exit(0 if '$RECORD_KEY' in json.load(open('$RECORD')) else 1)" 2>/dev/null; then fail "$RECORD already holds $RECORD_KEY. Remove it before rehearsing again." fi @@ -167,11 +176,19 @@ run_script configure scripts/deploy/Configure.s.sol:Configure run_script handover scripts/deploy/TransferOwnership.s.sol:TransferOwnership run_script rehearsal scripts/fork/ForkRehearsal.s.sol:ForkRehearsal -log "Deployment record written by the rehearsal" +log "Addresses written by the rehearsal" python3 -c " import json print(json.dumps(json.load(open('$RECORD'))['$RECORD_KEY'], indent=2)) " +log "Deployment record written by the rehearsal" +python3 -c " +import json +r = json.load(open('$RECORD_FILE')) +print(json.dumps({k: r[k] for k in ('chain', 'external', 'admin', 'status') if k in r}, indent=2)) +print('contracts:', ', '.join(r.get('contracts', {}))) +" + log "Rehearsal passed" echo "Full suite deployed to the fork, all four scripts clean, representative transactions executed." diff --git a/scripts/tools/compute-initCode.js b/scripts/tools/compute-initCode.js index c5442f80..0ec28f93 100644 --- a/scripts/tools/compute-initCode.js +++ b/scripts/tools/compute-initCode.js @@ -1,7 +1,6 @@ const hre = require("hardhat"); const {ethers, network} = hre; const dotenv = require("dotenv"); -const ADDRESS = require("../../deployments/address.json"); dotenv.config(); @@ -20,11 +19,15 @@ const CHAIN_LABELS = { 31337: "anvil", }; +// The full record for one deployment lives in its own file, named after the key. The flat +// deployments/address.json is the address book and carries no `contracts` section. function recordFor(chainId) { const key = `${CHAIN_LABELS[chainId] ?? "chain"}-${chainId}-${ENTRY_POINT_TAG}`; - const entry = ADDRESS[key]; - if (!entry) throw new Error(`No deployment recorded under ${key}`); - return entry; + try { + return require(`../../deployments/${key}.json`); + } catch { + throw new Error(`No deployment record at deployments/${key}.json`); + } } async function main () {