From fe3c1c29cd3382aebca1ec5d27f7fc3775f2e3a8 Mon Sep 17 00:00:00 2001 From: zer0dot Date: Tue, 24 Sep 2024 15:45:32 -0400 Subject: [PATCH] feat: deferred install gas benchmarks --- .../ModularAccount_UserOp_Erc20Transfer.snap | 2 +- .../ModularAccount_UserOp_NativeTransfer.snap | 2 +- ...ularAccount_UserOp_deferredValidation.snap | 1 + ...ularAccount_UserOp_deferredValidation.snap | 1 + gas/modular-account/ModularAccount.gas.t.sol | 25 ++++ .../ModularAccountBenchmarkBase.sol | 135 +++++++++++++++++- .../SemiModularAccount.gas.t.sol | 25 ++++ test/account/DeferredValidation.t.sol | 19 +-- 8 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 .forge-snapshots/ModularAccount_UserOp_deferredValidation.snap create mode 100644 .forge-snapshots/SemiModularAccount_UserOp_deferredValidation.snap diff --git a/.forge-snapshots/ModularAccount_UserOp_Erc20Transfer.snap b/.forge-snapshots/ModularAccount_UserOp_Erc20Transfer.snap index b1ad097..4427766 100644 --- a/.forge-snapshots/ModularAccount_UserOp_Erc20Transfer.snap +++ b/.forge-snapshots/ModularAccount_UserOp_Erc20Transfer.snap @@ -1 +1 @@ -194101 \ No newline at end of file +194113 \ No newline at end of file diff --git a/.forge-snapshots/ModularAccount_UserOp_NativeTransfer.snap b/.forge-snapshots/ModularAccount_UserOp_NativeTransfer.snap index a3352bd..97dd5a5 100644 --- a/.forge-snapshots/ModularAccount_UserOp_NativeTransfer.snap +++ b/.forge-snapshots/ModularAccount_UserOp_NativeTransfer.snap @@ -1 +1 @@ -169841 \ No newline at end of file +169829 \ No newline at end of file diff --git a/.forge-snapshots/ModularAccount_UserOp_deferredValidation.snap b/.forge-snapshots/ModularAccount_UserOp_deferredValidation.snap new file mode 100644 index 0000000..cee66cb --- /dev/null +++ b/.forge-snapshots/ModularAccount_UserOp_deferredValidation.snap @@ -0,0 +1 @@ +240257 \ No newline at end of file diff --git a/.forge-snapshots/SemiModularAccount_UserOp_deferredValidation.snap b/.forge-snapshots/SemiModularAccount_UserOp_deferredValidation.snap new file mode 100644 index 0000000..edbb127 --- /dev/null +++ b/.forge-snapshots/SemiModularAccount_UserOp_deferredValidation.snap @@ -0,0 +1 @@ +234620 \ No newline at end of file diff --git a/gas/modular-account/ModularAccount.gas.t.sol b/gas/modular-account/ModularAccount.gas.t.sol index f59a2fd..76f1fe4 100644 --- a/gas/modular-account/ModularAccount.gas.t.sol +++ b/gas/modular-account/ModularAccount.gas.t.sol @@ -154,4 +154,29 @@ contract ModularAccountGasTest is ModularAccountBenchmarkBase("ModularAccount") _snap(USER_OP, "Erc20Transfer", gasUsed); } + + function test_modularAccountGas_userOp_deferredValidationInstall() public { + _deployAccount1(); + + vm.deal(address(account1), 1 ether); + + PackedUserOperation memory userOp = PackedUserOperation({ + sender: address(account1), + nonce: 0, + initCode: "", + callData: abi.encodeCall(ModularAccount.execute, (recipient, 0.1 ether, "")), + // don't over-estimate by a lot here, otherwise a fee is assessed. + accountGasLimits: _encodeGasLimits(40_000, 160_000), + preVerificationGas: 0, + gasFees: _encodeGasFees(1, 1), + paymasterAndData: "", + signature: _buildFullDeferredInstallSig(false, account1, 0, 0) + }); + + uint256 gasUsed = _userOpBenchmark(userOp); + + assertEq(address(recipient).balance, 0.1 ether + 1 wei); + + _snap(USER_OP, "deferredValidation", gasUsed); + } } diff --git a/gas/modular-account/ModularAccountBenchmarkBase.sol b/gas/modular-account/ModularAccountBenchmarkBase.sol index c233b80..2039c5f 100644 --- a/gas/modular-account/ModularAccountBenchmarkBase.sol +++ b/gas/modular-account/ModularAccountBenchmarkBase.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.26; import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {ModularAccount} from "../../src/account/ModularAccount.sol"; import {SemiModularAccount} from "../../src/account/SemiModularAccount.sol"; @@ -9,12 +10,23 @@ import {AccountFactory} from "../../src/factory/AccountFactory.sol"; import {FALLBACK_VALIDATION} from "../../src/helpers/Constants.sol"; import {ModuleEntity, ModuleEntityLib} from "../../src/libraries/ModuleEntityLib.sol"; + +import {ValidationConfig, ValidationConfigLib} from "../../src/libraries/ValidationConfigLib.sol"; import {SingleSignerValidationModule} from "../../src/modules/validation/SingleSignerValidationModule.sol"; +import {MockUserOpValidationModule} from "../../test/mocks/modules/ValidationModuleMocks.sol"; import {ModuleSignatureUtils} from "../../test/utils/ModuleSignatureUtils.sol"; -import {BenchmarkBase} from "..//BenchmarkBase.sol"; + +import {BenchmarkBase} from "../BenchmarkBase.sol"; abstract contract ModularAccountBenchmarkBase is BenchmarkBase, ModuleSignatureUtils { + using ValidationConfigLib for ValidationConfig; + + bytes32 private constant _INSTALL_VALIDATION_TYPEHASH = keccak256( + // solhint-disable-next-line max-line-length + "InstallValidation(bytes25 validationConfig,bytes4[] selectors,bytes installData,bytes[] hooks,uint256 nonce,uint48 deadline)" + ); + AccountFactory public factory; ModularAccount public accountImpl; SemiModularAccount public semiModularImpl; @@ -23,12 +35,15 @@ abstract contract ModularAccountBenchmarkBase is BenchmarkBase, ModuleSignatureU ModularAccount public account1; ModuleEntity public signerValidation; + address internal _mockValidation; constructor(string memory accountImplName) BenchmarkBase(accountImplName) { accountImpl = _deployModularAccount(IEntryPoint(entryPoint)); semiModularImpl = _deploySemiModularAccount(IEntryPoint(entryPoint)); singleSignerValidationModule = _deploySingleSignerValidationModule(); + _mockValidation = address(new MockUserOpValidationModule()); + factory = new AccountFactory( entryPoint, accountImpl, semiModularImpl, address(singleSignerValidationModule), address(this) ); @@ -43,4 +58,122 @@ abstract contract ModularAccountBenchmarkBase is BenchmarkBase, ModuleSignatureU account1 = factory.createSemiModularAccount(owner1, 0); signerValidation = FALLBACK_VALIDATION; } + + // Internal Helpers + function _buildFullDeferredInstallSig( + bool isSemiModular, + ModularAccount account, + uint256 nonce, + uint48 deadline + ) internal view returns (bytes memory) { + /** + * Deferred validation signature structure: + * bytes 0-23: Outer validation moduleEntity (the validation used to validate the installation of the inner + * validation) + * byte 24 : Validation flags (rightmost bit == isGlobal, second-to-rightmost bit == + * isDeferredValidationInstall) + * + * This is where things diverge, if this is a deferred validation install, rather than using the remaining + * signature data as validation data, we decode it as follows: + * + * bytes 25-28 : uint32, abi-encoded parameters length (e.g. 100) + * bytes 29-128 (example) : abi-encoded parameters + * bytes 129-132 : deferred install validation sig length (e.g. 68) + * bytes 133-200 (example): install validation sig data (the data passed to the outer validation to + * validate the deferred installation) + * bytes 201... : signature data passed to the newly installed deferred validation to validate + * the UO + */ + uint8 outerValidationFlags = 3; + + ValidationConfig deferredConfig = ValidationConfigLib.pack({ + _module: _mockValidation, + _entityId: uint32(0), + _isGlobal: true, + _isSignatureValidation: false, + _isUserOpValidation: true + }); + + bytes memory deferredInstallData = + abi.encode(deferredConfig, new bytes4[](0), "", new bytes[](0), nonce, deadline); + + bytes32 domainSeparator; + + // Needed for initCode txs + if (address(account).code.length > 0) { + domainSeparator = account.domainSeparator(); + } else { + domainSeparator = _computeDomainSeparatorNotDeployed(account); + } + + bytes32 structHash = keccak256( + abi.encode( + _INSTALL_VALIDATION_TYPEHASH, deferredConfig, new bytes4[](0), "", new bytes[](0), nonce, deadline + ) + ); + bytes32 typedDataHash = MessageHashUtils.toTypedDataHash(domainSeparator, structHash); + + bytes32 replaySafeHash = isSemiModular + ? _getSmaReplaySafeHash(account, typedDataHash) + : singleSignerValidationModule.replaySafeHash(address(account), typedDataHash); + + bytes memory deferredInstallSig = _getDeferredInstallSig(replaySafeHash); + + bytes memory innerUoValidationSig = _packValidationResWithIndex(255, hex"1234"); + + bytes memory encodedDeferredInstall = abi.encodePacked( + signerValidation, + outerValidationFlags, + uint32(deferredInstallData.length), + deferredInstallData, + uint32(deferredInstallSig.length), + deferredInstallSig, + innerUoValidationSig + ); + + return encodedDeferredInstall; + } + + function _computeDomainSeparatorNotDeployed(ModularAccount account) internal view returns (bytes32) { + bytes32 domainSeparatorTypehash = 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218; + return keccak256(abi.encode(domainSeparatorTypehash, block.chainid, address(account))); + } + + function _getSmaReplaySafeHash(ModularAccount account, bytes32 typedDataHash) + internal + view + returns (bytes32) + { + if (address(account).code.length > 0) { + return SemiModularAccount(payable(account)).replaySafeHash(typedDataHash); + } else { + // precompute it as the SMA is not yet deployed + // for SMA, the domain separator used for the deferred validation installation is the same as the one + // used to compute the replay safe hash. + return MessageHashUtils.toTypedDataHash({ + domainSeparator: _computeDomainSeparatorNotDeployed(account), + structHash: _hashStruct(typedDataHash) + }); + } + } + + function _getDeferredInstallSig(bytes32 replaySafeHash) internal view returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(owner1Key, replaySafeHash); + + bytes memory rawDeferredInstallSig = abi.encodePacked(r, s, v); + + bytes memory deferredInstallSig = _packValidationResWithIndex(255, rawDeferredInstallSig); + return deferredInstallSig; + } + + function _hashStruct(bytes32 hash) internal pure virtual returns (bytes32) { + bytes32 replaySafeTypehash = keccak256("ReplaySafeHash(bytes32 hash)"); // const 0x.. in contract + bytes32 res; + assembly ("memory-safe") { + mstore(0x00, replaySafeTypehash) + mstore(0x20, hash) + res := keccak256(0, 0x40) + } + return res; + } } diff --git a/gas/modular-account/SemiModularAccount.gas.t.sol b/gas/modular-account/SemiModularAccount.gas.t.sol index aa6cbc2..5fe952e 100644 --- a/gas/modular-account/SemiModularAccount.gas.t.sol +++ b/gas/modular-account/SemiModularAccount.gas.t.sol @@ -147,4 +147,29 @@ contract ModularAccountGasTest is ModularAccountBenchmarkBase("SemiModularAccoun _snap(USER_OP, "Erc20Transfer", gasUsed); } + + function test_semiModularAccountGas_userOp_deferredValidationInstall() public { + _deploySemiModularAccount1(); + + vm.deal(address(account1), 1 ether); + + PackedUserOperation memory userOp = PackedUserOperation({ + sender: address(account1), + nonce: 0, + initCode: "", + callData: abi.encodeCall(ModularAccount.execute, (recipient, 0.1 ether, "")), + // don't over-estimate by a lot here, otherwise a fee is assessed. + accountGasLimits: _encodeGasLimits(40_000, 160_000), + preVerificationGas: 0, + gasFees: _encodeGasFees(1, 1), + paymasterAndData: "", + signature: _buildFullDeferredInstallSig(true, account1, 0, 0) + }); + + uint256 gasUsed = _userOpBenchmark(userOp); + + assertEq(address(recipient).balance, 0.1 ether + 1 wei); + + _snap(USER_OP, "deferredValidation", gasUsed); + } } diff --git a/test/account/DeferredValidation.t.sol b/test/account/DeferredValidation.t.sol index ad7f444..4bd4be5 100644 --- a/test/account/DeferredValidation.t.sol +++ b/test/account/DeferredValidation.t.sol @@ -16,7 +16,6 @@ import {AccountTestBase} from "../utils/AccountTestBase.sol"; contract DeferredValidationTest is AccountTestBase { using ValidationConfigLib for ValidationConfig; - using MessageHashUtils for bytes32; bytes32 private constant _INSTALL_VALIDATION_TYPEHASH = keccak256( // solhint-disable-next-line max-line-length @@ -33,7 +32,7 @@ contract DeferredValidationTest is AccountTestBase { // Negatives function test_fail_deferredValidation_nonceUsed() external { - _runUserOpWithCustomSig(_encodedCall, "", _buildSig(account1, 0, 0)); + _runUserOpWithCustomSig(_encodedCall, "", _buildFullDeferredInstallSig(account1, 0, 0)); bytes memory expectedRevertdata = abi.encodeWithSelector( IEntryPoint.FailedOpWithRevert.selector, @@ -42,7 +41,7 @@ contract DeferredValidationTest is AccountTestBase { abi.encodeWithSelector(ModularAccount.DeferredInstallNonceInvalid.selector) ); - _runUserOpWithCustomSig(_encodedCall, expectedRevertdata, _buildSig(account1, 0, 0)); + _runUserOpWithCustomSig(_encodedCall, expectedRevertdata, _buildFullDeferredInstallSig(account1, 0, 0)); } function test_fail_deferredValidation_pastDeadline() external { @@ -51,7 +50,7 @@ contract DeferredValidationTest is AccountTestBase { // Note that a deadline of 0 implies no expiry vm.warp(2); - _runUserOpWithCustomSig(_encodedCall, expectedRevertdata, _buildSig(account1, 0, 1)); + _runUserOpWithCustomSig(_encodedCall, expectedRevertdata, _buildFullDeferredInstallSig(account1, 0, 1)); } function test_fail_deferredValidation_invalidSig() external { @@ -61,7 +60,9 @@ contract DeferredValidationTest is AccountTestBase { "AA23 reverted", abi.encodeWithSelector(ModularAccount.DeferredInstallSignatureInvalid.selector) ); - _runUserOpWithCustomSig(_encodedCall, expectedRevertData, _buildSig(ModularAccount(payable(0)), 0, 0)); + _runUserOpWithCustomSig( + _encodedCall, expectedRevertData, _buildFullDeferredInstallSig(ModularAccount(payable(0)), 0, 0) + ); } function test_fail_deferredValidation_nonceInvalidated() external { @@ -75,14 +76,14 @@ contract DeferredValidationTest is AccountTestBase { abi.encodeWithSelector(ModularAccount.DeferredInstallNonceInvalid.selector) ); - _runUserOpWithCustomSig(_encodedCall, expectedRevertdata, _buildSig(account1, 0, 0)); + _runUserOpWithCustomSig(_encodedCall, expectedRevertdata, _buildFullDeferredInstallSig(account1, 0, 0)); } // TODO: Test with hooks // Positives function test_deferredValidation() external { - _runUserOpWithCustomSig(_encodedCall, "", _buildSig(account1, 0, 0)); + _runUserOpWithCustomSig(_encodedCall, "", _buildFullDeferredInstallSig(account1, 0, 0)); } function test_deferredValidation_initCode() external { @@ -112,14 +113,14 @@ contract DeferredValidationTest is AccountTestBase { preVerificationGas: 0, gasFees: _encodeGas(1, 2), paymasterAndData: "", - signature: _buildSig(account2, 0, 0) + signature: _buildFullDeferredInstallSig(account2, 0, 0) }); _sendOp(userOp, ""); } // Internal Helpers - function _buildSig(ModularAccount account, uint256 nonce, uint48 deadline) + function _buildFullDeferredInstallSig(ModularAccount account, uint256 nonce, uint48 deadline) internal view returns (bytes memory)