From ac1c5dca331e565c20e918191ab7c0476353dd34 Mon Sep 17 00:00:00 2001 From: David Philipson Date: Thu, 7 Sep 2023 17:31:06 -0700 Subject: [PATCH] Add tests --- .gasestimates.md | 115 +++++++++++-- src/CustomSlotInitializable.sol | 14 +- test/CustomSlotInitializable.t.sol | 118 ++++++++++++++ test/LightAccount.t.sol | 249 +++++++++++++++++++++++++++-- test/LightAccountFactory.t.sol | 36 +++++ 5 files changed, 497 insertions(+), 35 deletions(-) create mode 100644 test/CustomSlotInitializable.t.sol create mode 100644 test/LightAccountFactory.t.sol diff --git a/.gasestimates.md b/.gasestimates.md index f6a1495..bb9822b 100644 --- a/.gasestimates.md +++ b/.gasestimates.md @@ -4,6 +4,20 @@ Generated via `bash utils/inspect.sh`. --- `forge test --gas-report --no-match-path "test/script/**/*"` +| lib/account-abstraction/contracts/core/EntryPoint.sol:EntryPoint contract | | | | | | +|---------------------------------------------------------------------------|-----------------|--------|--------|--------|---------| +| Deployment Cost | Deployment Size | | | | | +| 4128022 | 20516 | | | | | +| Function Name | min | avg | median | max | # calls | +| balanceOf | 614 | 1014 | 614 | 2614 | 5 | +| depositTo | 22537 | 23870 | 24537 | 24537 | 3 | +| getUserOpHash | 2178 | 2181 | 2178 | 2187 | 3 | +| handleOps | 67556 | 114566 | 128637 | 147506 | 3 | +| innerHandleOp | 33718 | 43164 | 43164 | 52611 | 2 | +| receive | 22127 | 22127 | 22127 | 22127 | 3 | +| withdrawTo | 36996 | 36996 | 36996 | 36996 | 1 | + + | lib/account-abstraction/contracts/samples/SimpleAccount.sol:SimpleAccount contract | | | | | | |------------------------------------------------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | @@ -19,33 +33,109 @@ Generated via `bash utils/inspect.sh`. | Deployment Cost | Deployment Size | | | | | | 120671 | 1228 | | | | | | Function Name | min | avg | median | max | # calls | -| entryPoint | 723 | 723 | 723 | 723 | 1 | -| owner | 826 | 826 | 826 | 826 | 1 | -| transferOwnership | 7710 | 10026 | 10026 | 12342 | 2 | -| upgradeToAndCall | 8145 | 25133 | 25133 | 42122 | 2 | +| addDeposit | 30212 | 36212 | 39212 | 39212 | 3 | +| disableInitializers | 11105 | 11105 | 11105 | 11105 | 1 | +| entryPoint | 723 | 2984 | 2984 | 5245 | 2 | +| execute | 7455 | 16650 | 8658 | 32984 | 5 | +| executeBatch | 7794 | 20627 | 20627 | 33460 | 2 | +| getDeposit | 1825 | 4825 | 1825 | 10825 | 3 | +| getInitializedVersion | 677 | 677 | 677 | 677 | 1 | +| isValidSignature | 11229 | 12909 | 12909 | 14589 | 2 | +| owner | 826 | 826 | 826 | 826 | 2 | +| transferOwnership | 5863 | 8281 | 7744 | 12342 | 5 | +| upgradeToAndCall | 4786 | 17805 | 13316 | 42122 | 7 | +| validateUserOp | 40706 | 41828 | 40712 | 44066 | 3 | +| withdrawDepositTo | 2978 | 21626 | 21626 | 40275 | 2 | | src/LightAccount.sol:LightAccount contract | | | | | | |--------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | -| 1634741 | 8506 | | | | | +| 1661569 | 8640 | | | | | | Function Name | min | avg | median | max | # calls | -| initialize | 49609 | 49609 | 49609 | 49609 | 5 | -| owner | 510 | 510 | 510 | 510 | 1 | -| transferOwnership | 2878 | 5202 | 5202 | 7526 | 2 | +| addDeposit | 29899 | 32899 | 34399 | 34399 | 3 | +| entryPoint | 429 | 429 | 429 | 429 | 1 | +| execute | 2950 | 13606 | 7112 | 28147 | 5 | +| executeBatch | 2920 | 15762 | 15762 | 28605 | 2 | +| getDeposit | 1509 | 3009 | 1509 | 6009 | 3 | +| initialize | 49609 | 49609 | 49609 | 49609 | 25 | +| isValidSignature | 6383 | 8063 | 8063 | 9743 | 2 | +| owner | 510 | 510 | 510 | 510 | 2 | +| transferOwnership | 2878 | 4355 | 2912 | 7526 | 5 | | upgradeToAndCall | 3295 | 20290 | 20290 | 37285 | 2 | +| validateUserOp | 35733 | 36853 | 35733 | 39093 | 3 | +| withdrawDepositTo | 2643 | 21299 | 21299 | 39956 | 2 | | src/LightAccountFactory.sol:LightAccountFactory contract | | | | | | |----------------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 2127724 | 11008 | | | | | +| 2154580 | 11142 | | | | | | Function Name | min | avg | median | max | # calls | -| createAccount | 161027 | 161027 | 161027 | 161027 | 5 | +| createAccount | 4821 | 155115 | 161027 | 163527 | 26 | +| getAddress | 4672 | 4672 | 4672 | 4672 | 1 | + + +| test/CustomSlotInitializable.t.sol:DisablesInitializersWhileInitializing contract | | | | | | +|-----------------------------------------------------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 156238 | 852 | | | | | +| Function Name | min | avg | median | max | # calls | +| initialize | 22755 | 22755 | 22755 | 22755 | 1 | + + +| test/CustomSlotInitializable.t.sol:IsInitializingChecker contract | | | | | | +|-------------------------------------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 154653 | 851 | | | | | +| Function Name | min | avg | median | max | # calls | +| initialize | 46069 | 46069 | 46069 | 46069 | 1 | +| isInitializing | 303 | 303 | 303 | 303 | 1 | +| wasInitializing | 267 | 267 | 267 | 267 | 1 | + + +| test/CustomSlotInitializable.t.sol:V1 contract | | | | | | +|------------------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 762520 | 3920 | | | | | +| Function Name | min | avg | median | max | # calls | +| disableInitializers | 6292 | 6292 | 6292 | 6292 | 1 | +| getInitializedVersion | 391 | 391 | 391 | 391 | 1 | +| initialize | 684 | 19455 | 23898 | 23898 | 10 | +| proxiableUUID | 273 | 273 | 273 | 273 | 2 | +| upgradeToAndCall | 8469 | 13681 | 14454 | 17349 | 4 | + + +| test/CustomSlotInitializable.t.sol:V2 contract | | | | | | +|------------------------------------------------|-----------------|------|--------|------|---------| +| Deployment Cost | Deployment Size | | | | | +| 703047 | 3616 | | | | | +| Function Name | min | avg | median | max | # calls | +| getInitializedVersion | 361 | 361 | 361 | 361 | 1 | +| initialize | 504 | 4410 | 6363 | 6363 | 3 | +| proxiableUUID | 243 | 243 | 243 | 243 | 3 | +| upgradeToAndCall | 3552 | 3552 | 3552 | 3552 | 1 | + + +| test/LightAccount.t.sol:LightSwitch contract | | | | | | +|----------------------------------------------|-----------------|-------|--------|-------|---------| +| Deployment Cost | Deployment Size | | | | | +| 52111 | 288 | | | | | +| Function Name | min | avg | median | max | # calls | +| on | 267 | 267 | 267 | 267 | 3 | +| turnOn | 22274 | 22274 | 22274 | 22274 | 3 | + + +| test/LightAccount.t.sol:Reverter contract | | | | | | +|-------------------------------------------|-----------------|-----|--------|-----|---------| +| Deployment Cost | Deployment Size | | | | | +| 46905 | 261 | | | | | +| Function Name | min | avg | median | max | # calls | +| doRevert | 201 | 201 | 201 | 201 | 1 | -Ran 1 test suites: 5 tests passed, 0 failed, 0 skipped (5 total tests) +Ran 3 test suites: 31 tests passed, 0 failed, 0 skipped (31 total tests) `forge inspect src/CustomSlotInitializable.sol:CustomSlotInitializable gasestimates` ```json null @@ -55,7 +145,7 @@ null ```json { "creation": { - "codeDepositCost": "1609400", + "codeDepositCost": "1636200", "executionCost": "infinite", "totalCost": "infinite" }, @@ -86,7 +176,6 @@ null "_call(address,uint256,bytes memory)": "infinite", "_getStorage()": "infinite", "_initialize(address)": "infinite", - "_isValidSignature(bytes32,bytes memory)": "infinite", "_onlyOwner()": "infinite", "_requireFromEntryPointOrOwner()": "infinite", "_transferOwnership(address)": "27881", diff --git a/src/CustomSlotInitializable.sol b/src/CustomSlotInitializable.sol index 6eadaf3..6de945a 100644 --- a/src/CustomSlotInitializable.sol +++ b/src/CustomSlotInitializable.sol @@ -82,13 +82,6 @@ abstract contract CustomSlotInitializable { _storagePosition = storagePosition; } - function _getInitialiazableStorage() private view returns (CustomSlotInitializableStorage storage _storage) { - bytes32 position = _storagePosition; - assembly { - _storage.slot := position - } - } - /** * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, * `onlyInitializing` functions can be used to initialize parent contracts. @@ -186,4 +179,11 @@ abstract contract CustomSlotInitializable { function _isInitializing() internal view returns (bool) { return _getInitialiazableStorage().initializing; } + + function _getInitialiazableStorage() private view returns (CustomSlotInitializableStorage storage _storage) { + bytes32 position = _storagePosition; + assembly { + _storage.slot := position + } + } } diff --git a/test/CustomSlotInitializable.t.sol b/test/CustomSlotInitializable.t.sol new file mode 100644 index 0000000..d67f516 --- /dev/null +++ b/test/CustomSlotInitializable.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; + +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +import {CustomSlotInitializable} from "../src/CustomSlotInitializable.sol"; + +contract CustomSlotInitializableTest is Test { + using stdStorage for StdStorage; + + event Initialized(uint8 version); + + address v1Impl; + address v2Impl; + V1 v1Proxy; + + function setUp() public { + v1Impl = address(new V1()); + v2Impl = address(new V2()); + v1Proxy = V1(address(new ERC1967Proxy(v1Impl, abi.encodeCall(V1.initialize, ())))); + } + + function testSimpleInitialization() public { + V1 v1 = new V1(); + vm.expectEmit(false, false, false, true); + emit Initialized(1); + v1.initialize(); + assertEq(v1.getInitializedVersion(), 1); + } + + function testUpgrade() public { + vm.expectEmit(false, false, false, true); + emit Initialized(2); + v1Proxy.upgradeToAndCall(v2Impl, abi.encodeCall(V2.initialize, ())); + V2 v2Proxy = V2(address(v1Proxy)); + assertEq(v2Proxy.getInitializedVersion(), 2); + } + + function testCannotReinitialize() public { + vm.expectRevert(bytes("Initializable: contract is already initialized")); + v1Proxy.upgradeToAndCall(v1Impl, abi.encodeCall(V1.initialize, ())); + } + + function testCannotUpgradeBackwards() public { + v1Proxy.upgradeToAndCall(v2Impl, abi.encodeCall(V2.initialize, ())); + V2 v2Proxy = V2(address(v1Proxy)); + vm.expectRevert(bytes("Initializable: contract is already initialized")); + v2Proxy.upgradeToAndCall(v1Impl, abi.encodeCall(V1.initialize, ())); + } + + function testDisableInitializers() public { + v1Proxy.disableInitializers(); + vm.expectRevert(bytes("Initializable: contract is already initialized")); + v1Proxy.upgradeToAndCall(v2Impl, abi.encodeCall(V2.initialize, ())); + } + + function testCannotCallDisableInitializersInInitializer() public { + DisablesInitializersWhileInitializing account = new DisablesInitializersWhileInitializing(); + vm.expectRevert("Initializable: contract is initializing"); + account.initialize(); + } + + function testIsInitializing() public { + IsInitializingChecker checker = new IsInitializingChecker(); + checker.initialize(); + assertTrue(checker.wasInitializing()); + assertFalse(checker.isInitializing()); + } +} + +contract V1 is CustomSlotInitializable(keccak256("storage")), UUPSUpgradeable { + function initialize() public initializer {} + + function getInitializedVersion() public view returns (uint8) { + return _getInitializedVersion(); + } + + function disableInitializers() public { + _disableInitializers(); + } + + function _authorizeUpgrade(address newImplementation) internal pure override { + (newImplementation); + } +} + +contract V2 is CustomSlotInitializable(keccak256("storage")), UUPSUpgradeable { + function initialize() public reinitializer(2) {} + + function getInitializedVersion() public view returns (uint8) { + return _getInitializedVersion(); + } + + function _authorizeUpgrade(address newImplementation) internal pure override { + (newImplementation); + } +} + +contract DisablesInitializersWhileInitializing is CustomSlotInitializable(keccak256("storage")) { + function initialize() public initializer { + _disableInitializers(); + } +} + +contract IsInitializingChecker is CustomSlotInitializable(keccak256("storage")) { + bool public wasInitializing; + + function initialize() public initializer { + wasInitializing = _isInitializing(); + } + + function isInitializing() public view returns (bool) { + return _isInitializing(); + } +} diff --git a/test/LightAccount.t.sol b/test/LightAccount.t.sol index b477087..c21c137 100644 --- a/test/LightAccount.t.sol +++ b/test/LightAccount.t.sol @@ -3,48 +3,205 @@ pragma solidity ^0.8.21; import "forge-std/Test.sol"; -import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "account-abstraction/samples/SimpleAccount.sol"; +import {EntryPoint} from "account-abstraction/core/EntryPoint.sol"; +import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; +import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol"; +import {SimpleAccount} from "account-abstraction/samples/SimpleAccount.sol"; -import "../src/LightAccount.sol"; -import "../src/LightAccountFactory.sol"; +import {LightAccount} from "../src/LightAccount.sol"; +import {LightAccountFactory} from "../src/LightAccountFactory.sol"; contract LightAccountTest is Test { using stdStorage for StdStorage; + using ECDSA for bytes32; - IEntryPoint public constant ENTRY_POINT_ADDRESS = IEntryPoint(address(0x1000)); - + uint256 public constant EOA_PRIVATE_KEY = 1; + address payable public constant BENEFICIARY = payable(address(0xbe9ef1c1a2ee)); + address public eoaAddress; LightAccount public account; + LightAccount public contractOwnedAccount; + EntryPoint public entryPoint; + LightSwitch public lightSwitch; + Owner public contractOwner; event SimpleAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event Initialized(uint8 version); function setUp() public { - LightAccountFactory factory = new LightAccountFactory(ENTRY_POINT_ADDRESS); - account = factory.createAccount(address(this), 1); + eoaAddress = vm.addr(EOA_PRIVATE_KEY); + entryPoint = new EntryPoint(); + LightAccountFactory factory = new LightAccountFactory(entryPoint); + account = factory.createAccount(eoaAddress, 1); + vm.deal(address(account), 1 << 128); + lightSwitch = new LightSwitch(); + contractOwner = new Owner(); + } + + function testExecuteCanBeCalledByOwner() public { + vm.prank(eoaAddress); + account.execute(address(lightSwitch), 0, abi.encodeCall(LightSwitch.turnOn, ())); + assertTrue(lightSwitch.on()); + } + + function testExecuteCanBeCalledByEntryPointWithExternalOwner() public { + UserOperation memory op = + _getSignedOp(address(lightSwitch), abi.encodeCall(LightSwitch.turnOn, ()), EOA_PRIVATE_KEY); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, BENEFICIARY); + assertTrue(lightSwitch.on()); + } + + function testExecutedCanBeCalledByEntryPointWithContractOwner() public { + _useContractOwner(); + UserOperation memory op = _getUnsignedOp(address(lightSwitch), abi.encodeCall(LightSwitch.turnOn, ())); + op.signature = contractOwner.sign(entryPoint.getUserOpHash(op)); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + entryPoint.handleOps(ops, BENEFICIARY); + assertTrue(lightSwitch.on()); + } + + function testRejectsUserOpsWithInvalidSignature() public { + UserOperation memory op = _getSignedOp(address(lightSwitch), abi.encodeCall(LightSwitch.turnOn, ()), 1234); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, 0, "AA24 signature error")); + entryPoint.handleOps(ops, BENEFICIARY); + } + + function testExecuteCannotBeCalledByRandos() public { + vm.expectRevert(bytes("account: not Owner or EntryPoint")); + account.execute(address(lightSwitch), 0, abi.encodeCall(LightSwitch.turnOn, ())); + } + + function testExecuteRevertingCallShouldRevertWithSameData() public { + Reverter reverter = new Reverter(); + vm.prank(eoaAddress); + vm.expectRevert("did revert"); + account.execute(address(reverter), 0, abi.encodeCall(Reverter.doRevert, ())); + } + + function testExecuteBatchCalledByOwner() public { + vm.prank(eoaAddress); + address[] memory dest = new address[](1); + dest[0] = address(lightSwitch); + bytes[] memory func = new bytes[](1); + func[0] = abi.encodeCall(LightSwitch.turnOn, ()); + account.executeBatch(dest, func); + assertTrue(lightSwitch.on()); + } + + function testExecuteBatchFailsForUnevenInputArrays() public { + vm.prank(eoaAddress); + address[] memory dest = new address[](2); + dest[0] = address(lightSwitch); + dest[1] = address(lightSwitch); + bytes[] memory func = new bytes[](1); + func[0] = abi.encodeCall(LightSwitch.turnOn, ()); + vm.expectRevert(bytes("wrong array lengths")); + account.executeBatch(dest, func); + } + + function testInitialize() public { + LightAccountFactory factory = new LightAccountFactory(entryPoint); + vm.expectEmit(true, false, false, false); + emit Initialized(0); + account = factory.createAccount(eoaAddress, 1); + } + + function testAddDeposit() public { + assertEq(account.getDeposit(), 0); + account.addDeposit{value: 10}(); + assertEq(account.getDeposit(), 10); + assertEq(account.getDeposit(), entryPoint.balanceOf(address(account))); + } + + function testWithdrawDepositToCalledByOwner() public { + account.addDeposit{value: 10}(); + vm.prank(eoaAddress); + account.withdrawDepositTo(BENEFICIARY, 5); + assertEq(entryPoint.balanceOf(address(account)), 5); + } + + function testWithdrawDepositToCannotBeCalledByRandos() public { + account.addDeposit{value: 10}(); + vm.expectRevert(bytes("only owner")); + account.withdrawDepositTo(BENEFICIARY, 5); } function testOwnerCanTransferOwnership() public { address newOwner = address(0x100); + vm.prank(eoaAddress); vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(address(this), newOwner); + emit OwnershipTransferred(eoaAddress, newOwner); account.transferOwnership(newOwner); assertEq(account.owner(), newOwner); } - function testNonOwnerCannotTransferOwnership() public { - vm.prank(address(0x100)); - vm.expectRevert(); + function testEntryPointCanTransferOwnership() public { + address newOwner = address(0x100); + UserOperation memory op = + _getSignedOp(address(account), abi.encodeCall(LightAccount.transferOwnership, (newOwner)), EOA_PRIVATE_KEY); + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + vm.expectEmit(true, true, false, false); + emit OwnershipTransferred(eoaAddress, newOwner); + entryPoint.handleOps(ops, BENEFICIARY); + assertEq(account.owner(), newOwner); + } + + function testRandosCannotTransferOwnership() public { + vm.expectRevert(bytes("only owner")); account.transferOwnership(address(0x100)); } + function testCannotTransferOwnershipToZero() public { + vm.prank(eoaAddress); + vm.expectRevert(bytes("account: new owner is the zero address")); + account.transferOwnership(address(0)); + } + + function testCannotTransferOwnershipToLightContractItself() public { + vm.prank(eoaAddress); + vm.expectRevert(bytes("account: new owner is self")); + account.transferOwnership(address(account)); + } + + function testEntryPointGetter() public { + assertEq(address(account.entryPoint()), address(entryPoint)); + } + + function testIsValidSignatureForEoaOwner() public { + bytes32 digest = keccak256("digest"); + bytes memory signature = _sign(EOA_PRIVATE_KEY, digest); + assertEq(account.isValidSignature(digest, signature), bytes4(keccak256("isValidSignature(bytes32,bytes)"))); + } + + function testIsValidSignatureForContractOwner() public { + _useContractOwner(); + bytes32 digest = keccak256("digest"); + bytes memory signature = contractOwner.sign(digest); + assertEq(account.isValidSignature(digest, signature), bytes4(keccak256("isValidSignature(bytes32,bytes)"))); + } + + function testIsValidSignatureRejectsInvalid() public { + bytes32 digest = keccak256("digest"); + bytes memory signature = _sign(123, digest); + assertEq(account.isValidSignature(digest, signature), bytes4(0xffffffff)); + } + function testOwnerCanUpgrade() public { // Upgrade to a normal SimpleAccount with a different entry point. IEntryPoint newEntryPoint = IEntryPoint(address(0x2000)); SimpleAccount newImplementation = new SimpleAccount(newEntryPoint); vm.expectEmit(true, true, false, false); emit SimpleAccountInitialized(newEntryPoint, address(this)); + vm.prank(eoaAddress); account.upgradeToAndCall(address(newImplementation), abi.encodeCall(SimpleAccount.initialize, (address(this)))); SimpleAccount upgradedAccount = SimpleAccount(payable(account)); assertEq(address(upgradedAccount.entryPoint()), address(newEntryPoint)); @@ -54,8 +211,7 @@ contract LightAccountTest is Test { // Try to upgrade to a normal SimpleAccount with a different entry point. IEntryPoint newEntryPoint = IEntryPoint(address(0x2000)); SimpleAccount newImplementation = new SimpleAccount(newEntryPoint); - vm.prank(address(0)); - vm.expectRevert(); + vm.expectRevert(bytes("only owner")); account.upgradeToAndCall(address(newImplementation), abi.encodeCall(SimpleAccount.initialize, (address(this)))); } @@ -68,11 +224,74 @@ contract LightAccountTest is Test { bytes32 accountSlot = keccak256(abi.encode(uint256(keccak256("light_account_v1.storage")) - 1)) & ~bytes32(uint256(0xff)); address owner = abi.decode(abi.encode(vm.load(address(account), accountSlot)), (address)); - assertEq(owner, address(this)); + assertEq(owner, eoaAddress); bytes32 initializableSlot = keccak256(abi.encode(uint256(keccak256("light_account_v1.initializable")) - 1)) & ~bytes32(uint256(0xff)); uint8 initialized = abi.decode(abi.encode(vm.load(address(account), initializableSlot)), (uint8)); assertEq(initialized, 1); } + + function _useContractOwner() internal { + vm.prank(eoaAddress); + account.transferOwnership(address(contractOwner)); + } + + function _getUnsignedOp(address target, bytes memory innerCallData) internal view returns (UserOperation memory) { + return UserOperation({ + sender: address(account), + nonce: 0, + initCode: "", + callData: abi.encodeCall(LightAccount.execute, (target, 0, innerCallData)), + callGasLimit: 1 << 24, + verificationGasLimit: 1 << 24, + preVerificationGas: 1 << 24, + maxFeePerGas: 1 << 8, + maxPriorityFeePerGas: 1 << 8, + paymasterAndData: "", + signature: "" + }); + } + + function _getSignedOp(address target, bytes memory innerCallData, uint256 privateKey) + internal + view + returns (UserOperation memory) + { + UserOperation memory op = _getUnsignedOp(target, innerCallData); + op.signature = _sign(privateKey, entryPoint.getUserOpHash(op).toEthSignedMessageHash()); + return op; + } + + function _sign(uint256 privateKey, bytes32 digest) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return abi.encodePacked(r, s, v); + } +} + +contract LightSwitch { + bool public on; + + function turnOn() external { + on = true; + } +} + +contract Reverter { + function doRevert() external pure { + revert("did revert"); + } +} + +contract Owner is IERC1271 { + function sign(bytes32 digest) public pure returns (bytes memory) { + return abi.encodePacked("Signed: ", digest); + } + + function isValidSignature(bytes32 digest, bytes memory signature) public pure override returns (bytes4) { + if (keccak256(signature) == keccak256(sign(digest))) { + return bytes4(keccak256("isValidSignature(bytes32,bytes)")); + } + return 0xffffffff; + } } diff --git a/test/LightAccountFactory.t.sol b/test/LightAccountFactory.t.sol new file mode 100644 index 0000000..ec019dd --- /dev/null +++ b/test/LightAccountFactory.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; + +import {EntryPoint} from "account-abstraction/core/EntryPoint.sol"; + +import {LightAccount} from "../src/LightAccount.sol"; +import {LightAccountFactory} from "../src/LightAccountFactory.sol"; + +contract LightAccountTest is Test { + using stdStorage for StdStorage; + + address public constant OWNER_ADDRESS = address(0x100); + LightAccountFactory public factory; + EntryPoint public entryPoint; + + function setUp() public { + entryPoint = new EntryPoint(); + factory = new LightAccountFactory(entryPoint); + } + + function testReturnsAddressWhenAccountAlreadyExists() public { + LightAccount account = factory.createAccount(OWNER_ADDRESS, 1); + LightAccount otherAccount = factory.createAccount(OWNER_ADDRESS, 1); + assertEq(address(account), address(otherAccount)); + } + + function testGetAddress() public { + address counterfactual = factory.getAddress(OWNER_ADDRESS, 1); + assertEq(counterfactual.codehash, bytes32(0)); + LightAccount factual = factory.createAccount(OWNER_ADDRESS, 1); + assertTrue(address(factual).codehash != bytes32(0)); + assertEq(counterfactual, address(factual)); + } +}