Skip to content

Commit

Permalink
feat: deferred install gas benchmarks
Browse files Browse the repository at this point in the history
  • Loading branch information
Zer0dot committed Sep 24, 2024
1 parent 3e687bc commit fe3c1c2
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .forge-snapshots/ModularAccount_UserOp_Erc20Transfer.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
194101
194113
2 changes: 1 addition & 1 deletion .forge-snapshots/ModularAccount_UserOp_NativeTransfer.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
169841
169829
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
240257
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
234620
25 changes: 25 additions & 0 deletions gas/modular-account/ModularAccount.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
135 changes: 134 additions & 1 deletion gas/modular-account/ModularAccountBenchmarkBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,31 @@
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";
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;
Expand All @@ -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)
);
Expand All @@ -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;
}
}
25 changes: 25 additions & 0 deletions gas/modular-account/SemiModularAccount.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
19 changes: 10 additions & 9 deletions test/account/DeferredValidation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit fe3c1c2

Please sign in to comment.