diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..4d35e4c1 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ + +# Factory owner capable only of managing stake +OWNER= +# EP 0.7 address +ENTRYPOINT= + +# Create2 expected addresses of the contracts. +# When running for the first time, the error message will contain the expected addresses. +ACCOUNT_IMPL= +FACTORY= +SINGLE_SIGNER_VALIDATION= + +# Optional, defaults to bytes32(0) +ACCOUNT_IMPL_SALT= +FACTORY_SALT= +SINGLE_SIGNER_VALIDATION_SALT= + +# Optional, defaults to 0.1 ether and 1 day, respectively +STAKE_AMOUNT= +UNSTAKE_DELAY= diff --git a/.gitignore b/.gitignore index 3189d156..386aebb6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,14 @@ node_modules/ # Coverage report/ lcov.info + +# env vars +.env + +# deployments +broadcast/**/run-latest.json +broadcast/**/dry-run/**/* + +# misc +.DS_Store +**/.DS_Store diff --git a/.gitmodules b/.gitmodules index 813d955e..14a90be6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,15 @@ [submodule "lib/account-abstraction"] - path = lib/account-abstraction - url = https://github.com/eth-infinitism/account-abstraction +path = lib/account-abstraction +url = https://github.com/eth-infinitism/account-abstraction [submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts +path = lib/openzeppelin-contracts +url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "lib/forge-std"] - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std +path = lib/forge-std +url = https://github.com/foundry-rs/forge-std +[submodule "lib/modular-account-libs"] +path = lib/modular-account-libs +url = https://github.com/erc6900/modular-account-libs +[submodule "lib/solady"] +path = lib/solady +url = https://github.com/vectorized/solady diff --git a/.solhint-test.json b/.solhint-test.json index fd2b1007..3224b9d0 100644 --- a/.solhint-test.json +++ b/.solhint-test.json @@ -1,20 +1,20 @@ { - "extends": "solhint:recommended", - "rules": { - "func-name-mixedcase": "off", - "immutable-vars-naming": ["error"], - "no-unused-import": ["error"], - "compiler-version": ["error", ">=0.8.19"], - "custom-errors": "off", - "func-visibility": ["error", { "ignoreConstructors": true }], - "max-line-length": ["error", 120], - "max-states-count": ["warn", 30], - "modifier-name-mixedcase": ["error"], - "private-vars-leading-underscore": ["error"], - "no-inline-assembly": "off", - "avoid-low-level-calls": "off", - "one-contract-per-file": "off", - "no-empty-blocks": "off" - } + "extends": "solhint:recommended", + "rules": { + "func-name-mixedcase": "off", + "immutable-vars-naming": ["error"], + "no-unused-import": ["error"], + "compiler-version": ["error", ">=0.8.19"], + "custom-errors": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], + "max-line-length": ["error", 120], + "max-states-count": ["warn", 30], + "modifier-name-mixedcase": ["error"], + "private-vars-leading-underscore": ["error"], + "no-inline-assembly": "off", + "avoid-low-level-calls": "off", + "one-contract-per-file": "off", + "no-empty-blocks": "off", + "reason-string": ["warn", { "maxLength": 64 }] } - \ No newline at end of file +} diff --git a/README.md b/README.md index fb3e9dbc..97476b43 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Reference implementation for [ERC-6900](https://eips.ethereum.org/EIPS/eip-6900). It is an early draft implementation. -The implementation includes an upgradable modular account with two plugins (`SingleOwnerPlugin` and `TokenReceiverPlugin`). It is compliant with ERC-6900 with the latest updates. +The implementation includes an upgradable modular account with three modules (`SingleSignerValidation`, `TokenReceiverModule`, and `AllowlistModule`). It is compliant with ERC-6900 with the latest updates. ## Important Callouts @@ -11,7 +11,7 @@ The implementation includes an upgradable modular account with two plugins (`Sin ## Development -Anyone is welcome to submit feedback and/or PRs to improve code or add Plugins. +Anyone is welcome to submit feedback and/or PRs to improve code. ### Testing @@ -28,3 +28,14 @@ Since IR compilation generates different bytecode, it's useful to test against t FOUNDRY_PROFILE=optimized-build forge build FOUNDRY_PROFILE=optimized-test forge test -vvv ``` + +## Integration testing + +The reference implementation provides a sample factory and deploy script for the factory, account implementation, and the demo validation module `SingleSignerValidation`. This is not auditted, nor intended for production use. Limitations set by the GPL-V3 license apply. + +To run this script, provide appropriate values in a `.env` file based on the `.env.example` template, then run: +```bash +forge script script/Deploy.s.sol -r --broadcast +``` + +Where `` specifies a way to sign the deployment transaction (see [here](https://book.getfoundry.sh/reference/forge/forge-script#wallet-options---raw)) and `` specifies an RPC for the network you are deploying on. diff --git a/broadcast/Deploy.s.sol/421614/run-1722008916.json b/broadcast/Deploy.s.sol/421614/run-1722008916.json new file mode 100644 index 00000000..21b808df --- /dev/null +++ b/broadcast/Deploy.s.sol/421614/run-1722008916.json @@ -0,0 +1,214 @@ +{ + "transactions": [ + { + "hash": "0xf77f7595a83b6517b0bca017c6fe29990665132ba5bc0fc2e5f7e760e15f3a9e", + "transactionType": "CREATE2", + "contractName": "UpgradeableModularAccount", + "contractAddress": "0x0809bf385117a43a322a4e31d459c0ecaa3b1a08", + "function": null, + "arguments": [ + "0x0000000071727De22E5E9d8BAf0edAc6f37da032" + ], + "transaction": { + "from": "0x7f89ed1f3f0d52d303904101305471bca3cde710", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0x4438e33", + "value": "0x0", + "input": "0x000000000000000000000000000000000000000000000000000000000000000060c03461012057601f615f8938819003918201601f19168301916001600160401b038311848410176101255780849260209460405283398101031261012057516001600160a01b0381168103610120573060805260a052600080516020615f698339815191525460ff8160081c1661010f5760ff808216036100c7575b604051615e2d908161013c82396080518181816116180152613543015260a05181818161057b01528181611a9601528181611bfa01528181611f480152818161278201526129ad0152f35b60ff9081191617600080516020615f69833981519152557fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602060405160ff8152a13861007c565b63f92ee8a960e01b60005260046000fd5b600080fd5b634e487b7160e01b600052604160045260246000fdfe60806040526004361015610026575b36156100245761001c612887565b602081519101f35b005b60003560e01c806214490e1461018557806301ffc9a714610180578063104179a81461017b5780631626ba7e1461017657806319822f7c146101715780632de0f9341461016c57806334fcd5be146101675780633fa3f96914610162578063443f5daa1461015d5780634f1ef2861461015857806351fd4e1e1461015357806352d1902d1461014e5780635bf8fca214610149578063642f9dd414610144578063756e30981461013f578063808f49bc1461013a5780638dd7712f14610135578063ad3cb1cc14610130578063b0d691fe1461012b578063b61d27f614610126578063b6b1ccfe146101215763d087d2880361000e57611ee7565b611c98565b611c1e565b611bcd565b611b6e565b611a4e565b6119d7565b611892565b61179e565b6116a3565b6115f0565b611325565b6111a2565b610d5f565b610a7c565b610951565b610828565b61052b565b6104b2565b610380565b6102d6565b610278565b9181601f840112156101bb5782359167ffffffffffffffff83116101bb576020808501948460051b0101116101bb57565b600080fd5b9181601f840112156101bb5782359167ffffffffffffffff83116101bb57602083818601950101116101bb57565b9060806003198301126101bb5760043565ffffffffffff19811681036101bb579160243567ffffffffffffffff81116101bb578161022e9160040161018a565b9290929160443567ffffffffffffffff81116101bb5781610251916004016101c0565b929092916064359067ffffffffffffffff82116101bb576102749160040161018a565b9091565b346101bb576100246102a76102a761028f366101ee565b9361029f98959698939193612993565b979099612ea5565b612caa565b7fffffffff000000000000000000000000000000000000000000000000000000008116036101bb57565b346101bb5760206003193601126101bb5760206102fd6004356102f8816102ac565b611fb1565b6040519015158152f35b6004359067ffffffffffffffff19821682036101bb57565b602060408183019282815284518094520192019060005b8181106103435750505090565b909192602060606001926040875167ffffffffffffffff198151168352848101511515858401520151151560408201520194019101919091610336565b346101bb5760206003193601126101bb5760026103d861039e610307565b67ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b0180546103e4816120a1565b9160005b82811061040157604051806103fd868261031f565b0390f35b8065ffffffffffff1961041660019385612e21565b90549060031b1c166104966104456104376104378465ffffffffffff191690565b67ffffffffffffffff191690565b9161048d61046766020000000000008316151592660100000000000016151590565b916104846104736110bf565b67ffffffffffffffff199096168652565b15156020850152565b15156040830152565b6104a08287612152565b526104ab8186612152565b50016103e8565b346101bb5760406003193601126101bb5760043560243567ffffffffffffffff81116101bb576020916104ec6104f29236906004016101c0565b91612342565b7fffffffff0000000000000000000000000000000000000000000000000000000060405191168152f35b90816101209103126101bb5790565b346101bb5760606003193601126101bb5760043567ffffffffffffffff81116101bb5761055c90369060040161051c565b602435906044359073ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001633036107ca576060810160046105b1828461270b565b905010610768576101008201936105dd6104376105d76105d1888761270b565b906121a3565b9061216b565b9161063d600160ff61062a6106246105fe6105f88c8b61270b565b906127c9565b357fff000000000000000000000000000000000000000000000000000000000000001690565b60f81c90565b161484610637848861270b565b90613a3c565b600261067d8467ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b0154151590816106ff575b506106d5576106b9836106bf936106af6106a86103fd996106c59861270b565b80916121bf565b9390923690613262565b906143f2565b91613342565b6040519081529081906020820190565b7f5f49f00a0000000000000000000000000000000000000000000000000000000060005260046000fd5b7f8dd7712f00000000000000000000000000000000000000000000000000000000915061075f6107596107537fffffffff00000000000000000000000000000000000000000000000000000000938861270b565b906121b1565b906131fc565b16141538610688565b610759610778916107c69361270b565b7ffcfc5aad000000000000000000000000000000000000000000000000000000006000527fffffffff0000000000000000000000000000000000000000000000000000000016600452602490565b6000fd5b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f6163636f756e743a206e6f742066726f6d20456e747279506f696e74000000006044820152fd5b346101bb5760206003193601126101bb57602061084f60043561084a816102ac565b61253d565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b60005b8381106108805750506000910152565b8181015183820152602001610870565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f6020936108cc8151809281875287808801910161086d565b0116010190565b602081016020825282518091526040820191602060408360051b8301019401926000915b83831061090657505050505090565b9091929394602080610942837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187528951610890565b970193019301919392906108f7565b60206003193601126101bb5760043567ffffffffffffffff81116101bb5761097d90369060040161018a565b610985612993565b909261099083612089565b9261099e604051948561107e565b8084527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06109cb82612089565b0160005b818110610a6b57505060005b8181106109ff576103fd856109f3886102a788612caa565b604051918291826108d3565b80610a4f610a18610a1360019486886126c1565b612701565b6020610a258487896126c1565b0135610a49610a42610a3886898b6126c1565b604081019061270b565b369161114d565b91613361565b610a598288612152565b52610a648187612152565b50016109db565b8060606020809389010152016109cf565b346101bb57610a8a366101ee565b7f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e4054969590949193919067ffffffffffffffff610ae2610adc60ff60088c901c1615610ad5565b1590565b9a60ff1690565b60ff1690565b1680159081610cd0575b6001149081610cc6575b159081610cbd575b50610c9357610b7f9688610b7660017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff007f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e405416177f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e4055565b610c1f5761275c565b610b8557005b610bf07fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff7f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e4054167f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e4055565b604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a1005b610c8e6101007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff7f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e405416177f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e4055565b61275c565b7ff92ee8a90000000000000000000000000000000000000000000000000000000060005260046000fd5b90501538610afe565b303b159150610af6565b899150610aec565b73ffffffffffffffffffffffffffffffffffffffff8116036101bb57565b3590610d0182610cd8565b565b60606003198201126101bb57600435610d1b81610cd8565b9160243567ffffffffffffffff81116101bb57606060031982850301126101bb57600401916044359067ffffffffffffffff82116101bb57610274916004016101c0565b346101bb57610d6d36610d03565b9190610d84610d7a612993565b929094369161114d565b9160208101610d938183613381565b91905060005b828110610f4a57505050610dad8180613465565b905060005b818110610f2257505060408101610dc981836134c9565b92905060005b838110610e9d575050505073ffffffffffffffffffffffffffffffffffffffff60019416803b156101bb5761002494610e3a60006102a795604051809381927f8a91b0e300000000000000000000000000000000000000000000000000000000835260048301611692565b038183875af19081610e82575b50610e7d575060005b1515907fd7b1465298613b13a57ba2ee63b7b364d3a086a9ffcf7f591a9fa01166ed5097600080a3612caa565b610e50565b80610e916000610e979361107e565b806115e5565b38610e47565b80610f10610ebf610eba600194610eb488886134c9565b90612e8b565b612e9b565b7fffffffff00000000000000000000000000000000000000000000000000000000166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e43602052604060002090565b610f1a815461351d565b905501610dcf565b80610f44610f3f610eba600194610f398880613465565b906134b9565b6147bf565b01610db2565b80611028610f6c610f67600194610f61878a613381565b906133d5565b6133ef565b6110228b85610fef610f9e85517fffffffff000000000000000000000000000000000000000000000000000000001690565b7fffffffff00000000000000000000000000000000000000000000000000000000166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e41602052604060002090565b0192611002602082015163ffffffff1690565b61101c60606110146040850151151590565b930151151590565b92614700565b906147ac565b01610d99565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff82111761107957604052565b61102e565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761107957604052565b60405190610d0160608361107e565b60405190610d016101208361107e565b67ffffffffffffffff811161107957601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b919091611124816110de565b611131604051918261107e565b809382825282116101bb57818160009384602080950137010152565b929192611159826110de565b91611167604051938461107e565b8294818452818301116101bb578281602093846000960137010152565b9080601f830112156101bb5781602061119f9335910161114d565b90565b60406003193601126101bb576004356111ba81610cd8565b60243567ffffffffffffffff81116101bb576111da903690600401611184565b906111e361352c565b6111eb612993565b926111f461352c565b6040517f52d1902d00000000000000000000000000000000000000000000000000000000815260208160048173ffffffffffffffffffffffffffffffffffffffff88165afa600091816112f4575b5061128b577f4c9c8ce30000000000000000000000000000000000000000000000000000000060005273ffffffffffffffffffffffffffffffffffffffff841660045260246000fd5b92837f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc8695036112c75750926102a76102a79261002495615592565b7faa1d49a40000000000000000000000000000000000000000000000000000000060005260045260246000fd5b61131791925060203d60201161131e575b61130f818361107e565b810190612878565b9038611242565b503d611305565b346101bb5761133336610d03565b9161134961133f612993565b939094369161114d565b73ffffffffffffffffffffffffffffffffffffffff85169182156115bb57611373610ad18761483c565b611577576113818180613465565b905060005b8181106115275750506020810161139d8183613381565b91905060005b8281106114d257505050604081016113bb81836134c9565b92905060005b8381106114a95750505050813b156101bb57600061140c91604051809381927f6d61fe7000000000000000000000000000000000000000000000000000000000835260048301611692565b038183865af19081611494575b5061146157836114276127ef565b9061145d6040519283927f1672bd93000000000000000000000000000000000000000000000000000000008452600484016135dc565b0390fd5b61002492916102a7917fb4a437488482177b2d124ce7c50e57d8f8d42a9896b525c9c497ee0d533a95de600080a2612caa565b80610e9160006114a39361107e565b38611419565b806114c0610ebf610eba600194610eb488886134c9565b6114ca81546135c1565b9055016113c1565b806115216114e9610f67600194610f61878a613381565b61151b8c85610fef610f9e85517fffffffff000000000000000000000000000000000000000000000000000000001690565b90614ee6565b016113a3565b806115718961153f610eba600195610f398980613465565b611558602061155286610f398b80613465565b016135b7565b61156b604061155287610f398c80613465565b916149a6565b01611386565b7f2e06ed7c0000000000000000000000000000000000000000000000000000000060005273ffffffffffffffffffffffffffffffffffffffff861660045260246000fd5b7f21507ad00000000000000000000000000000000000000000000000000000000060005260046000fd5b60009103126101bb57565b346101bb5760006003193601126101bb5773ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001630036116685760206040517f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc8152f35b7fe07c8dba0000000000000000000000000000000000000000000000000000000060005260046000fd5b90602061119f928181520190610890565b60406003193601126101bb5760043567ffffffffffffffff81116101bb576116cf9036906004016101c0565b60243567ffffffffffffffff81116101bb57600091611757600261174561173c61039e611701889736906004016101c0565b806117146104376105d7838698966121a3565b946117378d878c600160ff61172f6106246105fe8a8a6127c9565b161492613a3c565b6121bf565b90878b85613cbd565b0161175136848861114d565b90613fca565b93611767604051809381936127e1565b039082305af1906117766127ef565b911561179557906117896103fd92612caa565b60405191829182611692565b50602081519101fd5b346101bb5760206003193601126101bb5760016117c0600435610f9e816102ac565b0180546117cc816120a1565b9160005b8281106117e557604051806103fd868261031f565b8065ffffffffffff196117fa60019385612e21565b90549060031b1c1661181a6104456104378367ffffffffffffffff191690565b6118248287612152565b5261182f8186612152565b50016117d0565b602060408183019282815284518094520192019060005b81811061185a5750505090565b82517fffffffff000000000000000000000000000000000000000000000000000000001684526020938401939092019160010161184d565b346101bb5760206003193601126101bb576118ab610307565b60046118eb8267ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b01546118f681612089565b91611904604051938461107e565b8183527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061193183612089565b0136602085013760005b82811061195057604051806103fd8682611836565b807fffffffff000000000000000000000000000000000000000000000000000000006119be60019360046119b88767ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b01612e21565b90549060031b1c166119d08287612152565b520161193b565b346101bb5760206003193601126101bb576119fe60016119f861039e610307565b0161281f565b60405180916020820160208352815180915260206040840192019060005b818110611a2a575050500390f35b825167ffffffffffffffff1916845285945060209384019390920191600101611a1c565b346101bb5760406003193601126101bb5760043567ffffffffffffffff81116101bb57611a7f90369060040161051c565b73ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000163303611b2f57600080611b01611afa611aef6002611ae461039e6104376105d76105d16101008c018c61270b565b016117513636611118565b94606081019061270b565b80916121fa565b90611b11604051809381936127e1565b039082305af190611b206127ef565b91156117955761002490612caa565b7fd663742a0000000000000000000000000000000000000000000000000000000060005260046000fd5b60405190611b6860208361107e565b60008252565b346101bb5760006003193601126101bb576103fd6040805190611b91818361107e565b600582527f352e302e30000000000000000000000000000000000000000000000000000000602083015251918291602083526020830190610890565b346101bb5760006003193601126101bb57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b60606003193601126101bb57600435611c3681610cd8565b60243560443567ffffffffffffffff81116101bb576103fd926102a7611c7e611c66611c849436906004016101c0565b939095610a49611c74612993565b989096369161114d565b93612caa565b604051918291602083526020830190610890565b346101bb5760606003193601126101bb57611cb1610307565b60243567ffffffffffffffff81116101bb57611cd19036906004016101c0565b9060443567ffffffffffffffff81116101bb57611cf290369060040161018a565b929091611cfd612993565b939094611d3e8767ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b91611d4888615229565b81611dda575b5050611d5c600182016140de565b6002810180549060005b828110611dbe5750505060040194855460005b818110611da257610024876102a78881898960608a901c60408b901c63ffffffff165b506152c5565b600190611db7611db18a6142a3565b8a615cb9565b5001611d79565b600190611dd3611dcd846142a3565b84615cb9565b5001611d66565b9295949093966001830195865492611df860028601948554906135cf565b8703611ebd576000998a5b89548c1015611e6457611e5c81611e578e8d611d9c611e428f8f600199611e3592611e2d92612dca565b959094612e21565b90549060031b1c60401b90565b606081901c9160409190911c63ffffffff1690565b6140b1565b9b019a611e03565b9a50919397509193976000995b88548b1015611eab57611ea381611e578d8c611d9c611e426104378f8f99611e9b9160019b612dca565b9590946142b6565b9a0199611e71565b50945094509450949095503880611d4e565b7fa24a13a60000000000000000000000000000000000000000000000000000000060005260046000fd5b346101bb5760006003193601126101bb576040517f35567e1a0000000000000000000000000000000000000000000000000000000081523060048201526000602482015260208160448173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa8015611fac576103fd91600091611f8d575b506040519081529081906020820190565b611fa6915060203d60201161131e5761130f818361107e565b38611f7c565b612336565b7fffffffff0000000000000000000000000000000000000000000000000000000081167fffffffff000000000000000000000000000000000000000000000000000000008114612082577f01ffc9a7000000000000000000000000000000000000000000000000000000001461207c57612076907fffffffff00000000000000000000000000000000000000000000000000000000166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e43602052604060002090565b54151590565b50600190565b5050600090565b67ffffffffffffffff81116110795760051b60200190565b906120ab82612089565b6120b8604051918261107e565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06120e68294612089565b019060005b8281106120f757505050565b6020906040516121068161105d565b6000815260008382015260006040820152828285010152016120eb565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b80518210156121665760209160051b010190565b612123565b9190913567ffffffffffffffff1981169260188110612188575050565b67ffffffffffffffff19929350829060180360031b1b161690565b906018116101bb5790601890565b906004116101bb5790600490565b90929192836019116101bb5783116101bb57601901917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe70190565b90929192836004116101bb5783116101bb57600401916003190190565b90929192836018116101bb5783116101bb57601801917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe80190565b90601a116101bb5790601a90565b9092919283601a116101bb5783116101bb57601a01917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe60190565b908160209103126101bb575161119f816102ac565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b9160a09363ffffffff73ffffffffffffffffffffffffffffffffffffffff928361119f9a9895168652166020850152166040830152606082015281608082015201916122b0565b6040513d6000823e3d90fd5b6123a792612353610437828561216b565b926123b1610ad1606086901c604087901c63ffffffff1697909667ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b5460081c60ff1690565b6124ef57916123da826020959373ffffffffffffffffffffffffffffffffffffffff9795612217565b959093612419604051978896879586947fe7db7f7e000000000000000000000000000000000000000000000000000000008652339030600488016122ef565b0392165afa908115611fac577f1626ba7e00000000000000000000000000000000000000000000000000000000917fffffffff00000000000000000000000000000000000000000000000000000000916000916124c0575b50161461249c577fffffffff0000000000000000000000000000000000000000000000000000000090565b7f1626ba7e0000000000000000000000000000000000000000000000000000000090565b6124e2915060203d6020116124e8575b6124da818361107e565b81019061229b565b38612471565b503d6124d0565b7f5f5943f70000000000000000000000000000000000000000000000000000000060005273ffffffffffffffffffffffffffffffffffffffff841660045263ffffffff851660245260446000fd5b7fffffffff0000000000000000000000000000000000000000000000000000000081167fb61d27f6000000000000000000000000000000000000000000000000000000008114908115612697575b811561266d575b8115612643575b8115612619575b506126145761260f73ffffffffffffffffffffffffffffffffffffffff917fffffffff00000000000000000000000000000000000000000000000000000000166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e41602052604060002090565b541690565b503090565b7f443f5daa00000000000000000000000000000000000000000000000000000000915014386125a0565b7f51fd4e1e0000000000000000000000000000000000000000000000000000000081149150612599565b7f4f1ef2860000000000000000000000000000000000000000000000000000000081149150612592565b7f34fcd5be000000000000000000000000000000000000000000000000000000008114915061258b565b91908110156121665760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1813603018212156101bb570190565b3561119f81610cd8565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1813603018212156101bb570180359067ffffffffffffffff82116101bb576020019181360383136101bb57565b9061276b969594939291612ea5565b73ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000167f9f08b8dca66d3393166c297eebdbe382963a15cce40f3a2f4bf32378553fe65a600080a2565b90601810156121665760180190565b90156121665790565b908092918237016000815290565b3d1561281a573d90612800826110de565b9161280e604051938461107e565b82523d6000602084013e565b606090565b906040519182815491828252602082019060005260206000209260005b818110612851575050610d019250038361107e565b845460401b67ffffffffffffffff191683526001948501948794506020909301920161283c565b908160209103126101bb575190565b7fffffffff000000000000000000000000000000000000000000000000000000006000351673ffffffffffffffffffffffffffffffffffffffff612916827fffffffff00000000000000000000000000000000000000000000000000000000166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e41602052604060002090565b541690811561296657506000908161292c612993565b93909260405136838237828136810182815203925af19161294b6127ef565b921561295e57906102a761119f92612caa565b825160208401fd5b7ffcfc5aad0000000000000000000000000000000000000000000000000000000060005260045260246000fd5b60609073ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001633141580612c0a575b80612b7b575b612a5f575b61119f6001611ae47fffffffff00000000000000000000000000000000000000000000000000000000600035167fffffffff00000000000000000000000000000000000000000000000000000000166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e41602052604060002090565b90503360601b7fffffffffffffffffffffffffffffffffffffffff000000000000000000000000166bffffffff000000000000000017612a9f8136613789565b612ae260016119f88367ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b80519060005b828110612b3b575050506002611ae4612b359267ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b906129e1565b80612b75612b5d612b4e60019486612152565b5167ffffffffffffffff191690565b612b673636611118565b612b6f611b59565b91614112565b01612ae8565b50612c05610ad1612bfb7fffffffff00000000000000000000000000000000000000000000000000000000600035167fffffffff00000000000000000000000000000000000000000000000000000000166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e41602052604060002090565b5460a01c60ff1690565b6129dc565b50303314156129d6565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8015612c50576000190190565b612c14565b60409063ffffffff61119f94931681528160208201520190610890565b63ffffffff61119f949373ffffffffffffffffffffffffffffffffffffffff6060941683521660208201528160408201520190610890565b8051805b612cb6575050565b612cbf90612c43565b91612cca8383612152565b5191612ce2602084015167ffffffffffffffff191690565b67ffffffffffffffff19811615612dc057925163ffffffff604085901c169460609490941c939084803b156101bb57612d506000929183926040519485809481937f5d413a810000000000000000000000000000000000000000000000000000000083528d60048401612c55565b03925af19081612dab575b50612da05750505061145d612d6e6127ef565b6040519384937f12c4c3e400000000000000000000000000000000000000000000000000000000855260048501612c72565b919350915080612cae565b80610e916000612dba9361107e565b38612d5b565b5091509180612cae565b90821015612166576102749160051b81019061270b565b9190913565ffffffffffff19811692601a8110612dfc575050565b65ffffffffffff199293508290601a0360031b1b161690565b65ffffffffffff191690565b80548210156121665760005260206000200190600090565b80546801000000000000000081101561107957612e5b91600182018155612e21565b77ffffffffffffffffffffffffffffffffffffffffffffffff829392549160031b9260401c831b921b1916179055565b91908110156121665760051b0190565b3561119f816102ac565b90969592939491612ebc8265ffffffffffff191690565b93612ed167ffffffffffffffff198616610437565b90612f108267ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b9460005b8281106130ad575050506004840160005b8381106130055750505050610d0195965091612fd5612ffa92612fa285612f90612f5f6130009865ffffffffffff196001911660181a1490565b849060ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0083541691151516179055565b65ffffffffffff191660191a60011490565b81547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff1690151560081b61ff0016179055565b7fffffffffffffffffffffffffffffffffffffffff0000000000000000000000001690565b60601c90565b614207565b613013610eba82868e612e8b565b613042610ad17fffffffff00000000000000000000000000000000000000000000000000000000831685615336565b61304f5750600101612f25565b7fd5d4419e000000000000000000000000000000000000000000000000000000006000527fffffffff000000000000000000000000000000000000000000000000000000001660045267ffffffffffffffff19831660245260446000fd5b6130cd612e156130c76130c1848787612dca565b90612252565b90612de1565b906130e36130dc828686612dca565b8091612260565b6130f884929467010000000000000016151590565b156131715760ff60018b0161311f61311961043765ffffffffffff19871681565b82612e39565b541161314757600193613000612ffa612fd56131419565ffffffffffff191690565b01612f14565b7f78f240550000000000000000000000000000000000000000000000000000000060005260046000fd5b919290613195610ad161318c65ffffffffffff198416612e15565b60028d01615336565b6131b7579161314191613000612ffa612fd56001979665ffffffffffff191690565b7fc349aaaa0000000000000000000000000000000000000000000000000000000060005267ffffffffffffffff19871660045265ffffffffffff191660245260446000fd5b919091357fffffffff0000000000000000000000000000000000000000000000000000000081169260048110613230575050565b7fffffffff00000000000000000000000000000000000000000000000000000000929350829060040360031b1b161690565b919091610120818403126101bb576132786110ce565b9261328282610cf6565b845260208201356020850152604082013567ffffffffffffffff81116101bb57816132ae918401611184565b6040850152606082013567ffffffffffffffff81116101bb57816132d3918401611184565b60608501526080820135608085015260a082013560a085015260c082013560c085015260e082013567ffffffffffffffff81116101bb5781613316918401611184565b60e085015261010082013567ffffffffffffffff81116101bb5761333a9201611184565b610100830152565b8061334a5750565b60008080809333600019f15061335e6127ef565b50565b916000928392602083519301915af1906133796127ef565b911561179557565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1813603018212156101bb570180359067ffffffffffffffff82116101bb57602001918160071b360383136101bb57565b91908110156121665760071b0190565b801515036101bb57565b6080813603126101bb57604051906080820182811067ffffffffffffffff821117611079576040528035613422816102ac565b825260208101359063ffffffff821682036101bb576060916020840152604081013561344d816133e5565b6040840152013561345d816133e5565b606082015290565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1813603018212156101bb570180359067ffffffffffffffff82116101bb576020019160608202360383136101bb57565b9190811015612166576060020190565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1813603018212156101bb570180359067ffffffffffffffff82116101bb57602001918160051b360383136101bb57565b906000198201918211612c5057565b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803014908115613575575b5061166857565b905073ffffffffffffffffffffffffffffffffffffffff7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc541614153861356e565b3561119f816133e5565b9060018201809211612c5057565b91908201809211612c5057565b60409073ffffffffffffffffffffffffffffffffffffffff61119f94931681528160208201520190610890565b6020818303126101bb5780359067ffffffffffffffff82116101bb57019080601f830112156101bb5781359161363e83612089565b9261364c604051948561107e565b80845260208085019160051b830101918383116101bb5760208101915b83831061367857505050505090565b823567ffffffffffffffff81116101bb5782019060607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe083880301126101bb57604051906136c58261105d565b60208301356136d381610cd8565b82526040830135602083015260608301359167ffffffffffffffff83116101bb5761370688602080969581960101611184565b6040820152815201920191613669565b90602082519201517fffffffff0000000000000000000000000000000000000000000000000000000081169260048110613230575050565b916060838303126101bb57823561376481610cd8565b92602081013592604082013567ffffffffffffffff81116101bb5761119f9201611184565b919091600061379c6107598360006121b1565b7f8dd7712f000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000008216146139f8575b806138146000877fffffffff0000000000000000000000000000000000000000000000000000000094614ef9565b167fb61d27f60000000000000000000000000000000000000000000000000000000081036138a05750613856929350908061384e926121fa565b81019061374e565b505073ffffffffffffffffffffffffffffffffffffffff16301461387657565b7f54ff929d0000000000000000000000000000000000000000000000000000000060005260046000fd5b7f34fcd5be00000000000000000000000000000000000000000000000000000000146138cd575b50509050565b816138e3926138db926121fa565b810190613609565b60005b81518110156139ec573061393461391b6139008486612152565b515173ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b14613942575b6001016138e6565b61395960406139518385612152565b510151613716565b907fffffffff0000000000000000000000000000000000000000000000000000000082167fb61d27f60000000000000000000000000000000000000000000000000000000081149081156139c2575b50613876576139bb600086600194614ef9565b905061393a565b7f34fcd5be00000000000000000000000000000000000000000000000000000000915014386139a8565b505090508038806138c7565b505080613a069160006121fa565b907fffffffff00000000000000000000000000000000000000000000000000000000613a3561075984846121b1565b90506137e6565b9392919091613a4e61075984876121b1565b7f8dd7712f000000000000000000000000000000000000000000000000000000007fffffffff00000000000000000000000000000000000000000000000000000000821614613c1a575b80613ac583857fffffffff0000000000000000000000000000000000000000000000000000000094614ef9565b167fb61d27f6000000000000000000000000000000000000000000000000000000008103613aff575050508061384e9161385693946121fa565b91949092917f34fcd5be0000000000000000000000000000000000000000000000000000000014613b32575b5050509050565b81613b40926138db926121fa565b60005b8151811015613c0c5730613b5d61391b6139008486612152565b14613b6b575b600101613b43565b613b7a60406139518385612152565b907fffffffff0000000000000000000000000000000000000000000000000000000082167fb61d27f6000000000000000000000000000000000000000000000000000000008114908115613be2575b5061387657613bdb8487600194614ef9565b9050613b63565b7f34fcd5be0000000000000000000000000000000000000000000000000000000091501438613bc9565b505050905080388080613b2b565b509180613c2792956121fa565b909390917fffffffff00000000000000000000000000000000000000000000000000000000613c5961075985886121b1565b9050613a98565b96949273ffffffffffffffffffffffffffffffffffffffff9063ffffffff61119f9a989483613caf9895168b521660208a0152166040880152606087015260c0608087015260c08601916122b0565b9260a08185039101526122b0565b919493613cca9193615067565b96929096909196613d1460016119f88767ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b976000935b8951851015613e1f5784613d36610adc6106246105fe86866127d8565b03613dfe57613d4891610a42916150b3565b91825115613dd457613d5991615067565b909690949193909180613d75610adc6106246105fe89896127d8565b1115613daa57613d9f600192613d8e612b4e848f612152565b613d99368b8e61114d565b90614112565b019392909591613d19565b7fd8ddc84c0000000000000000000000000000000000000000000000000000000060005260046000fd5b7fb91b669d0000000000000000000000000000000000000000000000000000000060005260046000fd5b919590929380613d9f8b613d8e612b4e600195613e19611b59565b93612152565b9493509695949750505060ff80613e3c6106246105fe89876127d8565b1603613f135763ffffffff604087901c169560601c948592613e5e91906150b3565b929093813b156101bb57600088613eab8296604051988997889687957f465d33e0000000000000000000000000000000000000000000000000000000008752349033903060048a01613c60565b03925af19081613efe575b50613ef9575061145d613ec76127ef565b6040519384937f92a47d6b00000000000000000000000000000000000000000000000000000000855260048501612c72565b915050565b80610e916000613f0d9361107e565b38613eb6565b7f151d90fe0000000000000000000000000000000000000000000000000000000060005260046000fd5b90613f4782612089565b613f54604051918261107e565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0613f828294612089565b0160005b818110613f9257505050565b60405190604082019180831067ffffffffffffffff841117611079576020926040526060815260008382015282828601015201613f86565b9190825490613fd882613f3d565b9360005b838110614058575060005b838110613ff45750505050565b80614004612e15600193856142b6565b6602000000000000811661401a575b5001613fe7565b6140416140328667ffffffffffffffff198416615128565b91660100000000000016151590565b15614013576140508289612152565b515238614013565b80614068612e15600193856142b6565b6601000000000000811661407e575b5001613fdc565b67ffffffffffffffff196140ab91166020614099848b612152565b51019067ffffffffffffffff19169052565b38614077565b6000198114612c505760010190565b916140da918354906000199060031b92831b921b19161790565b9055565b80549060008155816140ee575050565b6000526020600020908101905b818110614106575050565b600081556001016140fb565b63ffffffff604082901c169360609190911c929091839190823b156101bb576141929263ffffffff6000886141a48296604051988997889687957f3d6bda3200000000000000000000000000000000000000000000000000000000875216600486015233602486015234604486015260a0606486015260a4850190610890565b90600319848303016084850152610890565b03925af190816141f2575b50613ef9575061145d6141c06127ef565b6040519384937fa53ac97500000000000000000000000000000000000000000000000000000000855260048501612c72565b80610e9160006142019361107e565b386141af565b9160009281614217575b50505050565b73ffffffffffffffffffffffffffffffffffffffff1690813b1561429f5791839161427993836040518096819582947f6d61fe7000000000000000000000000000000000000000000000000000000000845260206004850181815201916122b0565b03925af18015611fac5761428f575b8080614211565b816142999161107e565b38614288565b8380fd5b8054156121665760005260206000205490565b906142c091612e21565b90549060031b1c90565b9392916143ed9063ffffffff6040931686526060602087015261430660608701825173ffffffffffffffffffffffffffffffffffffffff169052565b602081015160808701526101006143ba6143676143338685015161012060a08c01526101808b0190610890565b60608501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa08b83030160c08c0152610890565b608084015160e08a015260a0840151838a015260c08401516101208a015260e08401517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa08a8303016101408b0152610890565b9101517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa087830301610160880152610890565b930152565b9394926143ff9192615067565b96929194909691949660009261444e60016119f88a67ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b986000975b8a5189101561461157888b81614472610adc6106246105fe88886127d8565b036145e857505061448691610a42916150b3565b806101008801525115613dd45761449c91615067565b93909394886144b6610adc6106246105fe889780996127d8565b1115613daa57614509906144d0611e42612b4e8c8f612152565b60208a8c60405196879283927f2a3d428c00000000000000000000000000000000000000000000000000000000845286600485016142ca565b0381600073ffffffffffffffffffffffffffffffffffffffff87165af1938415611fac576000946145c8575b5073ffffffffffffffffffffffffffffffffffffffff8416916001831161457357505050600191614565916153a2565b980197949193929093614453565b7f093431410000000000000000000000000000000000000000000000000000000060005273ffffffffffffffffffffffffffffffffffffffff90811660045263ffffffff919091166024521660445260646000fd5b6145e191945060203d811161131e5761130f818361107e565b9238614535565b611e42612b4e614509949699936144d0938d6101006146099c999b9c611b59565b910152612152565b959497509792989591505060ff8061462f6106246105fe8b896127d8565b1603613f1357600061467d73ffffffffffffffffffffffffffffffffffffffff93614662610a426146b69b6020996150b3565b610100850152606081901c9160409190911c63ffffffff1690565b604094919451998a96879586937f0ab8785f000000000000000000000000000000000000000000000000000000008552600485016142ca565b0393165af1928315611fac576000936146df575b5051156146db579061119f91615483565b5090565b6146f991935060203d60201161131e5761130f818361107e565b91386146ca565b909265ffffffffffff199290918391156147a4576602000000000000925b1561476e577fffffffffffffffffffffffffffffffffffffffff0000000000000000000000006bffffffff00000000000000006601000000000000965b60401b169160601b161791161791161790565b7fffffffffffffffffffffffffffffffffffffffff0000000000000000000000006bffffffff000000000000000060009661475b565b60009261471e565b65ffffffffffff1961335e921690615cb9565b614814907fffffffff00000000000000000000000000000000000000000000000000000000166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e41602052604060002090565b80547fffffffffffffffffffff00000000000000000000000000000000000000000000169055565b60206000604051828101907f01ffc9a70000000000000000000000000000000000000000000000000000000082527f01ffc9a70000000000000000000000000000000000000000000000000000000060248201526024815261489f60448261107e565b519084617530fa903d600051908361499a575b5082614990575b50816148f5575b816148c9575090565b61119f91507fd2db51a800000000000000000000000000000000000000000000000000000000906156c5565b905060206000604051828101907f01ffc9a70000000000000000000000000000000000000000000000000000000082527fffffffff0000000000000000000000000000000000000000000000000000000060248201526024815261495a60448261107e565b519084617530fa6000513d82614984575b508161497a575b5015906148c0565b9050151538614972565b6020111591503861496b565b15159150386148b9565b602011159250386148b2565b9093926149fe827fffffffff00000000000000000000000000000000000000000000000000000000166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e41602052604060002090565b9173ffffffffffffffffffffffffffffffffffffffff835416614e97577fffffffff0000000000000000000000000000000000000000000000000000000081167f19822f7c000000000000000000000000000000000000000000000000000000008114908115614e6d575b8115614e43575b8115614e19575b8115614def575b8115614dc5575b8115614d9b575b8115614d71575b8115614d47575b8115614d1d575b8115614cf3575b8115614cc9575b8115614c9f575b50614c5057614ac481615758565b614c0157614ad181615988565b614bb25750610d019394614b23614b6992849073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055565b82547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff000000000000000000000000000000000000000016178255565b907fffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffff75ff000000000000000000000000000000000000000000835492151560a81b169116179055565b7f3cecfc37000000000000000000000000000000000000000000000000000000006000527fffffffff000000000000000000000000000000000000000000000000000000001660045260246000fd5b7fd133f366000000000000000000000000000000000000000000000000000000006000527fffffffff000000000000000000000000000000000000000000000000000000001660045260246000fd5b7fe171c779000000000000000000000000000000000000000000000000000000006000527fffffffff000000000000000000000000000000000000000000000000000000001660045260246000fd5b7f808f49bc0000000000000000000000000000000000000000000000000000000091501438614ab6565b7f642f9dd40000000000000000000000000000000000000000000000000000000081149150614aaf565b7f756e30980000000000000000000000000000000000000000000000000000000081149150614aa8565b7f2de0f9340000000000000000000000000000000000000000000000000000000081149150614aa1565b7f5bf8fca20000000000000000000000000000000000000000000000000000000081149150614a9a565b7f34fcd5be0000000000000000000000000000000000000000000000000000000081149150614a93565b7fb61d27f60000000000000000000000000000000000000000000000000000000081149150614a8c565b7f4f1ef2860000000000000000000000000000000000000000000000000000000081149150614a85565b7f52d1902d0000000000000000000000000000000000000000000000000000000081149150614a7e565b7f01ffc9a70000000000000000000000000000000000000000000000000000000081149150614a77565b7f443f5daa0000000000000000000000000000000000000000000000000000000081149150614a70565b7f51fd4e1e0000000000000000000000000000000000000000000000000000000081149150614a69565b7fec9cbcb3000000000000000000000000000000000000000000000000000000006000527fffffffff000000000000000000000000000000000000000000000000000000001660045260246000fd5b65ffffffffffff1961335e921690615336565b9115614fb457614f0882615a92565b15908115614f69575b50614f195750565b7fffffffff00000000000000000000000000000000000000000000000000000000907fcf7b49f6000000000000000000000000000000000000000000000000000000006000521660045260246000fd5b60ff9150614fab9067ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b54161538614f11565b7fffffffff0000000000000000000000000000000000000000000000000000000060046150186150329367ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b019216809260019160005201602052604060002054151590565b1561503a5750565b7fcf7b49f60000000000000000000000000000000000000000000000000000000060005260045260246000fd5b9190600091816004116150af576004843560e01c0163ffffffff8111612c505763ffffffff168060041161429f5782811161429f578060048601956003198201955001920390565b8280fd5b919091826001116101bb57600101916000190190565b6020818303126101bb5780519067ffffffffffffffff82116101bb570181601f820112156101bb5780516150fc816110de565b9261510a604051948561107e565b818452602082840101116101bb5761119f916020808501910161086d565b6000929061519590606081901c9060401c63ffffffff16949093604051809381927fed6dfb1300000000000000000000000000000000000000000000000000000000835263ffffffff89166004840152336024840152346044840152608060648401526084830190610890565b03818373ffffffffffffffffffffffffffffffffffffffff88165af160009181615204575b506151fe57505061145d6151cc6127ef565b6040519384937f7a7515d000000000000000000000000000000000000000000000000000000000855260048501612c72565b92509050565b6152229192503d806000833e61521a818361107e565b8101906150c9565b90386151ba565b61526960049167ffffffffffffffff19166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e42602052604060002090565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000815416815501805460005b8181106152a157505050565b600090835415612166576152be6020838660019552205485615cb9565b5001615295565b91600092816152d45750505050565b73ffffffffffffffffffffffffffffffffffffffff1690813b1561429f5791839161427993836040518096819582947f8a91b0e300000000000000000000000000000000000000000000000000000000845260206004850181815201916122b0565b60008281526001820160205260409020546120825780549068010000000000000000821015611079578261538b615374846001809601855584612e21565b81939154906000199060031b92831b921b19161790565b905580549260005201602052604060002055600190565b906153bb6153b08360a01c90565b65ffffffffffff1690565b65ffffffffffff811615615476575b6153d76153b08360a01c90565b65ffffffffffff811615615469575b65ffffffffffff90811691168181111561545e575060a01b915b73ffffffffffffffffffffffffffffffffffffffff806154236153b08460d01c90565b65ffffffffffff806154386153b08860d01c90565b16911681811015615453575060d01b935b1691161791171790565b60d01b905093615449565b60a01b905091615400565b5065ffffffffffff6153e6565b5065ffffffffffff6153ca565b6154906153b08260a01c90565b65ffffffffffff811615615585575b6154ac6153b08460a01c90565b65ffffffffffff811615615578575b65ffffffffffff90811691168181111561556d575060a01b915b600173ffffffffffffffffffffffffffffffffffffffff6154f96153b08560d01c90565b65ffffffffffff8061550e6153b08760d01c90565b16911681811015615562575060d01b935b1603615545575073ffffffffffffffffffffffffffffffffffffffff60015b1691171790565b73ffffffffffffffffffffffffffffffffffffffff80911661553e565b60d01b90509361551f565b60a01b9050916154d5565b5065ffffffffffff6154bb565b5065ffffffffffff61549f565b90813b156156815773ffffffffffffffffffffffffffffffffffffffff8216807fffffffffffffffffffffffff00000000000000000000000000000000000000007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5416177f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b600080a280511561564e5761335e91615d6a565b50503461565757565b7fb398979f0000000000000000000000000000000000000000000000000000000060005260046000fd5b73ffffffffffffffffffffffffffffffffffffffff827f4c9c8ce3000000000000000000000000000000000000000000000000000000006000521660045260246000fd5b6000906020926040517fffffffff00000000000000000000000000000000000000000000000000000000858201927f01ffc9a70000000000000000000000000000000000000000000000000000000084521660248201526024815261572b60448261107e565b5191617530fa6000513d8261574c575b5081615745575090565b9050151590565b6020111591503861573b565b7fffffffff00000000000000000000000000000000000000000000000000000000167f6d61fe7000000000000000000000000000000000000000000000000000000000811490811561595e575b8115615934575b811561590a575b81156158e0575b81156158b6575b811561588c575b8115615862575b8115615838575b811561580e575b81156157e7575090565b7f3d6bda320000000000000000000000000000000000000000000000000000000091501490565b7f2a3d428c00000000000000000000000000000000000000000000000000000000811491506157dd565b7fe7db7f7e00000000000000000000000000000000000000000000000000000000811491506157d6565b7f465d33e000000000000000000000000000000000000000000000000000000000811491506157cf565b7f0ab8785f00000000000000000000000000000000000000000000000000000000811491506157c8565b7f5d413a8100000000000000000000000000000000000000000000000000000000811491506157c1565b7fed6dfb1300000000000000000000000000000000000000000000000000000000811491506157ba565b7f01b2bdc600000000000000000000000000000000000000000000000000000000811491506157b3565b7f3499a2fd00000000000000000000000000000000000000000000000000000000811491506157ac565b7f8a91b0e300000000000000000000000000000000000000000000000000000000811491506157a5565b7fffffffff00000000000000000000000000000000000000000000000000000000167f2dd81133000000000000000000000000000000000000000000000000000000008114908115615a68575b8115615a3e575b8115615a14575b81156159ed575090565b7f7c627b210000000000000000000000000000000000000000000000000000000091501490565b7f52b7512c00000000000000000000000000000000000000000000000000000000811491506159e3565b7fae574a4300000000000000000000000000000000000000000000000000000000811491506159dc565b7f062a422b00000000000000000000000000000000000000000000000000000000811491506159d5565b7fffffffff0000000000000000000000000000000000000000000000000000000081167fb61d27f6000000000000000000000000000000000000000000000000000000008114908115615c38575b8115615c0e575b8115615be4575b8115615bbb575b8115615b91575b8115615b67575b5061207c57615b5f60ff917fffffffff00000000000000000000000000000000000000000000000000000000166000527f9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e41602052604060002090565b5460a81c1690565b7f4f1ef2860000000000000000000000000000000000000000000000000000000091501438615b03565b7fb6b1ccfe0000000000000000000000000000000000000000000000000000000081149150615afc565b7e14490e0000000000000000000000000000000000000000000000000000000081149150615af5565b7f443f5daa0000000000000000000000000000000000000000000000000000000081149150615aee565b7f51fd4e1e0000000000000000000000000000000000000000000000000000000081149150615ae7565b7f34fcd5be0000000000000000000000000000000000000000000000000000000081149150615ae0565b80548015615c8a576000190190615c798282612e21565b60001982549160031b1b1916905555565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b6001810191806000528260205260406000205492831515600014615d61576000198401848111612c50578354936000198501948511612c50576000958583615d1c97615d0d9503615d22575b505050615c62565b90600052602052604060002090565b55600190565b615d48615d4291615d396142c0615d589588612e21565b92839187612e21565b906140c0565b8590600052602052604060002090565b55388080615d05565b50505050600090565b60008061119f93602081519101845af4615d826127ef565b9190615dc25750805115615d9857805190602001fd5b7f1425ea420000000000000000000000000000000000000000000000000000000060005260046000fd5b81511580615e17575b615dd3575090565b73ffffffffffffffffffffffffffffffffffffffff907f9996b315000000000000000000000000000000000000000000000000000000006000521660045260246000fd5b50803b15615dcb56fea164736f6c634300081a000a9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e400000000000000000000000000000000071727de22e5e9d8baf0edac6f37da032", + "nonce": "0x7", + "chainId": "0x66eee" + }, + "additionalContracts": [], + "isFixedGasLimit": false + }, + { + "hash": "0xff7f87461cff12e51429198b1ba0790cf79df5386c3b0be587446dfb766b4b84", + "transactionType": "CREATE2", + "contractName": "SingleSignerValidation", + "contractAddress": "0x9da8c098a483e257dd96022831df308cb24fcbe6", + "function": null, + "arguments": null, + "transaction": { + "from": "0x7f89ed1f3f0d52d303904101305471bca3cde710", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0xe64afa", + "value": "0x0", + "input": "0x000000000000000000000000000000000000000000000000000000000000000060808060405234601557610fc5908161001b8239f35b600080fdfe6080604052600436101561001257600080fd5b60003560e01c806301b2bdc6146106a457806301ffc9a7146106035780630ab8785f146105ac5780630df0a05e146100955780633499a2fd14610401578063465d33e0146103115780634aeb3b03146102e75780636d61fe70146102715780638a91b0e31461019a578063e7db7f7e146101125763f1a7073e1461009557600080fd5b3461010d57604060031936011261010d576100ae61090f565b63ffffffff6100bb610946565b9116600052600060205273ffffffffffffffffffffffffffffffffffffffff60406000209116600052602052602073ffffffffffffffffffffffffffffffffffffffff60406000205416604051908152f35b600080fd5b3461010d5760a060031936011261010d5761012b610969565b610133610922565b9061013c61098c565b506084359067ffffffffffffffff821161010d576020926101646101709336906004016109af565b92909160643591610c2f565b7fffffffff0000000000000000000000000000000000000000000000000000000060405191168152f35b3461010d57602060031936011261010d5760043567ffffffffffffffff811161010d576101cd60209136906004016109af565b908092918101031261010d576101e763ffffffff91610935565b166000818152602081815260408083203380855290835281842080547fffffffffffffffffffffffff00000000000000000000000000000000000000008116909155825173ffffffffffffffffffffffffffffffffffffffff9091168152928301939093527f3819bcb8ad82b4cf05cf5cce33bd55aad1a15eb389cdd1cc832a99e7438c4a0791a3005b3461010d57602060031936011261010d5760043567ffffffffffffffff811161010d576102a460409136906004016109af565b908092918101031261010d5760206102bb82610935565b91013573ffffffffffffffffffffffffffffffffffffffff811680910361010d576102e591610e7e565b005b3461010d57604060031936011261010d576102e561030361090f565b61030b610946565b90610e7e565b3461010d5760c060031936011261010d5761032a610969565b610332610922565b9061033b61098c565b9160843567ffffffffffffffff811161010d5761035c9036906004016109af565b505060a4359067ffffffffffffffff821161010d5761038463ffffffff9236906004016109af565b505016600052600060205273ffffffffffffffffffffffffffffffffffffffff6040600020911660005260205273ffffffffffffffffffffffffffffffffffffffff80604060002054169116036103d757005b7fea8e4eb50000000000000000000000000000000000000000000000000000000060005260046000fd5b3461010d57600060031936011261010d5761041a610bff565b50610423610bff565b6040518091602082526080820190805191606060208501528251809152602060a0850193019060005b81811061055457505050602081015191601f198482030160408501526020808451928381520193019060005b8181106104ec575050506040015190601f198382030160608401526020808351928381520192019060005b8181106104b1575050500390f35b82517fffffffff00000000000000000000000000000000000000000000000000000000168452859450602093840193909201916001016104a3565b919450919260206080600192606088517fffffffff00000000000000000000000000000000000000000000000000000000815116835263ffffffff85820151168584015260408101511515604084015201511515606082015201950191019185949392610478565b919450919260206060600192604088517fffffffff000000000000000000000000000000000000000000000000000000008151168352848101511515858401520151151560408201520195019101918594939261044c565b3461010d57606060031936011261010d576105c561090f565b6024359067ffffffffffffffff821161010d57610120600319833603011261010d576020916105fb916044359160040190610abd565b604051908152f35b3461010d57602060031936011261010d576004357fffffffff00000000000000000000000000000000000000000000000000000000811680910361010d57807fd2db51a8000000000000000000000000000000000000000000000000000000006020921490811561067a575b506040519015158152f35b7f01ffc9a7000000000000000000000000000000000000000000000000000000009150148261066f565b3461010d57600060031936011261010d576106bd610a2f565b506107b16106c9610a2f565b604080516106d782826109dd565b601781527f53696e676c655369676e65722056616c69646174696f6e0000000000000000006020820152825280519161071082846109dd565b600583527f312e302e300000000000000000000000000000000000000000000000000000006020840152602081019283526107c4825161075084826109dd565b601081527f4552432d3639303020417574686f727300000000000000000000000000000000602082015283830190815283519586956020875261079f855160a060208a015260c08901906108ce565b9051601f1988830301878901526108ce565b9051601f198683030160608701526108ce565b91606082015192601f19858203016080860152835190818152602081016020808460051b8401019601936000925b8484106108675788808960808a015190601f198382030160a0840152815180825260208201916020808360051b8301019401926000915b8383106108365786860387f35b91939550919360208061085583601f19866001960301875289516108ce565b97019301930190928695949293610829565b919395966001919395985060206108bb82601f198684950301885286838d517fffffffff000000000000000000000000000000000000000000000000000000008151168452015191818582015201906108ce565b99019401940191889796959394916107f2565b919082519283825260005b8481106108fa575050601f19601f8460006020809697860101520116010190565b806020809284010151828286010152016108d9565b6004359063ffffffff8216820361010d57565b6024359063ffffffff8216820361010d57565b359063ffffffff8216820361010d57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361010d57565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361010d57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361010d57565b9181601f8401121561010d5782359167ffffffffffffffff831161010d576020838186019501011161010d57565b90601f601f19910116810190811067ffffffffffffffff821117610a0057604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040519060a0820182811067ffffffffffffffff821117610a0057604052606060808382815282602082015282604082015282808201520152565b67ffffffffffffffff8111610a0057601f01601f191660200190565b929192610a9282610a6a565b91610aa060405193846109dd565b82948184528183011161010d578281602093846000960137010152565b917f19457468657265756d205369676e6564204d6573736167653a0a333200000000600052601c52603c6000206101008201357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18336030181121561010d5782019081359167ffffffffffffffff831161010d5760200190823603821361010d57610b62610b689273ffffffffffffffffffffffffffffffffffffffff943691610a86565b90610e42565b505016908115928315610b87575b505050610b8257600090565b600190565b63ffffffff919293501660005260006020526040600020903573ffffffffffffffffffffffffffffffffffffffff8116810361010d5773ffffffffffffffffffffffffffffffffffffffff1660005260205273ffffffffffffffffffffffffffffffffffffffff604060002054161415388080610b76565b604051906060820182811067ffffffffffffffff821117610a005760405260606040838281528260208201520152565b63ffffffff610c8a9395949216600052600060205273ffffffffffffffffffffffffffffffffffffffff6040600020911660005260205273ffffffffffffffffffffffffffffffffffffffff60406000205416923691610a86565b91610c958382610e42565b506004819592951015610e1357159384610df3575b508315610d01575b505050610cdd577fffffffff0000000000000000000000000000000000000000000000000000000090565b7f1626ba7e0000000000000000000000000000000000000000000000000000000090565b6000935090610d53610d6185949360405192839160208301957f1626ba7e00000000000000000000000000000000000000000000000000000000875260248401526040604484015260648301906108ce565b03601f1981018352826109dd565b51915afa3d15610dec573d610d7581610a6a565b90610d8360405192836109dd565b81523d6000602083013e5b81610dde575b81610da3575b50388080610cb2565b905060208180518101031261010d57602001517f1626ba7e000000000000000000000000000000000000000000000000000000001438610d9a565b905060208151101590610d94565b6060610d8e565b73ffffffffffffffffffffffffffffffffffffffff168314935038610caa565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b8151919060418303610e7357610e6c92506020820151906060604084015193015160001a90610f16565b9192909190565b505060009160029190565b63ffffffff166000818152602081815260408083203380855290835292819020805473ffffffffffffffffffffffffffffffffffffffff9687167fffffffffffffffffffffffff00000000000000000000000000000000000000008216811790925582519616865291850191909152919290917f3819bcb8ad82b4cf05cf5cce33bd55aad1a15eb389cdd1cc832a99e7438c4a0791a3565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610fac579160209360809260ff60009560405194855216868401526040830152606082015282805260015afa15610fa05760005173ffffffffffffffffffffffffffffffffffffffff811615610f945790600090600090565b50600090600190600090565b6040513d6000823e3d90fd5b5050506000916003919056fea164736f6c634300081a000a", + "nonce": "0x8", + "chainId": "0x66eee" + }, + "additionalContracts": [], + "isFixedGasLimit": false + }, + { + "hash": "0x38a57d3b2c174b63b3aadd6f59536a8bca1f9837acc542a8d4aa5b16837a9eaf", + "transactionType": "CREATE2", + "contractName": "AccountFactory", + "contractAddress": "0x1c7ef41aa9896b74223a3956c7dde28f206e8b24", + "function": null, + "arguments": [ + "0x0000000071727De22E5E9d8BAf0edAc6f37da032", + "0x0809BF385117a43A322A4E31d459c0EcaA3B1A08", + "0x9DA8c098A483E257dd96022831DF308cB24fCBE6", + "0x7f89Ed1F3F0d52d303904101305471bca3cde710" + ], + "transaction": { + "from": "0x7f89ed1f3f0d52d303904101305471bca3cde710", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "gas": "0xf4c552", + "value": "0x0", + "input": "0x000000000000000000000000000000000000000000000000000000000000000061010080604052346101b85760808161131c803803809161002082856101bd565b8339810103126101b85780516001600160a01b03811681036101b8576020820151906001600160a01b0382168083036101b85761005f604085016101f6565b936001600160a01b0390610075906060016101f6565b169182156101a257600080546001600160a01b03198116851782556040519461012b9461013994909390926001600160a01b0316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09080a360c0526102a86100e160208201866101bd565b808552611074602086013960405190602082015260408082015260006060820152606081526101116080826101bd565b60405192839161012560208401809761020a565b9061020a565b03601f1981018352826101bd565b51902060a05260805260e052604051610e3e90816102368239608051818181610406015261094c015260a05181818161033f01526108dc015260c0518181816101f6015281816102b7015281816106df0152610830015260e05181818161052101526107e10152f35b631e4fbdf760e01b600052600060045260246000fd5b600080fd5b601f909101601f19168101906001600160401b038211908210176101e057604052565b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b03821682036101b857565b9081519160005b838110610222575050016000815290565b806020809284010151818501520161021156fe6080604052600436101561001257600080fd5b6000803560e01c806303c7e13114610970578063121ee5411461091f578063421a21e0146108bb5780635a627dbc1461080557806366ea05d0146107b4578063715018a6146107365780638da5cb5b1461070357806394430fa5146106b2578063afa63a101461031c578063bb9fe6bf14610286578063c23a5cea146101a3578063e35e5d84146101855763f2fde38b146100ac57600080fd5b346101825760206003193601126101825760043573ffffffffffffffffffffffffffffffffffffffff811680910361017e576100e6610b1a565b80156101525773ffffffffffffffffffffffffffffffffffffffff8254827fffffffffffffffffffffffff00000000000000000000000000000000000000008216178455167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08380a380f35b6024827f1e4fbdf700000000000000000000000000000000000000000000000000000000815280600452fd5b5080fd5b80fd5b5034610182578060031936011261018257602060405162093a808152f35b5034610182576020600319360112610182578060043573ffffffffffffffffffffffffffffffffffffffff8116809103610283576101df610b1a565b73ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001690813b1561027f5782916024839260405194859384927fc23a5cea00000000000000000000000000000000000000000000000000000000845260048401525af18015610274576102635750f35b8161026d916109d5565b6101825780f35b6040513d84823e3d90fd5b5050fd5b50fd5b503461018257806003193601126101825761029f610b1a565b8073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b15610283578180916004604051809481937fbb9fe6bf0000000000000000000000000000000000000000000000000000000083525af18015610274576102635750f35b50346101825761033861032e3661098c565b8293918193610a45565b91610364307f000000000000000000000000000000000000000000000000000000000000000085610b69565b92833b1561038e575b60208473ffffffffffffffffffffffffffffffffffffffff60405191168152f35b73ffffffffffffffffffffffffffffffffffffffff6040519263ffffffff85166020850152166040830152604082526103c86060836109d5565b6040516102a88082019082821067ffffffffffffffff831117610685576060918391610b8a833973ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001681526040602082015288604082015203019086f5156105e55773ffffffffffffffffffffffffffffffffffffffff831690602060405161046682826109d5565b86815260003681376040519261047c83856109d5565b878452843b1561068157949290918794926040519687957f3fa3f96900000000000000000000000000000000000000000000000000000000875266010000000000007fffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000006701000000000000006bffffffff000000000000000060848b019460401b167fffffffffffffffffffffffffffffffffffffffff0000000000000000000000007f000000000000000000000000000000000000000000000000000000000000000060601b161717161760048801526080602488015282518091528360a48801930190885b818110610645575050509061058391600319878303016044880152610abb565b906003198583030160648601528251908183528083019281808460051b83010195019388915b8483106105f0575050505050508383809203925af180156105e5576105d0575b808061036d565b6105db8380926109d5565b61017e57816105c9565b6040513d85823e3d90fd5b928096819295989a50600193969950807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08661063193030187528a51610abb565b9801930193018a97959293899795926105a9565b82517fffffffff000000000000000000000000000000000000000000000000000000001685528c99508a98509385019391850191600101610563565b8780fd5b6024887f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b5034610182578060031936011261018257602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461018257806003193601126101825773ffffffffffffffffffffffffffffffffffffffff6020915416604051908152f35b503461018257806003193601126101825761074f610b1a565b8073ffffffffffffffffffffffffffffffffffffffff81547fffffffffffffffffffffffff000000000000000000000000000000000000000081168355167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08280a380f35b5034610182578060031936011261018257602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b508060031936011261018257610819610b1a565b73ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001681813b156101825780602492604051938480927f0396cb6000000000000000000000000000000000000000000000000000000000825262093a80600483015234905af180156108ae576108a05780f35b6108a9916109d5565b388180f35b50604051903d90823e3d90fd5b50346101825760206109016108d86108d23661098c565b91610a45565b30907f000000000000000000000000000000000000000000000000000000000000000090610b69565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b5034610182578060031936011261018257602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346101825760206109846108d23661098c565b604051908152f35b60031960609101126109d05760043573ffffffffffffffffffffffffffffffffffffffff811681036109d057906024359060443563ffffffff811681036109d05790565b600080fd5b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610a1657604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b917fffffffff0000000000000000000000000000000000000000000000000000000090604051927fffffffffffffffffffffffffffffffffffffffff000000000000000000000000602085019560601b168552603484015260e01b16605482015260388152610ab56058826109d5565b51902090565b919082519283825260005b848110610b055750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8460006020809697860101520116010190565b80602080928401015182828601015201610ac6565b73ffffffffffffffffffffffffffffffffffffffff600054163303610b3b57565b7f118cdaa7000000000000000000000000000000000000000000000000000000006000523360045260246000fd5b90605592600b92604051926040840152602083015281520160ff8153209056fe60806040526102a88038038061001481610188565b928339810190604081830312610183578051906001600160a01b03821690818303610183576020810151906001600160401b038211610183570183601f820112156101835780519061006d610068836101c3565b610188565b94828652602083830101116101835760005b82811061016e575050602060009185010152813b1561015a577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b600080a28151156101415760008083602061012995519101845af43d15610139573d91610119610068846101c3565b9283523d6000602085013e6101de565b505b604051606890816102408239f35b6060916101de565b5050341561012b5763b398979f60e01b60005260046000fd5b634c9c8ce360e01b60005260045260246000fd5b8060208092840101518282890101520161007f565b600080fd5b6040519190601f01601f191682016001600160401b038111838210176101ad57604052565b634e487b7160e01b600052604160045260246000fd5b6001600160401b0381116101ad57601f01601f191660200190565b9061020457508051156101f357805190602001fd5b630a12f52160e11b60005260046000fd5b81511580610236575b610215575090565b639996b31560e01b60009081526001600160a01b0391909116600452602490fd5b50803b1561020d56fe608060405260008073ffffffffffffffffffffffffffffffffffffffff7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5416368280378136915af43d6000803e156056573d6000f35b3d6000fdfea164736f6c634300081a000aa164736f6c634300081a000a60806040526102a88038038061001481610188565b928339810190604081830312610183578051906001600160a01b03821690818303610183576020810151906001600160401b038211610183570183601f820112156101835780519061006d610068836101c3565b610188565b94828652602083830101116101835760005b82811061016e575050602060009185010152813b1561015a577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b600080a28151156101415760008083602061012995519101845af43d15610139573d91610119610068846101c3565b9283523d6000602085013e6101de565b505b604051606890816102408239f35b6060916101de565b5050341561012b5763b398979f60e01b60005260046000fd5b634c9c8ce360e01b60005260045260246000fd5b8060208092840101518282890101520161007f565b600080fd5b6040519190601f01601f191682016001600160401b038111838210176101ad57604052565b634e487b7160e01b600052604160045260246000fd5b6001600160401b0381116101ad57601f01601f191660200190565b9061020457508051156101f357805190602001fd5b630a12f52160e11b60005260046000fd5b81511580610236575b610215575090565b639996b31560e01b60009081526001600160a01b0391909116600452602490fd5b50803b1561020d56fe608060405260008073ffffffffffffffffffffffffffffffffffffffff7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5416368280378136915af43d6000803e156056573d6000f35b3d6000fdfea164736f6c634300081a000a0000000000000000000000000000000071727de22e5e9d8baf0edac6f37da0320000000000000000000000000809bf385117a43a322a4e31d459c0ecaa3b1a080000000000000000000000009da8c098a483e257dd96022831df308cb24fcbe60000000000000000000000007f89ed1f3f0d52d303904101305471bca3cde710", + "nonce": "0x9", + "chainId": "0x66eee" + }, + "additionalContracts": [], + "isFixedGasLimit": false + }, + { + "hash": "0xeac9bb033b7ea49e5da3fa029a92f4299e5c70ded305d08b91fc801712073120", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0x0000000071727de22e5e9d8baf0edac6f37da032", + "function": "addStake(uint32)", + "arguments": [ + "86400" + ], + "transaction": { + "from": "0x7f89ed1f3f0d52d303904101305471bca3cde710", + "to": "0x0000000071727de22e5e9d8baf0edac6f37da032", + "gas": "0x126022", + "value": "0x16345785d8a0000", + "input": "0x0396cb600000000000000000000000000000000000000000000000000000000000015180", + "nonce": "0xa", + "chainId": "0x66eee" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "status": "0x1", + "cumulativeGasUsed": "0x33d13fe", + "logs": [ + { + "address": "0x0809bf385117a43a322a4e31d459c0ecaa3b1a08", + "topics": [ + "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000000ff", + "blockHash": "0x2ce888a823b87bb6e33693e4018200e187c8c6fd0b33c758b3d279bc5fab37d5", + "blockNumber": "0x3fad9b9", + "transactionHash": "0xf77f7595a83b6517b0bca017c6fe29990665132ba5bc0fc2e5f7e760e15f3a9e", + "transactionIndex": "0x3", + "logIndex": "0xd", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0xf77f7595a83b6517b0bca017c6fe29990665132ba5bc0fc2e5f7e760e15f3a9e", + "transactionIndex": "0x3", + "blockHash": "0x2ce888a823b87bb6e33693e4018200e187c8c6fd0b33c758b3d279bc5fab37d5", + "blockNumber": "0x3fad9b9", + "gasUsed": "0x2f430a1", + "effectiveGasPrice": "0x5f5e100", + "from": "0x7f89ed1f3f0d52d303904101305471bca3cde710", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": null, + "gasUsedForL1": "0x2a432c0", + "l1BlockNumber": "0x61599b" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0x9d8792", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0xff7f87461cff12e51429198b1ba0790cf79df5386c3b0be587446dfb766b4b84", + "transactionIndex": "0x1", + "blockHash": "0x9ca68eaf1308f1dcdb8b066cf3b499a04ee1a3b87012f72c3648df2b6555d1df", + "blockNumber": "0x3fad9ba", + "gasUsed": "0x9d8792", + "effectiveGasPrice": "0x5f5e100", + "from": "0x7f89ed1f3f0d52d303904101305471bca3cde710", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": null, + "gasUsedForL1": "0x8f7914", + "l1BlockNumber": "0x61599b" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0xa7cd6c", + "logs": [ + { + "address": "0x1c7ef41aa9896b74223a3956c7dde28f206e8b24", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000007f89ed1f3f0d52d303904101305471bca3cde710" + ], + "data": "0x", + "blockHash": "0x32ae4caa6c30977dcf97fbb69e2fb51e533cf257f9ac418b7d3d6d94b963db70", + "blockNumber": "0x3fad9bc", + "transactionHash": "0x38a57d3b2c174b63b3aadd6f59536a8bca1f9837acc542a8d4aa5b16837a9eaf", + "transactionIndex": "0x1", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000001000000000000000000000000000000000100020000000000000000000800000000000000000000000000000000400000000000000000000000020400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000001000000000000000", + "type": "0x2", + "transactionHash": "0x38a57d3b2c174b63b3aadd6f59536a8bca1f9837acc542a8d4aa5b16837a9eaf", + "transactionIndex": "0x1", + "blockHash": "0x32ae4caa6c30977dcf97fbb69e2fb51e533cf257f9ac418b7d3d6d94b963db70", + "blockNumber": "0x3fad9bc", + "gasUsed": "0xa7cd6c", + "effectiveGasPrice": "0x5f5e100", + "from": "0x7f89ed1f3f0d52d303904101305471bca3cde710", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": null, + "gasUsedForL1": "0x9a5399", + "l1BlockNumber": "0x61599b" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0xad22f", + "logs": [ + { + "address": "0x0000000071727de22e5e9d8baf0edac6f37da032", + "topics": [ + "0xa5ae833d0bb1dcd632d98a8b70973e8516812898e19bf27b70071ebc8dc52c01", + "0x0000000000000000000000007f89ed1f3f0d52d303904101305471bca3cde710" + ], + "data": "0x000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000000015180", + "blockHash": "0x797cc9992c915b72e8934162a75584a41c09ae99d5a969f95247231c9b62265d", + "blockNumber": "0x3fad9bd", + "transactionHash": "0xeac9bb033b7ea49e5da3fa029a92f4299e5c70ded305d08b91fc801712073120", + "transactionIndex": "0x1", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000100000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000400000000000000000000040000000000000000000000000000100000000000000000000000000000000000200000000001000000000000000", + "type": "0x2", + "transactionHash": "0xeac9bb033b7ea49e5da3fa029a92f4299e5c70ded305d08b91fc801712073120", + "transactionIndex": "0x1", + "blockHash": "0x797cc9992c915b72e8934162a75584a41c09ae99d5a969f95247231c9b62265d", + "blockNumber": "0x3fad9bd", + "gasUsed": "0xad22f", + "effectiveGasPrice": "0x5f5e100", + "from": "0x7f89ed1f3f0d52d303904101305471bca3cde710", + "to": "0x0000000071727de22e5e9d8baf0edac6f37da032", + "contractAddress": null, + "gasUsedForL1": "0xa153a", + "l1BlockNumber": "0x61599b" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1722008916, + "chain": 421614, + "commit": "2bc08b0" +} \ No newline at end of file diff --git a/deployments/arb-sepolia.md b/deployments/arb-sepolia.md new file mode 100644 index 00000000..95d01b26 --- /dev/null +++ b/deployments/arb-sepolia.md @@ -0,0 +1,21 @@ +# Arbitrum Sepolia + +Chain ID: 421614 + +## AccountFactory + +| Version | Address | Explorer | Salt | +| -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ | ---- | +| v0.8.0-alpha.0 | `0x1c7EF41AA9896b74223a3956c7dDE28F206E8b24` | [explorer](https://sepolia.arbiscan.io/address/0x1c7EF41AA9896b74223a3956c7dDE28F206E8b24) | `0` | + +## UpgradeableModularAccount Implementation + +| Version | Address | Explorer | Salt | +| -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ | ---- | +| v0.8.0-alpha.0 | `0x0809BF385117a43A322A4E31d459c0EcaA3B1A08` | [explorer](https://sepolia.arbiscan.io/address/0x0809BF385117a43A322A4E31d459c0EcaA3B1A08) | `0` | + +## SingleSignerValidation + +| Version | Address | Explorer | Salt | +| -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ | ---- | +| v0.8.0-alpha.0 | `0x9DA8c098A483E257dd96022831DF308cB24fCBE6` | [explorer](https://sepolia.arbiscan.io/address/0x9DA8c098A483E257dd96022831DF308cB24fCBE6) | `0` | diff --git a/foundry.toml b/foundry.toml index 67d9eb7c..a1096640 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,21 +7,23 @@ libs = ['lib'] out = 'out' optimizer = true optimizer_runs = 200 -fs_permissions = [ - { access = "read", path = "./out-optimized" } -] +auto_detect_solc = false +bytecode_hash = "none" +auto_detect_remappings = false +fs_permissions = [{ access = "read", path = "./out-optimized" }] [fuzz] runs = 500 [invariant] -runs=500 +runs = 500 fail_on_revert = true depth = 10 [profile.optimized-build] via_ir = true test = 'src' +optimizer_runs = 20000 out = 'out-optimized' [profile.optimized-test] @@ -38,7 +40,7 @@ runs = 5000 depth = 32 [profile.deep.fuzz] -runs = 10000 +runs = 100000 [profile.deep.invariant] runs = 5000 @@ -47,13 +49,8 @@ depth = 32 [fmt] line_length = 115 wrap_comments = true - -[rpc_endpoints] -mainnet = "${RPC_URL_MAINNET}" -goerli = "${RPC_URL_GOERLI}" - -[etherscan] -mainnet = { key = "${ETHERSCAN_API_KEY}" } -goerli = { key = "${ETHERSCAN_API_KEY}" } +sort_imports = true +number_underscore = "thousands" +int_types = "long" # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/lib/modular-account-libs b/lib/modular-account-libs new file mode 160000 index 00000000..5d9d0e40 --- /dev/null +++ b/lib/modular-account-libs @@ -0,0 +1 @@ +Subproject commit 5d9d0e403332251045eee2954c2a8b7ea0bae953 diff --git a/lib/solady b/lib/solady new file mode 160000 index 00000000..753c57ba --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 753c57bad8912defb609fec6d2342f8396e6a5ac diff --git a/package.json b/package.json index a9cc7c23..1c1540e7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ }, "scripts": { "lint": "pnpm lint:src && pnpm lint:test", - "lint:src": "solhint -c .solhint-src.json './src/**/*.sol'", - "lint:test": "solhint -c .solhint-test.json './test/**/*.sol'" + "lint:src": "solhint --max-warnings 0 -c .solhint-src.json './src/**/*.sol'", + "lint:test": "solhint --max-warnings 0 -c .solhint-test.json './test/**/*.sol'" } } diff --git a/remappings.txt b/remappings.txt index 3d0ee0df..8d9639cf 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,3 +2,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ @eth-infinitism/account-abstraction/=lib/account-abstraction/contracts/ @openzeppelin/=lib/openzeppelin-contracts/ +@modular-account-libs/=lib/modular-account-libs/src/ +solady=lib/solady/src/ diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 00000000..042a0ab8 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/Test.sol"; + +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +import {AccountFactory} from "../src/account/AccountFactory.sol"; +import {UpgradeableModularAccount} from "../src/account/UpgradeableModularAccount.sol"; +import {SingleSignerValidation} from "../src/modules/validation/SingleSignerValidation.sol"; + +contract DeployScript is Script { + IEntryPoint public entryPoint = IEntryPoint(payable(vm.envAddress("ENTRYPOINT"))); + + address public owner = vm.envAddress("OWNER"); + + address public accountImpl = vm.envOr("ACCOUNT_IMPL", address(0)); + address public factory = vm.envOr("FACTORY", address(0)); + address public singleSignerValidation = vm.envOr("SINGLE_SIGNER_VALIDATION", address(0)); + + bytes32 public accountImplSalt = bytes32(vm.envOr("ACCOUNT_IMPL_SALT", uint256(0))); + bytes32 public factorySalt = bytes32(vm.envOr("FACTORY_SALT", uint256(0))); + bytes32 public singleSignerValidationSalt = bytes32(vm.envOr("SINGLE_SIGNER_VALIDATION_SALT", uint256(0))); + + uint256 public requiredStakeAmount = vm.envOr("STAKE_AMOUNT", uint256(0.1 ether)); + uint256 public requiredUnstakeDelay = vm.envOr("UNSTAKE_DELAY", uint256(1 days)); + + function run() public { + console2.log("******** Deploying ERC-6900 Reference Implementation ********"); + console2.log("Chain: ", block.chainid); + console2.log("EP: ", address(entryPoint)); + console2.log("Factory owner: ", owner); + + vm.startBroadcast(); + _deployAccountImpl(accountImplSalt, accountImpl); + _deploySingleSignerValidation(singleSignerValidationSalt, singleSignerValidation); + _deployAccountFactory(factorySalt, factory); + _addStakeForFactory(uint32(requiredUnstakeDelay), requiredStakeAmount); + vm.stopBroadcast(); + } + + function _deployAccountImpl(bytes32 salt, address expected) internal { + console2.log(string.concat("Deploying AccountImpl with salt: ", vm.toString(salt))); + + address addr = Create2.computeAddress( + salt, + keccak256(abi.encodePacked(type(UpgradeableModularAccount).creationCode, abi.encode(entryPoint))), + CREATE2_FACTORY + ); + if (addr != expected) { + console2.log("Expected address mismatch"); + console2.log("Expected: ", expected); + console2.log("Actual: ", addr); + revert(); + } + + if (addr.code.length == 0) { + console2.log("No code found at expected address, deploying..."); + UpgradeableModularAccount deployed = new UpgradeableModularAccount{salt: salt}(entryPoint); + + if (address(deployed) != expected) { + console2.log("Deployed address mismatch"); + console2.log("Expected: ", expected); + console2.log("Deployed: ", address(deployed)); + revert(); + } + + console2.log("Deployed AccountImpl at: ", address(deployed)); + } else { + console2.log("Code found at expected address, skipping deployment"); + } + } + + function _deploySingleSignerValidation(bytes32 salt, address expected) internal { + console2.log(string.concat("Deploying SingleSignerValidation with salt: ", vm.toString(salt))); + + address addr = Create2.computeAddress( + salt, keccak256(abi.encodePacked(type(SingleSignerValidation).creationCode)), CREATE2_FACTORY + ); + if (addr != expected) { + console2.log("Expected address mismatch"); + console2.log("Expected: ", expected); + console2.log("Actual: ", addr); + revert(); + } + + if (addr.code.length == 0) { + console2.log("No code found at expected address, deploying..."); + SingleSignerValidation deployed = new SingleSignerValidation{salt: salt}(); + + if (address(deployed) != expected) { + console2.log("Deployed address mismatch"); + console2.log("Expected: ", expected); + console2.log("Deployed: ", address(deployed)); + revert(); + } + + console2.log("Deployed SingleSignerValidation at: ", address(deployed)); + } else { + console2.log("Code found at expected address, skipping deployment"); + } + } + + function _deployAccountFactory(bytes32 salt, address expected) internal { + console2.log(string.concat("Deploying AccountFactory with salt: ", vm.toString(salt))); + + address addr = Create2.computeAddress( + salt, + keccak256( + abi.encodePacked( + type(AccountFactory).creationCode, + abi.encode(entryPoint, accountImpl, singleSignerValidation, owner) + ) + ), + CREATE2_FACTORY + ); + if (addr != expected) { + console2.log("Expected address mismatch"); + console2.log("Expected: ", expected); + console2.log("Actual: ", addr); + revert(); + } + + if (addr.code.length == 0) { + console2.log("No code found at expected address, deploying..."); + AccountFactory deployed = new AccountFactory{salt: salt}( + entryPoint, UpgradeableModularAccount(payable(accountImpl)), singleSignerValidation, owner + ); + + if (address(deployed) != expected) { + console2.log("Deployed address mismatch"); + console2.log("Expected: ", expected); + console2.log("Deployed: ", address(deployed)); + revert(); + } + + console2.log("Deployed AccountFactory at: ", address(deployed)); + } else { + console2.log("Code found at expected address, skipping deployment"); + } + } + + function _addStakeForFactory(uint32 unstakeDelay, uint256 stakeAmount) internal { + console2.log("Adding stake to factory"); + + uint256 currentStake = entryPoint.getDepositInfo(address(factory)).stake; + console2.log("Current stake: ", currentStake); + uint256 stakeToAdd = stakeAmount - currentStake; + + if (stakeToAdd > 0) { + console2.log("Adding stake: ", stakeToAdd); + entryPoint.addStake{value: stakeToAdd}(unstakeDelay); + console2.log("Staked factory: ", address(factory)); + console2.log("Total stake amount: ", entryPoint.getDepositInfo(address(factory)).stake); + console2.log("Unstake delay: ", entryPoint.getDepositInfo(address(factory)).unstakeDelaySec); + } else { + console2.log("No stake to add"); + } + } +} diff --git a/src/account/AccountFactory.sol b/src/account/AccountFactory.sol new file mode 100644 index 00000000..9dc1aa43 --- /dev/null +++ b/src/account/AccountFactory.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {UpgradeableModularAccount} from "../account/UpgradeableModularAccount.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; + +contract AccountFactory is Ownable { + UpgradeableModularAccount public immutable ACCOUNT_IMPL; + uint32 public constant UNSTAKE_DELAY = 1 weeks; + IEntryPoint public immutable ENTRY_POINT; + address public immutable SINGLE_SIGNER_VALIDATION; + + constructor( + IEntryPoint _entryPoint, + UpgradeableModularAccount _accountImpl, + address _singleSignerValidation, + address owner + ) Ownable(owner) { + ENTRY_POINT = _entryPoint; + ACCOUNT_IMPL = _accountImpl; + SINGLE_SIGNER_VALIDATION = _singleSignerValidation; + } + + /** + * Create an account, and return its address. + * Returns the address even if the account is already deployed. + * Note that during user operation execution, this method is called only if the account is not deployed. + * This method returns an existing account address so that entryPoint.getSenderAddress() would work even after + * account creation + */ + function createAccount(address owner, uint256 salt, uint32 entityId) + external + returns (UpgradeableModularAccount) + { + bytes32 fullSalt = getSalt(owner, salt, entityId); + bytes memory immutables = _getImmutableArgs(owner, entityId); + + address addr = getAddress(immutables, fullSalt); + // short circuit if exists + if (addr.code.length == 0) { + LibClone.createDeterministicERC1967(address(ACCOUNT_IMPL), immutables, fullSalt); + } + + return UpgradeableModularAccount(payable(addr)); + } + + function addStake() external payable onlyOwner { + ENTRY_POINT.addStake{value: msg.value}(UNSTAKE_DELAY); + } + + function unlockStake() external onlyOwner { + ENTRY_POINT.unlockStake(); + } + + function withdrawStake(address payable withdrawAddress) external onlyOwner { + ENTRY_POINT.withdrawStake(withdrawAddress); + } + + /** + * calculate the counterfactual address of this account as it would be returned by createAccount() + */ + function getAddress(bytes memory immutables, bytes32 salt) public view returns (address) { + return LibClone.predictDeterministicAddressERC1967(address(ACCOUNT_IMPL), immutables, salt, address(this)); + } + + function getSalt(address owner, uint256 salt, uint32 entityId) public pure returns (bytes32) { + return keccak256(abi.encodePacked(owner, salt, entityId)); + } + + function _getImmutableArgs(address owner, uint32 entityId) private view returns (bytes memory) { + return abi.encodePacked(ModuleEntityLib.pack(address(SINGLE_SIGNER_VALIDATION), entityId), owner); + } +} diff --git a/src/account/AccountLoupe.sol b/src/account/AccountLoupe.sol index d652f45c..110e7e72 100644 --- a/src/account/AccountLoupe.sol +++ b/src/account/AccountLoupe.sol @@ -2,34 +2,37 @@ pragma solidity ^0.8.25; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {IAccountLoupe, ExecutionHook} from "../interfaces/IAccountLoupe.sol"; -import {FunctionReference, IPluginManager} from "../interfaces/IPluginManager.sol"; +import {HookConfigLib} from "../helpers/HookConfigLib.sol"; +import {ExecutionHook, IAccountLoupe} from "../interfaces/IAccountLoupe.sol"; +import {HookConfig, IModuleManager, ModuleEntity} from "../interfaces/IModuleManager.sol"; import {IStandardExecutor} from "../interfaces/IStandardExecutor.sol"; -import {getAccountStorage, toExecutionHook, toSelector} from "./AccountStorage.sol"; +import {getAccountStorage, toHookConfig, toSelector} from "./AccountStorage.sol"; abstract contract AccountLoupe is IAccountLoupe { using EnumerableSet for EnumerableSet.Bytes32Set; using EnumerableMap for EnumerableMap.AddressToUintMap; + using HookConfigLib for HookConfig; /// @inheritdoc IAccountLoupe - function getExecutionFunctionHandler(bytes4 selector) external view override returns (address plugin) { + function getExecutionFunctionHandler(bytes4 selector) external view override returns (address module) { if ( selector == IStandardExecutor.execute.selector || selector == IStandardExecutor.executeBatch.selector || selector == UUPSUpgradeable.upgradeToAndCall.selector - || selector == IPluginManager.installPlugin.selector - || selector == IPluginManager.uninstallPlugin.selector + || selector == IModuleManager.installExecution.selector + || selector == IModuleManager.uninstallExecution.selector ) { return address(this); } - return getAccountStorage().selectorData[selector].plugin; + return getAccountStorage().selectorData[selector].module; } /// @inheritdoc IAccountLoupe - function getSelectors(FunctionReference validationFunction) external view returns (bytes4[] memory) { + function getSelectors(ModuleEntity validationFunction) external view returns (bytes4[] memory) { uint256 length = getAccountStorage().validationData[validationFunction].selectors.length(); bytes4[] memory selectors = new bytes4[](length); @@ -55,13 +58,17 @@ abstract contract AccountLoupe is IAccountLoupe { for (uint256 i = 0; i < executionHooksLength; ++i) { bytes32 key = hooks.at(i); - ExecutionHook memory execHook = execHooks[i]; - (execHook.hookFunction, execHook.isPreHook, execHook.isPostHook) = toExecutionHook(key); + HookConfig hookConfig = toHookConfig(key); + execHooks[i] = ExecutionHook({ + hookFunction: hookConfig.moduleEntity(), + isPreHook: hookConfig.hasPreHook(), + isPostHook: hookConfig.hasPostHook() + }); } } /// @inheritdoc IAccountLoupe - function getPermissionHooks(FunctionReference validationFunction) + function getPermissionHooks(ModuleEntity validationFunction) external view override @@ -73,23 +80,22 @@ abstract contract AccountLoupe is IAccountLoupe { permissionHooks = new ExecutionHook[](executionHooksLength); for (uint256 i = 0; i < executionHooksLength; ++i) { bytes32 key = hooks.at(i); - ExecutionHook memory execHook = permissionHooks[i]; - (execHook.hookFunction, execHook.isPreHook, execHook.isPostHook) = toExecutionHook(key); + HookConfig hookConfig = toHookConfig(key); + permissionHooks[i] = ExecutionHook({ + hookFunction: hookConfig.moduleEntity(), + isPreHook: hookConfig.hasPreHook(), + isPostHook: hookConfig.hasPostHook() + }); } } /// @inheritdoc IAccountLoupe - function getPreValidationHooks(FunctionReference validationFunction) + function getPreValidationHooks(ModuleEntity validationFunction) external view override - returns (FunctionReference[] memory preValidationHooks) + returns (ModuleEntity[] memory preValidationHooks) { preValidationHooks = getAccountStorage().validationData[validationFunction].preValidationHooks; } - - /// @inheritdoc IAccountLoupe - function getInstalledPlugins() external view override returns (address[] memory pluginAddresses) { - pluginAddresses = getAccountStorage().pluginManifestHashes.keys(); - } } diff --git a/src/account/AccountStorage.sol b/src/account/AccountStorage.sol index 242ddff8..25809742 100644 --- a/src/account/AccountStorage.sol +++ b/src/account/AccountStorage.sol @@ -2,20 +2,19 @@ pragma solidity ^0.8.25; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; -import {ExecutionHook} from "../interfaces/IAccountLoupe.sol"; -import {FunctionReference} from "../interfaces/IPluginManager.sol"; +import {HookConfig, ModuleEntity} from "../interfaces/IModuleManager.sol"; // bytes = keccak256("ERC6900.UpgradeableModularAccount.Storage") bytes32 constant _ACCOUNT_STORAGE_SLOT = 0x9f09680beaa4e5c9f38841db2460c401499164f368baef687948c315d9073e40; // Represents data associated with a specifc function selector. struct SelectorData { - // The plugin that implements this execution function. + // The module that implements this execution function. // If this is a native function, the address must remain address(0). - address plugin; - // Whether or not the function needs runtime validation, or can be called by anyone. + address module; + // Whether or not the function needs runtime validation, or can be called by anyone. The function can still be + // state changing if this flag is set to true. // Note that even if this is set to true, user op validation will still be required, otherwise anyone could // drain the account of native tokens by wasting gas. bool isPublic; @@ -30,8 +29,10 @@ struct ValidationData { bool isGlobal; // Whether or not this validation is a signature validator. bool isSignatureValidation; - // The pre validation hooks for this function selector. - FunctionReference[] preValidationHooks; + // Whether, in the case this is an appended bytecode validation, the validation is disabled + bool isAppendedBytecodeValidationDisabled; + // The pre validation hooks for this validation function. + ModuleEntity[] preValidationHooks; // Permission hooks for this validation function. EnumerableSet.Bytes32Set permissionHooks; // The set of selectors that may be validated by this validation function. @@ -42,11 +43,9 @@ struct AccountStorage { // AccountStorageInitializable variables uint8 initialized; bool initializing; - // Plugin metadata storage - EnumerableMap.AddressToUintMap pluginManifestHashes; // Execution functions and their associated functions mapping(bytes4 => SelectorData) selectorData; - mapping(FunctionReference validationFunction => ValidationData) validationData; + mapping(ModuleEntity validationFunction => ValidationData) validationData; // For ERC165 introspection mapping(bytes4 => uint256) supportedIfaces; } @@ -59,32 +58,25 @@ function getAccountStorage() pure returns (AccountStorage storage _storage) { using EnumerableSet for EnumerableSet.Bytes32Set; -function toSetValue(FunctionReference functionReference) pure returns (bytes32) { - return bytes32(FunctionReference.unwrap(functionReference)); +function toSetValue(ModuleEntity moduleEntity) pure returns (bytes32) { + return bytes32(ModuleEntity.unwrap(moduleEntity)); } -function toFunctionReference(bytes32 setValue) pure returns (FunctionReference) { - return FunctionReference.wrap(bytes21(setValue)); +function toModuleEntity(bytes32 setValue) pure returns (ModuleEntity) { + return ModuleEntity.wrap(bytes24(setValue)); } // ExecutionHook layout: -// 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF______________________ Hook Function Reference -// 0x__________________________________________AA____________________ is pre hook -// 0x____________________________________________BB__________________ is post hook - -function toSetValue(ExecutionHook memory executionHook) pure returns (bytes32) { - return bytes32(FunctionReference.unwrap(executionHook.hookFunction)) - | bytes32(executionHook.isPreHook ? uint256(1) << 80 : 0) - | bytes32(executionHook.isPostHook ? uint256(1) << 72 : 0); +// 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF______________________ Hook Module Entity +// 0x________________________________________________AA____________________ is pre hook +// 0x__________________________________________________BB__________________ is post hook + +function toSetValue(HookConfig hookConfig) pure returns (bytes32) { + return bytes32(HookConfig.unwrap(hookConfig)); } -function toExecutionHook(bytes32 setValue) - pure - returns (FunctionReference hookFunction, bool isPreHook, bool isPostHook) -{ - hookFunction = FunctionReference.wrap(bytes21(setValue)); - isPreHook = (uint256(setValue) >> 80) & 0xFF == 1; - isPostHook = (uint256(setValue) >> 72) & 0xFF == 1; +function toHookConfig(bytes32 setValue) pure returns (HookConfig) { + return HookConfig.wrap(bytes26(setValue)); } function toSetValue(bytes4 selector) pure returns (bytes32) { @@ -96,15 +88,12 @@ function toSelector(bytes32 setValue) pure returns (bytes4) { } /// @dev Helper function to get all elements of a set into memory. -function toFunctionReferenceArray(EnumerableSet.Bytes32Set storage set) - view - returns (FunctionReference[] memory) -{ +function toModuleEntityArray(EnumerableSet.Bytes32Set storage set) view returns (ModuleEntity[] memory) { uint256 length = set.length(); - FunctionReference[] memory result = new FunctionReference[](length); + ModuleEntity[] memory result = new ModuleEntity[](length); for (uint256 i = 0; i < length; ++i) { bytes32 key = set.at(i); - result[i] = FunctionReference.wrap(bytes21(key)); + result[i] = ModuleEntity.wrap(bytes24(key)); } return result; } diff --git a/src/account/ModuleManagerInternals.sol b/src/account/ModuleManagerInternals.sol new file mode 100644 index 00000000..c064d1a9 --- /dev/null +++ b/src/account/ModuleManagerInternals.sol @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {MAX_PRE_VALIDATION_HOOKS} from "../helpers/Constants.sol"; +import {HookConfigLib} from "../helpers/HookConfigLib.sol"; +import {KnownSelectors} from "../helpers/KnownSelectors.sol"; +import {ModuleEntityLib} from "../helpers/ModuleEntityLib.sol"; +import {ValidationConfigLib} from "../helpers/ValidationConfigLib.sol"; +import {ExecutionManifest, ManifestExecutionHook} from "../interfaces/IExecution.sol"; +import {IModule} from "../interfaces/IModule.sol"; +import {HookConfig, IModuleManager, ModuleEntity, ValidationConfig} from "../interfaces/IModuleManager.sol"; +import { + AccountStorage, + SelectorData, + ValidationData, + getAccountStorage, + toModuleEntity, + toSetValue +} from "./AccountStorage.sol"; + +abstract contract ModuleManagerInternals is IModuleManager { + using EnumerableSet for EnumerableSet.Bytes32Set; + using ModuleEntityLib for ModuleEntity; + using ValidationConfigLib for ValidationConfig; + using HookConfigLib for HookConfig; + + error ArrayLengthMismatch(); + error Erc4337FunctionNotAllowed(bytes4 selector); + error ExecutionFunctionAlreadySet(bytes4 selector); + error IModuleFunctionNotAllowed(bytes4 selector); + error NativeFunctionNotAllowed(bytes4 selector); + error NullModule(); + error PermissionAlreadySet(ModuleEntity validationFunction, HookConfig hookConfig); + error ModuleInstallCallbackFailed(address module, bytes revertReason); + error ModuleInterfaceNotSupported(address module); + error ModuleNotInstalled(address module); + error PreValidationHookLimitExceeded(); + error ValidationAlreadySet(bytes4 selector, ModuleEntity validationFunction); + + // Storage update operations + + function _setExecutionFunction(bytes4 selector, bool isPublic, bool allowGlobalValidation, address module) + internal + { + SelectorData storage _selectorData = getAccountStorage().selectorData[selector]; + + if (_selectorData.module != address(0)) { + revert ExecutionFunctionAlreadySet(selector); + } + + // Make sure incoming execution function does not collide with any native functions (data are stored on the + // account implementation contract) + if (KnownSelectors.isNativeFunction(selector)) { + revert NativeFunctionNotAllowed(selector); + } + + // Make sure incoming execution function is not a function in IModule + if (KnownSelectors.isIModuleFunction(selector)) { + revert IModuleFunctionNotAllowed(selector); + } + + // Also make sure it doesn't collide with functions defined by ERC-4337 + // and called by the entry point. This prevents a malicious module from + // sneaking in a function with the same selector as e.g. + // `validatePaymasterUserOp` and turning the account into their own + // personal paymaster. + if (KnownSelectors.isErc4337Function(selector)) { + revert Erc4337FunctionNotAllowed(selector); + } + + _selectorData.module = module; + _selectorData.isPublic = isPublic; + _selectorData.allowGlobalValidation = allowGlobalValidation; + } + + function _removeExecutionFunction(bytes4 selector) internal { + SelectorData storage _selectorData = getAccountStorage().selectorData[selector]; + + _selectorData.module = address(0); + _selectorData.isPublic = false; + _selectorData.allowGlobalValidation = false; + } + + function _addValidationFunction(ValidationConfig validationConfig, bytes4[] memory selectors) internal { + ValidationData storage _validationData = + getAccountStorage().validationData[validationConfig.moduleEntity()]; + + if (validationConfig.isGlobal()) { + _validationData.isGlobal = true; + } + + if (validationConfig.isSignatureValidation()) { + _validationData.isSignatureValidation = true; + } + + // Add the validation function to the selectors. + uint256 length = selectors.length; + for (uint256 i = 0; i < length; ++i) { + _validationData.selectors.add(toSetValue(selectors[i])); + } + } + + function _removeValidationFunction(ModuleEntity validationFunction) internal { + ValidationData storage _validationData = getAccountStorage().validationData[validationFunction]; + + _validationData.isGlobal = false; + _validationData.isSignatureValidation = false; + + // Clear the selectors + uint256 length = _validationData.selectors.length(); + for (uint256 i = 0; i < length; ++i) { + _validationData.selectors.remove(_validationData.selectors.at(0)); + } + } + + function _addExecHooks(EnumerableSet.Bytes32Set storage hooks, HookConfig hookConfig) internal { + hooks.add(toSetValue(hookConfig)); + } + + function _removeExecHooks(EnumerableSet.Bytes32Set storage hooks, HookConfig hookConfig) internal { + hooks.remove(toSetValue(hookConfig)); + } + + function _installExecution(address module, ExecutionManifest calldata manifest, bytes memory moduleInstallData) + internal + { + AccountStorage storage _storage = getAccountStorage(); + + if (module == address(0)) { + revert NullModule(); + } + + // TODO: do we need this check? Or switch to a non-165 checking function? + // Check that the module supports the IModule interface. + if (!ERC165Checker.supportsInterface(module, type(IModule).interfaceId)) { + revert ModuleInterfaceNotSupported(module); + } + + // Update components according to the manifest. + uint256 length = manifest.executionFunctions.length; + for (uint256 i = 0; i < length; ++i) { + bytes4 selector = manifest.executionFunctions[i].executionSelector; + bool isPublic = manifest.executionFunctions[i].isPublic; + bool allowGlobalValidation = manifest.executionFunctions[i].allowGlobalValidation; + _setExecutionFunction(selector, isPublic, allowGlobalValidation, module); + } + + length = manifest.executionHooks.length; + for (uint256 i = 0; i < length; ++i) { + ManifestExecutionHook memory mh = manifest.executionHooks[i]; + EnumerableSet.Bytes32Set storage execHooks = _storage.selectorData[mh.executionSelector].executionHooks; + HookConfig hookConfig = HookConfigLib.packExecHook({ + _module: module, + _entityId: mh.entityId, + _hasPre: mh.isPreHook, + _hasPost: mh.isPostHook + }); + _addExecHooks(execHooks, hookConfig); + } + + length = manifest.interfaceIds.length; + for (uint256 i = 0; i < length; ++i) { + _storage.supportedIfaces[manifest.interfaceIds[i]] += 1; + } + + // Initialize the module storage for the account. + // solhint-disable-next-line no-empty-blocks + try IModule(module).onInstall(moduleInstallData) {} + catch (bytes memory revertReason) { + revert ModuleInstallCallbackFailed(module, revertReason); + } + + emit ModuleInstalled(module); + } + + function _uninstallExecution(address module, ExecutionManifest calldata manifest, bytes memory uninstallData) + internal + { + AccountStorage storage _storage = getAccountStorage(); + + // Remove components according to the manifest, in reverse order (by component type) of their installation. + + uint256 length = manifest.executionHooks.length; + for (uint256 i = 0; i < length; ++i) { + ManifestExecutionHook memory mh = manifest.executionHooks[i]; + EnumerableSet.Bytes32Set storage execHooks = _storage.selectorData[mh.executionSelector].executionHooks; + HookConfig hookConfig = HookConfigLib.packExecHook({ + _module: module, + _entityId: mh.entityId, + _hasPre: mh.isPreHook, + _hasPost: mh.isPostHook + }); + _removeExecHooks(execHooks, hookConfig); + } + + length = manifest.executionFunctions.length; + for (uint256 i = 0; i < length; ++i) { + bytes4 selector = manifest.executionFunctions[i].executionSelector; + _removeExecutionFunction(selector); + } + + length = manifest.interfaceIds.length; + for (uint256 i = 0; i < length; ++i) { + _storage.supportedIfaces[manifest.interfaceIds[i]] -= 1; + } + + // Clear the module storage for the account. + bool onUninstallSuccess = true; + // solhint-disable-next-line no-empty-blocks + try IModule(module).onUninstall(uninstallData) {} + catch { + onUninstallSuccess = false; + } + + emit ModuleUninstalled(module, onUninstallSuccess); + } + + function _onInstall(address module, bytes calldata data) internal { + if (data.length > 0) { + IModule(module).onInstall(data); + } + } + + function _onUninstall(address module, bytes calldata data) internal { + if (data.length > 0) { + IModule(module).onUninstall(data); + } + } + + function _installValidation( + ValidationConfig validationConfig, + bytes4[] calldata selectors, + bytes calldata installData, + bytes[] calldata hooks + ) internal { + ValidationData storage _validationData = + getAccountStorage().validationData[validationConfig.moduleEntity()]; + + for (uint256 i = 0; i < hooks.length; ++i) { + HookConfig hookConfig = HookConfig.wrap(bytes26(hooks[i][:26])); + bytes calldata hookData = hooks[i][26:]; + + if (hookConfig.isValidationHook()) { + _validationData.preValidationHooks.push(hookConfig.moduleEntity()); + + // Avoid collision between reserved index and actual indices + if (_validationData.preValidationHooks.length > MAX_PRE_VALIDATION_HOOKS) { + revert PreValidationHookLimitExceeded(); + } + } // Hook is an execution hook + else if (!_validationData.permissionHooks.add(toSetValue(hookConfig))) { + revert PermissionAlreadySet(validationConfig.moduleEntity(), hookConfig); + } + + _onInstall(hookConfig.module(), hookData); + } + + for (uint256 i = 0; i < selectors.length; ++i) { + bytes4 selector = selectors[i]; + if (!_validationData.selectors.add(toSetValue(selector))) { + revert ValidationAlreadySet(selector, validationConfig.moduleEntity()); + } + } + + _validationData.isGlobal = validationConfig.isGlobal(); + _validationData.isSignatureValidation = validationConfig.isSignatureValidation(); + + _onInstall(validationConfig.module(), installData); + } + + function _uninstallValidation( + ModuleEntity validationFunction, + bytes calldata uninstallData, + bytes[] calldata hookUninstallDatas + ) internal { + ValidationData storage _validationData = getAccountStorage().validationData[validationFunction]; + + _removeValidationFunction(validationFunction); + + // Send `onUninstall` to hooks + if (hookUninstallDatas.length > 0) { + // If any uninstall data is provided, assert it is of the correct length. + if ( + hookUninstallDatas.length + != _validationData.preValidationHooks.length + _validationData.permissionHooks.length() + ) { + revert ArrayLengthMismatch(); + } + + // Hook uninstall data is provided in the order of pre-validation hooks, then permission hooks. + uint256 hookIndex = 0; + for (uint256 i = 0; i < _validationData.preValidationHooks.length; ++i) { + bytes calldata hookData = hookUninstallDatas[hookIndex]; + (address hookModule,) = ModuleEntityLib.unpack(_validationData.preValidationHooks[i]); + _onUninstall(hookModule, hookData); + hookIndex++; + } + + for (uint256 i = 0; i < _validationData.permissionHooks.length(); ++i) { + bytes calldata hookData = hookUninstallDatas[hookIndex]; + (address hookModule,) = + ModuleEntityLib.unpack(toModuleEntity(_validationData.permissionHooks.at(i))); + _onUninstall(hookModule, hookData); + hookIndex++; + } + } + + // Clear all stored hooks + delete _validationData.preValidationHooks; + + EnumerableSet.Bytes32Set storage permissionHooks = _validationData.permissionHooks; + uint256 permissionHookLen = permissionHooks.length(); + for (uint256 i = 0; i < permissionHookLen; ++i) { + bytes32 permissionHook = permissionHooks.at(0); + permissionHooks.remove(permissionHook); + } + + // Clear selectors + uint256 selectorLen = _validationData.selectors.length(); + for (uint256 i = 0; i < selectorLen; ++i) { + bytes32 selectorSetValue = _validationData.selectors.at(0); + _validationData.selectors.remove(selectorSetValue); + } + + (address module,) = ModuleEntityLib.unpack(validationFunction); + _onUninstall(module, uninstallData); + } +} diff --git a/src/account/PluginManager2.sol b/src/account/PluginManager2.sol deleted file mode 100644 index 28eb0ecf..00000000 --- a/src/account/PluginManager2.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.25; - -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - -import {IPlugin} from "../interfaces/IPlugin.sol"; -import {FunctionReference, ValidationConfig} from "../interfaces/IPluginManager.sol"; -import {FunctionReferenceLib} from "../helpers/FunctionReferenceLib.sol"; -import {ValidationConfigLib} from "../helpers/ValidationConfigLib.sol"; -import {ValidationData, getAccountStorage, toSetValue, toFunctionReference} from "./AccountStorage.sol"; -import {ExecutionHook} from "../interfaces/IAccountLoupe.sol"; - -// Temporary additional functions for a user-controlled install flow for validation functions. -abstract contract PluginManager2 { - using EnumerableSet for EnumerableSet.Bytes32Set; - using ValidationConfigLib for ValidationConfig; - - // Index marking the start of the data for the validation function. - uint8 internal constant _RESERVED_VALIDATION_DATA_INDEX = 255; - - error PreValidationAlreadySet(FunctionReference validationFunction, FunctionReference preValidationFunction); - error ValidationAlreadySet(bytes4 selector, FunctionReference validationFunction); - error ValidationNotSet(bytes4 selector, FunctionReference validationFunction); - error PermissionAlreadySet(FunctionReference validationFunction, ExecutionHook hook); - error PreValidationHookLimitExceeded(); - - function _installValidation( - ValidationConfig validationConfig, - bytes4[] memory selectors, - bytes calldata installData, - bytes memory preValidationHooks, - bytes memory permissionHooks - ) internal { - ValidationData storage _validationData = - getAccountStorage().validationData[validationConfig.functionReference()]; - - if (preValidationHooks.length > 0) { - (FunctionReference[] memory preValidationFunctions, bytes[] memory initDatas) = - abi.decode(preValidationHooks, (FunctionReference[], bytes[])); - - for (uint256 i = 0; i < preValidationFunctions.length; ++i) { - FunctionReference preValidationFunction = preValidationFunctions[i]; - - _validationData.preValidationHooks.push(preValidationFunction); - - if (initDatas[i].length > 0) { - (address preValidationPlugin,) = FunctionReferenceLib.unpack(preValidationFunction); - IPlugin(preValidationPlugin).onInstall(initDatas[i]); - } - } - - // Avoid collision between reserved index and actual indices - if (_validationData.preValidationHooks.length > _RESERVED_VALIDATION_DATA_INDEX) { - revert PreValidationHookLimitExceeded(); - } - } - - if (permissionHooks.length > 0) { - (ExecutionHook[] memory permissionFunctions, bytes[] memory initDatas) = - abi.decode(permissionHooks, (ExecutionHook[], bytes[])); - - for (uint256 i = 0; i < permissionFunctions.length; ++i) { - ExecutionHook memory permissionFunction = permissionFunctions[i]; - - if (!_validationData.permissionHooks.add(toSetValue(permissionFunction))) { - revert PermissionAlreadySet(validationConfig.functionReference(), permissionFunction); - } - - if (initDatas[i].length > 0) { - (address executionPlugin,) = FunctionReferenceLib.unpack(permissionFunction.hookFunction); - IPlugin(executionPlugin).onInstall(initDatas[i]); - } - } - } - - _validationData.isGlobal = validationConfig.isGlobal(); - _validationData.isSignatureValidation = validationConfig.isSignatureValidation(); - - for (uint256 i = 0; i < selectors.length; ++i) { - bytes4 selector = selectors[i]; - if (!_validationData.selectors.add(toSetValue(selector))) { - revert ValidationAlreadySet(selector, validationConfig.functionReference()); - } - } - - if (installData.length > 0) { - address plugin = validationConfig.plugin(); - IPlugin(plugin).onInstall(installData); - } - } - - function _uninstallValidation( - FunctionReference validationFunction, - bytes calldata uninstallData, - bytes calldata preValidationHookUninstallData, - bytes calldata permissionHookUninstallData - ) internal { - ValidationData storage _validationData = getAccountStorage().validationData[validationFunction]; - - _validationData.isGlobal = false; - _validationData.isSignatureValidation = false; - - { - bytes[] memory preValidationHookUninstallDatas = abi.decode(preValidationHookUninstallData, (bytes[])); - - // Clear pre validation hooks - FunctionReference[] storage preValidationHooks = _validationData.preValidationHooks; - for (uint256 i = 0; i < preValidationHooks.length; ++i) { - FunctionReference preValidationFunction = preValidationHooks[i]; - if (preValidationHookUninstallDatas[0].length > 0) { - (address preValidationPlugin,) = FunctionReferenceLib.unpack(preValidationFunction); - IPlugin(preValidationPlugin).onUninstall(preValidationHookUninstallDatas[0]); - } - } - delete _validationData.preValidationHooks; - } - - { - bytes[] memory permissionHookUninstallDatas = abi.decode(permissionHookUninstallData, (bytes[])); - - // Clear permission hooks - EnumerableSet.Bytes32Set storage permissionHooks = _validationData.permissionHooks; - uint256 i = 0; - while (permissionHooks.length() > 0) { - FunctionReference permissionHook = toFunctionReference(permissionHooks.at(0)); - permissionHooks.remove(toSetValue(permissionHook)); - (address permissionHookPlugin,) = FunctionReferenceLib.unpack(permissionHook); - IPlugin(permissionHookPlugin).onUninstall(permissionHookUninstallDatas[i++]); - } - } - delete _validationData.preValidationHooks; - - // Clear selectors - while (_validationData.selectors.length() > 0) { - bytes32 selector = _validationData.selectors.at(0); - _validationData.selectors.remove(selector); - } - - if (uninstallData.length > 0) { - (address plugin,) = FunctionReferenceLib.unpack(validationFunction); - IPlugin(plugin).onUninstall(uninstallData); - } - } -} diff --git a/src/account/PluginManagerInternals.sol b/src/account/PluginManagerInternals.sol deleted file mode 100644 index 4df90e1f..00000000 --- a/src/account/PluginManagerInternals.sol +++ /dev/null @@ -1,268 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.25; - -import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; - -import {FunctionReferenceLib} from "../helpers/FunctionReferenceLib.sol"; -import {IPlugin, ManifestExecutionHook, ManifestValidation, PluginManifest} from "../interfaces/IPlugin.sol"; -import {ExecutionHook} from "../interfaces/IAccountLoupe.sol"; -import {FunctionReference, IPluginManager} from "../interfaces/IPluginManager.sol"; -import {KnownSelectors} from "../helpers/KnownSelectors.sol"; -import {AccountStorage, getAccountStorage, SelectorData, toSetValue} from "./AccountStorage.sol"; - -abstract contract PluginManagerInternals is IPluginManager { - using EnumerableSet for EnumerableSet.Bytes32Set; - using EnumerableMap for EnumerableMap.AddressToUintMap; - using FunctionReferenceLib for FunctionReference; - - error ArrayLengthMismatch(); - error Erc4337FunctionNotAllowed(bytes4 selector); - error ExecutionFunctionAlreadySet(bytes4 selector); - error InvalidPluginManifest(); - error IPluginFunctionNotAllowed(bytes4 selector); - error NativeFunctionNotAllowed(bytes4 selector); - error NullFunctionReference(); - error NullPlugin(); - error PluginAlreadyInstalled(address plugin); - error PluginInstallCallbackFailed(address plugin, bytes revertReason); - error PluginInterfaceNotSupported(address plugin); - error PluginNotInstalled(address plugin); - error ValidationFunctionAlreadySet(bytes4 selector, FunctionReference validationFunction); - - // Storage update operations - - function _setExecutionFunction(bytes4 selector, bool isPublic, bool allowGlobalValidation, address plugin) - internal - { - SelectorData storage _selectorData = getAccountStorage().selectorData[selector]; - - if (_selectorData.plugin != address(0)) { - revert ExecutionFunctionAlreadySet(selector); - } - - // Make sure incoming execution function does not collide with any native functions (data are stored on the - // account implementation contract) - if (KnownSelectors.isNativeFunction(selector)) { - revert NativeFunctionNotAllowed(selector); - } - - // Make sure incoming execution function is not a function in IPlugin - if (KnownSelectors.isIPluginFunction(selector)) { - revert IPluginFunctionNotAllowed(selector); - } - - // Also make sure it doesn't collide with functions defined by ERC-4337 - // and called by the entry point. This prevents a malicious plugin from - // sneaking in a function with the same selector as e.g. - // `validatePaymasterUserOp` and turning the account into their own - // personal paymaster. - if (KnownSelectors.isErc4337Function(selector)) { - revert Erc4337FunctionNotAllowed(selector); - } - - _selectorData.plugin = plugin; - _selectorData.isPublic = isPublic; - _selectorData.allowGlobalValidation = allowGlobalValidation; - } - - function _removeExecutionFunction(bytes4 selector) internal { - SelectorData storage _selectorData = getAccountStorage().selectorData[selector]; - - _selectorData.plugin = address(0); - _selectorData.isPublic = false; - _selectorData.allowGlobalValidation = false; - } - - function _addValidationFunction(address plugin, ManifestValidation memory mv) internal { - AccountStorage storage _storage = getAccountStorage(); - - FunctionReference validationFunction = FunctionReferenceLib.pack(plugin, mv.functionId); - - if (mv.isDefault) { - _storage.validationData[validationFunction].isGlobal = true; - } - - if (mv.isSignatureValidation) { - _storage.validationData[validationFunction].isSignatureValidation = true; - } - - // Add the validation function to the selectors. - uint256 length = mv.selectors.length; - for (uint256 i = 0; i < length; ++i) { - bytes4 selector = mv.selectors[i]; - _storage.validationData[validationFunction].selectors.add(toSetValue(selector)); - } - } - - function _removeValidationFunction(address plugin, ManifestValidation memory mv) internal { - AccountStorage storage _storage = getAccountStorage(); - - FunctionReference validationFunction = FunctionReferenceLib.pack(plugin, mv.functionId); - - _storage.validationData[validationFunction].isGlobal = false; - _storage.validationData[validationFunction].isSignatureValidation = false; - - // Clear the selectors - while (_storage.validationData[validationFunction].selectors.length() > 0) { - bytes32 selector = _storage.validationData[validationFunction].selectors.at(0); - _storage.validationData[validationFunction].selectors.remove(selector); - } - } - - function _addExecHooks( - EnumerableSet.Bytes32Set storage hooks, - FunctionReference hookFunction, - bool isPreExecHook, - bool isPostExecHook - ) internal { - hooks.add( - toSetValue( - ExecutionHook({hookFunction: hookFunction, isPreHook: isPreExecHook, isPostHook: isPostExecHook}) - ) - ); - } - - function _removeExecHooks( - EnumerableSet.Bytes32Set storage hooks, - FunctionReference hookFunction, - bool isPreExecHook, - bool isPostExecHook - ) internal { - hooks.remove( - toSetValue( - ExecutionHook({hookFunction: hookFunction, isPreHook: isPreExecHook, isPostHook: isPostExecHook}) - ) - ); - } - - function _installPlugin(address plugin, bytes32 manifestHash, bytes memory pluginInstallData) internal { - AccountStorage storage _storage = getAccountStorage(); - - if (plugin == address(0)) { - revert NullPlugin(); - } - - // Check if the plugin exists. - if (_storage.pluginManifestHashes.contains(plugin)) { - revert PluginAlreadyInstalled(plugin); - } - - // Check that the plugin supports the IPlugin interface. - if (!ERC165Checker.supportsInterface(plugin, type(IPlugin).interfaceId)) { - revert PluginInterfaceNotSupported(plugin); - } - - // Check manifest hash. - PluginManifest memory manifest = IPlugin(plugin).pluginManifest(); - if (!_isValidPluginManifest(manifest, manifestHash)) { - revert InvalidPluginManifest(); - } - - // Add the plugin metadata to the account - _storage.pluginManifestHashes.set(plugin, uint256(manifestHash)); - - // Update components according to the manifest. - uint256 length = manifest.executionFunctions.length; - for (uint256 i = 0; i < length; ++i) { - bytes4 selector = manifest.executionFunctions[i].executionSelector; - bool isPublic = manifest.executionFunctions[i].isPublic; - bool allowGlobalValidation = manifest.executionFunctions[i].allowGlobalValidation; - _setExecutionFunction(selector, isPublic, allowGlobalValidation, plugin); - } - - length = manifest.validationFunctions.length; - for (uint256 i = 0; i < length; ++i) { - // Todo: limit this to only "direct runtime call" validation path (old EFP), - // and add a way for the user to specify permission/pre-val hooks here. - _addValidationFunction(plugin, manifest.validationFunctions[i]); - } - - length = manifest.executionHooks.length; - for (uint256 i = 0; i < length; ++i) { - ManifestExecutionHook memory mh = manifest.executionHooks[i]; - EnumerableSet.Bytes32Set storage execHooks = _storage.selectorData[mh.executionSelector].executionHooks; - FunctionReference hookFunction = FunctionReferenceLib.pack(plugin, mh.functionId); - _addExecHooks(execHooks, hookFunction, mh.isPreHook, mh.isPostHook); - } - - length = manifest.interfaceIds.length; - for (uint256 i = 0; i < length; ++i) { - _storage.supportedIfaces[manifest.interfaceIds[i]] += 1; - } - - // Initialize the plugin storage for the account. - // solhint-disable-next-line no-empty-blocks - try IPlugin(plugin).onInstall(pluginInstallData) {} - catch (bytes memory revertReason) { - revert PluginInstallCallbackFailed(plugin, revertReason); - } - - emit PluginInstalled(plugin, manifestHash); - } - - function _uninstallPlugin(address plugin, PluginManifest memory manifest, bytes memory uninstallData) - internal - { - AccountStorage storage _storage = getAccountStorage(); - - // Check if the plugin exists. - if (!_storage.pluginManifestHashes.contains(plugin)) { - revert PluginNotInstalled(plugin); - } - - // Check manifest hash. - bytes32 manifestHash = bytes32(_storage.pluginManifestHashes.get(plugin)); - if (!_isValidPluginManifest(manifest, manifestHash)) { - revert InvalidPluginManifest(); - } - - // Remove components according to the manifest, in reverse order (by component type) of their installation. - - uint256 length = manifest.executionHooks.length; - for (uint256 i = 0; i < length; ++i) { - ManifestExecutionHook memory mh = manifest.executionHooks[i]; - FunctionReference hookFunction = FunctionReferenceLib.pack(plugin, mh.functionId); - EnumerableSet.Bytes32Set storage execHooks = _storage.selectorData[mh.executionSelector].executionHooks; - _removeExecHooks(execHooks, hookFunction, mh.isPreHook, mh.isPostHook); - } - - length = manifest.validationFunctions.length; - for (uint256 i = 0; i < length; ++i) { - _removeValidationFunction(plugin, manifest.validationFunctions[i]); - } - - length = manifest.executionFunctions.length; - for (uint256 i = 0; i < length; ++i) { - bytes4 selector = manifest.executionFunctions[i].executionSelector; - _removeExecutionFunction(selector); - } - - length = manifest.interfaceIds.length; - for (uint256 i = 0; i < length; ++i) { - _storage.supportedIfaces[manifest.interfaceIds[i]] -= 1; - } - - // Remove the plugin metadata from the account. - _storage.pluginManifestHashes.remove(plugin); - - // Clear the plugin storage for the account. - bool onUninstallSuccess = true; - // solhint-disable-next-line no-empty-blocks - try IPlugin(plugin).onUninstall(uninstallData) {} - catch { - onUninstallSuccess = false; - } - - emit PluginUninstalled(plugin, onUninstallSuccess); - } - - function _isValidPluginManifest(PluginManifest memory manifest, bytes32 manifestHash) - internal - pure - returns (bool) - { - return manifestHash == keccak256(abi.encode(manifest)); - } -} diff --git a/src/account/UpgradeableModularAccount.sol b/src/account/UpgradeableModularAccount.sol index d2d1b2cc..4dd274e4 100644 --- a/src/account/UpgradeableModularAccount.sol +++ b/src/account/UpgradeableModularAccount.sol @@ -2,30 +2,37 @@ pragma solidity ^0.8.25; import {BaseAccount} from "@eth-infinitism/account-abstraction/core/BaseAccount.sol"; +import {IAccountExecute} from "@eth-infinitism/account-abstraction/interfaces/IAccountExecute.sol"; import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; -import {IAccountExecute} from "@eth-infinitism/account-abstraction/interfaces/IAccountExecute.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import {LibClone} from "solady/utils/LibClone.sol"; +import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol"; + import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {FunctionReferenceLib} from "../helpers/FunctionReferenceLib.sol"; -import {ValidationConfigLib} from "../helpers/ValidationConfigLib.sol"; +import {HookConfig, HookConfigLib} from "../helpers/HookConfigLib.sol"; +import {ModuleEntityLib} from "../helpers/ModuleEntityLib.sol"; + import {SparseCalldataSegmentLib} from "../helpers/SparseCalldataSegmentLib.sol"; -import {_coalescePreValidation, _coalesceValidation} from "../helpers/ValidationDataHelpers.sol"; -import {IPlugin, PluginManifest} from "../interfaces/IPlugin.sol"; +import {ValidationConfigLib} from "../helpers/ValidationConfigLib.sol"; +import {_coalescePreValidation, _coalesceValidation} from "../helpers/ValidationResHelpers.sol"; + +import {DIRECT_CALL_VALIDATION_ENTITYID, RESERVED_VALIDATION_DATA_INDEX} from "../helpers/Constants.sol"; + +import {ExecutionManifest} from "../interfaces/IExecution.sol"; +import {IExecutionHook} from "../interfaces/IExecutionHook.sol"; +import {IModuleManager, ModuleEntity, ValidationConfig} from "../interfaces/IModuleManager.sol"; +import {Call, IStandardExecutor} from "../interfaces/IStandardExecutor.sol"; import {IValidation} from "../interfaces/IValidation.sol"; import {IValidationHook} from "../interfaces/IValidationHook.sol"; -import {IExecutionHook} from "../interfaces/IExecutionHook.sol"; -import {FunctionReference, IPluginManager, ValidationConfig} from "../interfaces/IPluginManager.sol"; -import {IStandardExecutor, Call} from "../interfaces/IStandardExecutor.sol"; import {AccountExecutor} from "./AccountExecutor.sol"; import {AccountLoupe} from "./AccountLoupe.sol"; -import {AccountStorage, getAccountStorage, toSetValue, toExecutionHook} from "./AccountStorage.sol"; +import {AccountStorage, getAccountStorage, toHookConfig, toSetValue} from "./AccountStorage.sol"; import {AccountStorageInitializable} from "./AccountStorageInitializable.sol"; -import {PluginManagerInternals} from "./PluginManagerInternals.sol"; -import {PluginManager2} from "./PluginManager2.sol"; +import {ModuleManagerInternals} from "./ModuleManagerInternals.sol"; contract UpgradeableModularAccount is AccountExecutor, @@ -36,18 +43,18 @@ contract UpgradeableModularAccount is IERC1271, IStandardExecutor, IAccountExecute, - PluginManagerInternals, - PluginManager2, + ModuleManagerInternals, UUPSUpgradeable { using EnumerableSet for EnumerableSet.Bytes32Set; - using FunctionReferenceLib for FunctionReference; + using ModuleEntityLib for ModuleEntity; using ValidationConfigLib for ValidationConfig; + using HookConfigLib for HookConfig; using SparseCalldataSegmentLib for bytes; struct PostExecToRun { bytes preExecHookReturnData; - FunctionReference postExecHook; + ModuleEntity postExecHook; } IEntryPoint private immutable _ENTRY_POINT; @@ -63,37 +70,36 @@ contract UpgradeableModularAccount is event ModularAccountInitialized(IEntryPoint indexed entryPoint); error AuthorizeUpgradeReverted(bytes revertReason); - error ExecFromPluginNotPermitted(address plugin, bytes4 selector); - error ExecFromPluginExternalNotPermitted(address plugin, address target, uint256 value, bytes data); - error NativeTokenSpendingNotPermitted(address plugin); + error ExecFromModuleNotPermitted(address module, bytes4 selector); + error ExecFromModuleExternalNotPermitted(address module, address target, uint256 value, bytes data); + error NativeTokenSpendingNotPermitted(address module); error NonCanonicalEncoding(); error NotEntryPoint(); - error PostExecHookReverted(address plugin, uint8 functionId, bytes revertReason); - error PreExecHookReverted(address plugin, uint8 functionId, bytes revertReason); - error PreRuntimeValidationHookFailed(address plugin, uint8 functionId, bytes revertReason); + error PostExecHookReverted(address module, uint32 entityId, bytes revertReason); + error PreExecHookReverted(address module, uint32 entityId, bytes revertReason); + error PreRuntimeValidationHookFailed(address module, uint32 entityId, bytes revertReason); error RequireUserOperationContext(); error RuntimeValidationFunctionMissing(bytes4 selector); - error RuntimeValidationFunctionReverted(address plugin, uint8 functionId, bytes revertReason); + error RuntimeValidationFunctionReverted(address module, uint32 entityId, bytes revertReason); error SelfCallRecursionDepthExceeded(); - error SignatureValidationInvalid(address plugin, uint8 functionId); - error UnexpectedAggregator(address plugin, uint8 functionId, address aggregator); + error SignatureValidationInvalid(address module, uint32 entityId); + error UnexpectedAggregator(address module, uint32 entityId, address aggregator); error UnrecognizedFunction(bytes4 selector); - error UserOpValidationFunctionMissing(bytes4 selector); - error ValidationDoesNotApply(bytes4 selector, address plugin, uint8 functionId, bool isGlobal); + error ValidationFunctionMissing(bytes4 selector); + error ValidationDoesNotApply(bytes4 selector, address module, uint32 entityId, bool isGlobal); error ValidationSignatureSegmentMissing(); error SignatureSegmentOutOfOrder(); // Wraps execution of a native function with runtime validation and hooks - // Used for upgradeTo, upgradeToAndCall, execute, executeBatch, installPlugin, uninstallPlugin + // Used for upgradeTo, upgradeToAndCall, execute, executeBatch, installExecution, uninstallExecution modifier wrapNativeFunction() { - _checkPermittedCallerIfNotFromEP(); - - PostExecToRun[] memory postExecHooks = - _doPreHooks(getAccountStorage().selectorData[msg.sig].executionHooks, msg.data); + (PostExecToRun[] memory postPermissionHooks, PostExecToRun[] memory postExecHooks) = + _checkPermittedCallerAndAssociatedHooks(); _; _doCachedPostExecHooks(postExecHooks); + _doCachedPostExecHooks(postPermissionHooks); } constructor(IEntryPoint anEntryPoint) { @@ -103,56 +109,31 @@ contract UpgradeableModularAccount is // EXTERNAL FUNCTIONS - /// @notice Initializes the account with a set of plugins - /// @param plugins The plugins to install - /// @param manifestHashes The manifest hashes of the plugins to install - /// @param pluginInstallDatas The plugin install datas of the plugins to install - function initialize( - address[] memory plugins, - bytes32[] memory manifestHashes, - bytes[] memory pluginInstallDatas - ) external initializer { - uint256 length = plugins.length; - - if (length != manifestHashes.length || length != pluginInstallDatas.length) { - revert ArrayLengthMismatch(); - } - - for (uint256 i = 0; i < length; ++i) { - _installPlugin(plugins[i], manifestHashes[i], pluginInstallDatas[i]); - } - - emit ModularAccountInitialized(_ENTRY_POINT); - } - receive() external payable {} /// @notice Fallback function /// @dev We route calls to execution functions based on incoming msg.sig - /// @dev If there's no plugin associated with this function selector, revert + /// @dev If there's no module associated with this function selector, revert fallback(bytes calldata) external payable returns (bytes memory) { - address execPlugin = getAccountStorage().selectorData[msg.sig].plugin; - if (execPlugin == address(0)) { + address execModule = getAccountStorage().selectorData[msg.sig].module; + if (execModule == address(0)) { revert UnrecognizedFunction(msg.sig); } - - _checkPermittedCallerIfNotFromEP(); - - PostExecToRun[] memory postExecHooks; - // Cache post-exec hooks in memory - postExecHooks = _doPreHooks(getAccountStorage().selectorData[msg.sig].executionHooks, msg.data); + (PostExecToRun[] memory postPermissionHooks, PostExecToRun[] memory postExecHooks) = + _checkPermittedCallerAndAssociatedHooks(); // execute the function, bubbling up any reverts - (bool execSuccess, bytes memory execReturnData) = execPlugin.call(msg.data); + (bool execSuccess, bytes memory execReturnData) = execModule.call(msg.data); if (!execSuccess) { - // Bubble up revert reasons from plugins + // Bubble up revert reasons from modules assembly ("memory-safe") { revert(add(execReturnData, 32), mload(execReturnData)) } } _doCachedPostExecHooks(postExecHooks); + _doCachedPostExecHooks(postPermissionHooks); return execReturnData; } @@ -164,7 +145,7 @@ contract UpgradeableModularAccount is revert NotEntryPoint(); } - FunctionReference userOpValidationFunction = FunctionReference.wrap(bytes21(userOp.signature[:21])); + ModuleEntity userOpValidationFunction = ModuleEntity.wrap(bytes24(userOp.signature[:24])); PostExecToRun[] memory postPermissionHooks = _doPreHooks(getAccountStorage().validationData[userOpValidationFunction].permissionHooks, msg.data); @@ -217,13 +198,13 @@ contract UpgradeableModularAccount is returns (bytes memory) { // Revert if the provided `authorization` less than 21 bytes long, rather than right-padding. - FunctionReference runtimeValidationFunction = FunctionReference.wrap(bytes21(authorization[:21])); + ModuleEntity runtimeValidationFunction = ModuleEntity.wrap(bytes24(authorization[:24])); // Check if the runtime validation function is allowed to be called - bool isGlobalValidation = uint8(authorization[21]) == 1; + bool isGlobalValidation = uint8(authorization[24]) == 1; _checkIfValidationAppliesCallData(data, runtimeValidationFunction, isGlobalValidation); - _doRuntimeValidation(runtimeValidationFunction, data, authorization[22:]); + _doRuntimeValidation(runtimeValidationFunction, data, authorization[25:]); // If runtime validation passes, do runtime permission checks PostExecToRun[] memory postPermissionHooks = @@ -243,32 +224,33 @@ contract UpgradeableModularAccount is return returnData; } - /// @inheritdoc IPluginManager - /// @notice May be validated by a global validation. - function installPlugin(address plugin, bytes32 manifestHash, bytes calldata pluginInstallData) - external - override - wrapNativeFunction - { - _installPlugin(plugin, manifestHash, pluginInstallData); + // We pass a bool for "enabled" for ease of use, rather than the more efficient "disabled" + // We just negate it later. + function setBytecodeAppendedValidationEnabled(bool enabled) external wrapNativeFunction { + ModuleEntity appendedValidation = _getAppendedValidation(); + + getAccountStorage().validationData[appendedValidation].isAppendedBytecodeValidationDisabled = !enabled; + // TODO: event } - /// @inheritdoc IPluginManager + /// @inheritdoc IModuleManager /// @notice May be validated by a global validation. - function uninstallPlugin(address plugin, bytes calldata config, bytes calldata pluginUninstallData) - external - override - wrapNativeFunction - { - PluginManifest memory manifest; - - if (config.length > 0) { - manifest = abi.decode(config, (PluginManifest)); - } else { - manifest = IPlugin(plugin).pluginManifest(); - } + function installExecution( + address module, + ExecutionManifest calldata manifest, + bytes calldata moduleInstallData + ) external override wrapNativeFunction { + _installExecution(module, manifest, moduleInstallData); + } - _uninstallPlugin(plugin, manifest, pluginUninstallData); + /// @inheritdoc IModuleManager + /// @notice May be validated by a global validation. + function uninstallExecution( + address module, + ExecutionManifest calldata manifest, + bytes calldata moduleUninstallData + ) external override wrapNativeFunction { + _uninstallExecution(module, manifest, moduleUninstallData); } /// @notice Initializes the account with a validation function added to the global pool. @@ -277,38 +259,33 @@ contract UpgradeableModularAccount is /// @dev This function is only callable once, and only by the EntryPoint. function initializeWithValidation( ValidationConfig validationConfig, - bytes4[] memory selectors, + bytes4[] calldata selectors, bytes calldata installData, - bytes calldata preValidationHooks, - bytes calldata permissionHooks + bytes[] calldata hooks ) external initializer { - _installValidation(validationConfig, selectors, installData, preValidationHooks, permissionHooks); + _installValidation(validationConfig, selectors, installData, hooks); emit ModularAccountInitialized(_ENTRY_POINT); } - /// @inheritdoc IPluginManager + /// @inheritdoc IModuleManager /// @notice May be validated by a global validation. function installValidation( ValidationConfig validationConfig, - bytes4[] memory selectors, + bytes4[] calldata selectors, bytes calldata installData, - bytes calldata preValidationHooks, - bytes calldata permissionHooks + bytes[] calldata hooks ) external wrapNativeFunction { - _installValidation(validationConfig, selectors, installData, preValidationHooks, permissionHooks); + _installValidation(validationConfig, selectors, installData, hooks); } - /// @inheritdoc IPluginManager + /// @inheritdoc IModuleManager /// @notice May be validated by a global validation. function uninstallValidation( - FunctionReference validationFunction, + ModuleEntity validationFunction, bytes calldata uninstallData, - bytes calldata preValidationHookUninstallData, - bytes calldata permissionHookUninstallData + bytes[] calldata hookUninstallData ) external wrapNativeFunction { - _uninstallValidation( - validationFunction, uninstallData, preValidationHookUninstallData, permissionHookUninstallData - ); + _uninstallValidation(validationFunction, uninstallData, hookUninstallData); } /// @notice ERC165 introspection @@ -328,7 +305,7 @@ contract UpgradeableModularAccount is /// @inheritdoc UUPSUpgradeable /// @notice May be validated by a global validation. - function upgradeToAndCall(address newImplementation, bytes memory data) + function upgradeToAndCall(address newImplementation, bytes calldata data) public payable override @@ -341,15 +318,29 @@ contract UpgradeableModularAccount is function isValidSignature(bytes32 hash, bytes calldata signature) public view override returns (bytes4) { AccountStorage storage _storage = getAccountStorage(); - FunctionReference sigValidation = FunctionReference.wrap(bytes21(signature)); - - (address plugin, uint8 functionId) = sigValidation.unpack(); - if (!_storage.validationData[sigValidation].isSignatureValidation) { - revert SignatureValidationInvalid(plugin, functionId); + ModuleEntity sigValidation = ModuleEntity.wrap(bytes24(signature)); + + (address module, uint32 entityId) = sigValidation.unpack(); + // IF, in storage, the validation is not a signature validation THEN + // Is it the appended validation? + // No: revert + // Yes: Is it disabled as an appended bytecode validation? + // No: continue + // Yes: revert + // Written as: + // IF not storage AND (not appended OR appended-disabled) THEN revert ELSE continue + if ( + !_storage.validationData[sigValidation].isSignatureValidation + && ( + !_getAppendedValidation().eq(sigValidation) + || _storage.validationData[sigValidation].isAppendedBytecodeValidationDisabled + ) + ) { + revert SignatureValidationInvalid(module, entityId); } if ( - IValidation(plugin).validateSignature(functionId, msg.sender, hash, signature[21:]) + IValidation(module).validateSignature(address(this), entityId, msg.sender, hash, signature[24:]) == _1271_MAGIC_VALUE ) { return _1271_MAGIC_VALUE; @@ -377,8 +368,8 @@ contract UpgradeableModularAccount is } // Revert if the provided `authorization` less than 21 bytes long, rather than right-padding. - FunctionReference userOpValidationFunction = FunctionReference.wrap(bytes21(userOp.signature[:21])); - bool isGlobalValidation = uint8(userOp.signature[21]) == 1; + ModuleEntity userOpValidationFunction = ModuleEntity.wrap(bytes24(userOp.signature[:24])); + bool isGlobalValidation = uint8(userOp.signature[24]) == 1; _checkIfValidationAppliesCallData(userOp.callData, userOpValidationFunction, isGlobalValidation); @@ -393,12 +384,12 @@ contract UpgradeableModularAccount is revert RequireUserOperationContext(); } - validationData = _doUserOpValidation(userOpValidationFunction, userOp, userOp.signature[22:], userOpHash); + validationData = _doUserOpValidation(userOpValidationFunction, userOp, userOp.signature[25:], userOpHash); } // To support gas estimation, we don't fail early when the failure is caused by a signature failure function _doUserOpValidation( - FunctionReference userOpValidationFunction, + ModuleEntity userOpValidationFunction, PackedUserOperation memory userOp, bytes calldata signature, bytes32 userOpHash @@ -407,10 +398,10 @@ contract UpgradeableModularAccount is bytes calldata signatureSegment; (signatureSegment, signature) = signature.getNextSegment(); - uint256 validationData; + uint256 validationRes; // Do preUserOpValidation hooks - FunctionReference[] memory preUserOpValidationHooks = + ModuleEntity[] memory preUserOpValidationHooks = getAccountStorage().validationData[userOpValidationFunction].preValidationHooks; for (uint256 i = 0; i < preUserOpValidationHooks.length; ++i) { @@ -434,41 +425,41 @@ contract UpgradeableModularAccount is userOp.signature = ""; } - (address plugin, uint8 functionId) = preUserOpValidationHooks[i].unpack(); - uint256 currentValidationData = - IValidationHook(plugin).preUserOpValidationHook(functionId, userOp, userOpHash); + (address module, uint32 entityId) = preUserOpValidationHooks[i].unpack(); + uint256 currentValidationRes = + IValidationHook(module).preUserOpValidationHook(entityId, userOp, userOpHash); - if (uint160(currentValidationData) > 1) { + if (uint160(currentValidationRes) > 1) { // If the aggregator is not 0 or 1, it is an unexpected value - revert UnexpectedAggregator(plugin, functionId, address(uint160(currentValidationData))); + revert UnexpectedAggregator(module, entityId, address(uint160(currentValidationRes))); } - validationData = _coalescePreValidation(validationData, currentValidationData); + validationRes = _coalescePreValidation(validationRes, currentValidationRes); } // Run the user op validationFunction { - if (signatureSegment.getIndex() != _RESERVED_VALIDATION_DATA_INDEX) { + if (signatureSegment.getIndex() != RESERVED_VALIDATION_DATA_INDEX) { revert ValidationSignatureSegmentMissing(); } userOp.signature = signatureSegment.getBody(); - (address plugin, uint8 functionId) = userOpValidationFunction.unpack(); - uint256 currentValidationData = IValidation(plugin).validateUserOp(functionId, userOp, userOpHash); + (address module, uint32 entityId) = userOpValidationFunction.unpack(); + uint256 currentValidationRes = IValidation(module).validateUserOp(entityId, userOp, userOpHash); if (preUserOpValidationHooks.length != 0) { // If we have other validation data we need to coalesce with - validationData = _coalesceValidation(validationData, currentValidationData); + validationRes = _coalesceValidation(validationRes, currentValidationRes); } else { - validationData = currentValidationData; + validationRes = currentValidationRes; } } - return validationData; + return validationRes; } function _doRuntimeValidation( - FunctionReference runtimeValidationFunction, + ModuleEntity runtimeValidationFunction, bytes calldata callData, bytes calldata authorizationData ) internal { @@ -477,7 +468,7 @@ contract UpgradeableModularAccount is (authSegment, authorizationData) = authorizationData.getNextSegment(); // run all preRuntimeValidation hooks - FunctionReference[] memory preRuntimeValidationHooks = + ModuleEntity[] memory preRuntimeValidationHooks = getAccountStorage().validationData[runtimeValidationFunction].preValidationHooks; for (uint256 i = 0; i < preRuntimeValidationHooks.length; ++i) { @@ -500,31 +491,23 @@ contract UpgradeableModularAccount is } else { currentAuthData = ""; } - - (address hookPlugin, uint8 hookFunctionId) = preRuntimeValidationHooks[i].unpack(); - try IValidationHook(hookPlugin).preRuntimeValidationHook( - hookFunctionId, msg.sender, msg.value, callData, currentAuthData - ) - // forgefmt: disable-start - // solhint-disable-next-line no-empty-blocks - {} catch (bytes memory revertReason) { - // forgefmt: disable-end - revert PreRuntimeValidationHookFailed(hookPlugin, hookFunctionId, revertReason); - } + _doPreRuntimeValidationHook(preRuntimeValidationHooks[i], callData, currentAuthData); } - if (authSegment.getIndex() != _RESERVED_VALIDATION_DATA_INDEX) { + if (authSegment.getIndex() != RESERVED_VALIDATION_DATA_INDEX) { revert ValidationSignatureSegmentMissing(); } - (address plugin, uint8 functionId) = runtimeValidationFunction.unpack(); + (address module, uint32 entityId) = runtimeValidationFunction.unpack(); - try IValidation(plugin).validateRuntime(functionId, msg.sender, msg.value, callData, authSegment.getBody()) + try IValidation(module).validateRuntime( + address(this), entityId, msg.sender, msg.value, callData, authSegment.getBody() + ) // forgefmt: disable-start // solhint-disable-next-line no-empty-blocks {} catch (bytes memory revertReason) { // forgefmt: disable-end - revert RuntimeValidationFunctionReverted(plugin, functionId, revertReason); + revert RuntimeValidationFunctionReverted(module, entityId, revertReason); } } @@ -539,44 +522,42 @@ contract UpgradeableModularAccount is // Copy all post hooks to the array. This happens before any pre hooks are run, so we can // be sure that the set of hooks to run will not be affected by state changes mid-execution. for (uint256 i = 0; i < hooksLength; ++i) { - bytes32 key = executionHooks.at(i); - (FunctionReference hookFunction,, bool isPostHook) = toExecutionHook(key); - if (isPostHook) { - postHooksToRun[i].postExecHook = hookFunction; + HookConfig hookConfig = toHookConfig(executionHooks.at(i)); + if (hookConfig.hasPostHook()) { + postHooksToRun[i].postExecHook = hookConfig.moduleEntity(); } } // Run the pre hooks and copy their return data to the post hooks array, if an associated post-exec hook // exists. for (uint256 i = 0; i < hooksLength; ++i) { - bytes32 key = executionHooks.at(i); - (FunctionReference hookFunction, bool isPreHook, bool isPostHook) = toExecutionHook(key); + HookConfig hookConfig = toHookConfig(executionHooks.at(i)); - if (isPreHook) { + if (hookConfig.hasPreHook()) { bytes memory preExecHookReturnData; - preExecHookReturnData = _runPreExecHook(hookFunction, data); + preExecHookReturnData = _runPreExecHook(hookConfig.moduleEntity(), data); // If there is an associated post-exec hook, save the return data. - if (isPostHook) { + if (hookConfig.hasPostHook()) { postHooksToRun[i].preExecHookReturnData = preExecHookReturnData; } } } } - function _runPreExecHook(FunctionReference preExecHook, bytes memory data) + function _runPreExecHook(ModuleEntity preExecHook, bytes memory data) internal returns (bytes memory preExecHookReturnData) { - (address plugin, uint8 functionId) = preExecHook.unpack(); - try IExecutionHook(plugin).preExecutionHook(functionId, msg.sender, msg.value, data) returns ( + (address module, uint32 entityId) = preExecHook.unpack(); + try IExecutionHook(module).preExecutionHook(entityId, msg.sender, msg.value, data) returns ( bytes memory returnData ) { preExecHookReturnData = returnData; } catch (bytes memory revertReason) { - // TODO: same issue with EP0.6 - we can't do bytes4 error codes in plugins - revert PreExecHookReverted(plugin, functionId, revertReason); + // TODO: same issue with EP0.6 - we can't do bytes4 error codes in modules + revert PreExecHookReverted(module, entityId, revertReason); } } @@ -594,21 +575,92 @@ contract UpgradeableModularAccount is continue; } - (address plugin, uint8 functionId) = postHookToRun.postExecHook.unpack(); + (address module, uint32 entityId) = postHookToRun.postExecHook.unpack(); // solhint-disable-next-line no-empty-blocks - try IExecutionHook(plugin).postExecutionHook(functionId, postHookToRun.preExecHookReturnData) {} + try IExecutionHook(module).postExecutionHook(entityId, postHookToRun.preExecHookReturnData) {} catch (bytes memory revertReason) { - revert PostExecHookReverted(plugin, functionId, revertReason); + revert PostExecHookReverted(module, entityId, revertReason); } } } + function _doPreRuntimeValidationHook( + ModuleEntity validationHook, + bytes memory callData, + bytes memory currentAuthData + ) internal { + (address hookModule, uint32 hookEntityId) = validationHook.unpack(); + try IValidationHook(hookModule).preRuntimeValidationHook( + hookEntityId, msg.sender, msg.value, callData, currentAuthData + ) + // forgefmt: disable-start + // solhint-disable-next-line no-empty-blocks + {} catch (bytes memory revertReason) { + // forgefmt: disable-end + revert PreRuntimeValidationHookFailed(hookModule, hookEntityId, revertReason); + } + } + // solhint-disable-next-line no-empty-blocks function _authorizeUpgrade(address newImplementation) internal override {} + /** + * Order of operations: + * 1. Check if the sender is the entry point, the account itself, or the selector called is public. + * - Yes: Return an empty array, there are no post-permissionHooks. + * - No: Continue + * 2. Check if the called selector (msg.sig) is included in the set of selectors the msg.sender can + * directly call. + * - Yes: Continue + * - No: Revert, the caller is not allowed to call this selector + * 3. If there are runtime validation hooks associated with this caller-sig combination, run them. + * 4. Run the pre-permissionHooks associated with this caller-sig combination, and return the + * post-permissionHooks to run later. + */ + function _checkPermittedCallerAndAssociatedHooks() + internal + returns (PostExecToRun[] memory, PostExecToRun[] memory) + { + AccountStorage storage _storage = getAccountStorage(); + PostExecToRun[] memory postPermissionHooks; + + // We only need to handle permission hooks when the sender is not the entry point or the account itself, + // and the selector isn't public. + if ( + msg.sender != address(_ENTRY_POINT) && msg.sender != address(this) + && !_storage.selectorData[msg.sig].isPublic + ) { + ModuleEntity directCallValidationKey = + ModuleEntityLib.pack(msg.sender, DIRECT_CALL_VALIDATION_ENTITYID); + + _checkIfValidationAppliesCallData(msg.data, directCallValidationKey, false); + + // Direct call is allowed, run associated permission & validation hooks + + // Validation hooks + ModuleEntity[] memory preRuntimeValidationHooks = + _storage.validationData[directCallValidationKey].preValidationHooks; + + uint256 hookLen = preRuntimeValidationHooks.length; + for (uint256 i = 0; i < hookLen; ++i) { + _doPreRuntimeValidationHook(preRuntimeValidationHooks[i], msg.data, ""); + } + + // Permission hooks + postPermissionHooks = + _doPreHooks(_storage.validationData[directCallValidationKey].permissionHooks, msg.data); + } + + // Exec hooks + PostExecToRun[] memory postExecutionHooks = + _doPreHooks(_storage.selectorData[msg.sig].executionHooks, msg.data); + + return (postPermissionHooks, postExecutionHooks); + } + function _checkIfValidationAppliesCallData( bytes calldata callData, - FunctionReference validationFunction, + ModuleEntity validationFunction, bool isGlobal ) internal view { bytes4 outerSelector = bytes4(callData[:4]); @@ -647,7 +699,7 @@ contract UpgradeableModularAccount is // To prevent arbitrarily-deep recursive checking, we limit the depth of self-calls to one // for the purposes of batching. // This means that all self-calls must occur at the top level of the batch. - // Note that plugins of other contracts using `executeWithAuthorization` may still + // Note that modules of other contracts using `executeWithAuthorization` may still // independently call into this account with a different validation function, allowing // composition of multiple batches. revert SelfCallRecursionDepthExceeded(); @@ -659,47 +711,60 @@ contract UpgradeableModularAccount is } } - function _checkIfValidationAppliesSelector( - bytes4 selector, - FunctionReference validationFunction, - bool isGlobal - ) internal view { - AccountStorage storage _storage = getAccountStorage(); - - // Check that the provided validation function is applicable to the selector - if (isGlobal) { - if (!_globalValidationAllowed(selector) || !_storage.validationData[validationFunction].isGlobal) { - revert UserOpValidationFunctionMissing(selector); - } - } else { - // Not global validation, but per-selector - if (!getAccountStorage().validationData[validationFunction].selectors.contains(toSetValue(selector))) { - revert UserOpValidationFunctionMissing(selector); - } - } - } - function _globalValidationAllowed(bytes4 selector) internal view returns (bool) { if ( selector == this.execute.selector || selector == this.executeBatch.selector - || selector == this.installPlugin.selector || selector == this.uninstallPlugin.selector + || selector == this.installExecution.selector || selector == this.uninstallExecution.selector || selector == this.installValidation.selector || selector == this.uninstallValidation.selector || selector == this.upgradeToAndCall.selector ) { return true; } - return getAccountStorage().selectorData[selector].allowGlobalValidation; } - function _checkPermittedCallerIfNotFromEP() internal view { + function _checkIfValidationAppliesSelector(bytes4 selector, ModuleEntity validationFunction, bool isGlobal) + internal + view + { AccountStorage storage _storage = getAccountStorage(); - if ( - msg.sender != address(_ENTRY_POINT) && msg.sender != address(this) - && !_storage.selectorData[msg.sig].isPublic - ) { - revert ExecFromPluginNotPermitted(msg.sender, msg.sig); + // Check that the provided validation function is applicable to the selector + if (isGlobal) { + if (_globalValidationAllowed(selector)) { + if (_storage.validationData[validationFunction].isGlobal) { + return; + } + + if ( + _getAppendedValidation().eq(validationFunction) + && !validationFunction.eq(ModuleEntity.wrap(bytes24(0))) + && !_storage.validationData[validationFunction].isAppendedBytecodeValidationDisabled + ) { + return; + } + } + revert ValidationFunctionMissing(selector); + } else { + // Not global validation, but per-selector + if (!getAccountStorage().validationData[validationFunction].selectors.contains(toSetValue(selector))) { + revert ValidationFunctionMissing(selector); + } + } + } + + function _getAppendedValidation() internal view returns (ModuleEntity) { + // Get only the 24 first bytes of appended data + bytes memory appendedData = LibClone.argsOnERC1967(address(this), 0, 24); + // Appended bytecode is under the format abi.encodePacked(moduleEntity, any...) + // Validations will then decode this arbitrary data for whatever information they need + // bytecode-appended validation. + if (appendedData.length > 0) { + // TODO: Evaluate if it's better to somehow pass the data back from here and have it passed to the + // validation instead of having it be read from bytecode by the validation + ModuleEntity appendedValidationFunction = ModuleEntity.wrap(bytes24(appendedData)); + return appendedValidationFunction; } + return ModuleEntity.wrap(bytes24(0)); } } diff --git a/src/helpers/Constants.sol b/src/helpers/Constants.sol new file mode 100644 index 00000000..4ad649c1 --- /dev/null +++ b/src/helpers/Constants.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +// Index marking the start of the data for the validation function. +uint8 constant RESERVED_VALIDATION_DATA_INDEX = type(uint8).max; + +// Maximum number of pre-validation hooks that can be registered. +uint8 constant MAX_PRE_VALIDATION_HOOKS = type(uint8).max; + +// Magic value for the Entity ID of direct call validation. +uint32 constant DIRECT_CALL_VALIDATION_ENTITYID = type(uint32).max; diff --git a/src/helpers/FunctionReferenceLib.sol b/src/helpers/FunctionReferenceLib.sol deleted file mode 100644 index 7bddd94b..00000000 --- a/src/helpers/FunctionReferenceLib.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.25; - -import {FunctionReference} from "../interfaces/IPluginManager.sol"; - -library FunctionReferenceLib { - // Empty or unset function reference. - FunctionReference internal constant _EMPTY_FUNCTION_REFERENCE = FunctionReference.wrap(bytes21(0)); - // Magic value for hooks that should always revert. - FunctionReference internal constant _PRE_HOOK_ALWAYS_DENY = FunctionReference.wrap(bytes21(uint168(2))); - - function pack(address addr, uint8 functionId) internal pure returns (FunctionReference) { - return FunctionReference.wrap(bytes21(bytes20(addr)) | bytes21(uint168(functionId))); - } - - function unpack(FunctionReference fr) internal pure returns (address addr, uint8 functionId) { - bytes21 underlying = FunctionReference.unwrap(fr); - addr = address(bytes20(underlying)); - functionId = uint8(bytes1(underlying << 160)); - } - - function isEmpty(FunctionReference fr) internal pure returns (bool) { - return FunctionReference.unwrap(fr) == bytes21(0); - } - - function notEmpty(FunctionReference fr) internal pure returns (bool) { - return FunctionReference.unwrap(fr) != bytes21(0); - } - - function eq(FunctionReference a, FunctionReference b) internal pure returns (bool) { - return FunctionReference.unwrap(a) == FunctionReference.unwrap(b); - } - - function notEq(FunctionReference a, FunctionReference b) internal pure returns (bool) { - return FunctionReference.unwrap(a) != FunctionReference.unwrap(b); - } -} diff --git a/src/helpers/HookConfigLib.sol b/src/helpers/HookConfigLib.sol new file mode 100644 index 00000000..9c94e4bd --- /dev/null +++ b/src/helpers/HookConfigLib.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import {HookConfig, ModuleEntity} from "../interfaces/IModuleManager.sol"; + +// Hook types: +// Exec hook: bools for hasPre, hasPost +// Validation hook: no bools + +// Hook fields: +// module address +// entity ID +// hook type +// if exec hook: hasPre, hasPost + +// Hook config is a packed representation of a hook function and flags for its configuration. +// Layout: +// 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA________________________ // Address +// 0x________________________________________BBBBBBBB________________ // Entity ID +// 0x________________________________________________CC______________ // Type +// 0x__________________________________________________DD____________ // exec hook flags +// + +// Hook types: +// 0x00 // Exec (selector and validation associated) +// 0x01 // Validation + +// Exec hook flags layout: +// 0b000000__ // unused +// 0b______A_ // hasPre +// 0b_______B // hasPost + +library HookConfigLib { + // Hook type constants + // Exec has no bits set + bytes32 internal constant _HOOK_TYPE_EXEC = bytes32(uint256(0)); + // Validation has 1 in the 25th byte + bytes32 internal constant _HOOK_TYPE_VALIDATION = bytes32(uint256(1) << 56); + + // Exec hook flags constants + // Pre hook has 1 in 2's bit in the 26th byte + bytes32 internal constant _EXEC_HOOK_HAS_PRE = bytes32(uint256(1) << 49); + // Post hook has 1 in 1's bit in the 26th byte + bytes32 internal constant _EXEC_HOOK_HAS_POST = bytes32(uint256(1) << 48); + + function packValidationHook(ModuleEntity _hookFunction) internal pure returns (HookConfig) { + return + HookConfig.wrap(bytes26(bytes26(ModuleEntity.unwrap(_hookFunction)) | bytes26(_HOOK_TYPE_VALIDATION))); + } + + function packValidationHook(address _module, uint32 _entityId) internal pure returns (HookConfig) { + return HookConfig.wrap( + bytes25( + // module address stored in the first 20 bytes + bytes25(bytes20(_module)) + // entityId stored in the 21st - 24th byte + | bytes25(bytes24(uint192(_entityId))) | bytes25(_HOOK_TYPE_VALIDATION) + ) + ); + } + + function packExecHook(ModuleEntity _hookFunction, bool _hasPre, bool _hasPost) + internal + pure + returns (HookConfig) + { + return HookConfig.wrap( + bytes26( + bytes26(ModuleEntity.unwrap(_hookFunction)) + // | bytes26(_HOOK_TYPE_EXEC) // Can omit because exec type is 0 + | bytes26(_hasPre ? _EXEC_HOOK_HAS_PRE : bytes32(0)) + | bytes26(_hasPost ? _EXEC_HOOK_HAS_POST : bytes32(0)) + ) + ); + } + + function packExecHook(address _module, uint32 _entityId, bool _hasPre, bool _hasPost) + internal + pure + returns (HookConfig) + { + return HookConfig.wrap( + bytes26( + // module address stored in the first 20 bytes + bytes26(bytes20(_module)) + // entityId stored in the 21st - 24th byte + | bytes26(bytes24(uint192(_entityId))) + // | bytes26(_HOOK_TYPE_EXEC) // Can omit because exec type is 0 + | bytes26(_hasPre ? _EXEC_HOOK_HAS_PRE : bytes32(0)) + | bytes26(_hasPost ? _EXEC_HOOK_HAS_POST : bytes32(0)) + ) + ); + } + + function unpackValidationHook(HookConfig _config) internal pure returns (ModuleEntity _hookFunction) { + bytes26 configBytes = HookConfig.unwrap(_config); + _hookFunction = ModuleEntity.wrap(bytes24(configBytes)); + } + + function unpackExecHook(HookConfig _config) + internal + pure + returns (ModuleEntity _hookFunction, bool _hasPre, bool _hasPost) + { + bytes26 configBytes = HookConfig.unwrap(_config); + _hookFunction = ModuleEntity.wrap(bytes24(configBytes)); + _hasPre = configBytes & _EXEC_HOOK_HAS_PRE != 0; + _hasPost = configBytes & _EXEC_HOOK_HAS_POST != 0; + } + + function module(HookConfig _config) internal pure returns (address) { + return address(bytes20(HookConfig.unwrap(_config))); + } + + function entityId(HookConfig _config) internal pure returns (uint32) { + return uint32(bytes4(HookConfig.unwrap(_config) << 160)); + } + + function moduleEntity(HookConfig _config) internal pure returns (ModuleEntity) { + return ModuleEntity.wrap(bytes24(HookConfig.unwrap(_config))); + } + + // Check if the hook is a validation hook + // If false, it is an exec hook + function isValidationHook(HookConfig _config) internal pure returns (bool) { + return HookConfig.unwrap(_config) & _HOOK_TYPE_VALIDATION != 0; + } + + // Check if the exec hook has a pre hook + // Undefined behavior if the hook is not an exec hook + function hasPreHook(HookConfig _config) internal pure returns (bool) { + return HookConfig.unwrap(_config) & _EXEC_HOOK_HAS_PRE != 0; + } + + // Check if the exec hook has a post hook + // Undefined behavior if the hook is not an exec hook + function hasPostHook(HookConfig _config) internal pure returns (bool) { + return HookConfig.unwrap(_config) & _EXEC_HOOK_HAS_POST != 0; + } +} diff --git a/src/helpers/KnownSelectors.sol b/src/helpers/KnownSelectors.sol index 1d02d2a3..6e1cfca0 100644 --- a/src/helpers/KnownSelectors.sol +++ b/src/helpers/KnownSelectors.sol @@ -1,16 +1,18 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.25; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import {IAccount} from "@eth-infinitism/account-abstraction/interfaces/IAccount.sol"; import {IAggregator} from "@eth-infinitism/account-abstraction/interfaces/IAggregator.sol"; import {IPaymaster} from "@eth-infinitism/account-abstraction/interfaces/IPaymaster.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {IAccountLoupe} from "../interfaces/IAccountLoupe.sol"; + +import {IExecution} from "../interfaces/IExecution.sol"; import {IExecutionHook} from "../interfaces/IExecutionHook.sol"; -import {IPlugin} from "../interfaces/IPlugin.sol"; -import {IPluginManager} from "../interfaces/IPluginManager.sol"; +import {IModule} from "../interfaces/IModule.sol"; +import {IModuleManager} from "../interfaces/IModuleManager.sol"; import {IStandardExecutor} from "../interfaces/IStandardExecutor.sol"; import {IValidation} from "../interfaces/IValidation.sol"; import {IValidationHook} from "../interfaces/IValidationHook.sol"; @@ -22,8 +24,9 @@ library KnownSelectors { return // check against IAccount methods selector == IAccount.validateUserOp.selector - // check against IPluginManager methods - || selector == IPluginManager.installPlugin.selector || selector == IPluginManager.uninstallPlugin.selector + // check against IModuleManager methods + || selector == IModuleManager.installExecution.selector + || selector == IModuleManager.uninstallExecution.selector // check against IERC165 methods || selector == IERC165.supportsInterface.selector // check against UUPSUpgradeable methods @@ -35,8 +38,7 @@ library KnownSelectors { // check against IAccountLoupe methods || selector == IAccountLoupe.getExecutionFunctionHandler.selector || selector == IAccountLoupe.getSelectors.selector || selector == IAccountLoupe.getExecutionHooks.selector - || selector == IAccountLoupe.getPreValidationHooks.selector - || selector == IAccountLoupe.getInstalledPlugins.selector; + || selector == IAccountLoupe.getPreValidationHooks.selector; } function isErc4337Function(bytes4 selector) internal pure returns (bool) { @@ -46,9 +48,9 @@ library KnownSelectors { || selector == IPaymaster.validatePaymasterUserOp.selector || selector == IPaymaster.postOp.selector; } - function isIPluginFunction(bytes4 selector) internal pure returns (bool) { - return selector == IPlugin.onInstall.selector || selector == IPlugin.onUninstall.selector - || selector == IPlugin.pluginManifest.selector || selector == IPlugin.pluginMetadata.selector + function isIModuleFunction(bytes4 selector) internal pure returns (bool) { + return selector == IModule.onInstall.selector || selector == IModule.onUninstall.selector + || selector == IExecution.executionManifest.selector || selector == IModule.moduleMetadata.selector || selector == IExecutionHook.preExecutionHook.selector || selector == IExecutionHook.postExecutionHook.selector || selector == IValidation.validateUserOp.selector || selector == IValidation.validateRuntime.selector || selector == IValidation.validateSignature.selector diff --git a/src/helpers/ModuleEntityLib.sol b/src/helpers/ModuleEntityLib.sol new file mode 100644 index 00000000..d8473a5b --- /dev/null +++ b/src/helpers/ModuleEntityLib.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {ModuleEntity} from "../interfaces/IModuleManager.sol"; + +library ModuleEntityLib { + // Magic value for hooks that should always revert. + ModuleEntity internal constant _PRE_HOOK_ALWAYS_DENY = ModuleEntity.wrap(bytes24(uint192(2))); + + function pack(address addr, uint32 entityId) internal pure returns (ModuleEntity) { + return ModuleEntity.wrap(bytes24(bytes20(addr)) | bytes24(uint192(entityId))); + } + + function unpack(ModuleEntity fr) internal pure returns (address addr, uint32 entityId) { + bytes24 underlying = ModuleEntity.unwrap(fr); + addr = address(bytes20(underlying)); + entityId = uint32(bytes4(underlying << 160)); + } + + function isEmpty(ModuleEntity fr) internal pure returns (bool) { + return ModuleEntity.unwrap(fr) == bytes24(0); + } + + function notEmpty(ModuleEntity fr) internal pure returns (bool) { + return ModuleEntity.unwrap(fr) != bytes24(0); + } + + function eq(ModuleEntity a, ModuleEntity b) internal pure returns (bool) { + return ModuleEntity.unwrap(a) == ModuleEntity.unwrap(b); + } + + function notEq(ModuleEntity a, ModuleEntity b) internal pure returns (bool) { + return ModuleEntity.unwrap(a) != ModuleEntity.unwrap(b); + } +} diff --git a/src/helpers/ValidationConfigLib.sol b/src/helpers/ValidationConfigLib.sol index 71639f80..1c127c3b 100644 --- a/src/helpers/ValidationConfigLib.sol +++ b/src/helpers/ValidationConfigLib.sol @@ -1,48 +1,48 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.25; -import {FunctionReference, ValidationConfig} from "../interfaces/IPluginManager.sol"; +import {ModuleEntity, ValidationConfig} from "../interfaces/IModuleManager.sol"; // Validation config is a packed representation of a validation function and flags for its configuration. // Layout: // 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA________________________ // Address -// 0x________________________________________BB______________________ // Function ID -// 0x__________________________________________CC____________________ // isGlobal -// 0x____________________________________________DD__________________ // isSignatureValidation -// 0x______________________________________________000000000000000000 // unused +// 0x________________________________________BBBBBBBB________________ // Entity ID +// 0x________________________________________________CC______________ // isGlobal +// 0x__________________________________________________DD____________ // isSignatureValidation +// 0x____________________________________________________000000000000 // unused library ValidationConfigLib { - function pack(FunctionReference _validationFunction, bool _isGlobal, bool _isSignatureValidation) + function pack(ModuleEntity _validationFunction, bool _isGlobal, bool _isSignatureValidation) internal pure returns (ValidationConfig) { return ValidationConfig.wrap( - bytes23( - bytes23(FunctionReference.unwrap(_validationFunction)) - // isGlobal flag stored in the 22nd byte - | bytes23(bytes32(_isGlobal ? uint256(1) << 80 : 0)) - // isSignatureValidation flag stored in the 23rd byte - | bytes23(bytes32(_isSignatureValidation ? uint256(1) << 72 : 0)) + bytes26( + bytes26(ModuleEntity.unwrap(_validationFunction)) + // isGlobal flag stored in the 25th byte + | bytes26(bytes32(_isGlobal ? uint256(1) << 56 : 0)) + // isSignatureValidation flag stored in the 26th byte + | bytes26(bytes32(_isSignatureValidation ? uint256(1) << 48 : 0)) ) ); } - function pack(address _plugin, uint8 _functionId, bool _isGlobal, bool _isSignatureValidation) + function pack(address _module, uint32 _entityId, bool _isGlobal, bool _isSignatureValidation) internal pure returns (ValidationConfig) { return ValidationConfig.wrap( - bytes23( - // plugin address stored in the first 20 bytes - bytes23(bytes20(_plugin)) - // functionId stored in the 21st byte - | bytes23(bytes32(uint256(_functionId) << 168)) - // isGlobal flag stored in the 22nd byte - | bytes23(bytes32(_isGlobal ? uint256(1) << 80 : 0)) - // isSignatureValidation flag stored in the 23rd byte - | bytes23(bytes32(_isSignatureValidation ? uint256(1) << 72 : 0)) + bytes26( + // module address stored in the first 20 bytes + bytes26(bytes20(_module)) + // entityId stored in the 21st - 24th byte + | bytes26(bytes24(uint192(_entityId))) + // isGlobal flag stored in the 25th byte + | bytes26(bytes32(_isGlobal ? uint256(1) << 56 : 0)) + // isSignatureValidation flag stored in the 26th byte + | bytes26(bytes32(_isSignatureValidation ? uint256(1) << 48 : 0)) ) ); } @@ -50,43 +50,43 @@ library ValidationConfigLib { function unpackUnderlying(ValidationConfig config) internal pure - returns (address _plugin, uint8 _functionId, bool _isGlobal, bool _isSignatureValidation) + returns (address _module, uint32 _entityId, bool _isGlobal, bool _isSignatureValidation) { - bytes23 configBytes = ValidationConfig.unwrap(config); - _plugin = address(bytes20(configBytes)); - _functionId = uint8(configBytes[20]); - _isGlobal = uint8(configBytes[21]) == 1; - _isSignatureValidation = uint8(configBytes[22]) == 1; + bytes26 configBytes = ValidationConfig.unwrap(config); + _module = address(bytes20(configBytes)); + _entityId = uint32(bytes4(configBytes << 160)); + _isGlobal = uint8(configBytes[24]) == 1; + _isSignatureValidation = uint8(configBytes[25]) == 1; } function unpack(ValidationConfig config) internal pure - returns (FunctionReference _validationFunction, bool _isGlobal, bool _isSignatureValidation) + returns (ModuleEntity _validationFunction, bool _isGlobal, bool _isSignatureValidation) { - bytes23 configBytes = ValidationConfig.unwrap(config); - _validationFunction = FunctionReference.wrap(bytes21(configBytes)); - _isGlobal = uint8(configBytes[21]) == 1; - _isSignatureValidation = uint8(configBytes[22]) == 1; + bytes26 configBytes = ValidationConfig.unwrap(config); + _validationFunction = ModuleEntity.wrap(bytes24(configBytes)); + _isGlobal = uint8(configBytes[24]) == 1; + _isSignatureValidation = uint8(configBytes[25]) == 1; } - function plugin(ValidationConfig config) internal pure returns (address) { + function module(ValidationConfig config) internal pure returns (address) { return address(bytes20(ValidationConfig.unwrap(config))); } - function functionId(ValidationConfig config) internal pure returns (uint8) { - return uint8(ValidationConfig.unwrap(config)[20]); + function entityId(ValidationConfig config) internal pure returns (uint32) { + return uint32(bytes4(ValidationConfig.unwrap(config) << 160)); } - function functionReference(ValidationConfig config) internal pure returns (FunctionReference) { - return FunctionReference.wrap(bytes21(ValidationConfig.unwrap(config))); + function moduleEntity(ValidationConfig config) internal pure returns (ModuleEntity) { + return ModuleEntity.wrap(bytes24(ValidationConfig.unwrap(config))); } function isGlobal(ValidationConfig config) internal pure returns (bool) { - return uint8(ValidationConfig.unwrap(config)[21]) == 1; + return uint8(ValidationConfig.unwrap(config)[24]) == 1; } function isSignatureValidation(ValidationConfig config) internal pure returns (bool) { - return uint8(ValidationConfig.unwrap(config)[22]) == 1; + return uint8(ValidationConfig.unwrap(config)[25]) == 1; } } diff --git a/src/helpers/ValidationDataHelpers.sol b/src/helpers/ValidationResHelpers.sol similarity index 72% rename from src/helpers/ValidationDataHelpers.sol rename to src/helpers/ValidationResHelpers.sol index 3f61b19c..854d442c 100644 --- a/src/helpers/ValidationDataHelpers.sol +++ b/src/helpers/ValidationResHelpers.sol @@ -2,31 +2,31 @@ pragma solidity ^0.8.25; // solhint-disable-next-line private-vars-leading-underscore -function _coalescePreValidation(uint256 validationData1, uint256 validationData2) +function _coalescePreValidation(uint256 validationRes1, uint256 validationRes2) pure returns (uint256 resValidationData) { - uint48 validUntil1 = uint48(validationData1 >> 160); + uint48 validUntil1 = uint48(validationRes1 >> 160); if (validUntil1 == 0) { validUntil1 = type(uint48).max; } - uint48 validUntil2 = uint48(validationData2 >> 160); + uint48 validUntil2 = uint48(validationRes2 >> 160); if (validUntil2 == 0) { validUntil2 = type(uint48).max; } resValidationData = ((validUntil1 > validUntil2) ? uint256(validUntil2) << 160 : uint256(validUntil1) << 160); - uint48 validAfter1 = uint48(validationData1 >> 208); - uint48 validAfter2 = uint48(validationData2 >> 208); + uint48 validAfter1 = uint48(validationRes1 >> 208); + uint48 validAfter2 = uint48(validationRes2 >> 208); resValidationData |= ((validAfter1 < validAfter2) ? uint256(validAfter2) << 208 : uint256(validAfter1) << 208); // Once we know that the authorizer field is 0 or 1, we can safely bubble up SIG_FAIL with bitwise OR - resValidationData |= uint160(validationData1) | uint160(validationData2); + resValidationData |= uint160(validationRes1) | uint160(validationRes2); } // solhint-disable-next-line private-vars-leading-underscore -function _coalesceValidation(uint256 preValidationData, uint256 validationData) +function _coalesceValidation(uint256 preValidationData, uint256 validationRes) pure returns (uint256 resValidationData) { @@ -34,17 +34,17 @@ function _coalesceValidation(uint256 preValidationData, uint256 validationData) if (validUntil1 == 0) { validUntil1 = type(uint48).max; } - uint48 validUntil2 = uint48(validationData >> 160); + uint48 validUntil2 = uint48(validationRes >> 160); if (validUntil2 == 0) { validUntil2 = type(uint48).max; } resValidationData = ((validUntil1 > validUntil2) ? uint256(validUntil2) << 160 : uint256(validUntil1) << 160); uint48 validAfter1 = uint48(preValidationData >> 208); - uint48 validAfter2 = uint48(validationData >> 208); + uint48 validAfter2 = uint48(validationRes >> 208); resValidationData |= ((validAfter1 < validAfter2) ? uint256(validAfter2) << 208 : uint256(validAfter1) << 208); // If prevalidation failed, bubble up failure - resValidationData |= uint160(preValidationData) == 1 ? 1 : uint160(validationData); + resValidationData |= uint160(preValidationData) == 1 ? 1 : uint160(validationRes); } diff --git a/src/interfaces/IAccountLoupe.sol b/src/interfaces/IAccountLoupe.sol index d74c5940..f076de61 100644 --- a/src/interfaces/IAccountLoupe.sol +++ b/src/interfaces/IAccountLoupe.sol @@ -1,27 +1,27 @@ // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.25; -import {FunctionReference} from "../interfaces/IPluginManager.sol"; +import {ModuleEntity} from "../interfaces/IModuleManager.sol"; /// @notice Pre and post hooks for a given selector. /// @dev It's possible for one of either `preExecHook` or `postExecHook` to be empty. struct ExecutionHook { - FunctionReference hookFunction; + ModuleEntity hookFunction; bool isPreHook; bool isPostHook; } interface IAccountLoupe { - /// @notice Get the plugin address for a selector. - /// @dev If the selector is a native function, the plugin address will be the address of the account. + /// @notice Get the module address for a selector. + /// @dev If the selector is a native function, the module address will be the address of the account. /// @param selector The selector to get the configuration for. - /// @return plugin The plugin address for this selector. - function getExecutionFunctionHandler(bytes4 selector) external view returns (address plugin); + /// @return module The module address for this selector. + function getExecutionFunctionHandler(bytes4 selector) external view returns (address module); /// @notice Get the selectors for a validation function. /// @param validationFunction The validation function to get the selectors for. /// @return The allowed selectors for this validation function. - function getSelectors(FunctionReference validationFunction) external view returns (bytes4[] memory); + function getSelectors(ModuleEntity validationFunction) external view returns (bytes4[] memory); /// @notice Get the pre and post execution hooks for a selector. /// @param selector The selector to get the hooks for. @@ -31,20 +31,13 @@ interface IAccountLoupe { /// @notice Get the pre and post execution hooks for a validation function. /// @param validationFunction The validation function to get the hooks for. /// @return The pre and post execution hooks for this validation function. - function getPermissionHooks(FunctionReference validationFunction) - external - view - returns (ExecutionHook[] memory); + function getPermissionHooks(ModuleEntity validationFunction) external view returns (ExecutionHook[] memory); /// @notice Get the pre user op and runtime validation hooks associated with a selector. /// @param validationFunction The validation function to get the hooks for. /// @return preValidationHooks The pre validation hooks for this selector. - function getPreValidationHooks(FunctionReference validationFunction) + function getPreValidationHooks(ModuleEntity validationFunction) external view - returns (FunctionReference[] memory preValidationHooks); - - /// @notice Get an array of all installed plugins. - /// @return The addresses of all installed plugins. - function getInstalledPlugins() external view returns (address[] memory); + returns (ModuleEntity[] memory preValidationHooks); } diff --git a/src/interfaces/IExecution.sol b/src/interfaces/IExecution.sol new file mode 100644 index 00000000..8d21f9e2 --- /dev/null +++ b/src/interfaces/IExecution.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.25; + +import {IModule} from "./IModule.sol"; + +struct ManifestExecutionFunction { + // TODO(erc6900 spec): These fields can be packed into a single word + // The selector to install + bytes4 executionSelector; + // If true, the function won't need runtime validation, and can be called by anyone. + bool isPublic; + // If true, the function can be validated by a global validation function. + bool allowGlobalValidation; +} + +struct ManifestExecutionHook { + // TODO(erc6900 spec): These fields can be packed into a single word + bytes4 executionSelector; + uint32 entityId; + bool isPreHook; + bool isPostHook; +} + +/// @dev A struct describing how the module should be installed on a modular account. +struct ExecutionManifest { + // Execution functions defined in this module to be installed on the MSCA. + ManifestExecutionFunction[] executionFunctions; + ManifestExecutionHook[] executionHooks; + // List of ERC-165 interface IDs to add to account to support introspection checks. This MUST NOT include + // IModule's interface ID. + bytes4[] interfaceIds; +} + +interface IExecution is IModule { + /// @notice Describe the contents and intended configuration of the module. + /// @dev This manifest MUST stay constant over time. + /// @return A manifest describing the contents and intended configuration of the module. + function executionManifest() external pure returns (ExecutionManifest memory); +} diff --git a/src/interfaces/IExecutionHook.sol b/src/interfaces/IExecutionHook.sol index 3240c489..ad9e52b6 100644 --- a/src/interfaces/IExecutionHook.sol +++ b/src/interfaces/IExecutionHook.sol @@ -1,25 +1,25 @@ // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.25; -import {IPlugin} from "./IPlugin.sol"; +import {IModule} from "./IModule.sol"; -interface IExecutionHook is IPlugin { - /// @notice Run the pre execution hook specified by the `functionId`. +interface IExecutionHook is IModule { + /// @notice Run the pre execution hook specified by the `entityId`. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be - /// more than one. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param sender The caller address. /// @param value The call value. /// @param data The calldata sent. /// @return Context to pass to a post execution hook, if present. An empty bytes array MAY be returned. - function preExecutionHook(uint8 functionId, address sender, uint256 value, bytes calldata data) + function preExecutionHook(uint32 entityId, address sender, uint256 value, bytes calldata data) external returns (bytes memory); - /// @notice Run the post execution hook specified by the `functionId`. + /// @notice Run the post execution hook specified by the `entityId`. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be - /// more than one. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param preExecHookData The context returned by its associated pre execution hook. - function postExecutionHook(uint8 functionId, bytes calldata preExecHookData) external; + function postExecutionHook(uint32 entityId, bytes calldata preExecHookData) external; } diff --git a/src/interfaces/IModule.sol b/src/interfaces/IModule.sol new file mode 100644 index 00000000..10358619 --- /dev/null +++ b/src/interfaces/IModule.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.25; + +import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; + +struct SelectorPermission { + bytes4 functionSelector; + string permissionDescription; +} + +/// @dev A struct holding fields to describe the module in a purely view context. Intended for front end clients. +struct ModuleMetadata { + // A human-readable name of the module. + string name; + // The version of the module, following the semantic versioning scheme. + string version; + // The author field SHOULD be a username representing the identity of the user or organization + // that created this module. + string author; + // String desciptions of the relative sensitivity of specific functions. The selectors MUST be selectors for + // functions implemented by this module. + SelectorPermission[] permissionDescriptors; + // A list of all ERC-7715 permission strings that the module could possibly use + string[] permissionRequest; +} + +interface IModule is IERC165 { + /// @notice Initialize module data for the modular account. + /// @dev Called by the modular account during `installExecution`. + /// @param data Optional bytes array to be decoded and used by the module to setup initial module data for the + /// modular account. + function onInstall(bytes calldata data) external; + + /// @notice Clear module data for the modular account. + /// @dev Called by the modular account during `uninstallExecution`. + /// @param data Optional bytes array to be decoded and used by the module to clear module data for the modular + /// account. + function onUninstall(bytes calldata data) external; + + /// @notice Describe the metadata of the module. + /// @dev This metadata MUST stay constant over time. + /// @return A metadata struct describing the module. + function moduleMetadata() external pure returns (ModuleMetadata memory); +} diff --git a/src/interfaces/IModuleManager.sol b/src/interfaces/IModuleManager.sol new file mode 100644 index 00000000..74e3a338 --- /dev/null +++ b/src/interfaces/IModuleManager.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.25; + +import {ExecutionManifest} from "./IExecution.sol"; + +type ModuleEntity is bytes24; + +type ValidationConfig is bytes26; + +type HookConfig is bytes26; + +interface IModuleManager { + event ModuleInstalled(address indexed module); + + event ModuleUninstalled(address indexed module, bool indexed onUninstallSucceeded); + + /// @notice Install a module to the modular account. + /// @param module The module to install. + /// @param manifest the manifest describing functions to install + /// @param moduleInstallData Optional data to be decoded and used by the module to setup initial module data + /// for the modular account. + function installExecution( + address module, + ExecutionManifest calldata manifest, + bytes calldata moduleInstallData + ) external; + + /// @notice Temporary install function - pending a different user-supplied install config & manifest validation + /// path. + /// Installs a validation function across a set of execution selectors, and optionally mark it as a global + /// validation. + /// TODO: remove or update. + /// @dev This does not validate anything against the manifest - the caller must ensure validity. + /// @param validationConfig The validation function to install, along with configuration flags. + /// @param selectors The selectors to install the validation function for. + /// @param installData Optional data to be decoded and used by the module to setup initial module state. + /// @param hooks Optional hooks to install, associated with the validation function. These may be + /// pre-validation hooks or execution hooks. The expected format is a bytes26 HookConfig, followed by the + /// install data, if any. + function installValidation( + ValidationConfig validationConfig, + bytes4[] calldata selectors, + bytes calldata installData, + bytes[] calldata hooks + ) external; + + /// @notice Uninstall a validation function from a set of execution selectors. + /// TODO: remove or update. + /// @param validationFunction The validation function to uninstall. + /// @param uninstallData Optional data to be decoded and used by the module to clear module data for the + /// account. + /// @param hookUninstallData Optional data to be used by hooks for cleanup. If any are provided, the array must + /// be of a length equal to existing pre-validation hooks plus permission hooks. Hooks are indexed by + /// pre-validation hook order first, then permission hooks. + function uninstallValidation( + ModuleEntity validationFunction, + bytes calldata uninstallData, + bytes[] calldata hookUninstallData + ) external; + + /// @notice Uninstall a module from the modular account. + /// @param module The module to uninstall. + /// @param manifest the manifest describing functions to uninstall. + /// @param moduleUninstallData Optional data to be decoded and used by the module to clear module data for the + /// modular account. + function uninstallExecution( + address module, + ExecutionManifest calldata manifest, + bytes calldata moduleUninstallData + ) external; +} diff --git a/src/interfaces/IPlugin.sol b/src/interfaces/IPlugin.sol deleted file mode 100644 index eb10e96b..00000000 --- a/src/interfaces/IPlugin.sol +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.25; - -import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; - -struct ManifestExecutionFunction { - // TODO(erc6900 spec): These fields can be packed into a single word - // The selector to install - bytes4 executionSelector; - // If true, the function won't need runtime validation, and can be called by anyone. - bool isPublic; - // If true, the function can be validated by a global validation function. - bool allowGlobalValidation; -} - -// todo: do we need these at all? Or do we fully switch to `installValidation`? -struct ManifestValidation { - uint8 functionId; - bool isDefault; - bool isSignatureValidation; - bytes4[] selectors; -} - -struct ManifestExecutionHook { - // TODO(erc6900 spec): These fields can be packed into a single word - bytes4 executionSelector; - uint8 functionId; - bool isPreHook; - bool isPostHook; -} - -struct SelectorPermission { - bytes4 functionSelector; - string permissionDescription; -} - -/// @dev A struct holding fields to describe the plugin in a purely view context. Intended for front end clients. -struct PluginMetadata { - // A human-readable name of the plugin. - string name; - // The version of the plugin, following the semantic versioning scheme. - string version; - // The author field SHOULD be a username representing the identity of the user or organization - // that created this plugin. - string author; - // String desciptions of the relative sensitivity of specific functions. The selectors MUST be selectors for - // functions implemented by this plugin. - SelectorPermission[] permissionDescriptors; - // A list of all ERC-7715 permission strings that the plugin could possibly use - string[] permissionRequest; -} - -/// @dev A struct describing how the plugin should be installed on a modular account. -struct PluginManifest { - // Execution functions defined in this plugin to be installed on the MSCA. - ManifestExecutionFunction[] executionFunctions; - ManifestValidation[] validationFunctions; - ManifestExecutionHook[] executionHooks; - // List of ERC-165 interface IDs to add to account to support introspection checks. This MUST NOT include - // IPlugin's interface ID. - bytes4[] interfaceIds; -} - -interface IPlugin is IERC165 { - /// @notice Initialize plugin data for the modular account. - /// @dev Called by the modular account during `installPlugin`. - /// @param data Optional bytes array to be decoded and used by the plugin to setup initial plugin data for the - /// modular account. - function onInstall(bytes calldata data) external; - - /// @notice Clear plugin data for the modular account. - /// @dev Called by the modular account during `uninstallPlugin`. - /// @param data Optional bytes array to be decoded and used by the plugin to clear plugin data for the modular - /// account. - function onUninstall(bytes calldata data) external; - - /// @notice Describe the contents and intended configuration of the plugin. - /// @dev This manifest MUST stay constant over time. - /// @return A manifest describing the contents and intended configuration of the plugin. - function pluginManifest() external pure returns (PluginManifest memory); - - /// @notice Describe the metadata of the plugin. - /// @dev This metadata MUST stay constant over time. - /// @return A metadata struct describing the plugin. - function pluginMetadata() external pure returns (PluginMetadata memory); -} diff --git a/src/interfaces/IPluginManager.sol b/src/interfaces/IPluginManager.sol deleted file mode 100644 index bf1296e1..00000000 --- a/src/interfaces/IPluginManager.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.25; - -type FunctionReference is bytes21; - -type ValidationConfig is bytes23; - -interface IPluginManager { - event PluginInstalled(address indexed plugin, bytes32 manifestHash); - - event PluginUninstalled(address indexed plugin, bool indexed onUninstallSucceeded); - - /// @notice Install a plugin to the modular account. - /// @param plugin The plugin to install. - /// @param manifestHash The hash of the plugin manifest. - /// @param pluginInstallData Optional data to be decoded and used by the plugin to setup initial plugin data - /// for the modular account. - function installPlugin(address plugin, bytes32 manifestHash, bytes calldata pluginInstallData) external; - - /// @notice Temporary install function - pending a different user-supplied install config & manifest validation - /// path. - /// Installs a validation function across a set of execution selectors, and optionally mark it as a global - /// validation. - /// TODO: remove or update. - /// @dev This does not validate anything against the manifest - the caller must ensure validity. - /// @param validationConfig The validation function to install, along with configuration flags. - /// @param selectors The selectors to install the validation function for. - /// @param installData Optional data to be decoded and used by the plugin to setup initial plugin state. - /// @param preValidationHooks Optional pre-validation hooks to install for the validation function. - /// @param permissionHooks Optional permission hooks to install for the validation function. - function installValidation( - ValidationConfig validationConfig, - bytes4[] memory selectors, - bytes calldata installData, - bytes calldata preValidationHooks, - bytes calldata permissionHooks - ) external; - - /// @notice Uninstall a validation function from a set of execution selectors. - /// TODO: remove or update. - /// @param validationFunction The validation function to uninstall. - /// @param uninstallData Optional data to be decoded and used by the plugin to clear plugin data for the - /// account. - /// @param preValidationHookUninstallData Optional data to be decoded and used by the plugin to clear account - /// data - /// @param permissionHookUninstallData Optional data to be decoded and used by the plugin to clear account data - function uninstallValidation( - FunctionReference validationFunction, - bytes calldata uninstallData, - bytes calldata preValidationHookUninstallData, - bytes calldata permissionHookUninstallData - ) external; - - /// @notice Uninstall a plugin from the modular account. - /// @param plugin The plugin to uninstall. - /// @param config An optional, implementation-specific field that accounts may use to ensure consistency - /// guarantees. - /// @param pluginUninstallData Optional data to be decoded and used by the plugin to clear plugin data for the - /// modular account. - function uninstallPlugin(address plugin, bytes calldata config, bytes calldata pluginUninstallData) external; -} diff --git a/src/interfaces/IStandardExecutor.sol b/src/interfaces/IStandardExecutor.sol index fbeb89c4..db9a7c19 100644 --- a/src/interfaces/IStandardExecutor.sol +++ b/src/interfaces/IStandardExecutor.sol @@ -12,7 +12,6 @@ struct Call { interface IStandardExecutor { /// @notice Standard execute method. - /// @dev If the target is a plugin, the call SHOULD revert. /// @param target The target address for the account to call. /// @param value The value to send with the call. /// @param data The calldata for the call. @@ -20,7 +19,7 @@ interface IStandardExecutor { function execute(address target, uint256 value, bytes calldata data) external payable returns (bytes memory); /// @notice Standard executeBatch method. - /// @dev If the target is a plugin, the call SHOULD revert. If any of the calls revert, the entire batch MUST + /// @dev If the target is a module, the call SHOULD revert. If any of the calls revert, the entire batch MUST /// revert. /// @param calls The array of calls. /// @return An array containing the return data from the calls. diff --git a/src/interfaces/IValidation.sol b/src/interfaces/IValidation.sol index 38c8a139..4f8fbbb8 100644 --- a/src/interfaces/IValidation.sol +++ b/src/interfaces/IValidation.sol @@ -3,29 +3,31 @@ pragma solidity ^0.8.25; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; -import {IPlugin} from "./IPlugin.sol"; +import {IModule} from "./IModule.sol"; -interface IValidation is IPlugin { - /// @notice Run the user operation validationFunction specified by the `functionId`. - /// @param functionId An identifier that routes the call to different internal implementations, should there be - /// more than one. +interface IValidation is IModule { + /// @notice Run the user operation validationFunction specified by the `entityId`. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param userOp The user operation. /// @param userOpHash The user operation hash. /// @return Packed validation data for validAfter (6 bytes), validUntil (6 bytes), and authorizer (20 bytes). - function validateUserOp(uint8 functionId, PackedUserOperation calldata userOp, bytes32 userOpHash) + function validateUserOp(uint32 entityId, PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); - /// @notice Run the runtime validationFunction specified by the `functionId`. + /// @notice Run the runtime validationFunction specified by the `entityId`. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be - /// more than one. + /// @param account the account to validate for. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param sender The caller address. /// @param value The call value. /// @param data The calldata sent. /// @param authorization Additional data for the validation function to use. function validateRuntime( - uint8 functionId, + address account, + uint32 entityId, address sender, uint256 value, bytes calldata data, @@ -34,14 +36,18 @@ interface IValidation is IPlugin { /// @notice Validates a signature using ERC-1271. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be - /// more than one. + /// @param account the account to validate for. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param sender the address that sent the ERC-1271 request to the smart account /// @param hash the hash of the ERC-1271 request /// @param signature the signature of the ERC-1271 request /// @return the ERC-1271 `MAGIC_VALUE` if the signature is valid, or 0xFFFFFFFF if invalid. - function validateSignature(uint8 functionId, address sender, bytes32 hash, bytes calldata signature) - external - view - returns (bytes4); + function validateSignature( + address account, + uint32 entityId, + address sender, + bytes32 hash, + bytes calldata signature + ) external view returns (bytes4); } diff --git a/src/interfaces/IValidationHook.sol b/src/interfaces/IValidationHook.sol index 8300bbb8..1a8ee589 100644 --- a/src/interfaces/IValidationHook.sol +++ b/src/interfaces/IValidationHook.sol @@ -3,29 +3,29 @@ pragma solidity ^0.8.25; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; -import {IPlugin} from "./IPlugin.sol"; +import {IModule} from "./IModule.sol"; -interface IValidationHook is IPlugin { - /// @notice Run the pre user operation validation hook specified by the `functionId`. +interface IValidationHook is IModule { + /// @notice Run the pre user operation validation hook specified by the `entityId`. /// @dev Pre user operation validation hooks MUST NOT return an authorizer value other than 0 or 1. - /// @param functionId An identifier that routes the call to different internal implementations, should there be - /// more than one. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param userOp The user operation. /// @param userOpHash The user operation hash. /// @return Packed validation data for validAfter (6 bytes), validUntil (6 bytes), and authorizer (20 bytes). - function preUserOpValidationHook(uint8 functionId, PackedUserOperation calldata userOp, bytes32 userOpHash) + function preUserOpValidationHook(uint32 entityId, PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); - /// @notice Run the pre runtime validation hook specified by the `functionId`. + /// @notice Run the pre runtime validation hook specified by the `entityId`. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be - /// more than one. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param sender The caller address. /// @param value The call value. /// @param data The calldata sent. function preRuntimeValidationHook( - uint8 functionId, + uint32 entityId, address sender, uint256 value, bytes calldata data, @@ -34,14 +34,14 @@ interface IValidationHook is IPlugin { // TODO: support this hook type within the account & in the manifest - /// @notice Run the pre signature validation hook specified by the `functionId`. + /// @notice Run the pre signature validation hook specified by the `entityId`. /// @dev To indicate the call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be - /// more than one. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param sender The caller address. /// @param hash The hash of the message being signed. /// @param signature The signature of the message. - // function preSignatureValidationHook(uint8 functionId, address sender, bytes32 hash, bytes calldata + // function preSignatureValidationHook(uint32 entityId, address sender, bytes32 hash, bytes calldata // signature) // external // view diff --git a/src/modules/BaseModule.sol b/src/modules/BaseModule.sol new file mode 100644 index 00000000..b6d785c5 --- /dev/null +++ b/src/modules/BaseModule.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {IAccountExecute} from "@eth-infinitism/account-abstraction/interfaces/IAccountExecute.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +import {IModule} from "../interfaces/IModule.sol"; + +/// @title Base contract for modules +/// @dev Implements ERC-165 to support IModule's interface, which is a requirement +/// for module installation. This also ensures that module interactions cannot +/// happen via the standard execution funtions `execute` and `executeBatch`. +abstract contract BaseModule is ERC165, IModule { + error NotImplemented(); + + /// @dev Returns true if this contract implements the interface defined by + /// `interfaceId`. See the corresponding + /// https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + /// to learn more about how these ids are created. + /// + /// This function call must use less than 30 000 gas. + /// + /// Supporting the IModule interface is a requirement for module installation. This is also used + /// by the modular account to prevent standard execution functions `execute` and `executeBatch` from + /// making calls to modules. + /// @param interfaceId The interface ID to check for support. + /// @return True if the contract supports `interfaceId`. + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IModule).interfaceId || super.supportsInterface(interfaceId); + } + + function _getSelectorAndCalldata(bytes calldata data) internal pure returns (bytes4, bytes memory) { + if (bytes4(data[:4]) == IAccountExecute.executeUserOp.selector) { + (PackedUserOperation memory uo,) = abi.decode(data[4:], (PackedUserOperation, bytes32)); + bytes4 selector; + bytes memory callData = uo.callData; + // Bytes arr representation: [bytes32(len), bytes4(executeUserOp.selector), bytes4(actualSelector), + // bytes(actualCallData)] + // 1. Copy actualSelector into a new var + // 2. Shorten bytes arry by 8 by: store length - 8 into the new pointer location + // 3. Move the callData pointer by 8 + assembly { + selector := mload(add(callData, 36)) + + let len := mload(callData) + mstore(add(callData, 8), sub(len, 8)) + callData := add(callData, 8) + } + return (selector, callData); + } + return (bytes4(data[:4]), data[4:]); + } +} diff --git a/src/modules/ERC20TokenLimitModule.sol b/src/modules/ERC20TokenLimitModule.sol new file mode 100644 index 00000000..bb13dec3 --- /dev/null +++ b/src/modules/ERC20TokenLimitModule.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {UserOperationLib} from "@eth-infinitism/account-abstraction/core/UserOperationLib.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; + +import { + AssociatedLinkedListSet, + AssociatedLinkedListSetLib, + SetValue +} from "@modular-account-libs/libraries/AssociatedLinkedListSetLib.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {IExecutionHook} from "../interfaces/IExecutionHook.sol"; +import {IModule, ModuleMetadata} from "../interfaces/IModule.sol"; +import {Call, IStandardExecutor} from "../interfaces/IStandardExecutor.sol"; + +import {BaseModule, IERC165} from "./BaseModule.sol"; + +/// @title ERC20 Token Limit Module +/// @author ERC-6900 Authors +/// @notice This module supports an ERC20 token spend limit. This should be combined with a contract whitelist +/// module to make sure that token transfers not tracked by the module don't happen. +/// Note: this module is opinionated on what selectors can be called for token contracts to guard against weird +/// edge cases like DAI. You wouldn't be able to use uni v2 pairs directly as the pair contract is also the LP +/// token contract +contract ERC20TokenLimitModule is BaseModule, IExecutionHook { + using UserOperationLib for PackedUserOperation; + using EnumerableSet for EnumerableSet.AddressSet; + using AssociatedLinkedListSetLib for AssociatedLinkedListSet; + + struct ERC20SpendLimit { + address token; + uint256[] limits; + } + + string internal constant _NAME = "ERC20 Token Limit Module"; + string internal constant _VERSION = "1.0.0"; + string internal constant _AUTHOR = "ERC-6900 Authors"; + + mapping(uint32 entityId => mapping(address token => mapping(address account => uint256 limit))) public limits; + AssociatedLinkedListSet internal _tokenList; + + error ExceededTokenLimit(); + error ExceededNumberOfEntities(); + error SelectorNotAllowed(); + + function updateLimits(uint32 entityId, address token, uint256 newLimit) external { + _tokenList.tryAdd(msg.sender, SetValue.wrap(bytes30(bytes20(token)))); + limits[entityId][token][msg.sender] = newLimit; + } + + /// @inheritdoc IExecutionHook + function preExecutionHook(uint32 entityId, address, uint256, bytes calldata data) + external + override + returns (bytes memory) + { + (bytes4 selector, bytes memory callData) = _getSelectorAndCalldata(data); + + if (selector == IStandardExecutor.execute.selector) { + (address token,, bytes memory innerCalldata) = abi.decode(callData, (address, uint256, bytes)); + if (_tokenList.contains(msg.sender, SetValue.wrap(bytes30(bytes20(token))))) { + _decrementLimit(entityId, token, innerCalldata); + } + } else if (selector == IStandardExecutor.executeBatch.selector) { + Call[] memory calls = abi.decode(callData, (Call[])); + for (uint256 i = 0; i < calls.length; i++) { + if (_tokenList.contains(msg.sender, SetValue.wrap(bytes30(bytes20(calls[i].target))))) { + _decrementLimit(entityId, calls[i].target, calls[i].data); + } + } + } + + return ""; + } + + /// @inheritdoc IModule + function onInstall(bytes calldata data) external override { + (uint32 startEntityId, ERC20SpendLimit[] memory spendLimits) = + abi.decode(data, (uint32, ERC20SpendLimit[])); + + if (startEntityId + spendLimits.length > type(uint32).max) { + revert ExceededNumberOfEntities(); + } + + for (uint8 i = 0; i < spendLimits.length; i++) { + _tokenList.tryAdd(msg.sender, SetValue.wrap(bytes30(bytes20(spendLimits[i].token)))); + for (uint256 j = 0; j < spendLimits[i].limits.length; j++) { + limits[i + startEntityId][spendLimits[i].token][msg.sender] = spendLimits[i].limits[j]; + } + } + } + + /// @inheritdoc IModule + function onUninstall(bytes calldata data) external override { + (address token, uint32 entityId) = abi.decode(data, (address, uint32)); + delete limits[entityId][token][msg.sender]; + } + + function getTokensForAccount(address account) external view returns (address[] memory tokens) { + SetValue[] memory set = _tokenList.getAll(account); + tokens = new address[](set.length); + for (uint256 i = 0; i < tokens.length; i++) { + tokens[i] = address(bytes20(bytes32(SetValue.unwrap(set[i])))); + } + return tokens; + } + + /// @inheritdoc IExecutionHook + function postExecutionHook(uint32, bytes calldata) external pure override { + revert NotImplemented(); + } + + /// @inheritdoc IModule + function moduleMetadata() external pure virtual override returns (ModuleMetadata memory) { + ModuleMetadata memory metadata; + metadata.name = _NAME; + metadata.version = _VERSION; + metadata.author = _AUTHOR; + + metadata.permissionRequest = new string[](1); + metadata.permissionRequest[0] = "erc20-token-limit"; + return metadata; + } + + /// @inheritdoc BaseModule + function supportsInterface(bytes4 interfaceId) public view override(BaseModule, IERC165) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function _decrementLimit(uint32 entityId, address token, bytes memory innerCalldata) internal { + bytes4 selector; + uint256 spend; + assembly { + selector := mload(add(innerCalldata, 32)) // 0:32 is arr len, 32:36 is selector + spend := mload(add(innerCalldata, 68)) // 36:68 is recipient, 68:100 is spend + } + if (selector == IERC20.transfer.selector || selector == IERC20.approve.selector) { + uint256 limit = limits[entityId][token][msg.sender]; + if (spend > limit) { + revert ExceededTokenLimit(); + } + // solhint-disable-next-line reentrancy + limits[entityId][token][msg.sender] = limit - spend; + } else { + revert SelectorNotAllowed(); + } + } +} diff --git a/src/modules/NativeTokenLimitModule.sol b/src/modules/NativeTokenLimitModule.sol new file mode 100644 index 00000000..5baed34d --- /dev/null +++ b/src/modules/NativeTokenLimitModule.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {UserOperationLib} from "@eth-infinitism/account-abstraction/core/UserOperationLib.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {IExecutionHook} from "../interfaces/IExecutionHook.sol"; +import {IModule, ModuleMetadata} from "../interfaces/IModule.sol"; +import {Call, IStandardExecutor} from "../interfaces/IStandardExecutor.sol"; + +import {IValidationHook} from "../interfaces/IValidationHook.sol"; +import {BaseModule, IERC165} from "./BaseModule.sol"; + +/// @title Native Token Limit Module +/// @author ERC-6900 Authors +/// @notice This module supports a single total native token spend limit. +/// It tracks a total spend limit across UserOperation gas limits and native token transfers. +/// If a non whitelisted paymaster is used, UO gas would not cause the limit to decrease. +/// If a whitelisted paymaster is used, gas is still counted towards the limit +contract NativeTokenLimitModule is BaseModule, IExecutionHook, IValidationHook { + using UserOperationLib for PackedUserOperation; + using EnumerableSet for EnumerableSet.Bytes32Set; + + string internal constant _NAME = "Native Token Limit"; + string internal constant _VERSION = "1.0.0"; + string internal constant _AUTHOR = "ERC-6900 Authors"; + + mapping(uint256 funcIds => mapping(address account => uint256 limit)) public limits; + // Accounts should add paymasters that still use the accounts tokens here + // E.g. ERC20 paymasters that pull funds from the account + mapping(address paymaster => mapping(address account => bool allowed)) public specialPaymasters; + + error ExceededNativeTokenLimit(); + error ExceededNumberOfEntities(); + + function updateLimits(uint32 entityId, uint256 newLimit) external { + limits[entityId][msg.sender] = newLimit; + } + + function updateSpecialPaymaster(address paymaster, bool allowed) external { + specialPaymasters[paymaster][msg.sender] = allowed; + } + + /// @inheritdoc IValidationHook + function preUserOpValidationHook(uint32 entityId, PackedUserOperation calldata userOp, bytes32) + external + returns (uint256) + { + // Decrease limit only if no paymaster is used, or if its a special paymaster + if ( + userOp.paymasterAndData.length == 0 + || specialPaymasters[address(bytes20(userOp.paymasterAndData[:20]))][msg.sender] + ) { + uint256 vgl = UserOperationLib.unpackVerificationGasLimit(userOp); + uint256 cgl = UserOperationLib.unpackCallGasLimit(userOp); + uint256 pvgl; + uint256 ppogl; + if (userOp.paymasterAndData.length > 0) { + // Can skip the EP length check here since it would have reverted there if it was invalid + (, pvgl, ppogl) = UserOperationLib.unpackPaymasterStaticFields(userOp.paymasterAndData); + } + uint256 totalGas = userOp.preVerificationGas + vgl + cgl + pvgl + ppogl; + uint256 usage = totalGas * UserOperationLib.unpackMaxFeePerGas(userOp); + + uint256 limit = limits[entityId][msg.sender]; + if (usage > limit) { + revert ExceededNativeTokenLimit(); + } + limits[entityId][msg.sender] = limit - usage; + } + return 0; + } + + /// @inheritdoc IExecutionHook + function preExecutionHook(uint32 entityId, address, uint256, bytes calldata data) + external + override + returns (bytes memory) + { + return _checkAndDecrementLimit(entityId, data); + } + + /// @inheritdoc IModule + function onInstall(bytes calldata data) external override { + (uint32 startEntityId, uint256[] memory spendLimits) = abi.decode(data, (uint32, uint256[])); + + if (startEntityId + spendLimits.length > type(uint32).max) { + revert ExceededNumberOfEntities(); + } + + for (uint256 i = 0; i < spendLimits.length; i++) { + limits[i + startEntityId][msg.sender] = spendLimits[i]; + } + } + + /// @inheritdoc IModule + function onUninstall(bytes calldata data) external override { + // This is the highest entityId that's being used by the account + uint32 entityId = abi.decode(data, (uint32)); + for (uint256 i = 0; i < entityId; i++) { + delete limits[i][msg.sender]; + } + } + + /// @inheritdoc IExecutionHook + function postExecutionHook(uint32, bytes calldata) external pure override { + revert NotImplemented(); + } + + // No implementation, no revert + // Runtime spends no account gas, and we check native token spend limits in exec hooks + function preRuntimeValidationHook(uint32, address, uint256, bytes calldata, bytes calldata) + external + pure + override + {} // solhint-disable-line no-empty-blocks + + /// @inheritdoc IModule + function moduleMetadata() external pure virtual override returns (ModuleMetadata memory) { + ModuleMetadata memory metadata; + metadata.name = _NAME; + metadata.version = _VERSION; + metadata.author = _AUTHOR; + + metadata.permissionRequest = new string[](2); + metadata.permissionRequest[0] = "native-token-limit"; + metadata.permissionRequest[1] = "gas-limit"; + return metadata; + } + + // ┏━━━━━━━━━━━━━━━┓ + // ┃ EIP-165 ┃ + // ┗━━━━━━━━━━━━━━━┛ + + /// @inheritdoc BaseModule + function supportsInterface(bytes4 interfaceId) public view override(BaseModule, IERC165) returns (bool) { + return interfaceId == type(IExecutionHook).interfaceId || super.supportsInterface(interfaceId); + } + + function _checkAndDecrementLimit(uint32 entityId, bytes calldata data) internal returns (bytes memory) { + (bytes4 selector, bytes memory callData) = _getSelectorAndCalldata(data); + + uint256 value; + // Get value being sent + if (selector == IStandardExecutor.execute.selector) { + (, value) = abi.decode(callData, (address, uint256)); + } else if (selector == IStandardExecutor.executeBatch.selector) { + Call[] memory calls = abi.decode(callData, (Call[])); + for (uint256 i = 0; i < calls.length; i++) { + value += calls[i].value; + } + } + + uint256 limit = limits[entityId][msg.sender]; + if (value > limit) { + revert ExceededNativeTokenLimit(); + } + limits[entityId][msg.sender] = limit - value; + + return ""; + } +} diff --git a/src/plugins/TokenReceiverPlugin.sol b/src/modules/TokenReceiverModule.sol similarity index 69% rename from src/plugins/TokenReceiverPlugin.sol rename to src/modules/TokenReceiverModule.sol index d326a9d4..3991a29a 100644 --- a/src/plugins/TokenReceiverPlugin.sol +++ b/src/modules/TokenReceiverModule.sol @@ -1,20 +1,22 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.25; -import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/interfaces/IERC1155Receiver.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; -import {IPlugin, ManifestExecutionFunction, PluginManifest, PluginMetadata} from "../interfaces/IPlugin.sol"; -import {BasePlugin} from "./BasePlugin.sol"; +import {ExecutionManifest, ManifestExecutionFunction} from "../interfaces/IExecution.sol"; +import {ExecutionManifest, IExecution} from "../interfaces/IExecution.sol"; +import {IModule, ModuleMetadata} from "../interfaces/IModule.sol"; +import {BaseModule} from "./BaseModule.sol"; -/// @title Token Receiver Plugin +/// @title Token Receiver Module /// @author ERC-6900 Authors -/// @notice This plugin allows modular accounts to receive various types of tokens by implementing +/// @notice This module allows modular accounts to receive various types of tokens by implementing /// required token receiver interfaces. -contract TokenReceiverPlugin is BasePlugin, IERC721Receiver, IERC1155Receiver { - string public constant NAME = "Token Receiver Plugin"; - string public constant VERSION = "1.0.0"; - string public constant AUTHOR = "ERC-6900 Authors"; +contract TokenReceiverModule is BaseModule, IExecution, IERC721Receiver, IERC1155Receiver { + string internal constant _NAME = "Token Receiver Module"; + string internal constant _VERSION = "1.0.0"; + string internal constant _AUTHOR = "ERC-6900 Authors"; // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ Execution functions ┃ @@ -43,20 +45,20 @@ contract TokenReceiverPlugin is BasePlugin, IERC721Receiver, IERC1155Receiver { } // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Plugin interface functions ┃ + // ┃ Module interface functions ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - /// @inheritdoc IPlugin + /// @inheritdoc IModule // solhint-disable-next-line no-empty-blocks function onInstall(bytes calldata) external pure override {} - /// @inheritdoc IPlugin + /// @inheritdoc IModule // solhint-disable-next-line no-empty-blocks function onUninstall(bytes calldata) external pure override {} - /// @inheritdoc IPlugin - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; + /// @inheritdoc IExecution + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; manifest.executionFunctions = new ManifestExecutionFunction[](3); manifest.executionFunctions[0] = ManifestExecutionFunction({ @@ -82,12 +84,12 @@ contract TokenReceiverPlugin is BasePlugin, IERC721Receiver, IERC1155Receiver { return manifest; } - /// @inheritdoc IPlugin - function pluginMetadata() external pure virtual override returns (PluginMetadata memory) { - PluginMetadata memory metadata; - metadata.name = NAME; - metadata.version = VERSION; - metadata.author = AUTHOR; + /// @inheritdoc IModule + function moduleMetadata() external pure virtual override returns (ModuleMetadata memory) { + ModuleMetadata memory metadata; + metadata.name = _NAME; + metadata.version = _VERSION; + metadata.author = _AUTHOR; return metadata; } } diff --git a/src/samples/permissionhooks/AllowlistPlugin.sol b/src/modules/permissionhooks/AllowlistModule.sol similarity index 81% rename from src/samples/permissionhooks/AllowlistPlugin.sol rename to src/modules/permissionhooks/AllowlistModule.sol index 209d8370..2fdf1362 100644 --- a/src/samples/permissionhooks/AllowlistPlugin.sol +++ b/src/modules/permissionhooks/AllowlistModule.sol @@ -3,13 +3,14 @@ pragma solidity ^0.8.25; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; -import {PluginMetadata, PluginManifest} from "../../interfaces/IPlugin.sol"; +import {ModuleMetadata} from "../../interfaces/IModule.sol"; + +import {Call, IStandardExecutor} from "../../interfaces/IStandardExecutor.sol"; import {IValidationHook} from "../../interfaces/IValidationHook.sol"; -import {IStandardExecutor, Call} from "../../interfaces/IStandardExecutor.sol"; -import {BasePlugin} from "../../plugins/BasePlugin.sol"; +import {BaseModule} from "../../modules/BaseModule.sol"; -contract AllowlistPlugin is IValidationHook, BasePlugin { - enum FunctionId { +contract AllowlistModule is IValidationHook, BaseModule { + enum EntityId { PRE_VALIDATION_HOOK } @@ -68,25 +69,25 @@ contract AllowlistPlugin is IValidationHook, BasePlugin { selectorAllowlist[target][selector][msg.sender] = allowed; } - function preUserOpValidationHook(uint8 functionId, PackedUserOperation calldata userOp, bytes32) + function preUserOpValidationHook(uint32 entityId, PackedUserOperation calldata userOp, bytes32) external view override returns (uint256) { - if (functionId == uint8(FunctionId.PRE_VALIDATION_HOOK)) { + if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK)) { _checkAllowlistCalldata(userOp.callData); return 0; } revert NotImplemented(); } - function preRuntimeValidationHook(uint8 functionId, address, uint256, bytes calldata data, bytes calldata) + function preRuntimeValidationHook(uint32 entityId, address, uint256, bytes calldata data, bytes calldata) external view override { - if (functionId == uint8(FunctionId.PRE_VALIDATION_HOOK)) { + if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK)) { _checkAllowlistCalldata(data); return; } @@ -94,18 +95,15 @@ contract AllowlistPlugin is IValidationHook, BasePlugin { revert NotImplemented(); } - function pluginMetadata() external pure override returns (PluginMetadata memory) { - PluginMetadata memory metadata; - metadata.name = "Allowlist Plugin"; + function moduleMetadata() external pure override returns (ModuleMetadata memory) { + ModuleMetadata memory metadata; + metadata.name = "Allowlist Module"; metadata.version = "v0.0.1"; metadata.author = "ERC-6900 Working Group"; return metadata; } - // solhint-disable-next-line no-empty-blocks - function pluginManifest() external pure override returns (PluginManifest memory) {} - function _checkAllowlistCalldata(bytes calldata callData) internal view { if (bytes4(callData[:4]) == IStandardExecutor.execute.selector) { (address target,, bytes memory data) = abi.decode(callData[4:], (address, uint256, bytes)); diff --git a/src/modules/validation/ISingleSignerValidation.sol b/src/modules/validation/ISingleSignerValidation.sol new file mode 100644 index 00000000..2653b752 --- /dev/null +++ b/src/modules/validation/ISingleSignerValidation.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {IValidation} from "../../interfaces/IValidation.sol"; + +interface ISingleSignerValidation is IValidation { + /// @notice This event is emitted when Signer of the account's validation changes. + /// @param account The account whose validation Signer changed. + /// @param entityId The entityId for the account and the signer. + /// @param previousSigner The address of the previous signer. + /// @param newSigner The address of the new signer. + event SignerTransferred( + address indexed account, uint32 indexed entityId, address previousSigner, address newSigner + ); + + error NotAuthorized(); + + /// @notice Transfer Signer of the account's validation to `newSigner`. + /// @param entityId The entityId for the account and the signer. + /// @param newSigner The address of the new signer. + function transferSigner(uint32 entityId, address newSigner) external; + + /// @notice Get the signer of the `account`'s validation. + /// @param entityId The entityId for the account and the signer. + /// @param account The account to get the signer of. + /// @return The address of the signer. + function signerOf(uint32 entityId, address account) external view returns (address); +} diff --git a/src/modules/validation/SingleSignerValidation.sol b/src/modules/validation/SingleSignerValidation.sol new file mode 100644 index 00000000..2f24800d --- /dev/null +++ b/src/modules/validation/SingleSignerValidation.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {LibClone} from "solady/utils/LibClone.sol"; + +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; + +import {IModule, ModuleMetadata} from "../../interfaces/IModule.sol"; +import {IValidation} from "../../interfaces/IValidation.sol"; +import {BaseModule} from "../BaseModule.sol"; +import {ISingleSignerValidation} from "./ISingleSignerValidation.sol"; + +import {ModuleEntityLib} from "../../helpers/ModuleEntityLib.sol"; +import {ModuleEntity} from "../../interfaces/IModuleManager.sol"; + +/// @title ECSDA Validation +/// @author ERC-6900 Authors +/// @notice This validation enables any ECDSA (secp256k1 curve) signature validation. It handles installation by +/// each entity (entityId). +/// Note: Uninstallation will NOT disable all installed validation entities. None of the functions are installed on +/// the account. Account states are to be retrieved from this global singleton directly. +/// +/// - This validation supports ERC-1271. The signature is valid if it is signed by the owner's private key +/// (if the owner is an EOA) or if it is a valid ERC-1271 signature from the +/// owner (if the owner is a contract). +/// +/// - This validation supports composition that other validation can relay on entities in this validation +/// to validate partially or fully. +contract SingleSignerValidation is ISingleSignerValidation, BaseModule { + using ECDSA for bytes32; + using ModuleEntityLib for ModuleEntity; + using MessageHashUtils for bytes32; + + string internal constant _NAME = "SingleSigner Validation"; + string internal constant _VERSION = "1.0.0"; + string internal constant _AUTHOR = "ERC-6900 Authors"; + + uint256 internal constant _SIG_VALIDATION_PASSED = 0; + uint256 internal constant _SIG_VALIDATION_FAILED = 1; + + // bytes4(keccak256("isValidSignature(bytes32,bytes)")) + bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e; + bytes4 internal constant _1271_INVALID = 0xffffffff; + + error AppendedValidationMismatch(); + + mapping(uint32 entityId => mapping(address account => address)) public signer; + + /// @inheritdoc ISingleSignerValidation + function transferSigner(uint32 entityId, address newSigner) external { + _transferSigner(entityId, newSigner); + } + + /// @inheritdoc IModule + function onInstall(bytes calldata data) external override { + (uint32 entityId, address newSigner) = abi.decode(data, (uint32, address)); + _transferSigner(entityId, newSigner); + } + + /// @inheritdoc IModule + function onUninstall(bytes calldata data) external override { + // ToDo: what does it mean in the world of composable validation world to uninstall one type of validation + // We can either get rid of all SingleSigner signers. What about the nested ones? + _transferSigner(abi.decode(data, (uint32)), address(0)); + } + + /// @inheritdoc ISingleSignerValidation + function signerOf(uint32 entityId, address account) external view returns (address) { + return _getExpectedSigner(entityId, account); + } + + /// @inheritdoc IValidation + function validateUserOp(uint32 entityId, PackedUserOperation calldata userOp, bytes32 userOpHash) + external + view + override + returns (uint256) + { + // Validate the user op signature against the owner. + (address sigSigner,,) = (userOpHash.toEthSignedMessageHash()).tryRecover(userOp.signature); + if (sigSigner == address(0) || sigSigner != _getExpectedSigner(entityId, userOp.sender)) { + return _SIG_VALIDATION_FAILED; + } + return _SIG_VALIDATION_PASSED; + } + + /// @inheritdoc IValidation + function validateRuntime( + address account, + uint32 entityId, + address sender, + uint256, + bytes calldata, + bytes calldata + ) external view override { + if (sender != _getExpectedSigner(entityId, account)) { + revert NotAuthorized(); + } + return; + } + + /// @inheritdoc IValidation + /// @dev The signature is valid if it is signed by the owner's private key + /// (if the owner is an EOA) or if it is a valid ERC-1271 signature from the + /// owner (if the owner is a contract). Note that unlike the signature + /// validation used in `validateUserOp`, this does///*not** wrap the digest in + /// an "Ethereum Signed Message" envelope before checking the signature in + /// the EOA-owner case. + function validateSignature(address account, uint32 entityId, address, bytes32 digest, bytes calldata signature) + external + view + override + returns (bytes4) + { + if (SignatureChecker.isValidSignatureNow(_getExpectedSigner(entityId, account), digest, signature)) { + return _1271_MAGIC_VALUE; + } + return _1271_INVALID; + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Module interface functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + /// @inheritdoc IModule + function moduleMetadata() external pure virtual override returns (ModuleMetadata memory) { + ModuleMetadata memory metadata; + metadata.name = _NAME; + metadata.version = _VERSION; + metadata.author = _AUTHOR; + return metadata; + } + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Internal / Private functions ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + function _transferSigner(uint32 entityId, address newSigner) internal { + address previousSigner = signer[entityId][msg.sender]; + signer[entityId][msg.sender] = newSigner; + emit SignerTransferred(msg.sender, entityId, previousSigner, newSigner); + } + + function _getExpectedSigner(uint32 entityId, address account) internal view returns (address) { + address expectedSigner = signer[entityId][account]; + + if (expectedSigner == address(0)) { + // Check clone for expected validation (address(this)) and expected signer + bytes memory immutables = LibClone.argsOnERC1967(account); + // TODO: Add length check to prevent casting empty bytes, maybe? + + ModuleEntity validation = ModuleEntity.wrap(bytes24(immutables)); + + if (!validation.eq(ModuleEntityLib.pack(address(this), entityId))) { + revert AppendedValidationMismatch(); + } + + address decodedSigner; + assembly { + // shr to make sure the address is right-aligned + decodedSigner := shr(96, mload(add(add(immutables, 0x20), 0x18))) + } + + expectedSigner = decodedSigner; + } + + return expectedSigner; + } +} diff --git a/src/plugins/BasePlugin.sol b/src/plugins/BasePlugin.sol deleted file mode 100644 index 3b0fd521..00000000 --- a/src/plugins/BasePlugin.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.25; - -import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; - -import {IPlugin} from "../interfaces/IPlugin.sol"; - -/// @title Base contract for plugins -/// @dev Implements ERC-165 to support IPlugin's interface, which is a requirement -/// for plugin installation. This also ensures that plugin interactions cannot -/// happen via the standard execution funtions `execute` and `executeBatch`. -abstract contract BasePlugin is ERC165, IPlugin { - error NotImplemented(); - - /// @dev Returns true if this contract implements the interface defined by - /// `interfaceId`. See the corresponding - /// https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] - /// to learn more about how these ids are created. - /// - /// This function call must use less than 30 000 gas. - /// - /// Supporting the IPlugin interface is a requirement for plugin installation. This is also used - /// by the modular account to prevent standard execution functions `execute` and `executeBatch` from - /// making calls to plugins. - /// @param interfaceId The interface ID to check for support. - /// @return True if the contract supports `interfaceId`. - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { - return interfaceId == type(IPlugin).interfaceId || super.supportsInterface(interfaceId); - } -} diff --git a/src/plugins/owner/SingleOwnerPlugin.sol b/src/plugins/owner/SingleOwnerPlugin.sol deleted file mode 100644 index dbb5d56a..00000000 --- a/src/plugins/owner/SingleOwnerPlugin.sol +++ /dev/null @@ -1,188 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.25; - -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; -import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; -import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; - -import {PluginManifest, PluginMetadata, SelectorPermission} from "../../interfaces/IPlugin.sol"; -import {IPlugin} from "../../interfaces/IPlugin.sol"; -import {IValidation} from "../../interfaces/IValidation.sol"; -import {BasePlugin, IERC165} from "../BasePlugin.sol"; - -/// @title Single Owner Plugin -/// @author ERC-6900 Authors -/// @notice This plugin allows an EOA or smart contract to own a modular account. -/// It also supports [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) signature -/// validation for both validating the signature on user operations and in -/// exposing its own `isValidSignature` method. This only works when the owner of -/// modular account also support ERC-1271. -/// -/// ERC-4337's bundler validation rules limit the types of contracts that can be -/// used as owners to validate user operation signatures. For example, the -/// contract's `isValidSignature` function may not use any forbidden opcodes -/// such as `TIMESTAMP` or `NUMBER`, and the contract may not be an ERC-1967 -/// proxy as it accesses a constant implementation slot not associated with -/// the account, violating storage access rules. This also means that the -/// owner of a modular account may not be another modular account if you want to -/// send user operations through a bundler. -contract SingleOwnerPlugin is IValidation, BasePlugin { - using ECDSA for bytes32; - using MessageHashUtils for bytes32; - - string internal constant _NAME = "Single Owner Plugin"; - string internal constant _VERSION = "1.0.0"; - string internal constant _AUTHOR = "ERC-6900 Authors"; - - uint256 internal constant _SIG_VALIDATION_PASSED = 0; - uint256 internal constant _SIG_VALIDATION_FAILED = 1; - - // bytes4(keccak256("isValidSignature(bytes32,bytes)")) - bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e; - bytes4 internal constant _1271_INVALID = 0xffffffff; - - mapping(uint8 id => mapping(address account => address)) public owners; - - /// @notice This event is emitted when ownership of the account changes. - /// @param account The account whose ownership changed. - /// @param previousOwner The address of the previous owner. - /// @param newOwner The address of the new owner. - event OwnershipTransferred(address indexed account, address indexed previousOwner, address indexed newOwner); - - error AlreadyInitialized(); - error NotAuthorized(); - error NotInitialized(); - - // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Execution functions ┃ - // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - /// @notice Transfer ownership of an account and ID to `newOwner`. The caller address (`msg.sender`) is user to - /// identify the account. - /// @param newOwner The address of the new owner. - function transferOwnership(uint8 id, address newOwner) external { - _transferOwnership(id, newOwner); - } - - // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Plugin interface functions ┃ - // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - /// @inheritdoc IPlugin - function onInstall(bytes calldata data) external override { - (uint8 id, address owner) = abi.decode(data, (uint8, address)); - _transferOwnership(id, owner); - } - - /// @inheritdoc IPlugin - function onUninstall(bytes calldata data) external override { - uint8 id = abi.decode(data, (uint8)); - _transferOwnership(id, address(0)); - } - - /// @inheritdoc IValidation - function validateRuntime(uint8 functionId, address sender, uint256, bytes calldata, bytes calldata) - external - view - override - { - // Validate that the sender is the owner of the account or self. - if (sender != owners[functionId][msg.sender]) { - revert NotAuthorized(); - } - return; - } - - /// @inheritdoc IValidation - function validateUserOp(uint8 functionId, PackedUserOperation calldata userOp, bytes32 userOpHash) - external - view - override - returns (uint256) - { - // Validate the user op signature against the owner. - if ( - SignatureChecker.isValidSignatureNow( - owners[functionId][msg.sender], userOpHash.toEthSignedMessageHash(), userOp.signature - ) - ) { - return _SIG_VALIDATION_PASSED; - } - return _SIG_VALIDATION_FAILED; - } - - // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Execution view functions ┃ - // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - /// @inheritdoc IValidation - /// @dev The signature is valid if it is signed by the owner's private key - /// (if the owner is an EOA) or if it is a valid ERC-1271 signature from the - /// owner (if the owner is a contract). Note that unlike the signature - /// validation used in `validateUserOp`, this does///*not** wrap the digest in - /// an "Ethereum Signed Message" envelope before checking the signature in - /// the EOA-owner case. - function validateSignature(uint8 functionId, address, bytes32 digest, bytes calldata signature) - external - view - override - returns (bytes4) - { - if (SignatureChecker.isValidSignatureNow(owners[functionId][msg.sender], digest, signature)) { - return _1271_MAGIC_VALUE; - } - return _1271_INVALID; - } - - // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Plugin view functions ┃ - // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - /// @inheritdoc IPlugin - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; - return manifest; - } - - /// @inheritdoc IPlugin - function pluginMetadata() external pure virtual override returns (PluginMetadata memory) { - PluginMetadata memory metadata; - metadata.name = _NAME; - metadata.version = _VERSION; - metadata.author = _AUTHOR; - - // Permission strings - string memory modifyOwnershipPermission = "Modify Ownership"; - - // Permission descriptions - metadata.permissionDescriptors = new SelectorPermission[](1); - metadata.permissionDescriptors[0] = SelectorPermission({ - functionSelector: this.transferOwnership.selector, - permissionDescription: modifyOwnershipPermission - }); - - return metadata; - } - - // ┏━━━━━━━━━━━━━━━┓ - // ┃ EIP-165 ┃ - // ┗━━━━━━━━━━━━━━━┛ - - /// @inheritdoc BasePlugin - function supportsInterface(bytes4 interfaceId) public view override(BasePlugin, IERC165) returns (bool) { - return interfaceId == type(IValidation).interfaceId || super.supportsInterface(interfaceId); - } - - // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Internal / Private functions ┃ - // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - - // Transfers ownership and emits and event. - function _transferOwnership(uint8 id, address newOwner) internal { - address previousOwner = owners[id][msg.sender]; - owners[id][msg.sender] = newOwner; - // Todo: include id in event - emit OwnershipTransferred(msg.sender, previousOwner, newOwner); - } -} diff --git a/standard/ERCs/ERC6900Diagrams.excalidraw b/standard/ERCs/ERC6900Diagrams.excalidraw index aeef2f91..0b37d0b4 100644 --- a/standard/ERCs/ERC6900Diagrams.excalidraw +++ b/standard/ERCs/ERC6900Diagrams.excalidraw @@ -734,11 +734,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Native Function / (Plugin) Execution Function", + "text": "Native Function / (Module) Execution Function", "textAlign": "center", "verticalAlign": "middle", "containerId": "FSqrJM9FRySWN10YDIFuH", - "originalText": "Native Function / (Plugin) Execution Function", + "originalText": "Native Function / (Module) Execution Function", "lineHeight": 1.15, "baseline": 24 }, @@ -1274,11 +1274,11 @@ "locked": false, "fontSize": 20, "fontFamily": 3, - "text": "\n executeFromPlugin", + "text": "\n executeFromModule", "textAlign": "left", "verticalAlign": "top", "containerId": "V64RYeLbqd8REancwGi30", - "originalText": "\n executeFromPlugin", + "originalText": "\n executeFromModule", "lineHeight": 1.2, "baseline": 44 }, @@ -1513,11 +1513,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Permitted Plugin Execution Function", + "text": "Permitted Module Execution Function", "textAlign": "center", "verticalAlign": "middle", "containerId": "3HJCra6w4bVr0zNxhwPIg", - "originalText": "Permitted Plugin Execution Function", + "originalText": "Permitted Module Execution Function", "lineHeight": 1.15, "baseline": 24 }, @@ -1681,11 +1681,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Plugins", + "text": "Modules", "textAlign": "center", "verticalAlign": "middle", "containerId": "owBW-NHsmLmREuOjDCv06", - "originalText": "Plugins", + "originalText": "Modules", "lineHeight": 1.15, "baseline": 24 }, @@ -1720,11 +1720,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Plugin Permission Check", + "text": "Module Permission Check", "textAlign": "center", "verticalAlign": "middle", "containerId": "wBFdRyQG4wrtwbKkyNsa-", - "originalText": "Plugin Permission Check", + "originalText": "Module Permission Check", "lineHeight": 1.15, "baseline": 24 }, @@ -1796,11 +1796,11 @@ "locked": false, "fontSize": 20, "fontFamily": 3, - "text": "\n executeFromPluginExternal", + "text": "\n executeFromModuleExternal", "textAlign": "left", "verticalAlign": "top", "containerId": "dgC4KV1DfhkMlJgqA9-10", - "originalText": "\n executeFromPluginExternal", + "originalText": "\n executeFromModuleExternal", "lineHeight": 1.2, "baseline": 44 }, @@ -1876,11 +1876,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "(Plugin) Execution Function", + "text": "(Module) Execution Function", "textAlign": "center", "verticalAlign": "middle", "containerId": "j7kk61t1I6enSnhuaVZHW", - "originalText": "(Plugin) Execution Function", + "originalText": "(Module) Execution Function", "lineHeight": 1.15, "baseline": 24 }, @@ -1977,11 +1977,11 @@ "locked": false, "fontSize": 20, "fontFamily": 2, - "text": "1 calls plugin ", + "text": "1 calls module ", "textAlign": "center", "verticalAlign": "middle", "containerId": "71sd7fDDpQnB-DSlvQqP_", - "originalText": "1 calls plugin ", + "originalText": "1 calls module ", "lineHeight": 1.15, "baseline": 19 }, @@ -2133,11 +2133,11 @@ "locked": false, "fontSize": 20, "fontFamily": 2, - "text": "2.2 calls external contracts \nthrough\n executeFromPluginExternal", + "text": "2.2 calls external contracts \nthrough\n executeFromModuleExternal", "textAlign": "center", "verticalAlign": "middle", "containerId": "W_TcvUOGvsHsXmj31qq0O", - "originalText": "2.2 calls external contracts through\n executeFromPluginExternal", + "originalText": "2.2 calls external contracts through\n executeFromModuleExternal", "lineHeight": 1.15, "baseline": 65 }, @@ -2259,11 +2259,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Plugin Permission Check", + "text": "Module Permission Check", "textAlign": "center", "verticalAlign": "middle", "containerId": "K34YWjpoqir9H5g1FHSe1", - "originalText": "Plugin Permission Check", + "originalText": "Module Permission Check", "lineHeight": 1.15, "baseline": 24 }, @@ -2517,11 +2517,11 @@ "locked": false, "fontSize": 36, "fontFamily": 2, - "text": "Plugin Execution Flow", + "text": "Module Execution Flow", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "Plugin Execution Flow", + "originalText": "Module Execution Flow", "lineHeight": 1.15, "baseline": 33 }, @@ -2555,11 +2555,11 @@ "locked": false, "fontSize": 20, "fontFamily": 2, - "text": "2.1 calls other installed \nplugin through\n executeFromPlugin", + "text": "2.1 calls other installed \nmodule through\n executeFromModule", "textAlign": "center", "verticalAlign": "middle", "containerId": "fA1p68GcpvI-X1krxkJ29", - "originalText": "2.1 calls other installed plugin through\n executeFromPlugin", + "originalText": "2.1 calls other installed module through\n executeFromModule", "lineHeight": 1.15, "baseline": 65 }, @@ -2643,11 +2643,11 @@ "locked": false, "fontSize": 25.68325562404122, "fontFamily": 2, - "text": "Pre executeFromPluginExternal Hook(s)", + "text": "Pre executeFromModuleExternal Hook(s)", "textAlign": "center", "verticalAlign": "middle", "containerId": "emDobd4D91UDt1c8oZ0Iu", - "originalText": "Pre executeFromPluginExternal Hook(s)", + "originalText": "Pre executeFromModuleExternal Hook(s)", "lineHeight": 1.15, "baseline": 24 }, @@ -2727,11 +2727,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Post executeFromPluginExternal Hook(s)", + "text": "Post executeFromModuleExternal Hook(s)", "textAlign": "center", "verticalAlign": "middle", "containerId": "Hx9p9aaBWogvW3Z9zujgf", - "originalText": "Post executeFromPluginExternal Hook(s)", + "originalText": "Post executeFromModuleExternal Hook(s)", "lineHeight": 1.15, "baseline": 24 }, @@ -3325,11 +3325,11 @@ "locked": false, "fontSize": 20, "fontFamily": 3, - "text": "\n executeFromPlugin/executeFromPluginExternal", + "text": "\n executeFromModule/executeFromModuleExternal", "textAlign": "left", "verticalAlign": "top", "containerId": "2JLeKLisTh-Vc4SuDVk5b", - "originalText": "\n executeFromPlugin/executeFromPluginExternal", + "originalText": "\n executeFromModule/executeFromModuleExternal", "lineHeight": 1.2, "baseline": 44 }, @@ -3715,11 +3715,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Pre Plugin Execution Hook(s)", + "text": "Pre Module Execution Hook(s)", "textAlign": "center", "verticalAlign": "middle", "containerId": "uJ0egPTKleB3Aut1HFd2K", - "originalText": "Pre Plugin Execution Hook(s)", + "originalText": "Pre Module Execution Hook(s)", "lineHeight": 1.15, "baseline": 24 }, @@ -3793,11 +3793,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Post Plugin Execution Hook(s)", + "text": "Post Module Execution Hook(s)", "textAlign": "center", "verticalAlign": "middle", "containerId": "7Klv-GWoFcKLlBdaExqzT", - "originalText": "Post Plugin Execution Hook(s)", + "originalText": "Post Module Execution Hook(s)", "lineHeight": 1.15, "baseline": 24 }, @@ -4183,11 +4183,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Plugins", + "text": "Modules", "textAlign": "center", "verticalAlign": "middle", "containerId": "qLT39tSOFDyxvMCmG5W1h", - "originalText": "Plugins", + "originalText": "Modules", "lineHeight": 1.15, "baseline": 24 }, @@ -4781,11 +4781,11 @@ "locked": false, "fontSize": 20, "fontFamily": 3, - "text": "\n executeFromPlugin", + "text": "\n executeFromModule", "textAlign": "left", "verticalAlign": "top", "containerId": "h4-dGWuBPeBT6vEznfNUa", - "originalText": "\n executeFromPlugin", + "originalText": "\n executeFromModule", "lineHeight": 1.2, "baseline": 44 }, @@ -5171,11 +5171,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Pre Plugin Execution Hook(s)", + "text": "Pre Module Execution Hook(s)", "textAlign": "center", "verticalAlign": "middle", "containerId": "2tiy_HH9LYxsFnORyo1QN", - "originalText": "Pre Plugin Execution Hook(s)", + "originalText": "Pre Module Execution Hook(s)", "lineHeight": 1.15, "baseline": 24 }, @@ -5249,11 +5249,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Post Plugin Execution Hook(s)", + "text": "Post Module Execution Hook(s)", "textAlign": "center", "verticalAlign": "middle", "containerId": "T7fYn-6m0AnnYUsZ7Vu-6", - "originalText": "Post Plugin Execution Hook(s)", + "originalText": "Post Module Execution Hook(s)", "lineHeight": 1.15, "baseline": 24 }, @@ -5323,11 +5323,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Account-Native Function / Plugin Execution Function", + "text": "Account-Native Function / Module Execution Function", "textAlign": "center", "verticalAlign": "middle", "containerId": "Sj8y2LMf6pefmYIQqny_R", - "originalText": "Account-Native Function / Plugin Execution Function", + "originalText": "Account-Native Function / Module Execution Function", "lineHeight": 1.15, "baseline": 24 }, @@ -5639,11 +5639,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Plugins", + "text": "Modules", "textAlign": "center", "verticalAlign": "middle", "containerId": "UIOBlFmO5FdF3hqajkL-P", - "originalText": "Plugins", + "originalText": "Modules", "lineHeight": 1.15, "baseline": 24 }, @@ -6517,11 +6517,11 @@ "locked": false, "fontSize": 20, "fontFamily": 3, - "text": "\n executeFromPlugin", + "text": "\n executeFromModule", "textAlign": "left", "verticalAlign": "top", "containerId": "ejh1h99xkb4w1wuTImtiK", - "originalText": "\n executeFromPlugin", + "originalText": "\n executeFromModule", "lineHeight": 1.2, "baseline": 44 }, @@ -7207,11 +7207,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Plugins", + "text": "Modules", "textAlign": "center", "verticalAlign": "middle", "containerId": "9PEiLoAhsOByWsJZ9OKS9", - "originalText": "Plugins", + "originalText": "Modules", "lineHeight": 1.15, "baseline": 24 }, @@ -7339,11 +7339,11 @@ "locked": false, "fontSize": 25.664343526865803, "fontFamily": 2, - "text": "Plugin Permission Check", + "text": "Module Permission Check", "textAlign": "center", "verticalAlign": "middle", "containerId": "S3jaS7lY7njUd4iFC2CvV", - "originalText": "Plugin Permission Check", + "originalText": "Module Permission Check", "lineHeight": 1.15, "baseline": 24 } diff --git a/standard/ERCs/erc-6900.md b/standard/ERCs/erc-6900.md index 06b51e4d..d5d15744 100644 --- a/standard/ERCs/erc-6900.md +++ b/standard/ERCs/erc-6900.md @@ -1,6 +1,6 @@ --- eip: 6900 -title: Modular Smart Contract Accounts and Plugins +title: Modular Smart Contract Accounts and Modules description: Interfaces for composable contract accounts optionally supporting upgradability and introspection author: Adam Egyed (@adamegyed), Fangting Liu (@trinity-0111), Jay Paik (@jaypaik), Yoav Weiss (@yoavw) discussions-to: https://ethereum-magicians.org/t/eip-modular-smart-contract-accounts-and-plugins/13885 @@ -13,7 +13,7 @@ requires: 165, 4337 ## Abstract -This proposal standardizes smart contract accounts and account plugins, which are smart contract interfaces that allow for composable logic within smart contract accounts. This proposal is compliant with [ERC-4337](./eip-4337.md), and takes inspiration from [ERC-2535](./eip-2535.md) when defining interfaces for updating and querying modular function implementations. +This proposal standardizes smart contract accounts and account modules, which are smart contract interfaces that allow for composable logic within smart contract accounts. This proposal is compliant with [ERC-4337](./eip-4337.md), and takes inspiration from [ERC-2535](./eip-2535.md) when defining interfaces for updating and querying modular function implementations. This modular approach splits account functionality into three categories, implements them in external contracts, and defines an expected execution flow from accounts. @@ -21,24 +21,24 @@ This modular approach splits account functionality into three categories, implem One of the goals that ERC-4337 accomplishes is abstracting the logic for execution and validation to each smart contract account. -Many new features of accounts can be built by customizing the logic that goes into the validation and execution steps. Examples of such features include session keys, subscriptions, spending limits, and role-based access control. Currently, some of these features are implemented natively by specific smart contract accounts, and others are able to be implemented by plugin systems. Examples of proprietary plugin systems include Safe modules and ZeroDev plugins. +Many new features of accounts can be built by customizing the logic that goes into the validation and execution steps. Examples of such features include session keys, subscriptions, spending limits, and role-based access control. Currently, some of these features are implemented natively by specific smart contract accounts, and others are able to be implemented by module systems. Examples of proprietary module systems include Safe modules and ZeroDev modules. -However, managing multiple account instances provides a worse user experience, fragmenting accounts across supported features and security configurations. Additionally, it requires plugin developers to choose which platforms to support, causing either platform lock-in or duplicated development effort. +However, managing multiple account instances provides a worse user experience, fragmenting accounts across supported features and security configurations. Additionally, it requires module developers to choose which platforms to support, causing either platform lock-in or duplicated development effort. -We propose a standard that coordinates the implementation work between plugin developers and wallet developers. This standard defines a modular smart contract account capable of supporting all standard-conformant plugins. This allows users to have greater portability of their data, and for plugin developers to not have to choose specific account implementations to support. +We propose a standard that coordinates the implementation work between module developers and wallet developers. This standard defines a modular smart contract account capable of supporting all standard-conformant modules. This allows users to have greater portability of their data, and for module developers to not have to choose specific account implementations to support. -![diagram showing relationship between accounts and plugins with modular functions](../assets/eip-6900/MSCA_Shared_Components_Diagram.svg) +![diagram showing relationship between accounts and modules with modular functions](../assets/eip-6900/MSCA_Shared_Components_Diagram.svg) We take inspiration from ERC-2535's diamond pattern for routing execution based on function selectors, and create a similarly composable account. However, the standard does not require the multi-facet proxy pattern. -These plugins can contain execution logic, validation schemes, and hooks. Validation schemes define the circumstances under which the smart contract account will approve actions taken on its behalf, while hooks allow for pre- and post-execution controls. +These modules can contain execution logic, validation schemes, and hooks. Validation schemes define the circumstances under which the smart contract account will approve actions taken on its behalf, while hooks allow for pre- and post-execution controls. -Accounts adopting this standard will support modular, upgradable execution and validation logic. Defining this as a standard for smart contract accounts will make plugins easier to develop securely and will allow for greater interoperability. +Accounts adopting this standard will support modular, upgradable execution and validation logic. Defining this as a standard for smart contract accounts will make modules easier to develop securely and will allow for greater interoperability. Goals: - Provide standards for how validation, execution, and hook functions for smart contract accounts should be written. -- Provide standards for how compliant accounts should add, update, remove, and inspect plugins. +- Provide standards for how compliant accounts should add, update, remove, and inspect modules. ## Specification @@ -55,16 +55,16 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S - **User Operation Validation** functions handle calls to `validateUserOp` and check the validity of an ERC-4337 user operation. - **Runtime Validation** functions run before an execution function when not called via a user operation, and enforce checks. Common checks include allowing execution only by an owner. - An **execution function** is a smart contract function that defines the main execution step of a function for a modular account. -- The **standard execute** functions are two specific execute functions that are implemented natively by the modular account, and not on a plugin. These allow for open-ended execution. +- The **standard execute** functions are two specific execute functions that are implemented natively by the modular account, and not on a module. These allow for open-ended execution. - A **hook** is a smart contract function executed before or after another function, with the ability to modify state or cause the entire call to revert. There are four types of hooks: - **Pre User Operation Validation Hook** functions run before user operation validation functions. These can enforce permissions on what actions a validation function may perform via user operations. - **Pre Runtime Validation Hook** functions run before runtime validation functions. These can enforce permissions on what actions a validation function may perform via direct calls. - **Pre Execution Hook** functions run before an execution function. They may optionally return data to be consumed by their related post execution hook functions. - **Post Execution Hook** functions run after an execution function. They may optionally take returned data from their related pre execution hook functions. - An **associated function** refers to either a validation function or a hook. -- A **native function** refers to a function implemented natively by the modular account, as opposed to a function added by a plugin. -- A **plugin** is a deployed smart contract that hosts any amount of the above three kinds of modular functions: execution functions, validation functions, or hooks. -- A plugin **manifest** is responsible for describing the execution functions, validation functions, and hooks that will be configured on the MSCA during installation, as well as the plugin’s metadata, dependency requirements, and permissions. +- A **native function** refers to a function implemented natively by the modular account, as opposed to a function added by a module. +- A **module** is a deployed smart contract that hosts any amount of the above three kinds of modular functions: execution functions, validation functions, or hooks. +- A module **manifest** is responsible for describing the execution functions, validation functions, and hooks that will be configured on the MSCA during installation, as well as the module’s metadata, dependency requirements, and permissions. ### Overview @@ -74,12 +74,12 @@ A call to the smart contract account can be broken down into the steps as shown ![diagram showing call flow within an modular account](../assets/eip-6900/Modular_Account_Call_Flow.svg) -The following diagram shows permitted plugin execution flows. During a plugin's execution step from the above diagram, the plugin may perform a "Plugin Execution Function", using either `executeFromPlugin` or `executeFromPluginExternal`. These can be used by plugins to execute using the account's context. +The following diagram shows permitted module execution flows. During a module's execution step from the above diagram, the module may perform a "Module Execution Function", using either `executeFromModule` or `executeFromModuleExternal`. These can be used by modules to execute using the account's context. -- `executeFromPlugin` handles calls to other installed plugin's execution function on the modular account. -- `executeFromPluginExternal` handles calls to external addresses. +- `executeFromModule` handles calls to other installed module's execution function on the modular account. +- `executeFromModuleExternal` handles calls to external addresses. -![diagram showing a plugin execution flow](../assets/eip-6900/Plugin_Execution_Flow.svg) +![diagram showing a module execution flow](../assets/eip-6900/Module_Execution_Flow.svg) Each step is modular, supporting different implementations for each execution function, and composable, supporting multiple steps through hooks. Combined, these allow for open-ended programmable accounts. @@ -88,52 +88,52 @@ Each step is modular, supporting different implementations for each execution fu **Modular Smart Contract Accounts** **MUST** implement - `IAccount.sol` from [ERC-4337](./eip-4337.md). -- `IPluginManager.sol` to support installing and uninstalling plugins. -- `IStandardExecutor.sol` to support open-ended execution. **Calls to plugins through this SHOULD revert.** -- `IPluginExecutor.sol` to support execution from plugins. **Calls to plugins through `executeFromPluginExternal` SHOULD revert.** +- `IModuleManager.sol` to support installing and uninstalling modules. +- `IStandardExecutor.sol` to support open-ended execution. **Calls to modules through this SHOULD revert.** +- `IModuleExecutor.sol` to support execution from modules. **Calls to modules through `executeFromModuleExternal` SHOULD revert.** **Modular Smart Contract Accounts** **MAY** implement -- `IAccountLoupe.sol` to support visibility in plugin configuration on-chain. +- `IAccountLoupe.sol` to support visibility in module configuration on-chain. -**Plugins** **MUST** implement +**Modules** **MUST** implement -- `IPlugin.sol` described below and implement [ERC-165](./eip-165.md) for `IPlugin`. +- `IModule.sol` described below and implement [ERC-165](./eip-165.md) for `IModule`. -#### `IPluginManager.sol` +#### `IModuleManager.sol` -Plugin manager interface. Modular Smart Contract Accounts **MUST** implement this interface to support installing and uninstalling plugins. +Module manager interface. Modular Smart Contract Accounts **MUST** implement this interface to support installing and uninstalling modules. ```solidity // Treats the first 20 bytes as an address, and the last byte as a function identifier. -type FunctionReference is bytes21; +type ModuleEntity is bytes21; -interface IPluginManager { - event PluginInstalled(address indexed plugin, bytes32 manifestHash, FunctionReference[] dependencies); +interface IModuleManager { + event ModuleInstalled(address indexed module, bytes32 manifestHash, ModuleEntity[] dependencies); - event PluginUninstalled(address indexed plugin, bool indexed onUninstallSucceeded); + event ModuleUninstalled(address indexed module, bool indexed onUninstallSucceeded); - /// @notice Install a plugin to the modular account. - /// @param plugin The plugin to install. - /// @param manifestHash The hash of the plugin manifest. - /// @param pluginInstallData Optional data to be decoded and used by the plugin to setup initial plugin data + /// @notice Install a module to the modular account. + /// @param module The module to install. + /// @param manifestHash The hash of the module manifest. + /// @param moduleInstallData Optional data to be decoded and used by the module to setup initial module data /// for the modular account. - /// @param dependencies The dependencies of the plugin, as described in the manifest. Each FunctionReference - /// MUST be composed of an installed plugin's address and a function ID of its validation function. - function installPlugin( - address plugin, + /// @param dependencies The dependencies of the module, as described in the manifest. Each ModuleEntity + /// MUST be composed of an installed module's address and a function ID of its validation function. + function installModule( + address module, bytes32 manifestHash, - bytes calldata pluginInstallData, - FunctionReference[] calldata dependencies + bytes calldata moduleInstallData, + ModuleEntity[] calldata dependencies ) external; - /// @notice Uninstall a plugin from the modular account. - /// @param plugin The plugin to uninstall. + /// @notice Uninstall a module from the modular account. + /// @param module The module to uninstall. /// @param config An optional, implementation-specific field that accounts may use to ensure consistency /// guarantees. - /// @param pluginUninstallData Optional data to be decoded and used by the plugin to clear plugin data for the + /// @param moduleUninstallData Optional data to be decoded and used by the module to clear module data for the /// modular account. - function uninstallPlugin(address plugin, bytes calldata config, bytes calldata pluginUninstallData) external; + function uninstallModule(address module, bytes calldata config, bytes calldata moduleUninstallData) external; } ``` @@ -142,9 +142,9 @@ interface IPluginManager { Standard execute interface. Modular Smart Contract Accounts **MUST** implement this interface to support open-ended execution. -Standard execute functions SHOULD check whether the call's target implements the `IPlugin` interface via ERC-165. +Standard execute functions SHOULD check whether the call's target implements the `IModule` interface via ERC-165. -**If the target is a plugin, the call SHOULD revert.** This prevents accidental misconfiguration or misuse of plugins (both installed and uninstalled). +**If the target is a module, the call SHOULD revert.** This prevents accidental misconfiguration or misuse of modules (both installed and uninstalled). ```solidity struct Call { @@ -158,7 +158,7 @@ struct Call { interface IStandardExecutor { /// @notice Standard execute method. - /// @dev If the target is a plugin, the call SHOULD revert. + /// @dev If the target is a module, the call SHOULD revert. /// @param target The target address for account to call. /// @param value The value to send with the call. /// @param data The calldata for the call. @@ -166,7 +166,7 @@ interface IStandardExecutor { function execute(address target, uint256 value, bytes calldata data) external payable returns (bytes memory); /// @notice Standard executeBatch method. - /// @dev If the target is a plugin, the call SHOULD revert. If any of the calls revert, the entire batch MUST + /// @dev If the target is a module, the call SHOULD revert. If any of the calls revert, the entire batch MUST /// revert. /// @param calls The array of calls. /// @return An array containing the return data from the calls. @@ -174,32 +174,32 @@ interface IStandardExecutor { } ``` -#### `IPluginExecutor.sol` +#### `IModuleExecutor.sol` -Execution interface for calls made from plugins. Modular Smart Contract Accounts **MUST** implement this interface to support execution from plugins. +Execution interface for calls made from modules. Modular Smart Contract Accounts **MUST** implement this interface to support execution from modules. -The `executeFromPluginExternal` function SHOULD check whether the call's target implements the `IPlugin` interface via ERC-165. +The `executeFromModuleExternal` function SHOULD check whether the call's target implements the `IModule` interface via ERC-165. -**If the target of `executeFromPluginExternal` function is a plugin, the call SHOULD revert.** +**If the target of `executeFromModuleExternal` function is a module, the call SHOULD revert.** -This prevents accidental misconfiguration or misuse of plugins (both installed and uninstalled). Installed plugins MAY interact with other installed plugins via the `executeFromPlugin` function. +This prevents accidental misconfiguration or misuse of modules (both installed and uninstalled). Installed modules MAY interact with other installed modules via the `executeFromModule` function. ```solidity -interface IPluginExecutor { - /// @notice Execute a call from a plugin through the account. - /// @dev Permissions must be granted to the calling plugin for the call to go through. +interface IModuleExecutor { + /// @notice Execute a call from a module through the account. + /// @dev Permissions must be granted to the calling module for the call to go through. /// @param data The calldata to send to the account. /// @return The return data from the call. - function executeFromPlugin(bytes calldata data) external payable returns (bytes memory); + function executeFromModule(bytes calldata data) external payable returns (bytes memory); - /// @notice Execute a call from a plugin to a non-plugin address. - /// @dev If the target is a plugin, the call SHOULD revert. Permissions must be granted to the calling plugin + /// @notice Execute a call from a module to a non-module address. + /// @dev If the target is a module, the call SHOULD revert. Permissions must be granted to the calling module /// for the call to go through. /// @param target The address to be called. /// @param value The value to send with the call. /// @param data The calldata to send to the target. /// @return The return data from the call. - function executeFromPluginExternal(address target, uint256 value, bytes calldata data) + function executeFromModuleExternal(address target, uint256 value, bytes calldata data) external payable returns (bytes memory); @@ -208,26 +208,26 @@ interface IPluginExecutor { #### `IAccountLoupe.sol` -Plugin inspection interface. Modular Smart Contract Accounts **MAY** implement this interface to support visibility in plugin configuration on-chain. +Module inspection interface. Modular Smart Contract Accounts **MAY** implement this interface to support visibility in module configuration on-chain. ```solidity interface IAccountLoupe { /// @notice Config for an execution function, given a selector. struct ExecutionFunctionConfig { - address plugin; - FunctionReference validationFunction; + address module; + ModuleEntity validationFunction; } /// @notice Pre and post hooks for a given selector. /// @dev It's possible for one of either `preExecHook` or `postExecHook` to be empty. struct ExecutionHooks { - FunctionReference hookFunction; + ModuleEntity hookFunction; bool isPreHook; bool isPostHook; } - /// @notice Get the validation functions and plugin address for a selector. - /// @dev If the selector is a native function, the plugin address will be the address of the account. + /// @notice Get the validation functions and module address for a selector. + /// @dev If the selector is a native function, the module address will be the address of the account. /// @param selector The selector to get the configuration for. /// @return The configuration for this selector. function getExecutionFunctionConfig(bytes4 selector) external view returns (ExecutionFunctionConfig memory); @@ -243,104 +243,104 @@ interface IAccountLoupe { function getPreValidationHooks(bytes4 selector) external view - returns (FunctionReference[] memory preValidationHooks); + returns (ModuleEntity[] memory preValidationHooks); - /// @notice Get an array of all installed plugins. - /// @return The addresses of all installed plugins. - function getInstalledPlugins() external view returns (address[] memory); + /// @notice Get an array of all installed modules. + /// @return The addresses of all installed modules. + function getInstalledModules() external view returns (address[] memory); } ``` -#### `IPlugin.sol` +#### `IModule.sol` -Plugin interface. Plugins **MUST** implement this interface to support plugin management and interactions with MSCAs. +Module interface. Modules **MUST** implement this interface to support module management and interactions with MSCAs. ```solidity -interface IPlugin { - /// @notice Initialize plugin data for the modular account. - /// @dev Called by the modular account during `installPlugin`. - /// @param data Optional bytes array to be decoded and used by the plugin to setup initial plugin data for the modular account. +interface IModule { + /// @notice Initialize module data for the modular account. + /// @dev Called by the modular account during `installModule`. + /// @param data Optional bytes array to be decoded and used by the module to setup initial module data for the modular account. function onInstall(bytes calldata data) external; - /// @notice Clear plugin data for the modular account. - /// @dev Called by the modular account during `uninstallPlugin`. - /// @param data Optional bytes array to be decoded and used by the plugin to clear plugin data for the modular account. + /// @notice Clear module data for the modular account. + /// @dev Called by the modular account during `uninstallModule`. + /// @param data Optional bytes array to be decoded and used by the module to clear module data for the modular account. function onUninstall(bytes calldata data) external; - /// @notice Run the pre user operation validation hook specified by the `functionId`. + /// @notice Run the pre user operation validation hook specified by the `entityId`. /// @dev Pre user operation validation hooks MUST NOT return an authorizer value other than 0 or 1. - /// @param functionId An identifier that routes the call to different internal implementations, should there be more than one. + /// @param entityId An identifier that routes the call to different internal implementations, should there be more than one. /// @param userOp The user operation. /// @param userOpHash The user operation hash. /// @return Packed validation data for validAfter (6 bytes), validUntil (6 bytes), and authorizer (20 bytes). - function preUserOpValidationHook(uint8 functionId, PackedUserOperation memory userOp, bytes32 userOpHash) external returns (uint256); + function preUserOpValidationHook(uint8 entityId, PackedUserOperation memory userOp, bytes32 userOpHash) external returns (uint256); - /// @notice Run the user operation validationFunction specified by the `functionId`. - /// @param functionId An identifier that routes the call to different internal implementations, should there be + /// @notice Run the user operation validationFunction specified by the `entityId`. + /// @param entityId An identifier that routes the call to different internal implementations, should there be /// more than one. /// @param userOp The user operation. /// @param userOpHash The user operation hash. /// @return Packed validation data for validAfter (6 bytes), validUntil (6 bytes), and authorizer (20 bytes). - function userOpValidationFunction(uint8 functionId, PackedUserOperation calldata userOp, bytes32 userOpHash) + function userOpValidationFunction(uint8 entityId, PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); - /// @notice Run the pre runtime validation hook specified by the `functionId`. + /// @notice Run the pre runtime validation hook specified by the `entityId`. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be more than one. + /// @param entityId An identifier that routes the call to different internal implementations, should there be more than one. /// @param sender The caller address. /// @param value The call value. /// @param data The calldata sent. - function preRuntimeValidationHook(uint8 functionId, address sender, uint256 value, bytes calldata data) external; + function preRuntimeValidationHook(uint8 entityId, address sender, uint256 value, bytes calldata data) external; - /// @notice Run the runtime validationFunction specified by the `functionId`. + /// @notice Run the runtime validationFunction specified by the `entityId`. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be + /// @param entityId An identifier that routes the call to different internal implementations, should there be /// more than one. /// @param sender The caller address. /// @param value The call value. /// @param data The calldata sent. - function runtimeValidationFunction(uint8 functionId, address sender, uint256 value, bytes calldata data) + function runtimeValidationFunction(uint8 entityId, address sender, uint256 value, bytes calldata data) external; - /// @notice Run the pre execution hook specified by the `functionId`. + /// @notice Run the pre execution hook specified by the `entityId`. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be more than one. + /// @param entityId An identifier that routes the call to different internal implementations, should there be more than one. /// @param sender The caller address. /// @param value The call value. /// @param data The calldata sent. /// @return Context to pass to a post execution hook, if present. An empty bytes array MAY be returned. - function preExecutionHook(uint8 functionId, address sender, uint256 value, bytes calldata data) external returns (bytes memory); + function preExecutionHook(uint32 entityId, address sender, uint256 value, bytes calldata data) external returns (bytes memory); - /// @notice Run the post execution hook specified by the `functionId`. + /// @notice Run the post execution hook specified by the `entityId`. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be more than one. + /// @param entityId An identifier that routes the call to different internal implementations, should there be more than one. /// @param preExecHookData The context returned by its associated pre execution hook. - function postExecutionHook(uint8 functionId, bytes calldata preExecHookData) external; + function postExecutionHook(uint32 entityId, bytes calldata preExecHookData) external; - /// @notice Describe the contents and intended configuration of the plugin. + /// @notice Describe the contents and intended configuration of the module. /// @dev This manifest MUST stay constant over time. - /// @return A manifest describing the contents and intended configuration of the plugin. - function pluginManifest() external pure returns (PluginManifest memory); + /// @return A manifest describing the contents and intended configuration of the module. + function moduleManifest() external pure returns (ModuleManifest memory); - /// @notice Describe the metadata of the plugin. + /// @notice Describe the metadata of the module. /// @dev This metadata MUST stay constant over time. - /// @return A metadata struct describing the plugin. - function pluginMetadata() external pure returns (PluginMetadata memory); + /// @return A metadata struct describing the module. + function moduleMetadata() external pure returns (ModuleMetadata memory); } ``` -### Plugin manifest +### Module manifest -The plugin manifest is responsible for describing the execution functions, validation functions, and hooks that will be configured on the MSCA during installation, as well as the plugin's metadata, dependencies, and permissions. +The module manifest is responsible for describing the execution functions, validation functions, and hooks that will be configured on the MSCA during installation, as well as the module's metadata, dependencies, and permissions. ```solidity enum ManifestAssociatedFunctionType { // Function is not defined. NONE, - // Function belongs to this plugin. + // Function belongs to this module. SELF, - // Function belongs to an external plugin provided as a dependency during plugin installation. Plugins MAY depend + // Function belongs to an external module provided as a dependency during module installation. Modules MAY depend // on external validation functions. It MUST NOT depend on external hooks, or installation will fail. DEPENDENCY, // Resolves to a magic value to always bypass runtime validation for a given function. @@ -355,11 +355,11 @@ enum ManifestAssociatedFunctionType { PRE_HOOK_ALWAYS_DENY } -/// @dev For functions of type `ManifestAssociatedFunctionType.DEPENDENCY`, the MSCA MUST find the plugin address -/// of the function at `dependencies[dependencyIndex]` during the call to `installPlugin(config)`. +/// @dev For functions of type `ManifestAssociatedFunctionType.DEPENDENCY`, the MSCA MUST find the module address +/// of the function at `dependencies[dependencyIndex]` during the call to `installModule(config)`. struct ManifestFunction { ManifestAssociatedFunctionType functionType; - uint8 functionId; + uint8 entityId; uint256 dependencyIndex; } @@ -371,7 +371,7 @@ struct ManifestAssociatedFunction { struct ManifestExecutionHook { // TODO(erc6900 spec): These fields can be packed into a single word bytes4 executionSelector; - uint8 functionId; + uint8 entityId; bool isPreHook; bool isPostHook; } @@ -387,37 +387,37 @@ struct SelectorPermission { string permissionDescription; } -/// @dev A struct holding fields to describe the plugin in a purely view context. Intended for front end clients. -struct PluginMetadata { - // A human-readable name of the plugin. +/// @dev A struct holding fields to describe the module in a purely view context. Intended for front end clients. +struct ModuleMetadata { + // A human-readable name of the module. string name; - // The version of the plugin, following the semantic versioning scheme. + // The version of the module, following the semantic versioning scheme. string version; // The author field SHOULD be a username representing the identity of the user or organization - // that created this plugin. + // that created this module. string author; // String descriptions of the relative sensitivity of specific functions. The selectors MUST be selectors for - // functions implemented by this plugin. + // functions implemented by this module. SelectorPermission[] permissionDescriptors; } -/// @dev A struct describing how the plugin should be installed on a modular account. -struct PluginManifest { +/// @dev A struct describing how the module should be installed on a modular account. +struct ModuleManifest { // List of ERC-165 interface IDs to add to account to support introspection checks. This MUST NOT include - // IPlugin's interface ID. + // IModule's interface ID. bytes4[] interfaceIds; - // If this plugin depends on other plugins' validation functions, the interface IDs of those plugins MUST be + // If this module depends on other modules' validation functions, the interface IDs of those modules MUST be // provided here, with its position in the array matching the `dependencyIndex` members of `ManifestFunction` // structs used in the manifest. bytes4[] dependencyInterfaceIds; - // Execution functions defined in this plugin to be installed on the MSCA. + // Execution functions defined in this module to be installed on the MSCA. bytes4[] executionFunctions; - // Plugin execution functions already installed on the MSCA that this plugin will be able to call. + // Module execution functions already installed on the MSCA that this module will be able to call. bytes4[] permittedExecutionSelectors; - // Boolean to indicate whether the plugin can call any external address. + // Boolean to indicate whether the module can call any external address. bool permitAnyExternalAddress; - // Boolean to indicate whether the plugin needs access to spend native tokens of the account. If false, the - // plugin MUST still be able to spend up to the balance that it sends to the account in the same call. + // Boolean to indicate whether the module needs access to spend native tokens of the account. If false, the + // module MUST still be able to spend up to the balance that it sends to the account in the same call. bool canSpendNativeToken; ManifestExternalCallPermission[] permittedExternalCalls; ManifestAssociatedFunction[] validationFunctions; @@ -429,34 +429,34 @@ struct PluginManifest { ### Expected behavior -#### Responsibilties of `StandardExecutor` and `PluginExecutor` +#### Responsibilties of `StandardExecutor` and `ModuleExecutor` `StandardExecutor` functions are used for open-ended calls to external addresses. -`PluginExecutor` functions are specifically used by plugins to request the account to execute with account's context. Explicit permissions are required for plugins to use `PluginExecutor`. +`ModuleExecutor` functions are specifically used by modules to request the account to execute with account's context. Explicit permissions are required for modules to use `ModuleExecutor`. The following behavior MUST be followed: -- `StandardExecutor` can NOT call plugin execution functions and/or `PluginExecutor`. This is guaranteed by checking whether the call's target implements the `IPlugin` interface via ERC-165 as required. -- `StandardExecutor` can NOT be called by plugin execution functions and/or `PluginExecutor`. -- Plugin execution functions MUST NOT request access to `StandardExecutor`, they MAY request access to `PluginExecutor`. +- `StandardExecutor` can NOT call module execution functions and/or `ModuleExecutor`. This is guaranteed by checking whether the call's target implements the `IModule` interface via ERC-165 as required. +- `StandardExecutor` can NOT be called by module execution functions and/or `ModuleExecutor`. +- Module execution functions MUST NOT request access to `StandardExecutor`, they MAY request access to `ModuleExecutor`. -#### Calls to `installPlugin` +#### Calls to `installModule` -The function `installPlugin` accepts 4 parameters: the address of the plugin to install, the Keccak-256 hash of the plugin's manifest, ABI-encoded data to pass to the plugin's `onInstall` callback, and an array of function references that represent the plugin's install dependencies. +The function `installModule` accepts 3 parameters: the address of the module to install, the Keccak-256 hash of the module's manifest, ABI-encoded data to pass to the module's `onInstall` callback. -The function MUST retrieve the plugin's manifest by calling `pluginManifest()` using `staticcall`. +The function MUST retrieve the module's manifest by calling `moduleManifest()` using `staticcall`. The function MUST perform the following preliminary checks: -- Revert if the plugin has already been installed on the modular account. -- Revert if the plugin does not implement ERC-165 or does not support the `IPlugin` interface. -- Revert if `manifestHash` does not match the computed Keccak-256 hash of the plugin's returned manifest. This prevents installation of plugins that attempt to install a different plugin configuration than the one that was approved by the client. +- Revert if the module has already been installed on the modular account. +- Revert if the module does not implement ERC-165 or does not support the `IModule` interface. +- Revert if `manifestHash` does not match the computed Keccak-256 hash of the module's returned manifest. This prevents installation of modules that attempt to install a different module configuration than the one that was approved by the client. - Revert if any address in `dependencies` does not support the interface at its matching index in the manifest's `dependencyInterfaceIds`, or if the two array lengths do not match, or if any of the dependencies are not already installed on the modular account. -The function MUST record the manifest hash and dependencies that were used for the plugin's installation. Each dependency's record MUST also be updated to reflect that it has a new dependent. These records MUST be used to ensure calls to `uninstallPlugin` are comprehensive and undo all edited configuration state from installation. The mechanism by which these records are stored and validated is up to the implementation. +The function MUST record the manifest hash and dependencies that were used for the module's installation. Each dependency's record MUST also be updated to reflect that it has a new dependent. These records MUST be used to ensure calls to `uninstallModule` are comprehensive and undo all edited configuration state from installation. The mechanism by which these records are stored and validated is up to the implementation. -The function MUST store the plugin's permitted function selectors, permitted external calls, and whether it can spend the account's native tokens, to be able to validate calls to `executeFromPlugin` and `executeFromPluginExternal`. +The function MUST store the module's permitted function selectors, permitted external calls, and whether it can spend the account's native tokens, to be able to validate calls to `executeFromModule` and `executeFromModuleExternal`. The function MUST parse through the execution functions, validation functions, and hooks in the manifest and add them to the modular account after resolving each `ManifestFunction` type. @@ -465,36 +465,36 @@ The function MUST parse through the execution functions, validation functions, a The function MAY store the interface IDs provided in the manifest's `interfaceIds` and update its `supportsInterface` behavior accordingly. -Next, the function MUST call the plugin's `onInstall` callback with the data provided in the `pluginInstallData` parameter. This serves to initialize the plugin state for the modular account. If `onInstall` reverts, the `installPlugin` function MUST revert. +Next, the function MUST call the module's `onInstall` callback with the data provided in the `moduleInstallData` parameter. This serves to initialize the module state for the modular account. If `onInstall` reverts, the `installModule` function MUST revert. -Finally, the function MUST emit the event `PluginInstalled` with the plugin's address, the hash of its manifest, and the dependencies that were used. +Finally, the function MUST emit the event `ModuleInstalled` with the module's address, the hash of its manifest, and the dependencies that were used. -> **⚠️ The ability to install and uninstall plugins is very powerful. The security of these functions determines the security of the account. It is critical for modular account implementers to make sure the implementation of the functions in `IPluginManager` have the proper security consideration and access control in place.** +> **⚠️ The ability to install and uninstall modules is very powerful. The security of these functions determines the security of the account. It is critical for modular account implementers to make sure the implementation of the functions in `IModuleManager` have the proper security consideration and access control in place.** -#### Calls to `uninstallPlugin` +#### Calls to `uninstallModule` -The function `uninstallPlugin` accepts 3 parameters: the address of the plugin to uninstall, a bytes field that may have custom requirements or uses by the implementing account, and ABI-encoded data to pass to the plugin's `onUninstall` callback. +The function `uninstallModule` accepts 3 parameters: the address of the module to uninstall, a bytes field that may have custom requirements or uses by the implementing account, and ABI-encoded data to pass to the module's `onUninstall` callback. -The function MUST revert if the plugin is not installed on the modular account. +The function MUST revert if the module is not installed on the modular account. The function SHOULD perform the following checks: -- Revert if the hash of the manifest used at install time does not match the computed Keccak-256 hash of the plugin's current manifest. This prevents unclean removal of plugins that attempt to force a removal of a different plugin configuration than the one that was originally approved by the client for installation. To allow for removal of such plugins, the modular account MAY implement the capability for the manifest to be encoded in the config field as a parameter. -- Revert if there is at least 1 other installed plugin that depends on validation functions added by this plugin. Plugins used as dependencies SHOULD NOT be uninstalled while dependent plugins exist. +- Revert if the hash of the manifest used at install time does not match the computed Keccak-256 hash of the module's current manifest. This prevents unclean removal of modules that attempt to force a removal of a different module configuration than the one that was originally approved by the client for installation. To allow for removal of such modules, the modular account MAY implement the capability for the manifest to be encoded in the config field as a parameter. +- Revert if there is at least 1 other installed module that depends on validation functions added by this module. Modules used as dependencies SHOULD NOT be uninstalled while dependent modules exist. -The function SHOULD update account storage to reflect the uninstall via inspection functions, such as those defined by `IAccountLoupe`. Each dependency's record SHOULD also be updated to reflect that it has no longer has this plugin as a dependent. +The function SHOULD update account storage to reflect the uninstall via inspection functions, such as those defined by `IAccountLoupe`. Each dependency's record SHOULD also be updated to reflect that it has no longer has this module as a dependent. -The function MUST remove records for the plugin's manifest hash, dependencies, permitted function selectors, permitted external calls, and whether it can spend the account's native tokens. +The function MUST remove records for the module's manifest hash, dependencies, permitted function selectors, permitted external calls, and whether it can spend the account's native tokens. -The function MUST parse through the execution functions, validation functions, and hooks in the manifest and remove them from the modular account after resolving each `ManifestFunction` type. If multiple plugins added the same hook, it MUST persist until the last plugin is uninstalled. +The function MUST parse through the execution functions, validation functions, and hooks in the manifest and remove them from the modular account after resolving each `ManifestFunction` type. If multiple modules added the same hook, it MUST persist until the last module is uninstalled. -If the account stored the interface IDs provided in the manifest's `interfaceIds` during installation, it MUST remove them and update its `supportsInterface` behavior accordingly. If multiple plugins added the same interface ID, it MUST persist until the last plugin is uninstalled. +If the account stored the interface IDs provided in the manifest's `interfaceIds` during installation, it MUST remove them and update its `supportsInterface` behavior accordingly. If multiple modules added the same interface ID, it MUST persist until the last module is uninstalled. -Next, the function MUST call the plugin's `onUninstall` callback with the data provided in the `pluginUninstallData` parameter. This serves to clear the plugin state for the modular account. If `onUninstall` reverts, execution SHOULD continue to allow the uninstall to complete. +Next, the function MUST call the module's `onUninstall` callback with the data provided in the `moduleUninstallData` parameter. This serves to clear the module state for the modular account. If `onUninstall` reverts, execution SHOULD continue to allow the uninstall to complete. -Finally, the function MUST emit the event `PluginUninstalled` with the plugin's address and whether the `onUninstall` callback succeeded. +Finally, the function MUST emit the event `ModuleUninstalled` with the module's address and whether the `onUninstall` callback succeeded. -> **⚠️ Incorrectly uninstalled plugins can prevent uninstalls of their dependencies. Therefore, some form of validation that the uninstall step completely and correctly removes the plugin and its usage of dependencies is required.** +> **⚠️ Incorrectly uninstalled modules can prevent uninstalls of their dependencies. Therefore, some form of validation that the uninstall step completely and correctly removes the module and its usage of dependencies is required.** #### Calls to `validateUserOp` @@ -506,26 +506,26 @@ Then, the modular account MUST execute the validation function with the user ope #### Calls to execution functions -When a function other than a native function is called on an modular account, it MUST find the plugin configuration for the corresponding selector added via plugin installation. If no corresponding plugin is found, the modular account MUST revert. Otherwise, the following steps MUST be performed. +When a function other than a native function is called on an modular account, it MUST find the module configuration for the corresponding selector added via module installation. If no corresponding module is found, the modular account MUST revert. Otherwise, the following steps MUST be performed. -Additionally, when the modular account natively implements functions in `IPluginManager` and `IStandardExecutor`, the same following steps MUST be performed for those functions. Other native functions MAY perform these steps. +Additionally, when the modular account natively implements functions in `IModuleManager` and `IStandardExecutor`, the same following steps MUST be performed for those functions. Other native functions MAY perform these steps. The steps to perform are: - If the call is not from the `EntryPoint`, then find an associated runtime validation function. If one does not exist, execution MUST revert. The modular account MUST execute all pre runtime validation hooks, then the runtime validation function, with the `call` opcode. All of these functions MUST receive the caller, value, and execution function's calldata as parameters. If any of these functions revert, execution MUST revert. If any pre execution hooks are set to `PRE_HOOK_ALWAYS_DENY`, execution MUST revert. If the validation function is set to `RUNTIME_VALIDATION_ALWAYS_ALLOW`, the runtime validation function MUST be bypassed. -- If there are pre execution hooks defined for the execution function, execute those hooks with the caller, value, and execution function's calldata as parameters. If any of these hooks returns data, it MUST be preserved until the call to the post execution hook. The operation MUST be done with the `call` opcode. If there are duplicate pre execution hooks (i.e., hooks with identical `FunctionReference`s), run the hook only once. If any of these functions revert, execution MUST revert. +- If there are pre execution hooks defined for the execution function, execute those hooks with the caller, value, and execution function's calldata as parameters. If any of these hooks returns data, it MUST be preserved until the call to the post execution hook. The operation MUST be done with the `call` opcode. If there are duplicate pre execution hooks (i.e., hooks with identical `ModuleEntity`s), run the hook only once. If any of these functions revert, execution MUST revert. - Run the execution function. - If any post execution hooks are defined, run the functions. If a pre execution hook returned data to the account, that data MUST be passed as a parameter to the associated post execution hook. The operation MUST be done with the `call` opcode. If there are duplicate post execution hooks, run them once for each unique associated pre execution hook. For post execution hooks without an associated pre execution hook, run the hook only once. If any of these functions revert, execution MUST revert. -The set of hooks run for a given execution function MUST be the hooks specified by account state at the start of the execution phase. This is relevant for functions like `installPlugin` and `uninstallPlugin`, which modify the account state, and possibly other execution or native functions as well. +The set of hooks run for a given execution function MUST be the hooks specified by account state at the start of the execution phase. This is relevant for functions like `installModule` and `uninstallModule`, which modify the account state, and possibly other execution or native functions as well. -#### Calls made from plugins +#### Calls made from modules -Plugins MAY interact with other plugins and external addresses through the modular account using the functions defined in the `IPluginExecutor` interface. These functions MAY be called without a defined validation function, but the modular account MUST enforce these checks and behaviors: +Modules MAY interact with other modules and external addresses through the modular account using the functions defined in the `IModuleExecutor` interface. These functions MAY be called without a defined validation function, but the modular account MUST enforce these checks and behaviors: -The `executeFromPlugin` function MUST allow plugins to call execution functions installed by plugins on the modular account. Hooks matching the function selector provided in `data` MUST be called. If the calling plugin's manifest did not include the provided function selector within `permittedExecutionSelectors` at the time of installation, execution MUST revert. +The `executeFromModule` function MUST allow modules to call execution functions installed by modules on the modular account. Hooks matching the function selector provided in `data` MUST be called. If the calling module's manifest did not include the provided function selector within `permittedExecutionSelectors` at the time of installation, execution MUST revert. -The `executeFromPluginExternal` function MUST allow plugins to call external addresses as specified by its parameters on behalf of the modular account. If the calling plugin's manifest did not explicitly allow the external call within `permittedExternalCalls` at the time of installation, execution MUST revert. +The `executeFromModuleExternal` function MUST allow modules to call external addresses as specified by its parameters on behalf of the modular account. If the calling module's manifest did not explicitly allow the external call within `permittedExternalCalls` at the time of installation, execution MUST revert. ## Rationale @@ -533,7 +533,7 @@ ERC-4337 compatible accounts must implement the `IAccount` interface, which cons The function routing pattern of ERC-2535 is the logical starting point for achieving this extension into multi-functional accounts. It also meets our other primary design rationale of generalizing execution calls across multiple implementing contracts. However, a strict diamond pattern is constrained by its inability to customize validation schemes for specific execution functions in the context of `validateUserOp`, and its requirement of `delegatecall`. -This proposal includes several interfaces that build on ERC-4337 and are inspired by ERC-2535. First, we standardize a set of modular functions that allow smart contract developers greater flexibility in bundling validation, execution, and hook logic. We also propose interfaces that take inspiration from the diamond standard and provide methods for querying execution functions, validation functions, and hooks on a modular account. The rest of the interfaces describe a plugin's methods for exposing its modular functions and desired configuration, and the modular account's methods for installing and removing plugins and allowing execution across plugins and external addresses. +This proposal includes several interfaces that build on ERC-4337 and are inspired by ERC-2535. First, we standardize a set of modular functions that allow smart contract developers greater flexibility in bundling validation, execution, and hook logic. We also propose interfaces that take inspiration from the diamond standard and provide methods for querying execution functions, validation functions, and hooks on a modular account. The rest of the interfaces describe a module's methods for exposing its modular functions and desired configuration, and the modular account's methods for installing and removing modules and allowing execution across modules and external addresses. ## Backwards Compatibility diff --git a/standard/assets/eip-6900/MSCA_Shared_Components_Diagram.svg b/standard/assets/eip-6900/MSCA_Shared_Components_Diagram.svg index 836a8ad9..e443dd56 100644 --- a/standard/assets/eip-6900/MSCA_Shared_Components_Diagram.svg +++ b/standard/assets/eip-6900/MSCA_Shared_Components_Diagram.svg @@ -14,4 +14,4 @@ - Alice's MSCAUO / TxPluginHooksValidationExecutionPluginHooksValidationExecutionUO / TxBob's MSCA \ No newline at end of file + Alice's MSCAUO / TxModuleHooksValidationExecutionModuleHooksValidationExecutionUO / TxBob's MSCA \ No newline at end of file diff --git a/standard/assets/eip-6900/Modular_Account_Call_Flow.svg b/standard/assets/eip-6900/Modular_Account_Call_Flow.svg index c4112c33..8d7f7611 100644 --- a/standard/assets/eip-6900/Modular_Account_Call_Flow.svg +++ b/standard/assets/eip-6900/Modular_Account_Call_Flow.svg @@ -14,4 +14,4 @@ - Direct Call1 validation2. Execution Modular Account validateUserOpPre User Operation Validation Hook(s)User Operation Validation FunctionPre Execution Hook(s)Native Function / (Plugin) Execution FunctionPost Execution Hook(s)EntryPointEOA / SCRuntime Validation FunctionModular Account Call FlowPre Runtime Validation Hook(s) \ No newline at end of file + Direct Call1 validation2. Execution Modular Account validateUserOpPre User Operation Validation Hook(s)User Operation Validation FunctionPre Execution Hook(s)Native Function / (Module) Execution FunctionPost Execution Hook(s)EntryPointEOA / SCRuntime Validation FunctionModular Account Call FlowPre Runtime Validation Hook(s) \ No newline at end of file diff --git a/standard/assets/eip-6900/Plugin_Execution_Flow.svg b/standard/assets/eip-6900/Plugin_Execution_Flow.svg index 3a94b512..ed1ff580 100644 --- a/standard/assets/eip-6900/Plugin_Execution_Flow.svg +++ b/standard/assets/eip-6900/Plugin_Execution_Flow.svg @@ -18,4 +18,4 @@ - Permitted External Contracts & Methods executeFromPluginPre Execution Hook(s)Permitted Plugin Execution FunctionPost Execution Hook(s)PluginsPlugin Permission Check executeFromPluginExternal(Plugin) Execution Function1 calls plugin 2.2 calls external contracts through executeFromPluginExternalPlugin Permission CheckModular AccountPlugin Execution Flow2.1 calls other installed plugin through executeFromPluginPre executeFromPluginExternal Hook(s)Post executeFromPluginExternal Hook(s) \ No newline at end of file + Permitted External Contracts & Methods executeFromModulePre Execution Hook(s)Permitted Module Execution FunctionPost Execution Hook(s)ModulesModule Permission Check executeFromModuleExternal(Module) Execution Function1 calls module 2.2 calls external contracts through executeFromModuleExternalModule Permission CheckModular AccountModule Execution Flow2.1 calls other installed module through executeFromModulePre executeFromModuleExternal Hook(s)Post executeFromModuleExternal Hook(s) \ No newline at end of file diff --git a/test/account/AccountExecHooks.t.sol b/test/account/AccountExecHooks.t.sol index 14ad57fc..2e0a90bc 100644 --- a/test/account/AccountExecHooks.t.sol +++ b/test/account/AccountExecHooks.t.sol @@ -2,31 +2,29 @@ pragma solidity ^0.8.19; import { - IPlugin, - ManifestExecutionHook, + ExecutionManifest, + IModule, ManifestExecutionFunction, - PluginManifest -} from "../../src/interfaces/IPlugin.sol"; + ManifestExecutionHook +} from "../../src/interfaces/IExecution.sol"; import {IExecutionHook} from "../../src/interfaces/IExecutionHook.sol"; -import {MockPlugin} from "../mocks/MockPlugin.sol"; +import {MockModule} from "../mocks/MockModule.sol"; import {AccountTestBase} from "../utils/AccountTestBase.sol"; contract AccountExecHooksTest is AccountTestBase { - MockPlugin public mockPlugin1; - bytes32 public manifestHash1; - bytes32 public manifestHash2; + MockModule public mockModule1; bytes4 internal constant _EXEC_SELECTOR = bytes4(uint32(1)); - uint8 internal constant _PRE_HOOK_FUNCTION_ID_1 = 1; - uint8 internal constant _POST_HOOK_FUNCTION_ID_2 = 2; - uint8 internal constant _BOTH_HOOKS_FUNCTION_ID_3 = 3; + uint32 internal constant _PRE_HOOK_FUNCTION_ID_1 = 1; + uint32 internal constant _POST_HOOK_FUNCTION_ID_2 = 2; + uint32 internal constant _BOTH_HOOKS_FUNCTION_ID_3 = 3; - PluginManifest internal _m1; + ExecutionManifest internal _m1; - event PluginInstalled(address indexed plugin, bytes32 manifestHash); - event PluginUninstalled(address indexed plugin, bool indexed callbacksSucceeded); - // emitted by MockPlugin + event ModuleInstalled(address indexed module); + event ModuleUninstalled(address indexed module, bool indexed callbacksSucceeded); + // emitted by MockModule event ReceivedCall(bytes msgData, uint256 msgValue); function setUp() public { @@ -42,17 +40,17 @@ contract AccountExecHooksTest is AccountTestBase { } function test_preExecHook_install() public { - _installPlugin1WithHooks( + _installExecution1WithHooks( ManifestExecutionHook({ executionSelector: _EXEC_SELECTOR, - functionId: _PRE_HOOK_FUNCTION_ID_1, + entityId: _PRE_HOOK_FUNCTION_ID_1, isPreHook: true, isPostHook: false }) ); } - /// @dev Plugin 1 hook pair: [1, null] + /// @dev Module 1 hook pair: [1, null] /// Expected execution: [1, null] function test_preExecHook_run() public { test_preExecHook_install(); @@ -66,7 +64,7 @@ contract AccountExecHooksTest is AccountTestBase { uint256(0), // msg.value in call to account abi.encodeWithSelector(_EXEC_SELECTOR) ), - 0 // msg value in call to plugin + 0 // msg value in call to module ); (bool success,) = address(account1).call(abi.encodeWithSelector(_EXEC_SELECTOR)); @@ -76,21 +74,21 @@ contract AccountExecHooksTest is AccountTestBase { function test_preExecHook_uninstall() public { test_preExecHook_install(); - _uninstallPlugin(mockPlugin1); + _uninstallExecution(mockModule1); } function test_execHookPair_install() public { - _installPlugin1WithHooks( + _installExecution1WithHooks( ManifestExecutionHook({ executionSelector: _EXEC_SELECTOR, - functionId: _BOTH_HOOKS_FUNCTION_ID_3, + entityId: _BOTH_HOOKS_FUNCTION_ID_3, isPreHook: true, isPostHook: true }) ); } - /// @dev Plugin 1 hook pair: [1, 2] + /// @dev Module 1 hook pair: [1, 2] /// Expected execution: [1, 2] function test_execHookPair_run() public { test_execHookPair_install(); @@ -105,7 +103,7 @@ contract AccountExecHooksTest is AccountTestBase { uint256(0), // msg.value in call to account abi.encodeWithSelector(_EXEC_SELECTOR) ), - 0 // msg value in call to plugin + 0 // msg value in call to module ); vm.expectEmit(true, true, true, true); // exec call @@ -114,7 +112,7 @@ contract AccountExecHooksTest is AccountTestBase { // post hook call emit ReceivedCall( abi.encodeCall(IExecutionHook.postExecutionHook, (_BOTH_HOOKS_FUNCTION_ID_3, "")), - 0 // msg value in call to plugin + 0 // msg value in call to module ); (bool success,) = address(account1).call(abi.encodeWithSelector(_EXEC_SELECTOR)); @@ -124,21 +122,21 @@ contract AccountExecHooksTest is AccountTestBase { function test_execHookPair_uninstall() public { test_execHookPair_install(); - _uninstallPlugin(mockPlugin1); + _uninstallExecution(mockModule1); } function test_postOnlyExecHook_install() public { - _installPlugin1WithHooks( + _installExecution1WithHooks( ManifestExecutionHook({ executionSelector: _EXEC_SELECTOR, - functionId: _POST_HOOK_FUNCTION_ID_2, + entityId: _POST_HOOK_FUNCTION_ID_2, isPreHook: false, isPostHook: true }) ); } - /// @dev Plugin 1 hook pair: [null, 2] + /// @dev Module 1 hook pair: [null, 2] /// Expected execution: [null, 2] function test_postOnlyExecHook_run() public { test_postOnlyExecHook_install(); @@ -146,7 +144,7 @@ contract AccountExecHooksTest is AccountTestBase { vm.expectEmit(true, true, true, true); emit ReceivedCall( abi.encodeCall(IExecutionHook.postExecutionHook, (_POST_HOOK_FUNCTION_ID_2, "")), - 0 // msg value in call to plugin + 0 // msg value in call to module ); (bool success,) = address(account1).call(abi.encodeWithSelector(_EXEC_SELECTOR)); @@ -156,34 +154,35 @@ contract AccountExecHooksTest is AccountTestBase { function test_postOnlyExecHook_uninstall() public { test_postOnlyExecHook_install(); - _uninstallPlugin(mockPlugin1); + _uninstallExecution(mockModule1); } - function _installPlugin1WithHooks(ManifestExecutionHook memory execHooks) internal { + function _installExecution1WithHooks(ManifestExecutionHook memory execHooks) internal { _m1.executionHooks.push(execHooks); - mockPlugin1 = new MockPlugin(_m1); - manifestHash1 = keccak256(abi.encode(mockPlugin1.pluginManifest())); + mockModule1 = new MockModule(_m1); vm.expectEmit(true, true, true, true); - emit ReceivedCall(abi.encodeCall(IPlugin.onInstall, (bytes(""))), 0); + emit ReceivedCall(abi.encodeCall(IModule.onInstall, (bytes(""))), 0); vm.expectEmit(true, true, true, true); - emit PluginInstalled(address(mockPlugin1), manifestHash1); + emit ModuleInstalled(address(mockModule1)); - vm.prank(address(entryPoint)); - account1.installPlugin({ - plugin: address(mockPlugin1), - manifestHash: manifestHash1, - pluginInstallData: bytes("") + vm.startPrank(address(entryPoint)); + account1.installExecution({ + module: address(mockModule1), + manifest: mockModule1.executionManifest(), + moduleInstallData: bytes("") }); + vm.stopPrank(); } - function _uninstallPlugin(MockPlugin plugin) internal { + function _uninstallExecution(MockModule module) internal { vm.expectEmit(true, true, true, true); - emit ReceivedCall(abi.encodeCall(IPlugin.onUninstall, (bytes(""))), 0); + emit ReceivedCall(abi.encodeCall(IModule.onUninstall, (bytes(""))), 0); vm.expectEmit(true, true, true, true); - emit PluginUninstalled(address(plugin), true); + emit ModuleUninstalled(address(module), true); - vm.prank(address(entryPoint)); - account1.uninstallPlugin(address(plugin), bytes(""), bytes("")); + vm.startPrank(address(entryPoint)); + account1.uninstallExecution(address(module), module.executionManifest(), bytes("")); + vm.stopPrank(); } } diff --git a/test/account/AccountFactory.t.sol b/test/account/AccountFactory.t.sol new file mode 100644 index 00000000..37ffa74c --- /dev/null +++ b/test/account/AccountFactory.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {AccountFactory} from "../../src/account/AccountFactory.sol"; +import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract AccountFactoryTest is AccountTestBase { + AccountFactory internal _factory; + UpgradeableModularAccount internal _account; + + function setUp() public { + _account = new UpgradeableModularAccount(entryPoint); + _factory = new AccountFactory(entryPoint, _account, address(singleSignerValidation), address(this)); + } + + function test_createAccount() public { + UpgradeableModularAccount account = _factory.createAccount(address(this), 100, 0); + + assertEq(address(account.entryPoint()), address(entryPoint)); + } + + function test_createAccountAndGetAddress() public { + UpgradeableModularAccount account = _factory.createAccount(address(this), 100, 0); + + assertEq(address(account), address(_factory.createAccount(address(this), 100, 0))); + } + + function test_multipleDeploy() public { + UpgradeableModularAccount account = _factory.createAccount(address(this), 100, 0); + + uint256 startGas = gasleft(); + + UpgradeableModularAccount account2 = _factory.createAccount(address(this), 100, 0); + + // Assert that the 2nd deployment call cost less than 1 sstore + // Implies that no deployment was done on the second calls + assertLe(startGas - 22_000, gasleft()); + + // Assert the return addresses are the same + assertEq(address(account), address(account2)); + } +} diff --git a/test/account/AccountLoupe.t.sol b/test/account/AccountLoupe.t.sol index 69563b51..bc75d356 100644 --- a/test/account/AccountLoupe.t.sol +++ b/test/account/AccountLoupe.t.sol @@ -3,38 +3,35 @@ pragma solidity ^0.8.19; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; -import {FunctionReference, FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; import {ExecutionHook} from "../../src/interfaces/IAccountLoupe.sol"; -import {IPluginManager} from "../../src/interfaces/IPluginManager.sol"; +import {IModuleManager} from "../../src/interfaces/IModuleManager.sol"; import {IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; -import {ComprehensivePlugin} from "../mocks/plugins/ComprehensivePlugin.sol"; +import {ComprehensiveModule} from "../mocks/modules/ComprehensiveModule.sol"; import {CustomValidationTestBase} from "../utils/CustomValidationTestBase.sol"; contract AccountLoupeTest is CustomValidationTestBase { - ComprehensivePlugin public comprehensivePlugin; + ComprehensiveModule public comprehensiveModule; event ReceivedCall(bytes msgData, uint256 msgValue); + ModuleEntity public comprehensiveModuleValidation; + function setUp() public { - comprehensivePlugin = new ComprehensivePlugin(); + comprehensiveModule = new ComprehensiveModule(); + comprehensiveModuleValidation = + ModuleEntityLib.pack(address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.VALIDATION)); _customValidationSetup(); - bytes32 manifestHash = keccak256(abi.encode(comprehensivePlugin.pluginManifest())); - vm.prank(address(entryPoint)); - account1.installPlugin(address(comprehensivePlugin), manifestHash, ""); - } - - function test_pluginLoupe_getInstalledPlugins_initial() public { - address[] memory plugins = account1.getInstalledPlugins(); - - assertEq(plugins.length, 1); - - assertEq(plugins[0], address(comprehensivePlugin)); + vm.startPrank(address(entryPoint)); + account1.installExecution(address(comprehensiveModule), comprehensiveModule.executionManifest(), ""); + vm.stopPrank(); } - function test_pluginLoupe_getExecutionFunctionHandler_native() public { + function test_moduleLoupe_getExecutionFunctionHandler_native() public { bytes4[] memory selectorsToCheck = new bytes4[](5); selectorsToCheck[0] = IStandardExecutor.execute.selector; @@ -43,62 +40,58 @@ contract AccountLoupeTest is CustomValidationTestBase { selectorsToCheck[2] = UUPSUpgradeable.upgradeToAndCall.selector; - selectorsToCheck[3] = IPluginManager.installPlugin.selector; + selectorsToCheck[3] = IModuleManager.installExecution.selector; - selectorsToCheck[4] = IPluginManager.uninstallPlugin.selector; + selectorsToCheck[4] = IModuleManager.uninstallExecution.selector; for (uint256 i = 0; i < selectorsToCheck.length; i++) { - address plugin = account1.getExecutionFunctionHandler(selectorsToCheck[i]); + address module = account1.getExecutionFunctionHandler(selectorsToCheck[i]); - assertEq(plugin, address(account1)); + assertEq(module, address(account1)); } } - function test_pluginLoupe_getExecutionFunctionConfig_plugin() public { + function test_moduleLoupe_getExecutionFunctionConfig_module() public { bytes4[] memory selectorsToCheck = new bytes4[](1); - address[] memory expectedPluginAddress = new address[](1); + address[] memory expectedModuleAddress = new address[](1); - selectorsToCheck[0] = comprehensivePlugin.foo.selector; - expectedPluginAddress[0] = address(comprehensivePlugin); + selectorsToCheck[0] = comprehensiveModule.foo.selector; + expectedModuleAddress[0] = address(comprehensiveModule); for (uint256 i = 0; i < selectorsToCheck.length; i++) { - address plugin = account1.getExecutionFunctionHandler(selectorsToCheck[i]); + address module = account1.getExecutionFunctionHandler(selectorsToCheck[i]); - assertEq(plugin, expectedPluginAddress[i]); + assertEq(module, expectedModuleAddress[i]); } } - function test_pluginLoupe_getSelectors() public { - FunctionReference comprehensivePluginValidation = FunctionReferenceLib.pack( - address(comprehensivePlugin), uint8(ComprehensivePlugin.FunctionId.VALIDATION) - ); - - bytes4[] memory selectors = account1.getSelectors(comprehensivePluginValidation); + function test_moduleLoupe_getSelectors() public { + bytes4[] memory selectors = account1.getSelectors(comprehensiveModuleValidation); assertEq(selectors.length, 1); - assertEq(selectors[0], comprehensivePlugin.foo.selector); + assertEq(selectors[0], comprehensiveModule.foo.selector); } - function test_pluginLoupe_getExecutionHooks() public { - ExecutionHook[] memory hooks = account1.getExecutionHooks(comprehensivePlugin.foo.selector); + function test_moduleLoupe_getExecutionHooks() public { + ExecutionHook[] memory hooks = account1.getExecutionHooks(comprehensiveModule.foo.selector); ExecutionHook[3] memory expectedHooks = [ ExecutionHook({ - hookFunction: FunctionReferenceLib.pack( - address(comprehensivePlugin), uint8(ComprehensivePlugin.FunctionId.BOTH_EXECUTION_HOOKS) + hookFunction: ModuleEntityLib.pack( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.BOTH_EXECUTION_HOOKS) ), isPreHook: true, isPostHook: true }), ExecutionHook({ - hookFunction: FunctionReferenceLib.pack( - address(comprehensivePlugin), uint8(ComprehensivePlugin.FunctionId.PRE_EXECUTION_HOOK) + hookFunction: ModuleEntityLib.pack( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.PRE_EXECUTION_HOOK) ), isPreHook: true, isPostHook: false }), ExecutionHook({ - hookFunction: FunctionReferenceLib.pack( - address(comprehensivePlugin), uint8(ComprehensivePlugin.FunctionId.POST_EXECUTION_HOOK) + hookFunction: ModuleEntityLib.pack( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.POST_EXECUTION_HOOK) ), isPreHook: false, isPostHook: true @@ -108,31 +101,30 @@ contract AccountLoupeTest is CustomValidationTestBase { assertEq(hooks.length, 3); for (uint256 i = 0; i < hooks.length; i++) { assertEq( - FunctionReference.unwrap(hooks[i].hookFunction), - FunctionReference.unwrap(expectedHooks[i].hookFunction) + ModuleEntity.unwrap(hooks[i].hookFunction), ModuleEntity.unwrap(expectedHooks[i].hookFunction) ); assertEq(hooks[i].isPreHook, expectedHooks[i].isPreHook); assertEq(hooks[i].isPostHook, expectedHooks[i].isPostHook); } } - function test_pluginLoupe_getValidationHooks() public { - FunctionReference[] memory hooks = account1.getPreValidationHooks(_ownerValidation); + function test_moduleLoupe_getValidationHooks() public { + ModuleEntity[] memory hooks = account1.getPreValidationHooks(comprehensiveModuleValidation); assertEq(hooks.length, 2); assertEq( - FunctionReference.unwrap(hooks[0]), - FunctionReference.unwrap( - FunctionReferenceLib.pack( - address(comprehensivePlugin), uint8(ComprehensivePlugin.FunctionId.PRE_VALIDATION_HOOK_1) + ModuleEntity.unwrap(hooks[0]), + ModuleEntity.unwrap( + ModuleEntityLib.pack( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.PRE_VALIDATION_HOOK_1) ) ) ); assertEq( - FunctionReference.unwrap(hooks[1]), - FunctionReference.unwrap( - FunctionReferenceLib.pack( - address(comprehensivePlugin), uint8(ComprehensivePlugin.FunctionId.PRE_VALIDATION_HOOK_2) + ModuleEntity.unwrap(hooks[1]), + ModuleEntity.unwrap( + ModuleEntityLib.pack( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.PRE_VALIDATION_HOOK_2) ) ) ); @@ -144,25 +136,23 @@ contract AccountLoupeTest is CustomValidationTestBase { internal virtual override - returns (FunctionReference, bool, bool, bytes4[] memory, bytes memory, bytes memory, bytes memory) + returns (ModuleEntity, bool, bool, bytes4[] memory, bytes memory, bytes[] memory) { - FunctionReference[] memory preValidationHooks = new FunctionReference[](2); - preValidationHooks[0] = FunctionReferenceLib.pack( - address(comprehensivePlugin), uint8(ComprehensivePlugin.FunctionId.PRE_VALIDATION_HOOK_1) + bytes[] memory hooks = new bytes[](2); + hooks[0] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.PRE_VALIDATION_HOOK_1) + ) ); - preValidationHooks[1] = FunctionReferenceLib.pack( - address(comprehensivePlugin), uint8(ComprehensivePlugin.FunctionId.PRE_VALIDATION_HOOK_2) + hooks[1] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.PRE_VALIDATION_HOOK_2) + ) ); - bytes[] memory installDatas = new bytes[](2); - return ( - _ownerValidation, - true, - true, - new bytes4[](0), - bytes(""), - abi.encode(preValidationHooks, installDatas), - "" - ); + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = comprehensiveModule.foo.selector; + + return (comprehensiveModuleValidation, true, true, selectors, bytes(""), hooks); } } diff --git a/test/account/AccountReturnData.t.sol b/test/account/AccountReturnData.t.sol index 1738c722..56da489e 100644 --- a/test/account/AccountReturnData.t.sol +++ b/test/account/AccountReturnData.t.sol @@ -1,51 +1,61 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; +import {DIRECT_CALL_VALIDATION_ENTITYID} from "../../src/helpers/Constants.sol"; +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; import {Call} from "../../src/interfaces/IStandardExecutor.sol"; +import {IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; import { RegularResultContract, - ResultCreatorPlugin, - ResultConsumerPlugin -} from "../mocks/plugins/ReturnDataPluginMocks.sol"; + ResultConsumerModule, + ResultCreatorModule +} from "../mocks/modules/ReturnDataModuleMocks.sol"; import {AccountTestBase} from "../utils/AccountTestBase.sol"; -import {TEST_DEFAULT_OWNER_FUNCTION_ID} from "../utils/TestConstants.sol"; +import {TEST_DEFAULT_VALIDATION_ENTITY_ID} from "../utils/TestConstants.sol"; -// Tests all the different ways that return data can be read from plugins through an account +// Tests all the different ways that return data can be read from modules through an account contract AccountReturnDataTest is AccountTestBase { RegularResultContract public regularResultContract; - ResultCreatorPlugin public resultCreatorPlugin; - ResultConsumerPlugin public resultConsumerPlugin; + ResultCreatorModule public resultCreatorModule; + ResultConsumerModule public resultConsumerModule; function setUp() public { _transferOwnershipToTest(); regularResultContract = new RegularResultContract(); - resultCreatorPlugin = new ResultCreatorPlugin(); - resultConsumerPlugin = new ResultConsumerPlugin(resultCreatorPlugin, regularResultContract); - - // Add the result creator plugin to the account - bytes32 resultCreatorManifestHash = keccak256(abi.encode(resultCreatorPlugin.pluginManifest())); - vm.prank(address(entryPoint)); - account1.installPlugin({ - plugin: address(resultCreatorPlugin), - manifestHash: resultCreatorManifestHash, - pluginInstallData: "" + resultCreatorModule = new ResultCreatorModule(); + resultConsumerModule = new ResultConsumerModule(resultCreatorModule, regularResultContract); + + // Add the result creator module to the account + vm.startPrank(address(entryPoint)); + account1.installExecution({ + module: address(resultCreatorModule), + manifest: resultCreatorModule.executionManifest(), + moduleInstallData: "" }); - // Add the result consumer plugin to the account - bytes32 resultConsumerManifestHash = keccak256(abi.encode(resultConsumerPlugin.pluginManifest())); - vm.prank(address(entryPoint)); - account1.installPlugin({ - plugin: address(resultConsumerPlugin), - manifestHash: resultConsumerManifestHash, - pluginInstallData: "" + // Add the result consumer module to the account + account1.installExecution({ + module: address(resultConsumerModule), + manifest: resultConsumerModule.executionManifest(), + moduleInstallData: "" }); + // Allow the result consumer module to perform direct calls to the account + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IStandardExecutor.execute.selector; + account1.installValidation( + ValidationConfigLib.pack(address(resultConsumerModule), DIRECT_CALL_VALIDATION_ENTITYID, false, false), + selectors, + "", + new bytes[](0) + ); + vm.stopPrank(); } - // Tests the ability to read the result of plugin execution functions via the account's fallback + // Tests the ability to read the result of module execution functions via the account's fallback function test_returnData_fallback() public { - bytes32 result = ResultCreatorPlugin(address(account1)).foo(); + bytes32 result = ResultCreatorModule(address(account1)).foo(); assertEq(result, keccak256("bar")); } @@ -58,7 +68,7 @@ contract AccountReturnDataTest is AccountTestBase { (address(regularResultContract), 0, abi.encodeCall(RegularResultContract.foo, ())) ), _encodeSignature( - FunctionReferenceLib.pack(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID), + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, "" ) @@ -86,7 +96,7 @@ contract AccountReturnDataTest is AccountTestBase { bytes memory retData = account1.executeWithAuthorization( abi.encodeCall(account1.executeBatch, (calls)), _encodeSignature( - FunctionReferenceLib.pack(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID), + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, "" ) @@ -102,15 +112,15 @@ contract AccountReturnDataTest is AccountTestBase { } // Tests the ability to read data via routing to fallback functions - function test_returnData_execFromPlugin_fallback() public { - bool result = ResultConsumerPlugin(address(account1)).checkResultFallback(keccak256("bar")); + function test_returnData_execFromModule_fallback() public { + bool result = ResultConsumerModule(address(account1)).checkResultFallback(keccak256("bar")); assertTrue(result); } // Tests the ability to read data via executeWithAuthorization function test_returnData_authorized_exec() public { - bool result = ResultConsumerPlugin(address(account1)).checkResultExecuteWithAuthorization( + bool result = ResultConsumerModule(address(account1)).checkResultExecuteWithAuthorization( address(regularResultContract), keccak256("bar") ); diff --git a/test/account/DirectCallsFromModule.t.sol b/test/account/DirectCallsFromModule.t.sol new file mode 100644 index 00000000..889b65b9 --- /dev/null +++ b/test/account/DirectCallsFromModule.t.sol @@ -0,0 +1,133 @@ +pragma solidity ^0.8.19; + +import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; + +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfig, ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; +import {Call, IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; +import {DirectCallModule} from "../mocks/modules/DirectCallModule.sol"; + +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract DirectCallsFromModuleTest is AccountTestBase { + using ValidationConfigLib for ValidationConfig; + + DirectCallModule internal _module; + ModuleEntity internal _moduleEntity; + + function setUp() public { + _module = new DirectCallModule(); + assertFalse(_module.preHookRan()); + assertFalse(_module.postHookRan()); + _moduleEntity = ModuleEntityLib.pack(address(_module), type(uint32).max); + } + + /* -------------------------------------------------------------------------- */ + /* Negatives */ + /* -------------------------------------------------------------------------- */ + + function test_Fail_DirectCallModuleNotInstalled() external { + vm.prank(address(_module)); + vm.expectRevert(_buildDirectCallDisallowedError(IStandardExecutor.execute.selector)); + account1.execute(address(0), 0, ""); + } + + function test_Fail_DirectCallModuleUninstalled() external { + _installExecution(); + + _uninstallExecution(); + + vm.prank(address(_module)); + vm.expectRevert(_buildDirectCallDisallowedError(IStandardExecutor.execute.selector)); + account1.execute(address(0), 0, ""); + } + + function test_Fail_DirectCallModuleCallOtherSelector() external { + _installExecution(); + + Call[] memory calls = new Call[](0); + + vm.prank(address(_module)); + vm.expectRevert(_buildDirectCallDisallowedError(IStandardExecutor.executeBatch.selector)); + account1.executeBatch(calls); + } + + /* -------------------------------------------------------------------------- */ + /* Positives */ + /* -------------------------------------------------------------------------- */ + + function test_Pass_DirectCallFromModulePrank() external { + _installExecution(); + + vm.prank(address(_module)); + account1.execute(address(0), 0, ""); + + assertTrue(_module.preHookRan()); + assertTrue(_module.postHookRan()); + } + + function test_Pass_DirectCallFromModuleCallback() external { + _installExecution(); + + bytes memory encodedCall = abi.encodeCall(DirectCallModule.directCall, ()); + + vm.prank(address(entryPoint)); + bytes memory result = account1.execute(address(_module), 0, encodedCall); + + assertTrue(_module.preHookRan()); + assertTrue(_module.postHookRan()); + + // the directCall() function in the _module calls back into `execute()` with an encoded call back into the + // _module's getData() function. + assertEq(abi.decode(result, (bytes)), abi.encode(_module.getData())); + } + + function test_Flow_DirectCallFromModuleSequence() external { + // Install => Succeesfully call => uninstall => fail to call + + _installExecution(); + + vm.prank(address(_module)); + account1.execute(address(0), 0, ""); + + assertTrue(_module.preHookRan()); + assertTrue(_module.postHookRan()); + + _uninstallExecution(); + + vm.prank(address(_module)); + vm.expectRevert(_buildDirectCallDisallowedError(IStandardExecutor.execute.selector)); + account1.execute(address(0), 0, ""); + } + + /* -------------------------------------------------------------------------- */ + /* Internals */ + /* -------------------------------------------------------------------------- */ + + function _installExecution() internal { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IStandardExecutor.execute.selector; + + bytes[] memory hooks = new bytes[](1); + hooks[0] = abi.encodePacked( + HookConfigLib.packExecHook({_hookFunction: _moduleEntity, _hasPre: true, _hasPost: true}), + hex"00" // onInstall data + ); + + vm.prank(address(entryPoint)); + + ValidationConfig validationConfig = ValidationConfigLib.pack(_moduleEntity, false, false); + + account1.installValidation(validationConfig, selectors, "", hooks); + } + + function _uninstallExecution() internal { + vm.prank(address(entryPoint)); + account1.uninstallValidation(_moduleEntity, "", new bytes[](1)); + } + + function _buildDirectCallDisallowedError(bytes4 selector) internal pure returns (bytes memory) { + return abi.encodeWithSelector(UpgradeableModularAccount.ValidationFunctionMissing.selector, selector); + } +} diff --git a/test/account/GlobalValidationTest.t.sol b/test/account/GlobalValidationTest.t.sol index 9f40f806..b22d68eb 100644 --- a/test/account/GlobalValidationTest.t.sol +++ b/test/account/GlobalValidationTest.t.sol @@ -5,7 +5,7 @@ import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interface import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; import {AccountTestBase} from "../utils/AccountTestBase.sol"; @@ -26,7 +26,8 @@ contract GlobalValidationTest is AccountTestBase { account2 = UpgradeableModularAccount(payable(factory.getAddress(owner2, 0))); vm.deal(address(account2), 100 ether); - _ownerValidation = FunctionReferenceLib.pack(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID); + _signerValidation = + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID); ethRecipient = makeAddr("ethRecipient"); vm.deal(ethRecipient, 1 wei); @@ -48,7 +49,7 @@ contract GlobalValidationTest is AccountTestBase { // Generate signature bytes32 userOpHash = entryPoint.getUserOpHash(userOp); (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, userOpHash.toEthSignedMessageHash()); - userOp.signature = _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature(_signerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -65,7 +66,7 @@ contract GlobalValidationTest is AccountTestBase { vm.prank(owner2); account2.executeWithAuthorization( abi.encodeCall(UpgradeableModularAccount.execute, (ethRecipient, 1 wei, "")), - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, "") + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") ); assertEq(ethRecipient.balance, 2 wei); diff --git a/test/account/ImmutableAppend.t.sol b/test/account/ImmutableAppend.t.sol new file mode 100644 index 00000000..a2d4f4e7 --- /dev/null +++ b/test/account/ImmutableAppend.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import {IEntryPoint, UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfig, ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; +import {ExecutionHook} from "../../src/interfaces/IAccountLoupe.sol"; +import {Call, IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; +import {DirectCallModule} from "../mocks/modules/DirectCallModule.sol"; + +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; + +contract ImmutableAppendTest is AccountTestBase { + using ValidationConfigLib for ValidationConfig; + + /* -------------------------------------------------------------------------- */ + /* Negatives */ + /* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* Positives */ + /* -------------------------------------------------------------------------- */ + + function test_success_getData() public { + bytes memory expectedArgs = abi.encodePacked( + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID), + singleSignerValidation.signerOf(TEST_DEFAULT_VALIDATION_ENTITY_ID, address(account1)) + ); + + assertEq(keccak256(LibClone.argsOnERC1967(address(account1))), keccak256(expectedArgs)); + } + + /* -------------------------------------------------------------------------- */ + /* Internals */ + /* -------------------------------------------------------------------------- */ +} diff --git a/test/account/MultiValidation.t.sol b/test/account/MultiValidation.t.sol index 5254c50d..3e927ee6 100644 --- a/test/account/MultiValidation.t.sol +++ b/test/account/MultiValidation.t.sol @@ -1,33 +1,34 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.21; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {FunctionReference} from "../../src/interfaces/IPluginManager.sol"; -import {IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; -import {FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; + +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; -import {SingleOwnerPlugin} from "../../src/plugins/owner/SingleOwnerPlugin.sol"; +import {ModuleEntity} from "../../src/interfaces/IModuleManager.sol"; +import {IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; +import {SingleSignerValidation} from "../../src/modules/validation/SingleSignerValidation.sol"; import {AccountTestBase} from "../utils/AccountTestBase.sol"; -import {TEST_DEFAULT_OWNER_FUNCTION_ID} from "../utils/TestConstants.sol"; +import {TEST_DEFAULT_VALIDATION_ENTITY_ID} from "../utils/TestConstants.sol"; contract MultiValidationTest is AccountTestBase { using ECDSA for bytes32; using MessageHashUtils for bytes32; - SingleOwnerPlugin public validator2; + SingleSignerValidation public validator2; address public owner2; uint256 public owner2Key; function setUp() public { - validator2 = new SingleOwnerPlugin(); + validator2 = new SingleSignerValidation(); (owner2, owner2Key) = makeAddrAndKey("owner2"); } @@ -35,16 +36,15 @@ contract MultiValidationTest is AccountTestBase { function test_overlappingValidationInstall() public { vm.prank(address(entryPoint)); account1.installValidation( - ValidationConfigLib.pack(address(validator2), TEST_DEFAULT_OWNER_FUNCTION_ID, true, true), + ValidationConfigLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID, true, true), new bytes4[](0), - abi.encode(TEST_DEFAULT_OWNER_FUNCTION_ID, owner2), - "", - "" + abi.encode(TEST_DEFAULT_VALIDATION_ENTITY_ID, owner2), + new bytes[](0) ); - FunctionReference[] memory validations = new FunctionReference[](2); - validations[0] = FunctionReferenceLib.pack(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID); - validations[1] = FunctionReferenceLib.pack(address(validator2), TEST_DEFAULT_OWNER_FUNCTION_ID); + ModuleEntity[] memory validations = new ModuleEntity[](2); + validations[0] = ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID); + validations[1] = ModuleEntityLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID); bytes4[] memory selectors0 = account1.getSelectors(validations[0]); bytes4[] memory selectors1 = account1.getSelectors(validations[1]); @@ -64,16 +64,14 @@ contract MultiValidationTest is AccountTestBase { abi.encodeWithSelector( UpgradeableModularAccount.RuntimeValidationFunctionReverted.selector, address(validator2), - 0, + 1, abi.encodeWithSignature("NotAuthorized()") ) ); account1.executeWithAuthorization( abi.encodeCall(IStandardExecutor.execute, (address(0), 0, "")), _encodeSignature( - FunctionReferenceLib.pack(address(validator2), TEST_DEFAULT_OWNER_FUNCTION_ID), - GLOBAL_VALIDATION, - "" + ModuleEntityLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, "" ) ); @@ -81,9 +79,7 @@ contract MultiValidationTest is AccountTestBase { account1.executeWithAuthorization( abi.encodeCall(IStandardExecutor.execute, (address(0), 0, "")), _encodeSignature( - FunctionReferenceLib.pack(address(validator2), TEST_DEFAULT_OWNER_FUNCTION_ID), - GLOBAL_VALIDATION, - "" + ModuleEntityLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, "" ) ); } @@ -109,7 +105,7 @@ contract MultiValidationTest is AccountTestBase { bytes32 userOpHash = entryPoint.getUserOpHash(userOp); (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, userOpHash.toEthSignedMessageHash()); userOp.signature = _encodeSignature( - FunctionReferenceLib.pack(address(validator2), TEST_DEFAULT_OWNER_FUNCTION_ID), + ModuleEntityLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, abi.encodePacked(r, s, v) ); @@ -124,7 +120,7 @@ contract MultiValidationTest is AccountTestBase { userOp.nonce = 1; (v, r, s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); userOp.signature = _encodeSignature( - FunctionReferenceLib.pack(address(validator2), TEST_DEFAULT_OWNER_FUNCTION_ID), + ModuleEntityLib.pack(address(validator2), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, abi.encodePacked(r, s, v) ); diff --git a/test/account/PerHookData.t.sol b/test/account/PerHookData.t.sol index 9c90cf5c..cc3f3415 100644 --- a/test/account/PerHookData.t.sol +++ b/test/account/PerHookData.t.sol @@ -1,28 +1,30 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.25; -import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {FunctionReference, FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; -import {MockAccessControlHookPlugin} from "../mocks/plugins/MockAccessControlHookPlugin.sol"; +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntity} from "../../src/helpers/ModuleEntityLib.sol"; + import {Counter} from "../mocks/Counter.sol"; +import {MockAccessControlHookModule} from "../mocks/modules/MockAccessControlHookModule.sol"; import {CustomValidationTestBase} from "../utils/CustomValidationTestBase.sol"; contract PerHookDataTest is CustomValidationTestBase { using MessageHashUtils for bytes32; - MockAccessControlHookPlugin internal _accessControlHookPlugin; + MockAccessControlHookModule internal _accessControlHookModule; Counter internal _counter; function setUp() public { _counter = new Counter(); - _accessControlHookPlugin = new MockAccessControlHookPlugin(); + _accessControlHookModule = new MockAccessControlHookModule(); _customValidationSetup(); } @@ -37,8 +39,9 @@ contract PerHookDataTest is CustomValidationTestBase { PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); - userOp.signature = - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -59,8 +62,9 @@ contract PerHookDataTest is CustomValidationTestBase { validationData: abi.encodePacked(address(0x1234123412341234123412341234123412341234)) }); - userOp.signature = - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -80,7 +84,7 @@ contract PerHookDataTest is CustomValidationTestBase { (PackedUserOperation memory userOp, bytes32 userOpHash) = _getCounterUserOP(); (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); - userOp.signature = _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature(_signerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -104,8 +108,9 @@ contract PerHookDataTest is CustomValidationTestBase { preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(_counter)}); preValidationHookData[1] = PreValidationHookData({index: 1, validationData: abi.encodePacked(_counter)}); - userOp.signature = - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -142,8 +147,9 @@ contract PerHookDataTest is CustomValidationTestBase { PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); preValidationHookData[0] = PreValidationHookData({index: 0, validationData: abi.encodePacked(beneficiary)}); - userOp.signature = - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -166,8 +172,9 @@ contract PerHookDataTest is CustomValidationTestBase { PreValidationHookData[] memory preValidationHookData = new PreValidationHookData[](1); preValidationHookData[0] = PreValidationHookData({index: 0, validationData: ""}); - userOp.signature = - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature( + _signerValidation, GLOBAL_VALIDATION, preValidationHookData, abi.encodePacked(r, s, v) + ); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -195,7 +202,7 @@ contract PerHookDataTest is CustomValidationTestBase { UpgradeableModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) ), - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") ); assertEq(_counter.number(), 1); @@ -212,8 +219,8 @@ contract PerHookDataTest is CustomValidationTestBase { vm.expectRevert( abi.encodeWithSelector( UpgradeableModularAccount.PreRuntimeValidationHookFailed.selector, - _accessControlHookPlugin, - uint8(MockAccessControlHookPlugin.FunctionId.PRE_VALIDATION_HOOK), + _accessControlHookModule, + uint32(MockAccessControlHookModule.EntityId.PRE_VALIDATION_HOOK), abi.encodeWithSignature("Error(string)", "Proof doesn't match target") ) ); @@ -222,7 +229,7 @@ contract PerHookDataTest is CustomValidationTestBase { UpgradeableModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) ), - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") ); } @@ -231,8 +238,8 @@ contract PerHookDataTest is CustomValidationTestBase { vm.expectRevert( abi.encodeWithSelector( UpgradeableModularAccount.PreRuntimeValidationHookFailed.selector, - _accessControlHookPlugin, - uint8(MockAccessControlHookPlugin.FunctionId.PRE_VALIDATION_HOOK), + _accessControlHookModule, + uint32(MockAccessControlHookModule.EntityId.PRE_VALIDATION_HOOK), abi.encodeWithSignature("Error(string)", "Proof doesn't match target") ) ); @@ -241,7 +248,7 @@ contract PerHookDataTest is CustomValidationTestBase { UpgradeableModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) ), - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, "") + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") ); } @@ -259,7 +266,7 @@ contract PerHookDataTest is CustomValidationTestBase { UpgradeableModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) ), - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") ); } @@ -273,14 +280,14 @@ contract PerHookDataTest is CustomValidationTestBase { vm.expectRevert( abi.encodeWithSelector( UpgradeableModularAccount.PreRuntimeValidationHookFailed.selector, - _accessControlHookPlugin, - uint8(MockAccessControlHookPlugin.FunctionId.PRE_VALIDATION_HOOK), + _accessControlHookModule, + uint32(MockAccessControlHookModule.EntityId.PRE_VALIDATION_HOOK), abi.encodeWithSignature("Error(string)", "Target not allowed") ) ); account1.executeWithAuthorization( abi.encodeCall(UpgradeableModularAccount.execute, (beneficiary, 1 wei, "")), - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") ); } @@ -295,7 +302,7 @@ contract PerHookDataTest is CustomValidationTestBase { UpgradeableModularAccount.execute, (address(_counter), 0 wei, abi.encodeCall(Counter.increment, ())) ), - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, preValidationHookData, "") + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, preValidationHookData, "") ); } @@ -325,29 +332,23 @@ contract PerHookDataTest is CustomValidationTestBase { internal virtual override - returns (FunctionReference, bool, bool, bytes4[] memory, bytes memory, bytes memory, bytes memory) + returns (ModuleEntity, bool, bool, bytes4[] memory, bytes memory, bytes[] memory) { - FunctionReference accessControlHook = FunctionReferenceLib.pack( - address(_accessControlHookPlugin), uint8(MockAccessControlHookPlugin.FunctionId.PRE_VALIDATION_HOOK) + bytes[] memory hooks = new bytes[](1); + hooks[0] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(_accessControlHookModule), uint32(MockAccessControlHookModule.EntityId.PRE_VALIDATION_HOOK) + ), + abi.encode(_counter) ); - FunctionReference[] memory preValidationHooks = new FunctionReference[](1); - preValidationHooks[0] = accessControlHook; - - bytes[] memory preValidationHookData = new bytes[](1); - // Access control is restricted to only the counter - preValidationHookData[0] = abi.encode(_counter); - - bytes memory packedPreValidationHooks = abi.encode(preValidationHooks, preValidationHookData); - return ( - _ownerValidation, + _signerValidation, true, true, new bytes4[](0), - abi.encode(TEST_DEFAULT_OWNER_FUNCTION_ID, owner1), - packedPreValidationHooks, - "" + abi.encode(TEST_DEFAULT_VALIDATION_ENTITY_ID, owner1), + hooks ); } } diff --git a/test/account/PermittedCallPermissions.t.sol b/test/account/PermittedCallPermissions.t.sol index 18257955..f66382da 100644 --- a/test/account/PermittedCallPermissions.t.sol +++ b/test/account/PermittedCallPermissions.t.sol @@ -3,43 +3,41 @@ pragma solidity ^0.8.19; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {ResultCreatorPlugin} from "../mocks/plugins/ReturnDataPluginMocks.sol"; -import {PermittedCallerPlugin} from "../mocks/plugins/PermittedCallMocks.sol"; +import {PermittedCallerModule} from "../mocks/modules/PermittedCallMocks.sol"; +import {ResultCreatorModule} from "../mocks/modules/ReturnDataModuleMocks.sol"; import {AccountTestBase} from "../utils/AccountTestBase.sol"; contract PermittedCallPermissionsTest is AccountTestBase { - ResultCreatorPlugin public resultCreatorPlugin; + ResultCreatorModule public resultCreatorModule; - PermittedCallerPlugin public permittedCallerPlugin; + PermittedCallerModule public permittedCallerModule; function setUp() public { _transferOwnershipToTest(); - resultCreatorPlugin = new ResultCreatorPlugin(); + resultCreatorModule = new ResultCreatorModule(); - // Initialize the permitted caller plugins, which will attempt to use the permissions system to authorize + // Initialize the permitted caller modules, which will attempt to use the permissions system to authorize // calls. - permittedCallerPlugin = new PermittedCallerPlugin(); - - // Add the result creator plugin to the account - bytes32 resultCreatorManifestHash = keccak256(abi.encode(resultCreatorPlugin.pluginManifest())); - vm.prank(address(entryPoint)); - account1.installPlugin({ - plugin: address(resultCreatorPlugin), - manifestHash: resultCreatorManifestHash, - pluginInstallData: "" + permittedCallerModule = new PermittedCallerModule(); + + // Add the result creator module to the account + vm.startPrank(address(entryPoint)); + account1.installExecution({ + module: address(resultCreatorModule), + manifest: resultCreatorModule.executionManifest(), + moduleInstallData: "" }); - // Add the permitted caller plugin to the account - bytes32 permittedCallerManifestHash = keccak256(abi.encode(permittedCallerPlugin.pluginManifest())); - vm.prank(address(entryPoint)); - account1.installPlugin({ - plugin: address(permittedCallerPlugin), - manifestHash: permittedCallerManifestHash, - pluginInstallData: "" + // Add the permitted caller module to the account + account1.installExecution({ + module: address(permittedCallerModule), + manifest: permittedCallerModule.executionManifest(), + moduleInstallData: "" }); + vm.stopPrank(); } function test_permittedCall_Allowed() public { - bytes memory result = PermittedCallerPlugin(address(account1)).usePermittedCallAllowed(); + bytes memory result = PermittedCallerModule(address(account1)).usePermittedCallAllowed(); bytes32 actual = abi.decode(result, (bytes32)); assertEq(actual, keccak256("bar")); @@ -48,11 +46,9 @@ contract PermittedCallPermissionsTest is AccountTestBase { function test_permittedCall_NotAllowed() public { vm.expectRevert( abi.encodeWithSelector( - UpgradeableModularAccount.ExecFromPluginNotPermitted.selector, - address(permittedCallerPlugin), - ResultCreatorPlugin.bar.selector + UpgradeableModularAccount.ValidationFunctionMissing.selector, ResultCreatorModule.bar.selector ) ); - PermittedCallerPlugin(address(account1)).usePermittedCallNotAllowed(); + PermittedCallerModule(address(account1)).usePermittedCallNotAllowed(); } } diff --git a/test/account/SelfCallAuthorization.t.sol b/test/account/SelfCallAuthorization.t.sol index 5bcf64b3..ed158a30 100644 --- a/test/account/SelfCallAuthorization.t.sol +++ b/test/account/SelfCallAuthorization.t.sol @@ -6,43 +6,51 @@ import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntry import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {IStandardExecutor, Call} from "../../src/interfaces/IStandardExecutor.sol"; -import {FunctionReference, FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; + +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; +import {Call, IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; +import {ComprehensiveModule} from "../mocks/modules/ComprehensiveModule.sol"; import {AccountTestBase} from "../utils/AccountTestBase.sol"; -import {ComprehensivePlugin} from "../mocks/plugins/ComprehensivePlugin.sol"; contract SelfCallAuthorizationTest is AccountTestBase { - ComprehensivePlugin public comprehensivePlugin; + ComprehensiveModule public comprehensiveModule; - FunctionReference public comprehensivePluginValidation; + ModuleEntity public comprehensiveModuleValidation; function setUp() public { - // install the comprehensive plugin to get new exec functions with different validations configured. + // install the comprehensive module to get new exec functions with different validations configured. + + comprehensiveModule = new ComprehensiveModule(); - comprehensivePlugin = new ComprehensivePlugin(); + comprehensiveModuleValidation = + ModuleEntityLib.pack(address(comprehensiveModule), uint32(ComprehensiveModule.EntityId.VALIDATION)); - bytes32 manifestHash = keccak256(abi.encode(comprehensivePlugin.pluginManifest())); - vm.prank(address(entryPoint)); - account1.installPlugin(address(comprehensivePlugin), manifestHash, ""); + bytes4[] memory validationSelectors = new bytes4[](1); + validationSelectors[0] = ComprehensiveModule.foo.selector; - comprehensivePluginValidation = FunctionReferenceLib.pack( - address(comprehensivePlugin), uint8(ComprehensivePlugin.FunctionId.VALIDATION) + vm.startPrank(address(entryPoint)); + account1.installExecution(address(comprehensiveModule), comprehensiveModule.executionManifest(), ""); + account1.installValidation( + ValidationConfigLib.pack(comprehensiveModuleValidation, false, false), + validationSelectors, + "", + new bytes[](0) ); + vm.stopPrank(); } function test_selfCallFails_userOp() public { // Uses global validation _runUserOp( - abi.encodeCall(ComprehensivePlugin.foo, ()), + abi.encodeCall(ComprehensiveModule.foo, ()), abi.encodeWithSelector( IEntryPoint.FailedOpWithRevert.selector, 0, "AA23 reverted", abi.encodeWithSelector( - UpgradeableModularAccount.UserOpValidationFunctionMissing.selector, - ComprehensivePlugin.foo.selector + UpgradeableModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector ) ) ); @@ -51,14 +59,13 @@ contract SelfCallAuthorizationTest is AccountTestBase { function test_selfCallFails_execUserOp() public { // Uses global validation _runUserOp( - abi.encodePacked(IAccountExecute.executeUserOp.selector, abi.encodeCall(ComprehensivePlugin.foo, ())), + abi.encodePacked(IAccountExecute.executeUserOp.selector, abi.encodeCall(ComprehensiveModule.foo, ())), abi.encodeWithSelector( IEntryPoint.FailedOpWithRevert.selector, 0, "AA23 reverted", abi.encodeWithSelector( - UpgradeableModularAccount.UserOpValidationFunctionMissing.selector, - ComprehensivePlugin.foo.selector + UpgradeableModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector ) ) ); @@ -67,20 +74,19 @@ contract SelfCallAuthorizationTest is AccountTestBase { function test_selfCallFails_runtime() public { // Uses global validation _runtimeCall( - abi.encodeCall(ComprehensivePlugin.foo, ()), + abi.encodeCall(ComprehensiveModule.foo, ()), abi.encodeWithSelector( - UpgradeableModularAccount.UserOpValidationFunctionMissing.selector, - ComprehensivePlugin.foo.selector + UpgradeableModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector ) ); } function test_selfCallPrivilegeEscalation_prevented_userOp() public { - // Using global validation, self-call bypasses custom validation needed for ComprehensivePlugin.foo + // Using global validation, self-call bypasses custom validation needed for ComprehensiveModule.foo _runUserOp( abi.encodeCall( UpgradeableModularAccount.execute, - (address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())) + (address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())) ), abi.encodeWithSelector( IEntryPoint.FailedOpWithRevert.selector, @@ -91,7 +97,7 @@ contract SelfCallAuthorizationTest is AccountTestBase { ); Call[] memory calls = new Call[](1); - calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); _runUserOp( abi.encodeCall(IStandardExecutor.executeBatch, (calls)), @@ -100,21 +106,20 @@ contract SelfCallAuthorizationTest is AccountTestBase { 0, "AA23 reverted", abi.encodeWithSelector( - UpgradeableModularAccount.UserOpValidationFunctionMissing.selector, - ComprehensivePlugin.foo.selector + UpgradeableModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector ) ) ); } function test_selfCallPrivilegeEscalation_prevented_execUserOp() public { - // Using global validation, self-call bypasses custom validation needed for ComprehensivePlugin.foo + // Using global validation, self-call bypasses custom validation needed for ComprehensiveModule.foo _runUserOp( abi.encodePacked( IAccountExecute.executeUserOp.selector, abi.encodeCall( UpgradeableModularAccount.execute, - (address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())) + (address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())) ) ), abi.encodeWithSelector( @@ -126,7 +131,7 @@ contract SelfCallAuthorizationTest is AccountTestBase { ); Call[] memory calls = new Call[](1); - calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); _runUserOp( abi.encodePacked( @@ -137,31 +142,29 @@ contract SelfCallAuthorizationTest is AccountTestBase { 0, "AA23 reverted", abi.encodeWithSelector( - UpgradeableModularAccount.UserOpValidationFunctionMissing.selector, - ComprehensivePlugin.foo.selector + UpgradeableModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector ) ) ); } function test_selfCallPrivilegeEscalation_prevented_runtime() public { - // Using global validation, self-call bypasses custom validation needed for ComprehensivePlugin.foo + // Using global validation, self-call bypasses custom validation needed for ComprehensiveModule.foo _runtimeCall( abi.encodeCall( UpgradeableModularAccount.execute, - (address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())) + (address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())) ), abi.encodeWithSelector(UpgradeableModularAccount.SelfCallRecursionDepthExceeded.selector) ); Call[] memory calls = new Call[](1); - calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); _runtimeExecBatchExpFail( calls, abi.encodeWithSelector( - UpgradeableModularAccount.UserOpValidationFunctionMissing.selector, - ComprehensivePlugin.foo.selector + UpgradeableModularAccount.ValidationFunctionMissing.selector, ComprehensiveModule.foo.selector ) ); } @@ -170,17 +173,17 @@ contract SelfCallAuthorizationTest is AccountTestBase { _enableBatchValidation(); Call[] memory calls = new Call[](2); - calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); - calls[1] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + calls[1] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); - PackedUserOperation memory userOp = _generateUserOpWithComprehensivePluginValidation( + PackedUserOperation memory userOp = _generateUserOpWithComprehensiveModuleValidation( abi.encodeCall(IStandardExecutor.executeBatch, (calls)) ); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; - vm.expectCall(address(comprehensivePlugin), abi.encodeCall(ComprehensivePlugin.foo, ()), 2); + vm.expectCall(address(comprehensiveModule), abi.encodeCall(ComprehensiveModule.foo, ()), 2); entryPoint.handleOps(userOps, beneficiary); } @@ -188,10 +191,10 @@ contract SelfCallAuthorizationTest is AccountTestBase { _enableBatchValidation(); Call[] memory calls = new Call[](2); - calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); - calls[1] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + calls[1] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); - PackedUserOperation memory userOp = _generateUserOpWithComprehensivePluginValidation( + PackedUserOperation memory userOp = _generateUserOpWithComprehensiveModuleValidation( abi.encodePacked( IAccountExecute.executeUserOp.selector, abi.encodeCall(IStandardExecutor.executeBatch, (calls)) ) @@ -200,7 +203,7 @@ contract SelfCallAuthorizationTest is AccountTestBase { PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; - vm.expectCall(address(comprehensivePlugin), abi.encodeCall(ComprehensivePlugin.foo, ()), 2); + vm.expectCall(address(comprehensiveModule), abi.encodeCall(ComprehensiveModule.foo, ()), 2); entryPoint.handleOps(userOps, beneficiary); } @@ -208,13 +211,13 @@ contract SelfCallAuthorizationTest is AccountTestBase { _enableBatchValidation(); Call[] memory calls = new Call[](2); - calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); - calls[1] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); + calls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); + calls[1] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); - vm.expectCall(address(comprehensivePlugin), abi.encodeCall(ComprehensivePlugin.foo, ()), 2); + vm.expectCall(address(comprehensiveModule), abi.encodeCall(ComprehensiveModule.foo, ()), 2); account1.executeWithAuthorization( abi.encodeCall(IStandardExecutor.executeBatch, (calls)), - _encodeSignature(comprehensivePluginValidation, SELECTOR_ASSOCIATED_VALIDATION, "") + _encodeSignature(comprehensiveModuleValidation, SELECTOR_ASSOCIATED_VALIDATION, "") ); } @@ -222,12 +225,12 @@ contract SelfCallAuthorizationTest is AccountTestBase { _enableBatchValidation(); Call[] memory innerCalls = new Call[](1); - innerCalls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); + innerCalls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); Call[] memory outerCalls = new Call[](1); outerCalls[0] = Call(address(account1), 0, abi.encodeCall(IStandardExecutor.executeBatch, (innerCalls))); - PackedUserOperation memory userOp = _generateUserOpWithComprehensivePluginValidation( + PackedUserOperation memory userOp = _generateUserOpWithComprehensiveModuleValidation( abi.encodeCall(IStandardExecutor.executeBatch, (outerCalls)) ); @@ -249,12 +252,12 @@ contract SelfCallAuthorizationTest is AccountTestBase { _enableBatchValidation(); Call[] memory innerCalls = new Call[](1); - innerCalls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); + innerCalls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); Call[] memory outerCalls = new Call[](1); outerCalls[0] = Call(address(account1), 0, abi.encodeCall(IStandardExecutor.executeBatch, (innerCalls))); - PackedUserOperation memory userOp = _generateUserOpWithComprehensivePluginValidation( + PackedUserOperation memory userOp = _generateUserOpWithComprehensiveModuleValidation( abi.encodePacked( IAccountExecute.executeUserOp.selector, abi.encodeCall(IStandardExecutor.executeBatch, (outerCalls)) @@ -279,7 +282,7 @@ contract SelfCallAuthorizationTest is AccountTestBase { _enableBatchValidation(); Call[] memory innerCalls = new Call[](1); - innerCalls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensivePlugin.foo, ())); + innerCalls[0] = Call(address(account1), 0, abi.encodeCall(ComprehensiveModule.foo, ())); Call[] memory outerCalls = new Call[](1); outerCalls[0] = Call(address(account1), 0, abi.encodeCall(IStandardExecutor.executeBatch, (innerCalls))); @@ -287,12 +290,12 @@ contract SelfCallAuthorizationTest is AccountTestBase { vm.expectRevert(abi.encodeWithSelector(UpgradeableModularAccount.SelfCallRecursionDepthExceeded.selector)); account1.executeWithAuthorization( abi.encodeCall(IStandardExecutor.executeBatch, (outerCalls)), - _encodeSignature(comprehensivePluginValidation, SELECTOR_ASSOCIATED_VALIDATION, "") + _encodeSignature(comprehensiveModuleValidation, SELECTOR_ASSOCIATED_VALIDATION, "") ); } function _enableBatchValidation() internal { - // Extend ComprehensivePlugin's validation function to also validate `executeBatch`, to allow the + // Extend ComprehensiveModule's validation function to also validate `executeBatch`, to allow the // self-call. bytes4[] memory selectors = new bytes4[](1); @@ -302,13 +305,18 @@ contract SelfCallAuthorizationTest is AccountTestBase { account1.executeWithAuthorization( abi.encodeCall( UpgradeableModularAccount.installValidation, - (ValidationConfigLib.pack(comprehensivePluginValidation, false, false), selectors, "", "", "") + ( + ValidationConfigLib.pack(comprehensiveModuleValidation, false, false), + selectors, + "", + new bytes[](0) + ) ), - _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, "") + _encodeSignature(_signerValidation, GLOBAL_VALIDATION, "") ); } - function _generateUserOpWithComprehensivePluginValidation(bytes memory callData) + function _generateUserOpWithComprehensiveModuleValidation(bytes memory callData) internal view returns (PackedUserOperation memory) @@ -324,9 +332,9 @@ contract SelfCallAuthorizationTest is AccountTestBase { gasFees: _encodeGas(1, 1), paymasterAndData: hex"", signature: _encodeSignature( - comprehensivePluginValidation, + comprehensiveModuleValidation, SELECTOR_ASSOCIATED_VALIDATION, - // Comprehensive plugin's validation function doesn't actually check anything, so we don't need to + // Comprehensive module's validation function doesn't actually check anything, so we don't need to // sign anything. "" ) diff --git a/test/account/UpgradeableModularAccount.t.sol b/test/account/UpgradeableModularAccount.t.sol index dda78c66..0bb281a2 100644 --- a/test/account/UpgradeableModularAccount.t.sol +++ b/test/account/UpgradeableModularAccount.t.sol @@ -3,31 +3,35 @@ pragma solidity ^0.8.19; import {console} from "forge-std/Test.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; -import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import {PluginManagerInternals} from "../../src/account/PluginManagerInternals.sol"; +import {ModuleManagerInternals} from "../../src/account/ModuleManagerInternals.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {PluginManifest} from "../../src/interfaces/IPlugin.sol"; + import {IAccountLoupe} from "../../src/interfaces/IAccountLoupe.sol"; -import {IPluginManager} from "../../src/interfaces/IPluginManager.sol"; +import {ExecutionManifest} from "../../src/interfaces/IExecution.sol"; +import {IModuleManager} from "../../src/interfaces/IModuleManager.sol"; import {Call} from "../../src/interfaces/IStandardExecutor.sol"; -import {SingleOwnerPlugin} from "../../src/plugins/owner/SingleOwnerPlugin.sol"; -import {TokenReceiverPlugin} from "../../src/plugins/TokenReceiverPlugin.sol"; + +import {TokenReceiverModule} from "../../src/modules/TokenReceiverModule.sol"; +import {SingleSignerValidation} from "../../src/modules/validation/SingleSignerValidation.sol"; import {Counter} from "../mocks/Counter.sol"; -import {ComprehensivePlugin} from "../mocks/plugins/ComprehensivePlugin.sol"; -import {MockPlugin} from "../mocks/MockPlugin.sol"; + +import {MockModule} from "../mocks/MockModule.sol"; +import {ComprehensiveModule} from "../mocks/modules/ComprehensiveModule.sol"; import {AccountTestBase} from "../utils/AccountTestBase.sol"; -import {TEST_DEFAULT_OWNER_FUNCTION_ID} from "../utils/TestConstants.sol"; +import {TEST_DEFAULT_VALIDATION_ENTITY_ID} from "../utils/TestConstants.sol"; contract UpgradeableModularAccountTest is AccountTestBase { using ECDSA for bytes32; using MessageHashUtils for bytes32; - TokenReceiverPlugin public tokenReceiverPlugin; + TokenReceiverModule public tokenReceiverModule; // A separate account and owner that isn't deployed yet, used to test initcode address public owner2; @@ -36,14 +40,14 @@ contract UpgradeableModularAccountTest is AccountTestBase { address public ethRecipient; Counter public counter; - PluginManifest internal _manifest; + ExecutionManifest internal _manifest; - event PluginInstalled(address indexed plugin, bytes32 manifestHash); - event PluginUninstalled(address indexed plugin, bool indexed callbacksSucceeded); + event ModuleInstalled(address indexed module); + event ModuleUninstalled(address indexed module, bool indexed callbacksSucceeded); event ReceivedCall(bytes msgData, uint256 msgValue); function setUp() public { - tokenReceiverPlugin = _deployTokenReceiverPlugin(); + tokenReceiverModule = _deployTokenReceiverModule(); (owner2, owner2Key) = makeAddrAndKey("owner2"); @@ -77,7 +81,7 @@ contract UpgradeableModularAccountTest is AccountTestBase { // Generate signature bytes32 userOpHash = entryPoint.getUserOpHash(userOp); (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); - userOp.signature = _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature(_signerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -95,9 +99,9 @@ contract UpgradeableModularAccountTest is AccountTestBase { callData: abi.encodeCall( UpgradeableModularAccount.execute, ( - address(singleOwnerPlugin), + address(singleSignerValidation), 0, - abi.encodeCall(SingleOwnerPlugin.transferOwnership, (TEST_DEFAULT_OWNER_FUNCTION_ID, owner2)) + abi.encodeCall(SingleSignerValidation.transferSigner, (TEST_DEFAULT_VALIDATION_ENTITY_ID, owner2)) ) ), accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), @@ -110,7 +114,7 @@ contract UpgradeableModularAccountTest is AccountTestBase { // Generate signature bytes32 userOpHash = entryPoint.getUserOpHash(userOp); (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, userOpHash.toEthSignedMessageHash()); - userOp.signature = _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature(_signerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -136,7 +140,7 @@ contract UpgradeableModularAccountTest is AccountTestBase { // Generate signature bytes32 userOpHash = entryPoint.getUserOpHash(userOp); (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, userOpHash.toEthSignedMessageHash()); - userOp.signature = _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature(_signerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -162,7 +166,7 @@ contract UpgradeableModularAccountTest is AccountTestBase { // Generate signature bytes32 userOpHash = entryPoint.getUserOpHash(userOp); (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); - userOp.signature = _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature(_signerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -190,7 +194,7 @@ contract UpgradeableModularAccountTest is AccountTestBase { // Generate signature bytes32 userOpHash = entryPoint.getUserOpHash(userOp); (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); - userOp.signature = _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature(_signerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -221,7 +225,7 @@ contract UpgradeableModularAccountTest is AccountTestBase { // Generate signature bytes32 userOpHash = entryPoint.getUserOpHash(userOp); (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); - userOp.signature = _encodeSignature(_ownerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); + userOp.signature = _encodeSignature(_signerValidation, GLOBAL_VALIDATION, abi.encodePacked(r, s, v)); PackedUserOperation[] memory userOps = new PackedUserOperation[](1); userOps[0] = userOp; @@ -232,163 +236,104 @@ contract UpgradeableModularAccountTest is AccountTestBase { assertEq(ethRecipient.balance, 2 wei); } - function test_installPlugin() public { + function test_installExecution() public { vm.startPrank(address(entryPoint)); - bytes32 manifestHash = keccak256(abi.encode(tokenReceiverPlugin.pluginManifest())); - vm.expectEmit(true, true, true, true); - emit PluginInstalled(address(tokenReceiverPlugin), manifestHash); - IPluginManager(account1).installPlugin({ - plugin: address(tokenReceiverPlugin), - manifestHash: manifestHash, - pluginInstallData: abi.encode(uint48(1 days)) + emit ModuleInstalled(address(tokenReceiverModule)); + IModuleManager(account1).installExecution({ + module: address(tokenReceiverModule), + manifest: tokenReceiverModule.executionManifest(), + moduleInstallData: abi.encode(uint48(1 days)) }); - address[] memory plugins = IAccountLoupe(account1).getInstalledPlugins(); - assertEq(plugins.length, 1); - assertEq(plugins[0], address(tokenReceiverPlugin)); + address handler = + IAccountLoupe(account1).getExecutionFunctionHandler(TokenReceiverModule.onERC721Received.selector); + assertEq(handler, address(tokenReceiverModule)); } - function test_installPlugin_PermittedCallSelectorNotInstalled() public { + function test_installExecution_PermittedCallSelectorNotInstalled() public { vm.startPrank(address(entryPoint)); - PluginManifest memory m; + ExecutionManifest memory m; - MockPlugin mockPluginWithBadPermittedExec = new MockPlugin(m); - bytes32 manifestHash = keccak256(abi.encode(mockPluginWithBadPermittedExec.pluginManifest())); + MockModule mockModuleWithBadPermittedExec = new MockModule(m); - IPluginManager(account1).installPlugin({ - plugin: address(mockPluginWithBadPermittedExec), - manifestHash: manifestHash, - pluginInstallData: "" + IModuleManager(account1).installExecution({ + module: address(mockModuleWithBadPermittedExec), + manifest: mockModuleWithBadPermittedExec.executionManifest(), + moduleInstallData: "" }); } - function test_installPlugin_invalidManifest() public { + function test_installExecution_interfaceNotSupported() public { vm.startPrank(address(entryPoint)); - vm.expectRevert(abi.encodeWithSelector(PluginManagerInternals.InvalidPluginManifest.selector)); - IPluginManager(account1).installPlugin({ - plugin: address(tokenReceiverPlugin), - manifestHash: bytes32(0), - pluginInstallData: abi.encode(uint48(1 days)) - }); - } - - function test_installPlugin_interfaceNotSupported() public { - vm.startPrank(address(entryPoint)); - - address badPlugin = address(1); + address badModule = address(1); vm.expectRevert( - abi.encodeWithSelector(PluginManagerInternals.PluginInterfaceNotSupported.selector, address(badPlugin)) + abi.encodeWithSelector(ModuleManagerInternals.ModuleInterfaceNotSupported.selector, address(badModule)) ); - IPluginManager(account1).installPlugin({ - plugin: address(badPlugin), - manifestHash: bytes32(0), - pluginInstallData: "" - }); + + ExecutionManifest memory m; + + IModuleManager(account1).installExecution({module: address(badModule), manifest: m, moduleInstallData: ""}); } - function test_installPlugin_alreadyInstalled() public { - vm.startPrank(address(entryPoint)); + function test_installExecution_alreadyInstalled() public { + ExecutionManifest memory m = tokenReceiverModule.executionManifest(); - bytes32 manifestHash = keccak256(abi.encode(tokenReceiverPlugin.pluginManifest())); - IPluginManager(account1).installPlugin({ - plugin: address(tokenReceiverPlugin), - manifestHash: manifestHash, - pluginInstallData: abi.encode(uint48(1 days)) + vm.prank(address(entryPoint)); + IModuleManager(account1).installExecution({ + module: address(tokenReceiverModule), + manifest: m, + moduleInstallData: abi.encode(uint48(1 days)) }); + vm.prank(address(entryPoint)); vm.expectRevert( abi.encodeWithSelector( - PluginManagerInternals.PluginAlreadyInstalled.selector, address(tokenReceiverPlugin) + ModuleManagerInternals.ExecutionFunctionAlreadySet.selector, + TokenReceiverModule.onERC721Received.selector ) ); - IPluginManager(account1).installPlugin({ - plugin: address(tokenReceiverPlugin), - manifestHash: manifestHash, - pluginInstallData: abi.encode(uint48(1 days)) + IModuleManager(account1).installExecution({ + module: address(tokenReceiverModule), + manifest: m, + moduleInstallData: abi.encode(uint48(1 days)) }); } - function test_uninstallPlugin_default() public { + function test_uninstallExecution_default() public { vm.startPrank(address(entryPoint)); - ComprehensivePlugin plugin = new ComprehensivePlugin(); - bytes32 manifestHash = keccak256(abi.encode(plugin.pluginManifest())); - IPluginManager(account1).installPlugin({ - plugin: address(plugin), - manifestHash: manifestHash, - pluginInstallData: "" + ComprehensiveModule module = new ComprehensiveModule(); + IModuleManager(account1).installExecution({ + module: address(module), + manifest: module.executionManifest(), + moduleInstallData: "" }); vm.expectEmit(true, true, true, true); - emit PluginUninstalled(address(plugin), true); - IPluginManager(account1).uninstallPlugin({plugin: address(plugin), config: "", pluginUninstallData: ""}); - address[] memory plugins = IAccountLoupe(account1).getInstalledPlugins(); - assertEq(plugins.length, 0); - } - - function test_uninstallPlugin_manifestParameter() public { - vm.startPrank(address(entryPoint)); - - ComprehensivePlugin plugin = new ComprehensivePlugin(); - bytes memory serializedManifest = abi.encode(plugin.pluginManifest()); - bytes32 manifestHash = keccak256(serializedManifest); - IPluginManager(account1).installPlugin({ - plugin: address(plugin), - manifestHash: manifestHash, - pluginInstallData: "" + emit ModuleUninstalled(address(module), true); + IModuleManager(account1).uninstallExecution({ + module: address(module), + manifest: module.executionManifest(), + moduleUninstallData: "" }); - vm.expectEmit(true, true, true, true); - emit PluginUninstalled(address(plugin), true); - IPluginManager(account1).uninstallPlugin({ - plugin: address(plugin), - config: serializedManifest, - pluginUninstallData: "" - }); - address[] memory plugins = IAccountLoupe(account1).getInstalledPlugins(); - assertEq(plugins.length, 0); - } - - function test_uninstallPlugin_invalidManifestFails() public { - vm.startPrank(address(entryPoint)); - - ComprehensivePlugin plugin = new ComprehensivePlugin(); - bytes memory serializedManifest = abi.encode(plugin.pluginManifest()); - bytes32 manifestHash = keccak256(serializedManifest); - IPluginManager(account1).installPlugin({ - plugin: address(plugin), - manifestHash: manifestHash, - pluginInstallData: "" - }); - - // Attempt to uninstall with a blank _manifest - PluginManifest memory blankManifest; - - vm.expectRevert(abi.encodeWithSelector(PluginManagerInternals.InvalidPluginManifest.selector)); - IPluginManager(account1).uninstallPlugin({ - plugin: address(plugin), - config: abi.encode(blankManifest), - pluginUninstallData: "" - }); - address[] memory plugins = IAccountLoupe(account1).getInstalledPlugins(); - assertEq(plugins.length, 1); - assertEq(plugins[0], address(plugin)); + address handler = IAccountLoupe(account1).getExecutionFunctionHandler(module.foo.selector); + assertEq(handler, address(0)); } - function _installPluginWithExecHooks() internal returns (MockPlugin plugin) { + function _installExecutionWithExecHooks() internal returns (MockModule module) { vm.startPrank(address(entryPoint)); - plugin = new MockPlugin(_manifest); - bytes32 manifestHash = keccak256(abi.encode(plugin.pluginManifest())); + module = new MockModule(_manifest); - IPluginManager(account1).installPlugin({ - plugin: address(plugin), - manifestHash: manifestHash, - pluginInstallData: "" + IModuleManager(account1).installExecution({ + module: address(module), + manifest: module.executionManifest(), + moduleInstallData: "" }); vm.stopPrank(); @@ -409,16 +354,16 @@ contract UpgradeableModularAccountTest is AccountTestBase { } function test_transferOwnership() public { - assertEq(singleOwnerPlugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, address(account1)), owner1); + assertEq(singleSignerValidation.signerOf(TEST_DEFAULT_VALIDATION_ENTITY_ID, address(account1)), owner1); vm.prank(address(entryPoint)); account1.execute( - address(singleOwnerPlugin), + address(singleSignerValidation), 0, - abi.encodeCall(SingleOwnerPlugin.transferOwnership, (TEST_DEFAULT_OWNER_FUNCTION_ID, owner2)) + abi.encodeCall(SingleSignerValidation.transferSigner, (TEST_DEFAULT_VALIDATION_ENTITY_ID, owner2)) ); - assertEq(singleOwnerPlugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, address(account1)), owner2); + assertEq(singleSignerValidation.signerOf(TEST_DEFAULT_VALIDATION_ENTITY_ID, address(account1)), owner2); } function test_isValidSignature() public { @@ -426,10 +371,10 @@ contract UpgradeableModularAccountTest is AccountTestBase { (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, message); - // singleOwnerPlugin.ownerOf(address(account1)); + // singleSignerValidation.ownerOf(address(account1)); bytes memory signature = - abi.encodePacked(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID, r, s, v); + abi.encodePacked(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID, r, s, v); bytes4 validationResult = IERC1271(address(account1)).isValidSignature(message, signature); diff --git a/test/account/ValidationIntersection.t.sol b/test/account/ValidationIntersection.t.sol index 7f245031..73deb89f 100644 --- a/test/account/ValidationIntersection.t.sol +++ b/test/account/ValidationIntersection.t.sol @@ -4,105 +4,95 @@ pragma solidity ^0.8.19; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {FunctionReference, FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; + +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; import { - MockBaseUserOpValidationPlugin, - MockUserOpValidation1HookPlugin, - MockUserOpValidation2HookPlugin, - MockUserOpValidationPlugin -} from "../mocks/plugins/ValidationPluginMocks.sol"; + MockBaseUserOpValidationModule, + MockUserOpValidation1HookModule, + MockUserOpValidation2HookModule, + MockUserOpValidationModule +} from "../mocks/modules/ValidationModuleMocks.sol"; import {AccountTestBase} from "../utils/AccountTestBase.sol"; contract ValidationIntersectionTest is AccountTestBase { uint256 internal constant _SIG_VALIDATION_FAILED = 1; - MockUserOpValidationPlugin public noHookPlugin; - MockUserOpValidation1HookPlugin public oneHookPlugin; - MockUserOpValidation2HookPlugin public twoHookPlugin; + MockUserOpValidationModule public noHookModule; + MockUserOpValidation1HookModule public oneHookModule; + MockUserOpValidation2HookModule public twoHookModule; - FunctionReference public noHookValidation; - FunctionReference public oneHookValidation; - FunctionReference public twoHookValidation; + ModuleEntity public noHookValidation; + ModuleEntity public oneHookValidation; + ModuleEntity public twoHookValidation; function setUp() public { - noHookPlugin = new MockUserOpValidationPlugin(); - oneHookPlugin = new MockUserOpValidation1HookPlugin(); - twoHookPlugin = new MockUserOpValidation2HookPlugin(); + noHookModule = new MockUserOpValidationModule(); + oneHookModule = new MockUserOpValidation1HookModule(); + twoHookModule = new MockUserOpValidation2HookModule(); - noHookValidation = FunctionReferenceLib.pack({ - addr: address(noHookPlugin), - functionId: uint8(MockBaseUserOpValidationPlugin.FunctionId.USER_OP_VALIDATION) + noHookValidation = ModuleEntityLib.pack({ + addr: address(noHookModule), + entityId: uint32(MockBaseUserOpValidationModule.EntityId.USER_OP_VALIDATION) }); - oneHookValidation = FunctionReferenceLib.pack({ - addr: address(oneHookPlugin), - functionId: uint8(MockBaseUserOpValidationPlugin.FunctionId.USER_OP_VALIDATION) + oneHookValidation = ModuleEntityLib.pack({ + addr: address(oneHookModule), + entityId: uint32(MockBaseUserOpValidationModule.EntityId.USER_OP_VALIDATION) }); - twoHookValidation = FunctionReferenceLib.pack({ - addr: address(twoHookPlugin), - functionId: uint8(MockBaseUserOpValidationPlugin.FunctionId.USER_OP_VALIDATION) + twoHookValidation = ModuleEntityLib.pack({ + addr: address(twoHookModule), + entityId: uint32(MockBaseUserOpValidationModule.EntityId.USER_OP_VALIDATION) }); + bytes4[] memory validationSelectors = new bytes4[](1); + validationSelectors[0] = MockUserOpValidationModule.foo.selector; + vm.startPrank(address(entryPoint)); - account1.installPlugin({ - plugin: address(noHookPlugin), - manifestHash: keccak256(abi.encode(noHookPlugin.pluginManifest())), - pluginInstallData: "" - }); - account1.installPlugin({ - plugin: address(oneHookPlugin), - manifestHash: keccak256(abi.encode(oneHookPlugin.pluginManifest())), - pluginInstallData: "" - }); - // TODO: change with new install flow - // temporary fix to add the pre-validation hook - FunctionReference[] memory preValidationHooks = new FunctionReference[](1); - preValidationHooks[0] = FunctionReferenceLib.pack({ - addr: address(oneHookPlugin), - functionId: uint8(MockBaseUserOpValidationPlugin.FunctionId.PRE_VALIDATION_HOOK_1) - }); - bytes[] memory installDatas = new bytes[](1); + // Install noHookValidation account1.installValidation( - ValidationConfigLib.pack(oneHookValidation, true, true), - new bytes4[](0), - bytes(""), - abi.encode(preValidationHooks, installDatas), - bytes("") + ValidationConfigLib.pack(noHookValidation, true, true), validationSelectors, bytes(""), new bytes[](0) + ); + + // Install oneHookValidation + validationSelectors[0] = MockUserOpValidation1HookModule.bar.selector; + bytes[] memory hooks = new bytes[](1); + hooks[0] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(oneHookModule), uint32(MockBaseUserOpValidationModule.EntityId.PRE_VALIDATION_HOOK_1) + ) + ); + account1.installValidation( + ValidationConfigLib.pack(oneHookValidation, true, true), validationSelectors, bytes(""), hooks + ); + + // Install twoHookValidation + validationSelectors[0] = MockUserOpValidation2HookModule.baz.selector; + hooks = new bytes[](2); + hooks[0] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(twoHookModule), uint32(MockBaseUserOpValidationModule.EntityId.PRE_VALIDATION_HOOK_1) + ) + ); + hooks[1] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(twoHookModule), uint32(MockBaseUserOpValidationModule.EntityId.PRE_VALIDATION_HOOK_2) + ) ); - account1.installPlugin({ - plugin: address(twoHookPlugin), - manifestHash: keccak256(abi.encode(twoHookPlugin.pluginManifest())), - pluginInstallData: "" - }); - // temporary fix to add the pre-validation hook - preValidationHooks = new FunctionReference[](2); - preValidationHooks[0] = FunctionReferenceLib.pack({ - addr: address(twoHookPlugin), - functionId: uint8(MockBaseUserOpValidationPlugin.FunctionId.PRE_VALIDATION_HOOK_1) - }); - preValidationHooks[1] = FunctionReferenceLib.pack({ - addr: address(twoHookPlugin), - functionId: uint8(MockBaseUserOpValidationPlugin.FunctionId.PRE_VALIDATION_HOOK_2) - }); - installDatas = new bytes[](2); account1.installValidation( - ValidationConfigLib.pack(twoHookValidation, true, true), - new bytes4[](0), - bytes(""), - abi.encode(preValidationHooks, installDatas), - bytes("") + ValidationConfigLib.pack(twoHookValidation, true, true), validationSelectors, bytes(""), hooks ); vm.stopPrank(); } function testFuzz_validationIntersect_single(uint256 validationData) public { - noHookPlugin.setValidationData(validationData); + noHookModule.setValidationData(validationData); PackedUserOperation memory userOp; - userOp.callData = bytes.concat(noHookPlugin.foo.selector); + userOp.callData = bytes.concat(noHookModule.foo.selector); userOp.signature = _encodeSignature(noHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); bytes32 uoHash = entryPoint.getUserOpHash(userOp); @@ -113,13 +103,13 @@ contract ValidationIntersectionTest is AccountTestBase { } function test_validationIntersect_authorizer_sigfail_validationFunction() public { - oneHookPlugin.setValidationData( + oneHookModule.setValidationData( _SIG_VALIDATION_FAILED, 0 // returns OK ); PackedUserOperation memory userOp; - userOp.callData = bytes.concat(oneHookPlugin.bar.selector); + userOp.callData = bytes.concat(oneHookModule.bar.selector); userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); bytes32 uoHash = entryPoint.getUserOpHash(userOp); @@ -131,13 +121,13 @@ contract ValidationIntersectionTest is AccountTestBase { } function test_validationIntersect_authorizer_sigfail_hook() public { - oneHookPlugin.setValidationData( + oneHookModule.setValidationData( 0, // returns OK _SIG_VALIDATION_FAILED ); PackedUserOperation memory userOp; - userOp.callData = bytes.concat(oneHookPlugin.bar.selector); + userOp.callData = bytes.concat(oneHookModule.bar.selector); userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); bytes32 uoHash = entryPoint.getUserOpHash(userOp); @@ -155,19 +145,19 @@ contract ValidationIntersectionTest is AccountTestBase { uint48 start2 = uint48(15); uint48 end2 = uint48(25); - oneHookPlugin.setValidationData( - _packValidationData(address(0), start1, end1), _packValidationData(address(0), start2, end2) + oneHookModule.setValidationData( + _packValidationRes(address(0), start1, end1), _packValidationRes(address(0), start2, end2) ); PackedUserOperation memory userOp; - userOp.callData = bytes.concat(oneHookPlugin.bar.selector); + userOp.callData = bytes.concat(oneHookModule.bar.selector); userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); bytes32 uoHash = entryPoint.getUserOpHash(userOp); vm.prank(address(entryPoint)); uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); - assertEq(returnedValidationData, _packValidationData(address(0), start2, end1)); + assertEq(returnedValidationData, _packValidationRes(address(0), start2, end1)); } function test_validationIntersect_timeBounds_intersect_2() public { @@ -177,32 +167,32 @@ contract ValidationIntersectionTest is AccountTestBase { uint48 start2 = uint48(15); uint48 end2 = uint48(25); - oneHookPlugin.setValidationData( - _packValidationData(address(0), start2, end2), _packValidationData(address(0), start1, end1) + oneHookModule.setValidationData( + _packValidationRes(address(0), start2, end2), _packValidationRes(address(0), start1, end1) ); PackedUserOperation memory userOp; - userOp.callData = bytes.concat(oneHookPlugin.bar.selector); + userOp.callData = bytes.concat(oneHookModule.bar.selector); userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); bytes32 uoHash = entryPoint.getUserOpHash(userOp); vm.prank(address(entryPoint)); uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); - assertEq(returnedValidationData, _packValidationData(address(0), start2, end1)); + assertEq(returnedValidationData, _packValidationRes(address(0), start2, end1)); } function test_validationIntersect_revert_unexpectedAuthorizer() public { address badAuthorizer = makeAddr("badAuthorizer"); - oneHookPlugin.setValidationData( + oneHookModule.setValidationData( 0, // returns OK uint256(uint160(badAuthorizer)) // returns an aggregator, which preValidation hooks are not allowed to // do. ); PackedUserOperation memory userOp; - userOp.callData = bytes.concat(oneHookPlugin.bar.selector); + userOp.callData = bytes.concat(oneHookModule.bar.selector); userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); bytes32 uoHash = entryPoint.getUserOpHash(userOp); @@ -210,8 +200,8 @@ contract ValidationIntersectionTest is AccountTestBase { vm.expectRevert( abi.encodeWithSelector( UpgradeableModularAccount.UnexpectedAggregator.selector, - address(oneHookPlugin), - MockBaseUserOpValidationPlugin.FunctionId.PRE_VALIDATION_HOOK_1, + address(oneHookModule), + MockBaseUserOpValidationModule.EntityId.PRE_VALIDATION_HOOK_1, badAuthorizer ) ); @@ -221,13 +211,13 @@ contract ValidationIntersectionTest is AccountTestBase { function test_validationIntersect_validAuthorizer() public { address goodAuthorizer = makeAddr("goodAuthorizer"); - oneHookPlugin.setValidationData( + oneHookModule.setValidationData( uint256(uint160(goodAuthorizer)), // returns a valid aggregator 0 // returns OK ); PackedUserOperation memory userOp; - userOp.callData = bytes.concat(oneHookPlugin.bar.selector); + userOp.callData = bytes.concat(oneHookModule.bar.selector); userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); bytes32 uoHash = entryPoint.getUserOpHash(userOp); @@ -246,19 +236,19 @@ contract ValidationIntersectionTest is AccountTestBase { address goodAuthorizer = makeAddr("goodAuthorizer"); - oneHookPlugin.setValidationData( - _packValidationData(goodAuthorizer, start1, end1), _packValidationData(address(0), start2, end2) + oneHookModule.setValidationData( + _packValidationRes(goodAuthorizer, start1, end1), _packValidationRes(address(0), start2, end2) ); PackedUserOperation memory userOp; - userOp.callData = bytes.concat(oneHookPlugin.bar.selector); + userOp.callData = bytes.concat(oneHookModule.bar.selector); userOp.signature = _encodeSignature(oneHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); bytes32 uoHash = entryPoint.getUserOpHash(userOp); vm.prank(address(entryPoint)); uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); - assertEq(returnedValidationData, _packValidationData(goodAuthorizer, start2, end1)); + assertEq(returnedValidationData, _packValidationRes(goodAuthorizer, start2, end1)); } function test_validationIntersect_multiplePreValidationHooksIntersect() public { @@ -268,32 +258,32 @@ contract ValidationIntersectionTest is AccountTestBase { uint48 start2 = uint48(15); uint48 end2 = uint48(25); - twoHookPlugin.setValidationData( + twoHookModule.setValidationData( 0, // returns OK - _packValidationData(address(0), start1, end1), - _packValidationData(address(0), start2, end2) + _packValidationRes(address(0), start1, end1), + _packValidationRes(address(0), start2, end2) ); PackedUserOperation memory userOp; - userOp.callData = bytes.concat(twoHookPlugin.baz.selector); + userOp.callData = bytes.concat(twoHookModule.baz.selector); userOp.signature = _encodeSignature(twoHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); bytes32 uoHash = entryPoint.getUserOpHash(userOp); vm.prank(address(entryPoint)); uint256 returnedValidationData = account1.validateUserOp(userOp, uoHash, 1 wei); - assertEq(returnedValidationData, _packValidationData(address(0), start2, end1)); + assertEq(returnedValidationData, _packValidationRes(address(0), start2, end1)); } function test_validationIntersect_multiplePreValidationHooksSigFail() public { - twoHookPlugin.setValidationData( + twoHookModule.setValidationData( 0, // returns OK 0, // returns OK _SIG_VALIDATION_FAILED ); PackedUserOperation memory userOp; - userOp.callData = bytes.concat(twoHookPlugin.baz.selector); + userOp.callData = bytes.concat(twoHookModule.baz.selector); userOp.signature = _encodeSignature(twoHookValidation, SELECTOR_ASSOCIATED_VALIDATION, ""); bytes32 uoHash = entryPoint.getUserOpHash(userOp); @@ -318,7 +308,7 @@ contract ValidationIntersectionTest is AccountTestBase { validAfter = uint48(validationData >> (48 + 160)); } - function _packValidationData(address authorizer, uint48 validAfter, uint48 validUntil) + function _packValidationRes(address authorizer, uint48 validAfter, uint48 validUntil) internal pure returns (uint256) diff --git a/test/comparison/CompareSimpleAccount.t.sol b/test/comparison/CompareSimpleAccount.t.sol index 93b75ebe..a7f59b69 100644 --- a/test/comparison/CompareSimpleAccount.t.sol +++ b/test/comparison/CompareSimpleAccount.t.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {EntryPoint} from "@eth-infinitism/account-abstraction/core/EntryPoint.sol"; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {SimpleAccount} from "@eth-infinitism/account-abstraction/samples/SimpleAccount.sol"; import {SimpleAccountFactory} from "@eth-infinitism/account-abstraction/samples/SimpleAccountFactory.sol"; @@ -34,8 +34,8 @@ contract CompareSimpleAccountTest is Test { Counter public counter; - uint256 public constant CALL_GAS_LIMIT = 500000; - uint256 public constant VERIFICATION_GAS_LIMIT = 500000; + uint256 public constant CALL_GAS_LIMIT = 500_000; + uint256 public constant VERIFICATION_GAS_LIMIT = 500_000; // helper function to compress 2 gas values into a single bytes32 function _encodeGas(uint256 g1, uint256 g2) internal pure returns (bytes32) { diff --git a/test/libraries/AccountStorage.t.sol b/test/libraries/AccountStorage.t.sol index 25594cd2..71c35b6e 100644 --- a/test/libraries/AccountStorage.t.sol +++ b/test/libraries/AccountStorage.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {Test} from "forge-std/Test.sol"; import {_ACCOUNT_STORAGE_SLOT} from "../../src/account/AccountStorage.sol"; import {AccountStorageInitializable} from "../../src/account/AccountStorageInitializable.sol"; import {MockDiamondStorageContract} from "../mocks/MockDiamondStorageContract.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {Test} from "forge-std/Test.sol"; // Test implementation of AccountStorageInitializable which is contained in UpgradeableModularAccount contract AccountStorageTest is Test { diff --git a/test/libraries/FunctionReferenceLib.t.sol b/test/libraries/FunctionReferenceLib.t.sol deleted file mode 100644 index 6471fbd0..00000000 --- a/test/libraries/FunctionReferenceLib.t.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; - -import {FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; -import {FunctionReference} from "../../src/interfaces/IPluginManager.sol"; - -contract FunctionReferenceLibTest is Test { - using FunctionReferenceLib for FunctionReference; - - function testFuzz_functionReference_packing(address addr, uint8 functionId) public { - // console.log("addr: ", addr); - // console.log("functionId: ", vm.toString(functionId)); - FunctionReference fr = FunctionReferenceLib.pack(addr, functionId); - // console.log("packed: ", vm.toString(FunctionReference.unwrap(fr))); - (address addr2, uint8 functionId2) = FunctionReferenceLib.unpack(fr); - // console.log("addr2: ", addr2); - // console.log("functionId2: ", vm.toString(functionId2)); - assertEq(addr, addr2); - assertEq(functionId, functionId2); - } - - function testFuzz_functionReference_operators(FunctionReference a, FunctionReference b) public { - assertTrue(a.eq(a)); - assertTrue(b.eq(b)); - - if (FunctionReference.unwrap(a) == FunctionReference.unwrap(b)) { - assertTrue(a.eq(b)); - assertTrue(b.eq(a)); - assertFalse(a.notEq(b)); - assertFalse(b.notEq(a)); - } else { - assertTrue(a.notEq(b)); - assertTrue(b.notEq(a)); - assertFalse(a.eq(b)); - assertFalse(b.eq(a)); - } - } -} diff --git a/test/libraries/HookConfigLib.t.sol b/test/libraries/HookConfigLib.t.sol new file mode 100644 index 00000000..7a4671b8 --- /dev/null +++ b/test/libraries/HookConfigLib.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; + +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {HookConfig, ModuleEntity} from "../../src/interfaces/IModuleManager.sol"; + +contract HookConfigLibTest is Test { + using ModuleEntityLib for ModuleEntity; + using HookConfigLib for HookConfig; + + // Tests the packing and unpacking of a hook config with a randomized state + + function testFuzz_hookConfig_packingUnderlying( + address addr, + uint32 entityId, + bool isValidation, + bool hasPre, + bool hasPost + ) public { + HookConfig hookConfig; + + if (isValidation) { + hookConfig = HookConfigLib.packValidationHook(addr, entityId); + } else { + hookConfig = HookConfigLib.packExecHook(addr, entityId, hasPre, hasPost); + } + + assertEq(hookConfig.module(), addr, "module mismatch"); + assertEq(hookConfig.entityId(), entityId, "entityId mismatch"); + assertEq(hookConfig.isValidationHook(), isValidation, "isValidation mismatch"); + + if (!isValidation) { + assertEq(hookConfig.hasPreHook(), hasPre, "hasPre mismatch"); + assertEq(hookConfig.hasPostHook(), hasPost, "hasPost mismatch"); + } + } + + function testFuzz_hookConfig_packingModuleEntity( + ModuleEntity hookFunction, + bool isValidation, + bool hasPre, + bool hasPost + ) public { + HookConfig hookConfig; + + if (isValidation) { + hookConfig = HookConfigLib.packValidationHook(hookFunction); + } else { + hookConfig = HookConfigLib.packExecHook(hookFunction, hasPre, hasPost); + } + + assertEq( + ModuleEntity.unwrap(hookConfig.moduleEntity()), + ModuleEntity.unwrap(hookFunction), + "moduleEntity mismatch" + ); + assertEq(hookConfig.isValidationHook(), isValidation, "isValidation mismatch"); + + if (!isValidation) { + assertEq(hookConfig.hasPreHook(), hasPre, "hasPre mismatch"); + assertEq(hookConfig.hasPostHook(), hasPost, "hasPost mismatch"); + } + } +} diff --git a/test/libraries/KnowSelectors.t.sol b/test/libraries/KnowSelectors.t.sol index 893b831b..48bd17e9 100644 --- a/test/libraries/KnowSelectors.t.sol +++ b/test/libraries/KnowSelectors.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.25; -import {Test} from "forge-std/Test.sol"; import {IAccount} from "@eth-infinitism/account-abstraction/interfaces/IAccount.sol"; import {IPaymaster} from "@eth-infinitism/account-abstraction/interfaces/IPaymaster.sol"; +import {Test} from "forge-std/Test.sol"; import {KnownSelectors} from "../../src/helpers/KnownSelectors.sol"; -import {IPlugin} from "../../src/interfaces/IPlugin.sol"; +import {IModule} from "../../src/interfaces/IModule.sol"; contract KnownSelectorsTest is Test { function test_isNativeFunction() public { @@ -17,7 +17,7 @@ contract KnownSelectorsTest is Test { assertTrue(KnownSelectors.isErc4337Function(IPaymaster.validatePaymasterUserOp.selector)); } - function test_isIPluginFunction() public { - assertTrue(KnownSelectors.isIPluginFunction(IPlugin.pluginMetadata.selector)); + function test_isIModuleFunction() public { + assertTrue(KnownSelectors.isIModuleFunction(IModule.moduleMetadata.selector)); } } diff --git a/test/libraries/ModuleEntityLib.t.sol b/test/libraries/ModuleEntityLib.t.sol new file mode 100644 index 00000000..205e53b9 --- /dev/null +++ b/test/libraries/ModuleEntityLib.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {Test} from "forge-std/Test.sol"; + +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ModuleEntity} from "../../src/interfaces/IModuleManager.sol"; + +contract ModuleEntityLibTest is Test { + using ModuleEntityLib for ModuleEntity; + + function testFuzz_moduleEntity_packing(address addr, uint32 entityId) public { + // console.log("addr: ", addr); + // console.log("entityId: ", vm.toString(entityId)); + ModuleEntity fr = ModuleEntityLib.pack(addr, entityId); + // console.log("packed: ", vm.toString(ModuleEntity.unwrap(fr))); + (address addr2, uint32 entityId2) = ModuleEntityLib.unpack(fr); + // console.log("addr2: ", addr2); + // console.log("entityId2: ", vm.toString(entityId2)); + assertEq(addr, addr2); + assertEq(entityId, entityId2); + } + + function testFuzz_moduleEntity_operators(ModuleEntity a, ModuleEntity b) public { + assertTrue(a.eq(a)); + assertTrue(b.eq(b)); + + if (ModuleEntity.unwrap(a) == ModuleEntity.unwrap(b)) { + assertTrue(a.eq(b)); + assertTrue(b.eq(a)); + assertFalse(a.notEq(b)); + assertFalse(b.notEq(a)); + } else { + assertTrue(a.notEq(b)); + assertTrue(b.notEq(a)); + assertFalse(a.eq(b)); + assertFalse(b.eq(a)); + } + } +} diff --git a/test/libraries/ValidationConfigLib.t.sol b/test/libraries/ValidationConfigLib.t.sol new file mode 100644 index 00000000..4d49c383 --- /dev/null +++ b/test/libraries/ValidationConfigLib.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; + +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; +import {ModuleEntity, ValidationConfig} from "../../src/interfaces/IModuleManager.sol"; + +contract ValidationConfigLibTest is Test { + using ModuleEntityLib for ModuleEntity; + using ValidationConfigLib for ValidationConfig; + + // Tests the packing and unpacking of a validation config with a randomized state + + function testFuzz_validationConfig_packingUnderlying( + address module, + uint32 entityId, + bool isGlobal, + bool isSignatureValidation + ) public { + ValidationConfig validationConfig = + ValidationConfigLib.pack(module, entityId, isGlobal, isSignatureValidation); + + // Test unpacking underlying + (address module2, uint32 entityId2, bool isGlobal2, bool isSignatureValidation2) = + validationConfig.unpackUnderlying(); + + assertEq(module, module2, "module mismatch"); + assertEq(entityId, entityId2, "entityId mismatch"); + assertEq(isGlobal, isGlobal2, "isGlobal mismatch"); + assertEq(isSignatureValidation, isSignatureValidation2, "isSignatureValidation mismatch"); + + // Test unpacking to ModuleEntity + + ModuleEntity expectedModuleEntity = ModuleEntityLib.pack(module, entityId); + + (ModuleEntity validationFunction, bool isGlobal3, bool isSignatureValidation3) = validationConfig.unpack(); + + assertEq( + ModuleEntity.unwrap(validationFunction), + ModuleEntity.unwrap(expectedModuleEntity), + "validationFunction mismatch" + ); + assertEq(isGlobal, isGlobal3, "isGlobal mismatch"); + assertEq(isSignatureValidation, isSignatureValidation3, "isSignatureValidation mismatch"); + + // Test individual view functions + + assertEq(validationConfig.module(), module, "module mismatch"); + assertEq(validationConfig.entityId(), entityId, "entityId mismatch"); + assertEq( + ModuleEntity.unwrap(validationConfig.moduleEntity()), + ModuleEntity.unwrap(expectedModuleEntity), + "moduleEntity mismatch" + ); + assertEq(validationConfig.isGlobal(), isGlobal, "isGlobal mismatch"); + assertEq(validationConfig.isSignatureValidation(), isSignatureValidation, "isSignatureValidation mismatch"); + } + + function testFuzz_validationConfig_packingModuleEntity( + ModuleEntity validationFunction, + bool isGlobal, + bool isSignatureValidation + ) public { + ValidationConfig validationConfig = + ValidationConfigLib.pack(validationFunction, isGlobal, isSignatureValidation); + + // Test unpacking underlying + + (address expectedModule, uint32 expectedEntityId) = validationFunction.unpack(); + + (address module, uint32 entityId, bool isGlobal2, bool isSignatureValidation2) = + validationConfig.unpackUnderlying(); + + assertEq(expectedModule, module, "module mismatch"); + assertEq(expectedEntityId, entityId, "entityId mismatch"); + assertEq(isGlobal, isGlobal2, "isGlobal mismatch"); + assertEq(isSignatureValidation, isSignatureValidation2, "isSignatureValidation mismatch"); + + // Test unpacking to ModuleEntity + + (ModuleEntity validationFunction2, bool isGlobal3, bool isSignatureValidation3) = validationConfig.unpack(); + + assertEq( + ModuleEntity.unwrap(validationFunction), + ModuleEntity.unwrap(validationFunction2), + "validationFunction mismatch" + ); + assertEq(isGlobal, isGlobal3, "isGlobal mismatch"); + assertEq(isSignatureValidation, isSignatureValidation3, "isSignatureValidation mismatch"); + + // Test individual view functions + + assertEq(validationConfig.module(), expectedModule, "module mismatch"); + assertEq(validationConfig.entityId(), expectedEntityId, "entityId mismatch"); + assertEq( + ModuleEntity.unwrap(validationConfig.moduleEntity()), + ModuleEntity.unwrap(validationFunction), + "validationFunction mismatch" + ); + assertEq(validationConfig.isGlobal(), isGlobal, "isGlobal mismatch"); + assertEq(validationConfig.isSignatureValidation(), isSignatureValidation, "isSignatureValidation mismatch"); + } +} diff --git a/test/mocks/Counter.t.sol b/test/mocks/Counter.t.sol index 8b0f5ccf..4749a846 100644 --- a/test/mocks/Counter.t.sol +++ b/test/mocks/Counter.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {Test} from "forge-std/Test.sol"; import {Counter} from "./Counter.sol"; +import {Test} from "forge-std/Test.sol"; contract CounterTest is Test { Counter public counter; diff --git a/test/mocks/MockERC20.sol b/test/mocks/MockERC20.sol new file mode 100644 index 00000000..131e0d1a --- /dev/null +++ b/test/mocks/MockERC20.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20 { + constructor() ERC20("MockERC20", "MERC") {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/test/mocks/MockPlugin.sol b/test/mocks/MockModule.sol similarity index 67% rename from test/mocks/MockPlugin.sol rename to test/mocks/MockModule.sol index 3a2bf984..0ada610f 100644 --- a/test/mocks/MockPlugin.sol +++ b/test/mocks/MockModule.sol @@ -3,11 +3,12 @@ pragma solidity ^0.8.19; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import {PluginManifest, IPlugin, PluginMetadata} from "../../src/interfaces/IPlugin.sol"; -import {IValidation} from "../../src/interfaces/IValidation.sol"; +import {ExecutionManifest} from "../../src/interfaces/IExecution.sol"; import {IExecutionHook} from "../../src/interfaces/IExecutionHook.sol"; +import {IModule, ModuleMetadata} from "../../src/interfaces/IModule.sol"; +import {IValidation} from "../../src/interfaces/IValidation.sol"; -contract MockPlugin is ERC165 { +contract MockModule is ERC165 { // It's super inefficient to hold the entire abi-encoded manifest in storage, but this is fine since it's // just a mock. Note that the reason we do this is to allow copying the entire contents of the manifest // into storage in a single line, since solidity fails to compile with memory -> storage copying of nested @@ -17,44 +18,44 @@ contract MockPlugin is ERC165 { // struct ManifestAssociatedFunction memory[] memory to storage not yet supported. bytes internal _manifest; - string public constant NAME = "Mock Plugin Modifiable"; - string public constant VERSION = "1.0.0"; - string public constant AUTHOR = "ERC-6900 Authors"; + string internal constant _NAME = "Mock Module Modifiable"; + string internal constant _VERSION = "1.0.0"; + string internal constant _AUTHOR = "ERC-6900 Authors"; event ReceivedCall(bytes msgData, uint256 msgValue); // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Plugin interface functions ┃ + // ┃ Module interface functions ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - constructor(PluginManifest memory _pluginManifest) { - _manifest = abi.encode(_pluginManifest); + constructor(ExecutionManifest memory _executionManifest) { + _manifest = abi.encode(_executionManifest); } - function _getManifest() internal view returns (PluginManifest memory) { - PluginManifest memory m = abi.decode(_manifest, (PluginManifest)); + function _getManifest() internal view returns (ExecutionManifest memory) { + ExecutionManifest memory m = abi.decode(_manifest, (ExecutionManifest)); return m; } - function _castToPure(function() internal view returns (PluginManifest memory) fnIn) + function _castToPure(function() internal view returns (ExecutionManifest memory) fnIn) internal pure - returns (function() internal pure returns (PluginManifest memory) fnOut) + returns (function() internal pure returns (ExecutionManifest memory) fnOut) { assembly ("memory-safe") { fnOut := fnIn } } - function pluginManifest() external pure returns (PluginManifest memory) { + function executionManifest() external pure returns (ExecutionManifest memory) { return _castToPure(_getManifest)(); } - function pluginMetadata() external pure returns (PluginMetadata memory) { - PluginMetadata memory metadata; - metadata.name = NAME; - metadata.version = VERSION; - metadata.author = AUTHOR; + function moduleMetadata() external pure returns (ModuleMetadata memory) { + ModuleMetadata memory metadata; + metadata.name = _NAME; + metadata.version = _VERSION; + metadata.author = _AUTHOR; return metadata; } @@ -65,14 +66,14 @@ contract MockPlugin is ERC165 { /// /// This function call must use less than 30 000 gas. /// - /// Supporting the IPlugin interface is a requirement for plugin installation. This is also used + /// Supporting the IModule interface is a requirement for module installation. This is also used /// by the modular account to prevent standard execution functions `execute` and `executeBatch` from - /// making calls to plugins. + /// making calls to modules. /// @param interfaceId The interface ID to check for support. /// @return True if the contract supports `interfaceId`. function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return interfaceId == type(IPlugin).interfaceId || super.supportsInterface(interfaceId); + return interfaceId == type(IModule).interfaceId || super.supportsInterface(interfaceId); } receive() external payable {} diff --git a/test/mocks/MSCAFactoryFixture.sol b/test/mocks/SingleSignerFactoryFixture.sol similarity index 60% rename from test/mocks/MSCAFactoryFixture.sol rename to test/mocks/SingleSignerFactoryFixture.sol index 8ca3a51f..48f7a778 100644 --- a/test/mocks/MSCAFactoryFixture.sol +++ b/test/mocks/SingleSignerFactoryFixture.sol @@ -1,38 +1,36 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; - import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; -import {SingleOwnerPlugin} from "../../src/plugins/owner/SingleOwnerPlugin.sol"; +import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +import {SingleSignerValidation} from "../../src/modules/validation/SingleSignerValidation.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; + +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; import {OptimizedTest} from "../utils/OptimizedTest.sol"; -import {TEST_DEFAULT_OWNER_FUNCTION_ID} from "../utils/TestConstants.sol"; +import {TEST_DEFAULT_VALIDATION_ENTITY_ID} from "../utils/TestConstants.sol"; -/** - * @title MSCAFactoryFixture - * @dev a factory that initializes UpgradeableModularAccounts with a single plugin, SingleOwnerPlugin - * intended for unit tests and local development, not for production. - */ -contract MSCAFactoryFixture is OptimizedTest { +contract SingleSignerFactoryFixture is OptimizedTest { UpgradeableModularAccount public accountImplementation; - SingleOwnerPlugin public singleOwnerPlugin; + SingleSignerValidation public singleSignerValidation; bytes32 private immutable _PROXY_BYTECODE_HASH; uint32 public constant UNSTAKE_DELAY = 1 weeks; IEntryPoint public entryPoint; - constructor(IEntryPoint _entryPoint, SingleOwnerPlugin _singleOwnerPlugin) { + constructor(IEntryPoint _entryPoint, SingleSignerValidation _singleSignerValidation) { entryPoint = _entryPoint; accountImplementation = _deployUpgradeableModularAccount(_entryPoint); _PROXY_BYTECODE_HASH = keccak256( abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(address(accountImplementation), "")) ); - singleOwnerPlugin = _singleOwnerPlugin; + singleSignerValidation = _singleSignerValidation; } /** @@ -43,21 +41,13 @@ contract MSCAFactoryFixture is OptimizedTest { * account creation */ function createAccount(address owner, uint256 salt) public returns (UpgradeableModularAccount) { - address addr = Create2.computeAddress(getSalt(owner, salt), _PROXY_BYTECODE_HASH); + // address addr = Create2.computeAddress(getSalt(owner, salt), _PROXY_BYTECODE_HASH); + address addr = getAddress(owner, salt); // short circuit if exists if (addr.code.length == 0) { - bytes memory pluginInstallData = abi.encode(TEST_DEFAULT_OWNER_FUNCTION_ID, owner); - // not necessary to check return addr since next call will fail if so - new ERC1967Proxy{salt: getSalt(owner, salt)}(address(accountImplementation), ""); - - // point proxy to actual implementation and init plugins - UpgradeableModularAccount(payable(addr)).initializeWithValidation( - ValidationConfigLib.pack(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID, true, true), - new bytes4[](0), - pluginInstallData, - "", - "" + LibClone.createDeterministicERC1967( + address(accountImplementation), _getImmutableArgs(owner), getSalt(owner, salt) ); } @@ -68,7 +58,10 @@ contract MSCAFactoryFixture is OptimizedTest { * calculate the counterfactual address of this account as it would be returned by createAccount() */ function getAddress(address owner, uint256 salt) public view returns (address) { - return Create2.computeAddress(getSalt(owner, salt), _PROXY_BYTECODE_HASH); + // return Create2.computeAddress(getSalt(owner, salt), _PROXY_BYTECODE_HASH); + return LibClone.predictDeterministicAddressERC1967( + address(accountImplementation), _getImmutableArgs(owner), getSalt(owner, salt), address(this) + ); } function addStake() external payable { @@ -78,4 +71,10 @@ contract MSCAFactoryFixture is OptimizedTest { function getSalt(address owner, uint256 salt) public pure returns (bytes32) { return keccak256(abi.encodePacked(owner, salt)); } + + function _getImmutableArgs(address owner) private view returns (bytes memory) { + return abi.encodePacked( + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID), owner + ); + } } diff --git a/test/mocks/plugins/ComprehensivePlugin.sol b/test/mocks/modules/ComprehensiveModule.sol similarity index 52% rename from test/mocks/plugins/ComprehensivePlugin.sol rename to test/mocks/modules/ComprehensiveModule.sol index cd455c88..420e2a82 100644 --- a/test/mocks/plugins/ComprehensivePlugin.sol +++ b/test/mocks/modules/ComprehensiveModule.sol @@ -4,21 +4,22 @@ pragma solidity ^0.8.19; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; import { - ManifestExecutionHook, + ExecutionManifest, + IExecution, ManifestExecutionFunction, - ManifestValidation, - PluginManifest, - PluginMetadata -} from "../../../src/interfaces/IPlugin.sol"; -import {PluginManifest} from "../../../src/interfaces/IPlugin.sol"; + ManifestExecutionHook +} from "../../../src/interfaces/IExecution.sol"; + +import {IExecution} from "../../../src/interfaces/IExecution.sol"; +import {IExecutionHook} from "../../../src/interfaces/IExecutionHook.sol"; +import {ModuleMetadata} from "../../../src/interfaces/IModule.sol"; import {IValidation} from "../../../src/interfaces/IValidation.sol"; import {IValidationHook} from "../../../src/interfaces/IValidationHook.sol"; -import {IExecutionHook} from "../../../src/interfaces/IExecutionHook.sol"; -import {BasePlugin} from "../../../src/plugins/BasePlugin.sol"; +import {BaseModule} from "../../../src/modules/BaseModule.sol"; -contract ComprehensivePlugin is IValidation, IValidationHook, IExecutionHook, BasePlugin { - enum FunctionId { +contract ComprehensiveModule is IExecution, IValidation, IValidationHook, IExecutionHook, BaseModule { + enum EntityId { PRE_VALIDATION_HOOK_1, PRE_VALIDATION_HOOK_2, VALIDATION, @@ -28,9 +29,9 @@ contract ComprehensivePlugin is IValidation, IValidationHook, IExecutionHook, Ba SIG_VALIDATION } - string public constant NAME = "Comprehensive Plugin"; - string public constant VERSION = "1.0.0"; - string public constant AUTHOR = "ERC-6900 Authors"; + string internal constant _NAME = "Comprehensive Module"; + string internal constant _VERSION = "1.0.0"; + string internal constant _AUTHOR = "ERC-6900 Authors"; // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ Execution functions ┃ @@ -39,99 +40,99 @@ contract ComprehensivePlugin is IValidation, IValidationHook, IExecutionHook, Ba function foo() external {} // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Plugin interface functions ┃ + // ┃ Module interface functions ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ function onInstall(bytes calldata) external override {} function onUninstall(bytes calldata) external override {} - function preUserOpValidationHook(uint8 functionId, PackedUserOperation calldata, bytes32) + function preUserOpValidationHook(uint32 entityId, PackedUserOperation calldata, bytes32) external pure override returns (uint256) { - if (functionId == uint8(FunctionId.PRE_VALIDATION_HOOK_1)) { + if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK_1)) { return 0; - } else if (functionId == uint8(FunctionId.PRE_VALIDATION_HOOK_2)) { + } else if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK_2)) { return 0; } revert NotImplemented(); } - function validateUserOp(uint8 functionId, PackedUserOperation calldata, bytes32) + function validateUserOp(uint32 entityId, PackedUserOperation calldata, bytes32) external pure override returns (uint256) { - if (functionId == uint8(FunctionId.VALIDATION)) { + if (entityId == uint32(EntityId.VALIDATION)) { return 0; } revert NotImplemented(); } - function preRuntimeValidationHook(uint8 functionId, address, uint256, bytes calldata, bytes calldata) + function preRuntimeValidationHook(uint32 entityId, address, uint256, bytes calldata, bytes calldata) external pure override { - if (functionId == uint8(FunctionId.PRE_VALIDATION_HOOK_1)) { + if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK_1)) { return; - } else if (functionId == uint8(FunctionId.PRE_VALIDATION_HOOK_2)) { + } else if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK_2)) { return; } revert NotImplemented(); } - function validateRuntime(uint8 functionId, address, uint256, bytes calldata, bytes calldata) + function validateRuntime(address, uint32 entityId, address, uint256, bytes calldata, bytes calldata) external pure override { - if (functionId == uint8(FunctionId.VALIDATION)) { + if (entityId == uint32(EntityId.VALIDATION)) { return; } revert NotImplemented(); } - function validateSignature(uint8 functionId, address, bytes32, bytes calldata) + function validateSignature(address, uint32 entityId, address, bytes32, bytes calldata) external pure returns (bytes4) { - if (functionId == uint8(FunctionId.SIG_VALIDATION)) { + if (entityId == uint32(EntityId.SIG_VALIDATION)) { return 0xffffffff; } revert NotImplemented(); } - function preExecutionHook(uint8 functionId, address, uint256, bytes calldata) + function preExecutionHook(uint32 entityId, address, uint256, bytes calldata) external pure override returns (bytes memory) { - if (functionId == uint8(FunctionId.PRE_EXECUTION_HOOK)) { + if (entityId == uint32(EntityId.PRE_EXECUTION_HOOK)) { return ""; - } else if (functionId == uint8(FunctionId.BOTH_EXECUTION_HOOKS)) { + } else if (entityId == uint32(EntityId.BOTH_EXECUTION_HOOKS)) { return ""; } revert NotImplemented(); } - function postExecutionHook(uint8 functionId, bytes calldata) external pure override { - if (functionId == uint8(FunctionId.POST_EXECUTION_HOOK)) { + function postExecutionHook(uint32 entityId, bytes calldata) external pure override { + if (entityId == uint32(EntityId.POST_EXECUTION_HOOK)) { return; - } else if (functionId == uint8(FunctionId.BOTH_EXECUTION_HOOKS)) { + } else if (entityId == uint32(EntityId.BOTH_EXECUTION_HOOKS)) { return; } revert NotImplemented(); } - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; manifest.executionFunctions = new ManifestExecutionFunction[](1); manifest.executionFunctions[0] = ManifestExecutionFunction({ @@ -140,33 +141,22 @@ contract ComprehensivePlugin is IValidation, IValidationHook, IExecutionHook, Ba allowGlobalValidation: false }); - bytes4[] memory validationSelectors = new bytes4[](1); - validationSelectors[0] = this.foo.selector; - - manifest.validationFunctions = new ManifestValidation[](1); - manifest.validationFunctions[0] = ManifestValidation({ - functionId: uint8(FunctionId.VALIDATION), - isDefault: true, - isSignatureValidation: false, - selectors: validationSelectors - }); - manifest.executionHooks = new ManifestExecutionHook[](3); manifest.executionHooks[0] = ManifestExecutionHook({ executionSelector: this.foo.selector, - functionId: uint8(FunctionId.BOTH_EXECUTION_HOOKS), + entityId: uint32(EntityId.BOTH_EXECUTION_HOOKS), isPreHook: true, isPostHook: true }); manifest.executionHooks[1] = ManifestExecutionHook({ executionSelector: this.foo.selector, - functionId: uint8(FunctionId.PRE_EXECUTION_HOOK), + entityId: uint32(EntityId.PRE_EXECUTION_HOOK), isPreHook: true, isPostHook: false }); manifest.executionHooks[2] = ManifestExecutionHook({ executionSelector: this.foo.selector, - functionId: uint8(FunctionId.POST_EXECUTION_HOOK), + entityId: uint32(EntityId.POST_EXECUTION_HOOK), isPreHook: false, isPostHook: true }); @@ -174,11 +164,11 @@ contract ComprehensivePlugin is IValidation, IValidationHook, IExecutionHook, Ba return manifest; } - function pluginMetadata() external pure virtual override returns (PluginMetadata memory) { - PluginMetadata memory metadata; - metadata.name = NAME; - metadata.version = VERSION; - metadata.author = AUTHOR; + function moduleMetadata() external pure virtual override returns (ModuleMetadata memory) { + ModuleMetadata memory metadata; + metadata.name = _NAME; + metadata.version = _VERSION; + metadata.author = _AUTHOR; return metadata; } } diff --git a/test/mocks/modules/DirectCallModule.sol b/test/mocks/modules/DirectCallModule.sol new file mode 100644 index 00000000..6cd053f7 --- /dev/null +++ b/test/mocks/modules/DirectCallModule.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IExecutionHook} from "../../../src/interfaces/IExecutionHook.sol"; +import {ModuleMetadata} from "../../../src/interfaces/IModule.sol"; +import {IStandardExecutor} from "../../../src/interfaces/IStandardExecutor.sol"; + +import {BaseModule} from "../../../src/modules/BaseModule.sol"; + +contract DirectCallModule is BaseModule, IExecutionHook { + bool public preHookRan = false; + bool public postHookRan = false; + + function onInstall(bytes calldata) external override {} + + function onUninstall(bytes calldata) external override {} + + function directCall() external returns (bytes memory) { + return IStandardExecutor(msg.sender).execute(address(this), 0, abi.encodeCall(this.getData, ())); + } + + function getData() external pure returns (bytes memory) { + return hex"04546b"; + } + + function moduleMetadata() external pure override returns (ModuleMetadata memory) {} + + function preExecutionHook(uint32, address sender, uint256, bytes calldata) + external + override + returns (bytes memory) + { + require(sender == address(this), "mock direct call pre permission hook failed"); + preHookRan = true; + return abi.encode(keccak256(hex"04546b")); + } + + function postExecutionHook(uint32, bytes calldata preExecHookData) external override { + require( + abi.decode(preExecHookData, (bytes32)) == keccak256(hex"04546b"), + "mock direct call post permission hook failed" + ); + postHookRan = true; + } +} diff --git a/test/mocks/plugins/MockAccessControlHookPlugin.sol b/test/mocks/modules/MockAccessControlHookModule.sol similarity index 76% rename from test/mocks/plugins/MockAccessControlHookPlugin.sol rename to test/mocks/modules/MockAccessControlHookModule.sol index c17868a8..2c076d3f 100644 --- a/test/mocks/plugins/MockAccessControlHookPlugin.sol +++ b/test/mocks/modules/MockAccessControlHookModule.sol @@ -3,17 +3,17 @@ pragma solidity ^0.8.25; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; -import {PluginMetadata, PluginManifest} from "../../../src/interfaces/IPlugin.sol"; -import {IValidationHook} from "../../../src/interfaces/IValidationHook.sol"; +import {ModuleMetadata} from "../../../src/interfaces/IModule.sol"; import {IStandardExecutor} from "../../../src/interfaces/IStandardExecutor.sol"; -import {BasePlugin} from "../../../src/plugins/BasePlugin.sol"; +import {IValidationHook} from "../../../src/interfaces/IValidationHook.sol"; +import {BaseModule} from "../../../src/modules/BaseModule.sol"; -// A pre validaiton hook plugin that uses per-hook data. +// A pre validaiton hook module that uses per-hook data. // This example enforces that the target of an `execute` call must only be the previously specified address. // This is just a mock - it does not enforce this over `executeBatch` and other methods of making calls, and should // not be used in production.. -contract MockAccessControlHookPlugin is IValidationHook, BasePlugin { - enum FunctionId { +contract MockAccessControlHookModule is IValidationHook, BaseModule { + enum EntityId { PRE_VALIDATION_HOOK } @@ -28,13 +28,13 @@ contract MockAccessControlHookPlugin is IValidationHook, BasePlugin { delete allowedTargets[msg.sender]; } - function preUserOpValidationHook(uint8 functionId, PackedUserOperation calldata userOp, bytes32) + function preUserOpValidationHook(uint32 entityId, PackedUserOperation calldata userOp, bytes32) external view override returns (uint256) { - if (functionId == uint8(FunctionId.PRE_VALIDATION_HOOK)) { + if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK)) { if (bytes4(userOp.callData[:4]) == IStandardExecutor.execute.selector) { address target = abi.decode(userOp.callData[4:36], (address)); @@ -49,13 +49,13 @@ contract MockAccessControlHookPlugin is IValidationHook, BasePlugin { } function preRuntimeValidationHook( - uint8 functionId, + uint32 entityId, address, uint256, bytes calldata data, bytes calldata authorization ) external view override { - if (functionId == uint8(FunctionId.PRE_VALIDATION_HOOK)) { + if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK)) { if (bytes4(data[:4]) == IStandardExecutor.execute.selector) { address target = abi.decode(data[4:36], (address)); @@ -72,7 +72,5 @@ contract MockAccessControlHookPlugin is IValidationHook, BasePlugin { revert NotImplemented(); } - function pluginMetadata() external pure override returns (PluginMetadata memory) {} - - function pluginManifest() external pure override returns (PluginManifest memory) {} + function moduleMetadata() external pure override returns (ModuleMetadata memory) {} } diff --git a/test/mocks/plugins/PermittedCallMocks.sol b/test/mocks/modules/PermittedCallMocks.sol similarity index 50% rename from test/mocks/plugins/PermittedCallMocks.sol rename to test/mocks/modules/PermittedCallMocks.sol index 77548225..167279e5 100644 --- a/test/mocks/plugins/PermittedCallMocks.sol +++ b/test/mocks/modules/PermittedCallMocks.sol @@ -1,18 +1,19 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -import {ManifestExecutionFunction, PluginManifest, PluginMetadata} from "../../../src/interfaces/IPlugin.sol"; +import {ExecutionManifest, IExecution, ManifestExecutionFunction} from "../../../src/interfaces/IExecution.sol"; +import {ModuleMetadata} from "../../../src/interfaces/IModule.sol"; -import {BasePlugin} from "../../../src/plugins/BasePlugin.sol"; -import {ResultCreatorPlugin} from "./ReturnDataPluginMocks.sol"; +import {BaseModule} from "../../../src/modules/BaseModule.sol"; +import {ResultCreatorModule} from "./ReturnDataModuleMocks.sol"; -contract PermittedCallerPlugin is BasePlugin { +contract PermittedCallerModule is IExecution, BaseModule { function onInstall(bytes calldata) external override {} function onUninstall(bytes calldata) external override {} - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; manifest.executionFunctions = new ManifestExecutionFunction[](2); manifest.executionFunctions[0].executionSelector = this.usePermittedCallAllowed.selector; @@ -25,15 +26,15 @@ contract PermittedCallerPlugin is BasePlugin { return manifest; } - function pluginMetadata() external pure override returns (PluginMetadata memory) {} + function moduleMetadata() external pure override returns (ModuleMetadata memory) {} - // The manifest requested access to use the plugin-defined method "foo" + // The manifest requested access to use the module-defined method "foo" function usePermittedCallAllowed() external view returns (bytes memory) { - return abi.encode(ResultCreatorPlugin(msg.sender).foo()); + return abi.encode(ResultCreatorModule(msg.sender).foo()); } - // The manifest has not requested access to use the plugin-defined method "bar", so this should revert. + // The manifest has not requested access to use the module-defined method "bar", so this should revert. function usePermittedCallNotAllowed() external view returns (bytes memory) { - return abi.encode(ResultCreatorPlugin(msg.sender).bar()); + return abi.encode(ResultCreatorModule(msg.sender).bar()); } } diff --git a/test/mocks/plugins/ReturnDataPluginMocks.sol b/test/mocks/modules/ReturnDataModuleMocks.sol similarity index 64% rename from test/mocks/plugins/ReturnDataPluginMocks.sol rename to test/mocks/modules/ReturnDataModuleMocks.sol index 211f79af..d0e8180e 100644 --- a/test/mocks/plugins/ReturnDataPluginMocks.sol +++ b/test/mocks/modules/ReturnDataModuleMocks.sol @@ -3,16 +3,15 @@ pragma solidity ^0.8.19; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; -import { - ManifestExecutionFunction, - ManifestValidation, - PluginManifest, - PluginMetadata -} from "../../../src/interfaces/IPlugin.sol"; -import {IValidation} from "../../../src/interfaces/IValidation.sol"; +import {ExecutionManifest, IExecution, ManifestExecutionFunction} from "../../../src/interfaces/IExecution.sol"; +import {ModuleMetadata} from "../../../src/interfaces/IModule.sol"; + +import {DIRECT_CALL_VALIDATION_ENTITYID} from "../../../src/helpers/Constants.sol"; + import {IStandardExecutor} from "../../../src/interfaces/IStandardExecutor.sol"; +import {IValidation} from "../../../src/interfaces/IValidation.sol"; -import {BasePlugin} from "../../../src/plugins/BasePlugin.sol"; +import {BaseModule} from "../../../src/modules/BaseModule.sol"; contract RegularResultContract { function foo() external pure returns (bytes32) { @@ -24,7 +23,7 @@ contract RegularResultContract { } } -contract ResultCreatorPlugin is BasePlugin { +contract ResultCreatorModule is IExecution, BaseModule { function onInstall(bytes calldata) external override {} function onUninstall(bytes calldata) external override {} @@ -37,8 +36,8 @@ contract ResultCreatorPlugin is BasePlugin { return keccak256("foo"); } - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; manifest.executionFunctions = new ManifestExecutionFunction[](2); manifest.executionFunctions[0] = ManifestExecutionFunction({ @@ -55,16 +54,16 @@ contract ResultCreatorPlugin is BasePlugin { return manifest; } - function pluginMetadata() external pure override returns (PluginMetadata memory) {} + function moduleMetadata() external pure override returns (ModuleMetadata memory) {} } -contract ResultConsumerPlugin is BasePlugin, IValidation { - ResultCreatorPlugin public immutable RESULT_CREATOR; +contract ResultConsumerModule is IExecution, BaseModule, IValidation { + ResultCreatorModule public immutable RESULT_CREATOR; RegularResultContract public immutable REGULAR_RESULT_CONTRACT; error NotAuthorized(); - constructor(ResultCreatorPlugin _resultCreator, RegularResultContract _regularResultContract) { + constructor(ResultCreatorModule _resultCreator, RegularResultContract _regularResultContract) { RESULT_CREATOR = _resultCreator; REGULAR_RESULT_CONTRACT = _regularResultContract; } @@ -72,24 +71,27 @@ contract ResultConsumerPlugin is BasePlugin, IValidation { // Validation function implementations. We only care about the runtime validation function, to authorize // itself. - function validateUserOp(uint8, PackedUserOperation calldata, bytes32) external pure returns (uint256) { + function validateUserOp(uint32, PackedUserOperation calldata, bytes32) external pure returns (uint256) { revert NotImplemented(); } - function validateRuntime(uint8, address sender, uint256, bytes calldata, bytes calldata) external view { + function validateRuntime(address, uint32, address sender, uint256, bytes calldata, bytes calldata) + external + view + { if (sender != address(this)) { revert NotAuthorized(); } } - function validateSignature(uint8, address, bytes32, bytes calldata) external pure returns (bytes4) { + function validateSignature(address, uint32, address, bytes32, bytes calldata) external pure returns (bytes4) { revert NotImplemented(); } // Check the return data through the fallback function checkResultFallback(bytes32 expected) external view returns (bool) { // This result should be allowed based on the manifest permission request - bytes32 actual = ResultCreatorPlugin(msg.sender).foo(); + bytes32 actual = ResultCreatorModule(msg.sender).foo(); return actual == expected; } @@ -99,7 +101,8 @@ contract ResultConsumerPlugin is BasePlugin, IValidation { // This result should be allowed based on the manifest permission request bytes memory returnData = IStandardExecutor(msg.sender).executeWithAuthorization( abi.encodeCall(IStandardExecutor.execute, (target, 0, abi.encodeCall(RegularResultContract.foo, ()))), - abi.encodePacked(this, uint8(0), uint8(0), uint32(1), uint8(255)) // Validation function of self, + abi.encodePacked(this, DIRECT_CALL_VALIDATION_ENTITYID, uint8(0), uint32(1), uint8(255)) // Validation + // function of self, // selector-associated, with no auth data ); @@ -112,20 +115,8 @@ contract ResultConsumerPlugin is BasePlugin, IValidation { function onUninstall(bytes calldata) external override {} - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; - - // todo: this is the exact workflow that would benefit from a "permiteed call" setup in the manifest. - bytes4[] memory validationSelectors = new bytes4[](1); - validationSelectors[0] = IStandardExecutor.execute.selector; - - manifest.validationFunctions = new ManifestValidation[](1); - manifest.validationFunctions[0] = ManifestValidation({ - functionId: 0, - isDefault: true, - isSignatureValidation: false, - selectors: validationSelectors - }); + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; manifest.executionFunctions = new ManifestExecutionFunction[](2); manifest.executionFunctions[0] = ManifestExecutionFunction({ @@ -142,5 +133,5 @@ contract ResultConsumerPlugin is BasePlugin, IValidation { return manifest; } - function pluginMetadata() external pure override returns (PluginMetadata memory) {} + function moduleMetadata() external pure override returns (ModuleMetadata memory) {} } diff --git a/test/mocks/plugins/ValidationPluginMocks.sol b/test/mocks/modules/ValidationModuleMocks.sol similarity index 59% rename from test/mocks/plugins/ValidationPluginMocks.sol rename to test/mocks/modules/ValidationModuleMocks.sol index a59d5ee3..3470495f 100644 --- a/test/mocks/plugins/ValidationPluginMocks.sol +++ b/test/mocks/modules/ValidationModuleMocks.sol @@ -3,18 +3,14 @@ pragma solidity ^0.8.19; import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; -import { - ManifestExecutionFunction, - ManifestValidation, - PluginMetadata, - PluginManifest -} from "../../../src/interfaces/IPlugin.sol"; +import {ExecutionManifest, IExecution, ManifestExecutionFunction} from "../../../src/interfaces/IExecution.sol"; +import {ModuleMetadata} from "../../../src/interfaces/IModule.sol"; import {IValidation} from "../../../src/interfaces/IValidation.sol"; import {IValidationHook} from "../../../src/interfaces/IValidationHook.sol"; -import {BasePlugin} from "../../../src/plugins/BasePlugin.sol"; +import {BaseModule} from "../../../src/modules/BaseModule.sol"; -abstract contract MockBaseUserOpValidationPlugin is IValidation, IValidationHook, BasePlugin { - enum FunctionId { +abstract contract MockBaseUserOpValidationModule is IExecution, IValidation, IValidationHook, BaseModule { + enum EntityId { USER_OP_VALIDATION, PRE_VALIDATION_HOOK_1, PRE_VALIDATION_HOOK_2 @@ -25,47 +21,52 @@ abstract contract MockBaseUserOpValidationPlugin is IValidation, IValidationHook uint256 internal _preUserOpValidationHook2Data; // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Plugin interface functions ┃ + // ┃ Module interface functions ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ function onInstall(bytes calldata) external override {} function onUninstall(bytes calldata) external override {} - function preUserOpValidationHook(uint8 functionId, PackedUserOperation calldata, bytes32) + function preUserOpValidationHook(uint32 entityId, PackedUserOperation calldata, bytes32) external view override returns (uint256) { - if (functionId == uint8(FunctionId.PRE_VALIDATION_HOOK_1)) { + if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK_1)) { return _preUserOpValidationHook1Data; - } else if (functionId == uint8(FunctionId.PRE_VALIDATION_HOOK_2)) { + } else if (entityId == uint32(EntityId.PRE_VALIDATION_HOOK_2)) { return _preUserOpValidationHook2Data; } revert NotImplemented(); } - function validateUserOp(uint8 functionId, PackedUserOperation calldata, bytes32) + function validateUserOp(uint32 entityId, PackedUserOperation calldata, bytes32) external view override returns (uint256) { - if (functionId == uint8(FunctionId.USER_OP_VALIDATION)) { + if (entityId == uint32(EntityId.USER_OP_VALIDATION)) { return _userOpValidationFunctionData; } revert NotImplemented(); } - function validateSignature(uint8, address, bytes32, bytes calldata) external pure override returns (bytes4) { + function validateSignature(address, uint32, address, bytes32, bytes calldata) + external + pure + override + returns (bytes4) + { revert NotImplemented(); } // Empty stubs - function pluginMetadata() external pure override returns (PluginMetadata memory) {} + function moduleMetadata() external pure override returns (ModuleMetadata memory) {} - function preRuntimeValidationHook(uint8, address, uint256, bytes calldata, bytes calldata) + function preRuntimeValidationHook(uint32, address, uint256, bytes calldata, bytes calldata) external pure override @@ -73,12 +74,16 @@ abstract contract MockBaseUserOpValidationPlugin is IValidation, IValidationHook revert NotImplemented(); } - function validateRuntime(uint8, address, uint256, bytes calldata, bytes calldata) external pure override { + function validateRuntime(address, uint32, address, uint256, bytes calldata, bytes calldata) + external + pure + override + { revert NotImplemented(); } } -contract MockUserOpValidationPlugin is MockBaseUserOpValidationPlugin { +contract MockUserOpValidationModule is MockBaseUserOpValidationModule { function setValidationData(uint256 userOpValidationFunctionData) external { _userOpValidationFunctionData = userOpValidationFunctionData; } @@ -90,11 +95,11 @@ contract MockUserOpValidationPlugin is MockBaseUserOpValidationPlugin { function foo() external {} // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Plugin interface functions ┃ + // ┃ Module interface functions ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; manifest.executionFunctions = new ManifestExecutionFunction[](1); manifest.executionFunctions[0] = ManifestExecutionFunction({ @@ -103,22 +108,11 @@ contract MockUserOpValidationPlugin is MockBaseUserOpValidationPlugin { allowGlobalValidation: false }); - bytes4[] memory validationSelectors = new bytes4[](1); - validationSelectors[0] = this.foo.selector; - - manifest.validationFunctions = new ManifestValidation[](1); - manifest.validationFunctions[0] = ManifestValidation({ - functionId: uint8(FunctionId.USER_OP_VALIDATION), - isDefault: false, - isSignatureValidation: false, - selectors: validationSelectors - }); - return manifest; } } -contract MockUserOpValidation1HookPlugin is MockBaseUserOpValidationPlugin { +contract MockUserOpValidation1HookModule is MockBaseUserOpValidationModule { function setValidationData(uint256 userOpValidationFunctionData, uint256 preUserOpValidationHook1Data) external { @@ -133,11 +127,11 @@ contract MockUserOpValidation1HookPlugin is MockBaseUserOpValidationPlugin { function bar() external {} // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Plugin interface functions ┃ + // ┃ Module interface functions ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; manifest.executionFunctions = new ManifestExecutionFunction[](1); manifest.executionFunctions[0] = ManifestExecutionFunction({ @@ -146,22 +140,11 @@ contract MockUserOpValidation1HookPlugin is MockBaseUserOpValidationPlugin { allowGlobalValidation: false }); - bytes4[] memory validationSelectors = new bytes4[](1); - validationSelectors[0] = this.bar.selector; - - manifest.validationFunctions = new ManifestValidation[](2); - manifest.validationFunctions[0] = ManifestValidation({ - functionId: uint8(FunctionId.USER_OP_VALIDATION), - isDefault: false, - isSignatureValidation: false, - selectors: validationSelectors - }); - return manifest; } } -contract MockUserOpValidation2HookPlugin is MockBaseUserOpValidationPlugin { +contract MockUserOpValidation2HookModule is MockBaseUserOpValidationModule { function setValidationData( uint256 userOpValidationFunctionData, uint256 preUserOpValidationHook1Data, @@ -179,11 +162,11 @@ contract MockUserOpValidation2HookPlugin is MockBaseUserOpValidationPlugin { function baz() external {} // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - // ┃ Plugin interface functions ┃ + // ┃ Module interface functions ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - function pluginManifest() external pure override returns (PluginManifest memory) { - PluginManifest memory manifest; + function executionManifest() external pure override returns (ExecutionManifest memory) { + ExecutionManifest memory manifest; manifest.executionFunctions = new ManifestExecutionFunction[](1); manifest.executionFunctions[0] = ManifestExecutionFunction({ @@ -192,17 +175,6 @@ contract MockUserOpValidation2HookPlugin is MockBaseUserOpValidationPlugin { allowGlobalValidation: false }); - bytes4[] memory validationSelectors = new bytes4[](1); - validationSelectors[0] = this.baz.selector; - - manifest.validationFunctions = new ManifestValidation[](1); - manifest.validationFunctions[0] = ManifestValidation({ - functionId: uint8(FunctionId.USER_OP_VALIDATION), - isDefault: false, - isSignatureValidation: false, - selectors: validationSelectors - }); - return manifest; } } diff --git a/test/samples/AllowlistPlugin.t.sol b/test/module/AllowlistModule.t.sol similarity index 79% rename from test/samples/AllowlistPlugin.t.sol rename to test/module/AllowlistModule.t.sol index d81d5f79..b0e09c90 100644 --- a/test/samples/AllowlistPlugin.t.sol +++ b/test/module/AllowlistModule.t.sol @@ -3,23 +3,26 @@ pragma solidity ^0.8.25; import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; -import {Call} from "../../src/interfaces/IStandardExecutor.sol"; -import {FunctionReference, FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {AllowlistPlugin} from "../../src/samples/permissionhooks/AllowlistPlugin.sol"; -import {CustomValidationTestBase} from "../utils/CustomValidationTestBase.sol"; +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntity} from "../../src/helpers/ModuleEntityLib.sol"; +import {Call} from "../../src/interfaces/IStandardExecutor.sol"; + +import {AllowlistModule} from "../../src/modules/permissionhooks/AllowlistModule.sol"; + import {Counter} from "../mocks/Counter.sol"; +import {CustomValidationTestBase} from "../utils/CustomValidationTestBase.sol"; -contract AllowlistPluginTest is CustomValidationTestBase { - AllowlistPlugin public allowlistPlugin; +contract AllowlistModuleTest is CustomValidationTestBase { + AllowlistModule public allowlistModule; - AllowlistPlugin.AllowlistInit[] public allowlistInit; + AllowlistModule.AllowlistInit[] public allowlistInit; Counter[] public counters; function setUp() public { - allowlistPlugin = new AllowlistPlugin(); + allowlistModule = new AllowlistModule(); counters = new Counter[](10); @@ -31,7 +34,7 @@ contract AllowlistPluginTest is CustomValidationTestBase { } function testFuzz_allowlistHook_userOp_single(uint256 seed) public { - AllowlistPlugin.AllowlistInit[] memory inits; + AllowlistModule.AllowlistInit[] memory inits; (inits, seed) = _generateRandomizedAllowlistInit(seed); _copyInitToStorage(inits); @@ -45,7 +48,7 @@ contract AllowlistPluginTest is CustomValidationTestBase { } function testFuzz_allowlistHook_userOp_batch(uint256 seed) public { - AllowlistPlugin.AllowlistInit[] memory inits; + AllowlistModule.AllowlistInit[] memory inits; (inits, seed) = _generateRandomizedAllowlistInit(seed); _copyInitToStorage(inits); @@ -59,7 +62,7 @@ contract AllowlistPluginTest is CustomValidationTestBase { } function testFuzz_allowlistHook_runtime_single(uint256 seed) public { - AllowlistPlugin.AllowlistInit[] memory inits; + AllowlistModule.AllowlistInit[] memory inits; (inits, seed) = _generateRandomizedAllowlistInit(seed); _copyInitToStorage(inits); @@ -77,7 +80,7 @@ contract AllowlistPluginTest is CustomValidationTestBase { } function testFuzz_allowlistHook_runtime_batch(uint256 seed) public { - AllowlistPlugin.AllowlistInit[] memory inits; + AllowlistModule.AllowlistInit[] memory inits; (inits, seed) = _generateRandomizedAllowlistInit(seed); _copyInitToStorage(inits); @@ -144,17 +147,17 @@ contract AllowlistPluginTest is CustomValidationTestBase { Call memory call = calls[i]; (bool allowed, bool hasSelectorAllowlist) = - allowlistPlugin.targetAllowlist(call.target, address(account1)); + allowlistModule.targetAllowlist(call.target, address(account1)); if (allowed) { if ( hasSelectorAllowlist - && !allowlistPlugin.selectorAllowlist(call.target, bytes4(call.data), address(account1)) + && !allowlistModule.selectorAllowlist(call.target, bytes4(call.data), address(account1)) ) { return abi.encodeWithSelector( IEntryPoint.FailedOpWithRevert.selector, 0, "AA23 reverted", - abi.encodeWithSelector(AllowlistPlugin.SelectorNotAllowed.selector) + abi.encodeWithSelector(AllowlistModule.SelectorNotAllowed.selector) ); } } else { @@ -162,7 +165,7 @@ contract AllowlistPluginTest is CustomValidationTestBase { IEntryPoint.FailedOpWithRevert.selector, 0, "AA23 reverted", - abi.encodeWithSelector(AllowlistPlugin.TargetNotAllowed.selector) + abi.encodeWithSelector(AllowlistModule.TargetNotAllowed.selector) ); } } @@ -175,30 +178,30 @@ contract AllowlistPluginTest is CustomValidationTestBase { Call memory call = calls[i]; (bool allowed, bool hasSelectorAllowlist) = - allowlistPlugin.targetAllowlist(call.target, address(account1)); + allowlistModule.targetAllowlist(call.target, address(account1)); if (allowed) { if ( hasSelectorAllowlist - && !allowlistPlugin.selectorAllowlist(call.target, bytes4(call.data), address(account1)) + && !allowlistModule.selectorAllowlist(call.target, bytes4(call.data), address(account1)) ) { return abi.encodeWithSelector( UpgradeableModularAccount.PreRuntimeValidationHookFailed.selector, - address(allowlistPlugin), - uint8(AllowlistPlugin.FunctionId.PRE_VALIDATION_HOOK), - abi.encodeWithSelector(AllowlistPlugin.SelectorNotAllowed.selector) + address(allowlistModule), + uint32(AllowlistModule.EntityId.PRE_VALIDATION_HOOK), + abi.encodeWithSelector(AllowlistModule.SelectorNotAllowed.selector) ); } } else { return abi.encodeWithSelector( UpgradeableModularAccount.PreRuntimeValidationHookFailed.selector, - address(allowlistPlugin), - uint8(AllowlistPlugin.FunctionId.PRE_VALIDATION_HOOK), - abi.encodeWithSelector(AllowlistPlugin.TargetNotAllowed.selector) + address(allowlistModule), + uint32(AllowlistModule.EntityId.PRE_VALIDATION_HOOK), + abi.encodeWithSelector(AllowlistModule.TargetNotAllowed.selector) ); } } - // At this point, we have returned any error that would come from the AllowlistPlugin. + // At this point, we have returned any error that would come from the AllowlistModule. // But, because this is in the runtime path, the Counter itself may throw if it is not a valid selector. for (uint256 i = 0; i < calls.length; i++) { @@ -220,12 +223,12 @@ contract AllowlistPluginTest is CustomValidationTestBase { function _generateRandomizedAllowlistInit(uint256 seed) internal view - returns (AllowlistPlugin.AllowlistInit[] memory, uint256) + returns (AllowlistModule.AllowlistInit[] memory, uint256) { uint256 length = seed % 10; seed = _next(seed); - AllowlistPlugin.AllowlistInit[] memory init = new AllowlistPlugin.AllowlistInit[](length); + AllowlistModule.AllowlistInit[] memory init = new AllowlistModule.AllowlistInit[](length); for (uint256 i = 0; i < length; i++) { // Half the time, the target is a random counter, the other half, it's a random address. @@ -270,7 +273,7 @@ contract AllowlistPluginTest is CustomValidationTestBase { seed = _next(seed); } - init[i] = AllowlistPlugin.AllowlistInit(target, hasSelectorAllowlist, selectors); + init[i] = AllowlistModule.AllowlistInit(target, hasSelectorAllowlist, selectors); } return (init, seed); @@ -290,35 +293,29 @@ contract AllowlistPluginTest is CustomValidationTestBase { internal virtual override - returns (FunctionReference, bool, bool, bytes4[] memory, bytes memory, bytes memory, bytes memory) + returns (ModuleEntity, bool, bool, bytes4[] memory, bytes memory, bytes[] memory) { - FunctionReference accessControlHook = FunctionReferenceLib.pack( - address(allowlistPlugin), uint8(AllowlistPlugin.FunctionId.PRE_VALIDATION_HOOK) + bytes[] memory hooks = new bytes[](1); + hooks[0] = abi.encodePacked( + HookConfigLib.packValidationHook( + address(allowlistModule), uint32(AllowlistModule.EntityId.PRE_VALIDATION_HOOK) + ), + abi.encode(allowlistInit) ); - FunctionReference[] memory preValidationHooks = new FunctionReference[](1); - preValidationHooks[0] = accessControlHook; - - bytes[] memory preValidationHookData = new bytes[](1); - // Access control is restricted to only the counter - preValidationHookData[0] = abi.encode(allowlistInit); - - bytes memory packedPreValidationHooks = abi.encode(preValidationHooks, preValidationHookData); - return ( - _ownerValidation, + _signerValidation, true, true, new bytes4[](0), - abi.encode(TEST_DEFAULT_OWNER_FUNCTION_ID, owner1), - packedPreValidationHooks, - "" + abi.encode(TEST_DEFAULT_VALIDATION_ENTITY_ID, owner1), + hooks ); } // Unfortunately, this is a feature that solidity has only implemented in via-ir, so we need to do it manually // to be able to run the tests in lite mode. - function _copyInitToStorage(AllowlistPlugin.AllowlistInit[] memory init) internal { + function _copyInitToStorage(AllowlistModule.AllowlistInit[] memory init) internal { for (uint256 i = 0; i < init.length; i++) { allowlistInit.push(init[i]); } diff --git a/test/module/ERC20TokenLimitModule.t.sol b/test/module/ERC20TokenLimitModule.t.sol new file mode 100644 index 00000000..96c846e4 --- /dev/null +++ b/test/module/ERC20TokenLimitModule.t.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {MockERC20} from "../mocks/MockERC20.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; +import {ModuleEntity} from "../../src/helpers/ModuleEntityLib.sol"; + +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; + +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; +import {ExecutionHook} from "../../src/interfaces/IAccountLoupe.sol"; +import {ExecutionManifest} from "../../src/interfaces/IExecution.sol"; +import {Call, IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; +import {ERC20TokenLimitModule} from "../../src/modules/ERC20TokenLimitModule.sol"; +import {MockModule} from "../mocks/MockModule.sol"; + +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract ERC20TokenLimitModuleTest is AccountTestBase { + address public recipient = address(1); + MockERC20 public erc20; + address payable public bundler = payable(address(2)); + ExecutionManifest internal _m; + MockModule public validationModule = new MockModule(_m); + ModuleEntity public validationFunction; + + UpgradeableModularAccount public acct; + ERC20TokenLimitModule public module = new ERC20TokenLimitModule(); + uint256 public spendLimit = 10 ether; + + function setUp() public { + // Set up a validator with hooks from the erc20 spend limit module attached + acct = factory.createAccount(address(this), 0); + + erc20 = new MockERC20(); + erc20.mint(address(acct), 10 ether); + + ExecutionHook[] memory permissionHooks = new ExecutionHook[](1); + permissionHooks[0] = ExecutionHook({ + hookFunction: ModuleEntityLib.pack(address(module), 0), + isPreHook: true, + isPostHook: false + }); + + // arr idx 0 => functionId of 0 has that spend + uint256[] memory limits = new uint256[](1); + limits[0] = spendLimit; + + ERC20TokenLimitModule.ERC20SpendLimit[] memory limit = new ERC20TokenLimitModule.ERC20SpendLimit[](1); + limit[0] = ERC20TokenLimitModule.ERC20SpendLimit({token: address(erc20), limits: limits}); + + bytes[] memory hooks = new bytes[](1); + hooks[0] = abi.encodePacked( + HookConfigLib.packExecHook({_module: address(module), _entityId: 0, _hasPre: true, _hasPost: false}), + abi.encode(uint32(0), limit) + ); + + vm.prank(address(acct)); + acct.installValidation( + ValidationConfigLib.pack(address(validationModule), 0, true, true), new bytes4[](0), "", hooks + ); + + validationFunction = ModuleEntityLib.pack(address(validationModule), 0); + } + + function _getPackedUO(bytes memory callData) internal view returns (PackedUserOperation memory uo) { + uo = PackedUserOperation({ + sender: address(acct), + nonce: 0, + initCode: "", + callData: abi.encodePacked(UpgradeableModularAccount.executeUserOp.selector, callData), + accountGasLimits: bytes32(bytes16(uint128(200_000))) | bytes32(uint256(200_000)), + preVerificationGas: 200_000, + gasFees: bytes32(uint256(uint128(0))), + paymasterAndData: "", + signature: _encodeSignature(ModuleEntityLib.pack(address(validationModule), 0), 1, "") + }); + } + + function _getExecuteWithSpend(uint256 value) internal view returns (bytes memory) { + return abi.encodeCall( + UpgradeableModularAccount.execute, + (address(erc20), 0, abi.encodeCall(IERC20.transfer, (recipient, value))) + ); + } + + function test_userOp_executeLimit() public { + vm.startPrank(address(entryPoint)); + assertEq(module.limits(0, address(erc20), address(acct)), 10 ether); + acct.executeUserOp(_getPackedUO(_getExecuteWithSpend(5 ether)), bytes32(0)); + assertEq(module.limits(0, address(erc20), address(acct)), 5 ether); + } + + function test_userOp_executeBatchLimit() public { + Call[] memory calls = new Call[](3); + calls[0] = + Call({target: address(erc20), value: 0, data: abi.encodeCall(IERC20.transfer, (recipient, 1 wei))}); + calls[1] = + Call({target: address(erc20), value: 0, data: abi.encodeCall(IERC20.transfer, (recipient, 1 ether))}); + calls[2] = Call({ + target: address(erc20), + value: 0, + data: abi.encodeCall(IERC20.transfer, (recipient, 5 ether + 100_000)) + }); + + vm.startPrank(address(entryPoint)); + assertEq(module.limits(0, address(erc20), address(acct)), 10 ether); + acct.executeUserOp(_getPackedUO(abi.encodeCall(IStandardExecutor.executeBatch, (calls))), bytes32(0)); + assertEq(module.limits(0, address(erc20), address(acct)), 10 ether - 6 ether - 100_001); + } + + function test_userOp_executeBatch_approveAndTransferLimit() public { + Call[] memory calls = new Call[](3); + calls[0] = + Call({target: address(erc20), value: 0, data: abi.encodeCall(IERC20.approve, (recipient, 1 wei))}); + calls[1] = + Call({target: address(erc20), value: 0, data: abi.encodeCall(IERC20.transfer, (recipient, 1 ether))}); + calls[2] = Call({ + target: address(erc20), + value: 0, + data: abi.encodeCall(IERC20.approve, (recipient, 5 ether + 100_000)) + }); + + vm.startPrank(address(entryPoint)); + assertEq(module.limits(0, address(erc20), address(acct)), 10 ether); + acct.executeUserOp(_getPackedUO(abi.encodeCall(IStandardExecutor.executeBatch, (calls))), bytes32(0)); + assertEq(module.limits(0, address(erc20), address(acct)), 10 ether - 6 ether - 100_001); + } + + function test_userOp_executeBatch_approveAndTransferLimit_fail() public { + Call[] memory calls = new Call[](3); + calls[0] = + Call({target: address(erc20), value: 0, data: abi.encodeCall(IERC20.approve, (recipient, 1 wei))}); + calls[1] = + Call({target: address(erc20), value: 0, data: abi.encodeCall(IERC20.transfer, (recipient, 1 ether))}); + calls[2] = Call({ + target: address(erc20), + value: 0, + data: abi.encodeCall(IERC20.approve, (recipient, 9 ether + 100_000)) + }); + + vm.startPrank(address(entryPoint)); + assertEq(module.limits(0, address(erc20), address(acct)), 10 ether); + PackedUserOperation[] memory uos = new PackedUserOperation[](1); + uos[0] = _getPackedUO(abi.encodeCall(IStandardExecutor.executeBatch, (calls))); + entryPoint.handleOps(uos, bundler); + // no spend consumed + assertEq(module.limits(0, address(erc20), address(acct)), 10 ether); + } + + function test_runtime_executeLimit() public { + assertEq(module.limits(0, address(erc20), address(acct)), 10 ether); + acct.executeWithAuthorization( + _getExecuteWithSpend(5 ether), + _encodeSignature(ModuleEntityLib.pack(address(validationModule), 0), 1, "") + ); + assertEq(module.limits(0, address(erc20), address(acct)), 5 ether); + } + + function test_runtime_executeBatchLimit() public { + Call[] memory calls = new Call[](3); + calls[0] = + Call({target: address(erc20), value: 0, data: abi.encodeCall(IERC20.approve, (recipient, 1 wei))}); + calls[1] = + Call({target: address(erc20), value: 0, data: abi.encodeCall(IERC20.transfer, (recipient, 1 ether))}); + calls[2] = Call({ + target: address(erc20), + value: 0, + data: abi.encodeCall(IERC20.approve, (recipient, 5 ether + 100_000)) + }); + + assertEq(module.limits(0, address(erc20), address(acct)), 10 ether); + acct.executeWithAuthorization( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), + _encodeSignature(ModuleEntityLib.pack(address(validationModule), 0), 1, "") + ); + assertEq(module.limits(0, address(erc20), address(acct)), 10 ether - 6 ether - 100_001); + } +} diff --git a/test/module/NativeTokenLimitModule.t.sol b/test/module/NativeTokenLimitModule.t.sol new file mode 100644 index 00000000..895cd6da --- /dev/null +++ b/test/module/NativeTokenLimitModule.t.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; + +import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; +import {ModuleEntity} from "../../src/helpers/ModuleEntityLib.sol"; + +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; + +import {HookConfigLib} from "../../src/helpers/HookConfigLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; +import {ExecutionManifest} from "../../src/interfaces/IExecution.sol"; +import {Call, IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; +import {NativeTokenLimitModule} from "../../src/modules/NativeTokenLimitModule.sol"; +import {MockModule} from "../mocks/MockModule.sol"; + +import {AccountTestBase} from "../utils/AccountTestBase.sol"; + +contract NativeTokenLimitModuleTest is AccountTestBase { + address public recipient = address(1); + address payable public bundler = payable(address(2)); + ExecutionManifest internal _m; + MockModule public validationModule = new MockModule(_m); + ModuleEntity public validationFunction; + + UpgradeableModularAccount public acct; + NativeTokenLimitModule public module = new NativeTokenLimitModule(); + uint256 public spendLimit = 10 ether; + + function setUp() public { + // Set up a validator with hooks from the gas spend limit module attached + + acct = factory.createAccount(address(this), 0); + + vm.deal(address(acct), 10 ether); + + ModuleEntity[] memory preValidationHooks = new ModuleEntity[](1); + preValidationHooks[0] = ModuleEntityLib.pack(address(module), 0); + + uint256[] memory spendLimits = new uint256[](1); + spendLimits[0] = spendLimit; + + bytes[] memory hooks = new bytes[](2); + hooks[0] = abi.encodePacked(HookConfigLib.packValidationHook({_module: address(module), _entityId: 0})); + // No init data for pre validation + + hooks[1] = abi.encodePacked( + HookConfigLib.packExecHook({_module: address(module), _entityId: 0, _hasPre: true, _hasPost: false}), + abi.encode(0, spendLimits) + ); + + vm.prank(address(acct)); + acct.installValidation( + ValidationConfigLib.pack(address(validationModule), 0, true, true), + new bytes4[](0), + new bytes(0), + hooks + ); + + validationFunction = ModuleEntityLib.pack(address(validationModule), 0); + } + + function _getExecuteWithValue(uint256 value) internal view returns (bytes memory) { + return abi.encodeCall(UpgradeableModularAccount.execute, (recipient, value, "")); + } + + function _getPackedUO(uint256 gas1, uint256 gas2, uint256 gas3, uint256 gasPrice, bytes memory callData) + internal + view + returns (PackedUserOperation memory uo) + { + uo = PackedUserOperation({ + sender: address(acct), + nonce: 0, + initCode: "", + callData: abi.encodePacked(UpgradeableModularAccount.executeUserOp.selector, callData), + accountGasLimits: bytes32(bytes16(uint128(gas1))) | bytes32(uint256(gas2)), + preVerificationGas: gas3, + gasFees: bytes32(uint256(uint128(gasPrice))), + paymasterAndData: "", + signature: _encodeSignature(ModuleEntityLib.pack(address(validationModule), 0), 1, "") + }); + } + + function test_userOp_gasLimit() public { + vm.startPrank(address(entryPoint)); + + // uses 10e - 200000 of gas + assertEq(module.limits(0, address(acct)), 10 ether); + uint256 result = acct.validateUserOp( + _getPackedUO(100_000, 100_000, 10 ether - 400_000, 1, _getExecuteWithValue(0)), bytes32(0), 0 + ); + assertEq(module.limits(0, address(acct)), 200_000); + + uint256 expected = uint256(type(uint48).max) << 160; + assertEq(result, expected); + + // uses 200k + 1 wei of gas + vm.expectRevert(NativeTokenLimitModule.ExceededNativeTokenLimit.selector); + result = acct.validateUserOp(_getPackedUO(100_000, 100_000, 1, 1, _getExecuteWithValue(0)), bytes32(0), 0); + } + + function test_userOp_executeLimit() public { + vm.startPrank(address(entryPoint)); + + // uses 5e of native tokens + assertEq(module.limits(0, address(acct)), 10 ether); + acct.executeUserOp(_getPackedUO(0, 0, 0, 0, _getExecuteWithValue(5 ether)), bytes32(0)); + assertEq(module.limits(0, address(acct)), 5 ether); + + // uses 5e + 1wei of native tokens + vm.expectRevert( + abi.encodePacked( + UpgradeableModularAccount.PreExecHookReverted.selector, + abi.encode( + address(module), + uint32(0), + abi.encodePacked(NativeTokenLimitModule.ExceededNativeTokenLimit.selector) + ) + ) + ); + acct.executeUserOp(_getPackedUO(0, 0, 0, 0, _getExecuteWithValue(5 ether + 1)), bytes32(0)); + } + + function test_userOp_executeBatchLimit() public { + Call[] memory calls = new Call[](3); + calls[0] = Call({target: recipient, value: 1, data: ""}); + calls[1] = Call({target: recipient, value: 1 ether, data: ""}); + calls[2] = Call({target: recipient, value: 5 ether + 100_000, data: ""}); + + vm.startPrank(address(entryPoint)); + assertEq(module.limits(0, address(acct)), 10 ether); + acct.executeUserOp( + _getPackedUO(0, 0, 0, 0, abi.encodeCall(IStandardExecutor.executeBatch, (calls))), bytes32(0) + ); + assertEq(module.limits(0, address(acct)), 10 ether - 6 ether - 100_001); + assertEq(recipient.balance, 6 ether + 100_001); + } + + function test_userOp_combinedExecLimit_success() public { + assertEq(module.limits(0, address(acct)), 10 ether); + PackedUserOperation[] memory uos = new PackedUserOperation[](1); + uos[0] = _getPackedUO(200_000, 200_000, 200_000, 1, _getExecuteWithValue(5 ether)); + entryPoint.handleOps(uos, bundler); + + assertEq(module.limits(0, address(acct)), 5 ether - 600_000); + assertEq(recipient.balance, 5 ether); + } + + function test_userOp_combinedExecBatchLimit_success() public { + Call[] memory calls = new Call[](3); + calls[0] = Call({target: recipient, value: 1, data: ""}); + calls[1] = Call({target: recipient, value: 1 ether, data: ""}); + calls[2] = Call({target: recipient, value: 5 ether + 100_000, data: ""}); + + vm.startPrank(address(entryPoint)); + assertEq(module.limits(0, address(acct)), 10 ether); + PackedUserOperation[] memory uos = new PackedUserOperation[](1); + uos[0] = + _getPackedUO(200_000, 200_000, 200_000, 1, abi.encodeCall(IStandardExecutor.executeBatch, (calls))); + entryPoint.handleOps(uos, bundler); + + assertEq(module.limits(0, address(acct)), 10 ether - 6 ether - 700_001); + assertEq(recipient.balance, 6 ether + 100_001); + } + + function test_userOp_combinedExecLimit_failExec() public { + assertEq(module.limits(0, address(acct)), 10 ether); + PackedUserOperation[] memory uos = new PackedUserOperation[](1); + uos[0] = _getPackedUO(200_000, 200_000, 200_000, 1, _getExecuteWithValue(10 ether)); + entryPoint.handleOps(uos, bundler); + + assertEq(module.limits(0, address(acct)), 10 ether - 600_000); + assertEq(recipient.balance, 0); + } + + function test_runtime_executeLimit() public { + assertEq(module.limits(0, address(acct)), 10 ether); + acct.executeWithAuthorization(_getExecuteWithValue(5 ether), _encodeSignature(validationFunction, 1, "")); + assertEq(module.limits(0, address(acct)), 5 ether); + } + + function test_runtime_executeBatchLimit() public { + Call[] memory calls = new Call[](3); + calls[0] = Call({target: recipient, value: 1, data: ""}); + calls[1] = Call({target: recipient, value: 1 ether, data: ""}); + calls[2] = Call({target: recipient, value: 5 ether + 100_000, data: ""}); + + assertEq(module.limits(0, address(acct)), 10 ether); + acct.executeWithAuthorization( + abi.encodeCall(IStandardExecutor.executeBatch, (calls)), _encodeSignature(validationFunction, 1, "") + ); + assertEq(module.limits(0, address(acct)), 4 ether - 100_001); + } +} diff --git a/test/module/SingleSignerValidation.t.sol b/test/module/SingleSignerValidation.t.sol new file mode 100644 index 00000000..9358b59f --- /dev/null +++ b/test/module/SingleSignerValidation.t.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; +import {ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; + +import {ContractOwner} from "../mocks/ContractOwner.sol"; +import {AccountTestBase} from "../utils/AccountTestBase.sol"; +import {TEST_DEFAULT_VALIDATION_ENTITY_ID} from "../utils/TestConstants.sol"; + +contract SingleSignerValidationTest is AccountTestBase { + using MessageHashUtils for bytes32; + + bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e; + + address public ethRecipient; + address public owner2; + uint256 public owner2Key; + UpgradeableModularAccount public account; + + ContractOwner public contractOwner; + + function setUp() public { + ethRecipient = makeAddr("ethRecipient"); + (owner2, owner2Key) = makeAddrAndKey("owner2"); + account = factory.createAccount(owner1, 0); + vm.deal(address(account), 100 ether); + + contractOwner = new ContractOwner(); + } + + function test_userOpValidation() public { + PackedUserOperation memory userOp = PackedUserOperation({ + sender: address(account), + nonce: 0, + initCode: "", + callData: abi.encodeCall(UpgradeableModularAccount.execute, (ethRecipient, 1 wei, "")), + accountGasLimits: _encodeGas(VERIFICATION_GAS_LIMIT, CALL_GAS_LIMIT), + preVerificationGas: 0, + gasFees: _encodeGas(1, 1), + paymasterAndData: "", + signature: "" + }); + + // Generate signature + bytes32 userOpHash = entryPoint.getUserOpHash(userOp); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); + userOp.signature = _encodeSignature( + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID), + GLOBAL_VALIDATION, + abi.encodePacked(r, s, v) + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + entryPoint.handleOps(userOps, beneficiary); + + assertEq(ethRecipient.balance, 1 wei); + } + + function test_runtimeValidate() public { + vm.prank(owner1); + account.executeWithAuthorization( + abi.encodeCall(UpgradeableModularAccount.execute, (ethRecipient, 1 wei, "")), + _encodeSignature( + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID), + GLOBAL_VALIDATION, + "" + ) + ); + assertEq(ethRecipient.balance, 1 wei); + } + + function test_runtime_with2SameValidationInstalled() public { + uint32 newEntityId = TEST_DEFAULT_VALIDATION_ENTITY_ID + 1; + vm.prank(address(entryPoint)); + account.installValidation( + ValidationConfigLib.pack(address(singleSignerValidation), newEntityId, true, false), + new bytes4[](0), + abi.encode(newEntityId, owner2), + new bytes[](0) + ); + + vm.prank(owner2); + account.executeWithAuthorization( + abi.encodeCall(UpgradeableModularAccount.execute, (ethRecipient, 1 wei, "")), + _encodeSignature( + ModuleEntityLib.pack(address(singleSignerValidation), newEntityId), GLOBAL_VALIDATION, "" + ) + ); + assertEq(ethRecipient.balance, 1 wei); + } + + function testFuzz_isValidSignatureForEOAOwner(string memory salt, bytes32 digest) public { + // range bound the possible set of priv keys + (address signer, uint256 privateKey) = makeAddrAndKey(salt); + + address accountAddr = address(account); + + vm.startPrank(accountAddr); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + + // sig check should fail + assertEq( + singleSignerValidation.validateSignature( + accountAddr, TEST_DEFAULT_VALIDATION_ENTITY_ID, address(this), digest, abi.encodePacked(r, s, v) + ), + bytes4(0xFFFFFFFF) + ); + + // transfer ownership to signer + singleSignerValidation.transferSigner(TEST_DEFAULT_VALIDATION_ENTITY_ID, signer); + assertEq(signer, singleSignerValidation.signerOf(TEST_DEFAULT_VALIDATION_ENTITY_ID, accountAddr)); + + // sig check should pass + assertEq( + singleSignerValidation.validateSignature( + accountAddr, TEST_DEFAULT_VALIDATION_ENTITY_ID, address(this), digest, abi.encodePacked(r, s, v) + ), + _1271_MAGIC_VALUE + ); + } + + function testFuzz_isValidSignatureForContractOwner(bytes32 digest) public { + address accountAddr = address(account); + vm.startPrank(accountAddr); + singleSignerValidation.transferSigner(TEST_DEFAULT_VALIDATION_ENTITY_ID, address(contractOwner)); + bytes memory signature = contractOwner.sign(digest); + assertEq( + singleSignerValidation.validateSignature( + accountAddr, TEST_DEFAULT_VALIDATION_ENTITY_ID, address(this), digest, signature + ), + _1271_MAGIC_VALUE + ); + } +} diff --git a/test/plugin/TokenReceiverPlugin.t.sol b/test/module/TokenReceiverModule.t.sol similarity index 88% rename from test/plugin/TokenReceiverPlugin.t.sol rename to test/module/TokenReceiverModule.t.sol index 2f52a988..d5df2a97 100644 --- a/test/plugin/TokenReceiverPlugin.t.sol +++ b/test/module/TokenReceiverModule.t.sol @@ -2,21 +2,23 @@ pragma solidity ^0.8.19; import {EntryPoint} from "@eth-infinitism/account-abstraction/core/EntryPoint.sol"; -import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {TokenReceiverPlugin} from "../../src/plugins/TokenReceiverPlugin.sol"; +import {TokenReceiverModule} from "../../src/modules/TokenReceiverModule.sol"; -import {MSCAFactoryFixture} from "../mocks/MSCAFactoryFixture.sol"; -import {MockERC721} from "../mocks/MockERC721.sol"; import {MockERC1155} from "../mocks/MockERC1155.sol"; +import {MockERC721} from "../mocks/MockERC721.sol"; +import {SingleSignerFactoryFixture} from "../mocks/SingleSignerFactoryFixture.sol"; + import {OptimizedTest} from "../utils/OptimizedTest.sol"; -contract TokenReceiverPluginTest is OptimizedTest, IERC1155Receiver { +contract TokenReceiverModuleTest is OptimizedTest, IERC1155Receiver { EntryPoint public entryPoint; UpgradeableModularAccount public acct; - TokenReceiverPlugin public plugin; + TokenReceiverModule public module; MockERC721 public t0; MockERC1155 public t1; @@ -33,10 +35,11 @@ contract TokenReceiverPluginTest is OptimizedTest, IERC1155Receiver { function setUp() public { entryPoint = new EntryPoint(); - MSCAFactoryFixture factory = new MSCAFactoryFixture(entryPoint, _deploySingleOwnerPlugin()); + SingleSignerFactoryFixture factory = + new SingleSignerFactoryFixture(entryPoint, _deploySingleSignerValidation()); acct = factory.createAccount(address(this), 0); - plugin = _deployTokenReceiverPlugin(); + module = _deployTokenReceiverModule(); t0 = new MockERC721("t0", "t0"); t0.mint(address(this), _TOKEN_ID); @@ -51,11 +54,10 @@ contract TokenReceiverPluginTest is OptimizedTest, IERC1155Receiver { } } - function _initPlugin() internal { - bytes32 manifestHash = keccak256(abi.encode(plugin.pluginManifest())); - - vm.prank(address(entryPoint)); - acct.installPlugin(address(plugin), manifestHash, ""); + function _initModule() internal { + vm.startPrank(address(entryPoint)); + acct.installExecution(address(module), module.executionManifest(), ""); + vm.stopPrank(); } function test_failERC721Transfer() public { @@ -70,7 +72,7 @@ contract TokenReceiverPluginTest is OptimizedTest, IERC1155Receiver { } function test_passERC721Transfer() public { - _initPlugin(); + _initModule(); assertEq(t0.ownerOf(_TOKEN_ID), address(this)); t0.safeTransferFrom(address(this), address(acct), _TOKEN_ID); assertEq(t0.ownerOf(_TOKEN_ID), address(acct)); @@ -99,7 +101,7 @@ contract TokenReceiverPluginTest is OptimizedTest, IERC1155Receiver { } function test_passERC1155Transfer() public { - _initPlugin(); + _initModule(); assertEq(t1.balanceOf(address(this), _TOKEN_ID), _TOKEN_AMOUNT); assertEq(t1.balanceOf(address(acct), _TOKEN_ID), 0); @@ -128,7 +130,7 @@ contract TokenReceiverPluginTest is OptimizedTest, IERC1155Receiver { } function test_passIntrospection() public { - _initPlugin(); + _initModule(); bool isSupported; diff --git a/test/plugin/SingleOwnerPlugin.t.sol b/test/plugin/SingleOwnerPlugin.t.sol deleted file mode 100644 index ddfe4d41..00000000 --- a/test/plugin/SingleOwnerPlugin.t.sol +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {EntryPoint} from "@eth-infinitism/account-abstraction/core/EntryPoint.sol"; -import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; - -import {SingleOwnerPlugin} from "../../src/plugins/owner/SingleOwnerPlugin.sol"; - -import {ContractOwner} from "../mocks/ContractOwner.sol"; -import {OptimizedTest} from "../utils/OptimizedTest.sol"; -import {TEST_DEFAULT_OWNER_FUNCTION_ID} from "../utils/TestConstants.sol"; - -contract SingleOwnerPluginTest is OptimizedTest { - using ECDSA for bytes32; - using MessageHashUtils for bytes32; - - SingleOwnerPlugin public plugin; - EntryPoint public entryPoint; - - bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e; - address public a; - address public b; - - address public owner1; - address public owner2; - ContractOwner public contractOwner; - - // Event declarations (needed for vm.expectEmit) - event OwnershipTransferred(address indexed account, address indexed previousOwner, address indexed newOwner); - - function setUp() public { - plugin = _deploySingleOwnerPlugin(); - entryPoint = new EntryPoint(); - - a = makeAddr("a"); - b = makeAddr("b"); - owner1 = makeAddr("owner1"); - owner2 = makeAddr("owner2"); - contractOwner = new ContractOwner(); - } - - // Tests: - // - uninitialized owner is zero address - // - transferOwnership result is returned via owner afterwards - // - transferOwnership emits OwnershipTransferred event - // - owner() returns correct value after transferOwnership - // - owner() does not return a different account's owner - // - requireFromOwner succeeds when called by owner - // - requireFromOwner reverts when called by non-owner - - function test_uninitializedOwner() public { - vm.startPrank(a); - assertEq(address(0), plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - } - - function test_ownerInitialization() public { - vm.startPrank(a); - assertEq(address(0), plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, owner1); - assertEq(owner1, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - } - - function test_ownerInitializationEvent() public { - vm.startPrank(a); - assertEq(address(0), plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - - vm.expectEmit(true, true, true, true); - emit OwnershipTransferred(a, address(0), owner1); - - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, owner1); - assertEq(owner1, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - } - - function test_ownerMigration() public { - vm.startPrank(a); - assertEq(address(0), plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, owner1); - assertEq(owner1, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, owner2); - assertEq(owner2, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - } - - function test_ownerMigrationEvents() public { - vm.startPrank(a); - assertEq(address(0), plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - - vm.expectEmit(true, true, true, true); - emit OwnershipTransferred(a, address(0), owner1); - - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, owner1); - assertEq(owner1, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - - vm.expectEmit(true, true, true, true); - emit OwnershipTransferred(a, owner1, owner2); - - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, owner2); - assertEq(owner2, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - } - - function test_ownerForSender() public { - vm.startPrank(a); - assertEq(address(0), plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, owner1); - assertEq(owner1, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - vm.startPrank(b); - assertEq(address(0), plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, b)); - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, owner2); - assertEq(owner2, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, b)); - } - - function test_requireOwner() public { - vm.startPrank(a); - assertEq(address(0), plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, owner1); - assertEq(owner1, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - plugin.validateRuntime(TEST_DEFAULT_OWNER_FUNCTION_ID, owner1, 0, "", ""); - - vm.startPrank(b); - vm.expectRevert(SingleOwnerPlugin.NotAuthorized.selector); - plugin.validateRuntime(TEST_DEFAULT_OWNER_FUNCTION_ID, owner1, 0, "", ""); - } - - function testFuzz_validateUserOpSig(string memory salt, PackedUserOperation memory userOp) public { - // range bound the possible set of priv keys - (address signer, uint256 privateKey) = makeAddrAndKey(salt); - - vm.startPrank(a); - bytes32 userOpHash = entryPoint.getUserOpHash(userOp); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, userOpHash.toEthSignedMessageHash()); - - // sig cannot cover the whole userop struct since userop struct has sig field - userOp.signature = abi.encodePacked(r, s, v); - - // sig check should fail - uint256 success = plugin.validateUserOp(TEST_DEFAULT_OWNER_FUNCTION_ID, userOp, userOpHash); - assertEq(success, 1); - - // transfer ownership to signer - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, signer); - assertEq(signer, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - - // sig check should pass - success = plugin.validateUserOp(TEST_DEFAULT_OWNER_FUNCTION_ID, userOp, userOpHash); - assertEq(success, 0); - } - - function testFuzz_isValidSignatureForEOAOwner(string memory salt, bytes32 digest) public { - // range bound the possible set of priv keys - (address signer, uint256 privateKey) = makeAddrAndKey(salt); - - vm.startPrank(a); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - - // sig check should fail - assertEq( - plugin.validateSignature( - TEST_DEFAULT_OWNER_FUNCTION_ID, address(this), digest, abi.encodePacked(r, s, v) - ), - bytes4(0xFFFFFFFF) - ); - - // transfer ownership to signer - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, signer); - assertEq(signer, plugin.owners(TEST_DEFAULT_OWNER_FUNCTION_ID, a)); - - // sig check should pass - assertEq( - plugin.validateSignature( - TEST_DEFAULT_OWNER_FUNCTION_ID, address(this), digest, abi.encodePacked(r, s, v) - ), - _1271_MAGIC_VALUE - ); - } - - function testFuzz_isValidSignatureForContractOwner(bytes32 digest) public { - vm.startPrank(a); - plugin.transferOwnership(TEST_DEFAULT_OWNER_FUNCTION_ID, address(contractOwner)); - bytes memory signature = contractOwner.sign(digest); - assertEq( - plugin.validateSignature(TEST_DEFAULT_OWNER_FUNCTION_ID, address(this), digest, signature), - _1271_MAGIC_VALUE - ); - } -} diff --git a/test/script/Deploy.s.t.sol b/test/script/Deploy.s.t.sol new file mode 100644 index 00000000..105ce9b8 --- /dev/null +++ b/test/script/Deploy.s.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; + +import {EntryPoint} from "@eth-infinitism/account-abstraction/core/EntryPoint.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +import {DeployScript} from "../../script/Deploy.s.sol"; + +import {AccountFactory} from "../../src/account/AccountFactory.sol"; +import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; +import {SingleSignerValidation} from "../../src/modules/validation/SingleSignerValidation.sol"; + +contract DeployTest is Test { + DeployScript internal _deployScript; + + EntryPoint internal _entryPoint; + + address internal _owner; + + address internal _accountImpl; + address internal _singleSignerValidation; + address internal _factory; + + function setUp() public { + _entryPoint = new EntryPoint(); + _owner = makeAddr("OWNER"); + + vm.setEnv("ENTRYPOINT", vm.toString(address(_entryPoint))); + vm.setEnv("OWNER", vm.toString(_owner)); + + _accountImpl = Create2.computeAddress( + bytes32(0), + keccak256( + abi.encodePacked(type(UpgradeableModularAccount).creationCode, abi.encode(address(_entryPoint))) + ), + CREATE2_FACTORY + ); + + _singleSignerValidation = Create2.computeAddress( + bytes32(0), keccak256(abi.encodePacked(type(SingleSignerValidation).creationCode)), CREATE2_FACTORY + ); + + _factory = Create2.computeAddress( + bytes32(0), + keccak256( + abi.encodePacked( + type(AccountFactory).creationCode, + abi.encode(address(_entryPoint), _accountImpl, _singleSignerValidation, _owner) + ) + ), + CREATE2_FACTORY + ); + + vm.setEnv("ACCOUNT_IMPL", vm.toString(address(_accountImpl))); + vm.setEnv("FACTORY", vm.toString(address(_factory))); + vm.setEnv("SINGLE_SIGNER_VALIDATION", vm.toString(address(_singleSignerValidation))); + + vm.setEnv("ACCOUNT_IMPL_SALT", vm.toString(uint256(0))); + vm.setEnv("FACTORY_SALT", vm.toString(uint256(0))); + vm.setEnv("SINGLE_SIGNER_VALIDATION_SALT", vm.toString(uint256(0))); + + _deployScript = new DeployScript(); + + vm.deal(address(_deployScript), 0.1 ether); + } + + function test_deployScript_run() public { + _deployScript.run(); + + assertTrue(_accountImpl.code.length > 0); + assertTrue(_factory.code.length > 0); + assertTrue(_singleSignerValidation.code.length > 0); + } +} diff --git a/test/utils/AccountTestBase.sol b/test/utils/AccountTestBase.sol index a312fbdf..733b93e4 100644 --- a/test/utils/AccountTestBase.sol +++ b/test/utils/AccountTestBase.sol @@ -5,41 +5,43 @@ import {EntryPoint} from "@eth-infinitism/account-abstraction/core/EntryPoint.so import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import {FunctionReference, FunctionReferenceLib} from "../../src/helpers/FunctionReferenceLib.sol"; -import {IStandardExecutor, Call} from "../../src/interfaces/IStandardExecutor.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {SingleOwnerPlugin} from "../../src/plugins/owner/SingleOwnerPlugin.sol"; +import {ModuleEntity, ModuleEntityLib} from "../../src/helpers/ModuleEntityLib.sol"; +import {Call, IStandardExecutor} from "../../src/interfaces/IStandardExecutor.sol"; +import {SingleSignerValidation} from "../../src/modules/validation/SingleSignerValidation.sol"; import {OptimizedTest} from "./OptimizedTest.sol"; -import {TEST_DEFAULT_OWNER_FUNCTION_ID as EXT_CONST_TEST_DEFAULT_OWNER_FUNCTION_ID} from "./TestConstants.sol"; +import {TEST_DEFAULT_VALIDATION_ENTITY_ID as EXT_CONST_TEST_DEFAULT_VALIDATION_ENTITY_ID} from + "./TestConstants.sol"; -import {MSCAFactoryFixture} from "../mocks/MSCAFactoryFixture.sol"; +import {SingleSignerFactoryFixture} from "../mocks/SingleSignerFactoryFixture.sol"; /// @dev This contract handles common boilerplate setup for tests using UpgradeableModularAccount with -/// SingleOwnerPlugin. +/// SingleSignerValidation. abstract contract AccountTestBase is OptimizedTest { - using FunctionReferenceLib for FunctionReference; + using ModuleEntityLib for ModuleEntity; using MessageHashUtils for bytes32; EntryPoint public entryPoint; address payable public beneficiary; - SingleOwnerPlugin public singleOwnerPlugin; - MSCAFactoryFixture public factory; + + SingleSignerValidation public singleSignerValidation; + SingleSignerFactoryFixture public factory; address public owner1; uint256 public owner1Key; UpgradeableModularAccount public account1; - FunctionReference internal _ownerValidation; + ModuleEntity internal _signerValidation; uint8 public constant SELECTOR_ASSOCIATED_VALIDATION = 0; uint8 public constant GLOBAL_VALIDATION = 1; // Re-declare the constant to prevent derived test contracts from having to import it - uint8 public constant TEST_DEFAULT_OWNER_FUNCTION_ID = EXT_CONST_TEST_DEFAULT_OWNER_FUNCTION_ID; + uint32 public constant TEST_DEFAULT_VALIDATION_ENTITY_ID = EXT_CONST_TEST_DEFAULT_VALIDATION_ENTITY_ID; - uint256 public constant CALL_GAS_LIMIT = 100000; - uint256 public constant VERIFICATION_GAS_LIMIT = 1200000; + uint256 public constant CALL_GAS_LIMIT = 100_000; + uint256 public constant VERIFICATION_GAS_LIMIT = 1_200_000; struct PreValidationHookData { uint8 index; @@ -51,13 +53,14 @@ abstract contract AccountTestBase is OptimizedTest { (owner1, owner1Key) = makeAddrAndKey("owner1"); beneficiary = payable(makeAddr("beneficiary")); - singleOwnerPlugin = _deploySingleOwnerPlugin(); - factory = new MSCAFactoryFixture(entryPoint, singleOwnerPlugin); + singleSignerValidation = _deploySingleSignerValidation(); + factory = new SingleSignerFactoryFixture(entryPoint, singleSignerValidation); account1 = factory.createAccount(owner1, 0); vm.deal(address(account1), 100 ether); - _ownerValidation = FunctionReferenceLib.pack(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID); + _signerValidation = + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID); } function _runExecUserOp(address target, bytes memory callData) internal { @@ -100,7 +103,7 @@ abstract contract AccountTestBase is OptimizedTest { (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, userOpHash.toEthSignedMessageHash()); userOp.signature = _encodeSignature( - FunctionReferenceLib.pack(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID), + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, abi.encodePacked(r, s, v) ); @@ -153,7 +156,7 @@ abstract contract AccountTestBase is OptimizedTest { account1.executeWithAuthorization( callData, _encodeSignature( - FunctionReferenceLib.pack(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID), + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, "" ) @@ -168,7 +171,7 @@ abstract contract AccountTestBase is OptimizedTest { account1.executeWithAuthorization( callData, _encodeSignature( - FunctionReferenceLib.pack(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID), + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, "" ) @@ -182,15 +185,15 @@ abstract contract AccountTestBase is OptimizedTest { abi.encodeCall( account1.execute, ( - address(singleOwnerPlugin), + address(singleSignerValidation), 0, abi.encodeCall( - SingleOwnerPlugin.transferOwnership, (TEST_DEFAULT_OWNER_FUNCTION_ID, address(this)) + SingleSignerValidation.transferSigner, (TEST_DEFAULT_VALIDATION_ENTITY_ID, address(this)) ) ) ), _encodeSignature( - FunctionReferenceLib.pack(address(singleOwnerPlugin), TEST_DEFAULT_OWNER_FUNCTION_ID), + ModuleEntityLib.pack(address(singleSignerValidation), TEST_DEFAULT_VALIDATION_ENTITY_ID), GLOBAL_VALIDATION, "" ) @@ -204,7 +207,7 @@ abstract contract AccountTestBase is OptimizedTest { // helper function to encode a signature, according to the per-hook and per-validation data format. function _encodeSignature( - FunctionReference validationFunction, + ModuleEntity validationFunction, uint8 globalOrNot, PreValidationHookData[] memory preValidationHookData, bytes memory validationData @@ -214,7 +217,7 @@ abstract contract AccountTestBase is OptimizedTest { for (uint256 i = 0; i < preValidationHookData.length; ++i) { sig = abi.encodePacked( sig, - _packValidationDataWithIndex( + _packValidationResWithIndex( preValidationHookData[i].index, preValidationHookData[i].validationData ) ); @@ -222,13 +225,13 @@ abstract contract AccountTestBase is OptimizedTest { // Index of the actual validation data is the length of the preValidationHooksRetrieved - aka // one-past-the-end - sig = abi.encodePacked(sig, _packValidationDataWithIndex(255, validationData)); + sig = abi.encodePacked(sig, _packValidationResWithIndex(255, validationData)); return sig; } // overload for the case where there are no pre-validation hooks - function _encodeSignature(FunctionReference validationFunction, uint8 globalOrNot, bytes memory validationData) + function _encodeSignature(ModuleEntity validationFunction, uint8 globalOrNot, bytes memory validationData) internal pure returns (bytes memory) @@ -238,7 +241,7 @@ abstract contract AccountTestBase is OptimizedTest { } // helper function to pack validation data with an index, according to the sparse calldata segment spec. - function _packValidationDataWithIndex(uint8 index, bytes memory validationData) + function _packValidationResWithIndex(uint8 index, bytes memory validationData) internal pure returns (bytes memory) diff --git a/test/utils/CustomValidationTestBase.sol b/test/utils/CustomValidationTestBase.sol index 2244c865..3b313039 100644 --- a/test/utils/CustomValidationTestBase.sol +++ b/test/utils/CustomValidationTestBase.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.25; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {FunctionReference} from "../../src/helpers/FunctionReferenceLib.sol"; -import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; +import {ModuleEntity} from "../../src/helpers/ModuleEntityLib.sol"; +import {ValidationConfigLib} from "../../src/helpers/ValidationConfigLib.sol"; import {AccountTestBase} from "./AccountTestBase.sol"; @@ -16,13 +16,12 @@ import {AccountTestBase} from "./AccountTestBase.sol"; abstract contract CustomValidationTestBase is AccountTestBase { function _customValidationSetup() internal { ( - FunctionReference validationFunction, + ModuleEntity validationFunction, bool isGlobal, bool isSignatureValidation, bytes4[] memory selectors, bytes memory installData, - bytes memory preValidationHooks, - bytes memory permissionHooks + bytes[] memory hooks ) = _initialValidationConfig(); address accountImplementation = address(factory.accountImplementation()); @@ -33,8 +32,7 @@ abstract contract CustomValidationTestBase is AccountTestBase { ValidationConfigLib.pack(validationFunction, isGlobal, isSignatureValidation), selectors, installData, - preValidationHooks, - permissionHooks + hooks ); vm.deal(address(account1), 100 ether); @@ -44,12 +42,11 @@ abstract contract CustomValidationTestBase is AccountTestBase { internal virtual returns ( - FunctionReference validationFunction, + ModuleEntity validationFunction, bool shared, bool isSignatureValidation, bytes4[] memory selectors, bytes memory installData, - bytes memory preValidationHooks, - bytes memory permissionHooks + bytes[] memory hooks ); } diff --git a/test/utils/OptimizedTest.sol b/test/utils/OptimizedTest.sol index f9431acc..870d416a 100644 --- a/test/utils/OptimizedTest.sol +++ b/test/utils/OptimizedTest.sol @@ -6,8 +6,9 @@ import {Test} from "forge-std/Test.sol"; import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol"; -import {SingleOwnerPlugin} from "../../src/plugins/owner/SingleOwnerPlugin.sol"; -import {TokenReceiverPlugin} from "../../src/plugins/TokenReceiverPlugin.sol"; + +import {TokenReceiverModule} from "../../src/modules/TokenReceiverModule.sol"; +import {SingleSignerValidation} from "../../src/modules/validation/SingleSignerValidation.sol"; /// @dev This contract provides functions to deploy optimized (via IR) precompiled contracts. By compiling just /// the source contracts (excluding the test suite) via IR, and using the resulting bytecode within the tests @@ -44,15 +45,17 @@ abstract contract OptimizedTest is Test { : new UpgradeableModularAccount(entryPoint); } - function _deploySingleOwnerPlugin() internal returns (SingleOwnerPlugin) { + function _deployTokenReceiverModule() internal returns (TokenReceiverModule) { return _isOptimizedTest() - ? SingleOwnerPlugin(deployCode("out-optimized/SingleOwnerPlugin.sol/SingleOwnerPlugin.json")) - : new SingleOwnerPlugin(); + ? TokenReceiverModule(deployCode("out-optimized/TokenReceiverModule.sol/TokenReceiverModule.json")) + : new TokenReceiverModule(); } - function _deployTokenReceiverPlugin() internal returns (TokenReceiverPlugin) { + function _deploySingleSignerValidation() internal returns (SingleSignerValidation) { return _isOptimizedTest() - ? TokenReceiverPlugin(deployCode("out-optimized/TokenReceiverPlugin.sol/TokenReceiverPlugin.json")) - : new TokenReceiverPlugin(); + ? SingleSignerValidation( + deployCode("out-optimized/SingleSignerValidation.sol/SingleSignerValidation.json") + ) + : new SingleSignerValidation(); } } diff --git a/test/utils/TestConstants.sol b/test/utils/TestConstants.sol index 923692a7..c15b2dd3 100644 --- a/test/utils/TestConstants.sol +++ b/test/utils/TestConstants.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.25; -uint8 constant TEST_DEFAULT_OWNER_FUNCTION_ID = 0; +uint32 constant TEST_DEFAULT_VALIDATION_ENTITY_ID = 1;