Skip to content

Commit

Permalink
Feat/improve fix tests (#75)
Browse files Browse the repository at this point in the history
* add tests validate user operation from entry point

* add accesscontrol+unauthorized access tests

* refactor: add InvalidSignature error to EventsAndErrors.sol

* add tests cases + fix wrong asserts

* chore: Update deployment account funding amount in test

* Remove unnecessary code in TestERC4337Account_ValidateUserOp.t.sol

* add test for validation of user operation nonce

* Merge branch 'dev' of https://github.com/bcnmy/erc7579-modular-smart-account into dev

* ✏️ add comments on tests (validateUserOp)
  • Loading branch information
Aboudjem authored May 17, 2024
1 parent 592b025 commit 6a0ff5d
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ contract TestERC4337Account_addDeposit is Test, SmartAccountTestLab {
function test_AddDeposit_Success() public {
uint256 depositBefore = ENTRYPOINT.balanceOf(address(account));
account.addDeposit{ value: defaultDepositAmount }();
assertEq(
depositBefore + defaultDepositAmount,
ENTRYPOINT.balanceOf(address(account)),
"Deposit should be added to EntryPoint"
);
assertEq(depositBefore + defaultDepositAmount, ENTRYPOINT.balanceOf(address(account)), "Deposit should be added to EntryPoint");
}

function test_AddDeposit_EventEmitted() public {
Expand All @@ -45,26 +41,20 @@ contract TestERC4337Account_addDeposit is Test, SmartAccountTestLab {
_prefundSmartAccountAndAssertSuccess(address(account), defaultDepositAmount + 1 ether);
uint256 depositBefore = ENTRYPOINT.balanceOf(address(account));

Execution[] memory executions =
_prepareSingleExecution(address(account), defaultDepositAmount, abi.encodeWithSignature("addDeposit()"));
Execution[] memory executions = _prepareSingleExecution(address(account), defaultDepositAmount, abi.encodeWithSignature("addDeposit()"));
PackedUserOperation[] memory userOps = preparePackedUserOperation(BOB, account, EXECTYPE_DEFAULT, executions);
uint256 gasUsed = handleUserOpAndMeasureGas(userOps, BOB.addr);

// Using almostEq to compare balances with a tolerance for gas costs
almostEq(
depositBefore + defaultDepositAmount - (gasUsed * tx.gasprice),
ENTRYPOINT.balanceOf(address(account)),
defaultMaxPercentDelta
);
almostEq(depositBefore + defaultDepositAmount - (gasUsed * tx.gasprice), ENTRYPOINT.balanceOf(address(account)), defaultMaxPercentDelta);
}

function test_AddDeposit_BatchDepositViaHandleOps() public {
uint256 executionsNumber = 5;
_prefundSmartAccountAndAssertSuccess(address(account), defaultDepositAmount * 10);
uint256 depositBefore = ENTRYPOINT.balanceOf(address(account));

Execution memory execution =
Execution(address(account), defaultDepositAmount, abi.encodeWithSignature("addDeposit()"));
Execution memory execution = Execution(address(account), defaultDepositAmount, abi.encodeWithSignature("addDeposit()"));
Execution[] memory executions = _prepareSeveralIdenticalExecutions(execution, executionsNumber);
PackedUserOperation[] memory userOps = preparePackedUserOperation(BOB, account, EXECTYPE_DEFAULT, executions);
uint256 gasUsed = handleUserOpAndMeasureGas(userOps, BOB.addr);
Expand All @@ -79,25 +69,19 @@ contract TestERC4337Account_addDeposit is Test, SmartAccountTestLab {
_prefundSmartAccountAndAssertSuccess(address(account), defaultDepositAmount + 1 ether);
uint256 depositBefore = ENTRYPOINT.balanceOf(address(account));

Execution[] memory executions =
_prepareSingleExecution(address(account), defaultDepositAmount, abi.encodeWithSignature("addDeposit()"));
Execution[] memory executions = _prepareSingleExecution(address(account), defaultDepositAmount, abi.encodeWithSignature("addDeposit()"));
PackedUserOperation[] memory userOps = preparePackedUserOperation(BOB, account, EXECTYPE_TRY, executions);
uint256 gasUsed = handleUserOpAndMeasureGas(userOps, BOB.addr);

almostEq(
depositBefore + defaultDepositAmount - (gasUsed * tx.gasprice),
ENTRYPOINT.balanceOf(address(account)),
defaultMaxPercentDelta
);
almostEq(depositBefore + defaultDepositAmount - (gasUsed * tx.gasprice), ENTRYPOINT.balanceOf(address(account)), defaultMaxPercentDelta);
}

function test_AddDeposit_Try_BatchDepositViaHandleOps() public {
_prefundSmartAccountAndAssertSuccess(address(account), defaultDepositAmount * 10);
uint256 depositBefore = ENTRYPOINT.balanceOf(address(account));
uint256 executionsNumber = 5;

Execution memory execution =
Execution(address(account), defaultDepositAmount, abi.encodeWithSignature("addDeposit()"));
Execution memory execution = Execution(address(account), defaultDepositAmount, abi.encodeWithSignature("addDeposit()"));
Execution[] memory executions = _prepareSeveralIdenticalExecutions(execution, executionsNumber);
PackedUserOperation[] memory userOps = preparePackedUserOperation(BOB, account, EXECTYPE_TRY, executions);
uint256 gasUsed = handleUserOpAndMeasureGas(userOps, BOB.addr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,14 @@ contract TestERC4337Account_Nonce is Test, SmartAccountTestLab {

function test_InitialNonce() public {
uint256 nonce = ENTRYPOINT.getNonce(address(BOB_ACCOUNT), makeNonceKeyFromAddress(address(VALIDATOR_MODULE)));
assertEq(
BOB_ACCOUNT.nonce(makeNonceKeyFromAddress(address(VALIDATOR_MODULE))),
nonce,
"Nonce in the account and EP should be same"
);
assertEq(BOB_ACCOUNT.nonce(makeNonceKeyFromAddress(address(VALIDATOR_MODULE))), nonce, "Nonce in the account and EP should be same");
}

function test_NonceIncrementAfterOperation() public {
uint256 initialNonce = BOB_ACCOUNT.nonce(makeNonceKeyFromAddress(address(VALIDATOR_MODULE)));
assertEq(counter.getNumber(), 0, "Counter should start at 0");

Execution[] memory executions =
_prepareSingleExecution(address(counter), 0, abi.encodeWithSelector(Counter.incrementNumber.selector));
Execution[] memory executions = _prepareSingleExecution(address(counter), 0, abi.encodeWithSelector(Counter.incrementNumber.selector));
PackedUserOperation[] memory userOps = preparePackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, executions);
ENTRYPOINT.handleOps(userOps, payable(BOB.addr));

Expand All @@ -40,8 +35,7 @@ contract TestERC4337Account_Nonce is Test, SmartAccountTestLab {
uint256 initialNonce = BOB_ACCOUNT.nonce(makeNonceKeyFromAddress(address(VALIDATOR_MODULE)));
assertEq(counter.getNumber(), 0, "Counter should start at 0");

Execution[] memory executions =
_prepareSingleExecution(address(counter), 0, abi.encodeWithSelector(Counter.revertOperation.selector));
Execution[] memory executions = _prepareSingleExecution(address(counter), 0, abi.encodeWithSelector(Counter.revertOperation.selector));

// Assuming the method should fail
PackedUserOperation[] memory userOps = preparePackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, executions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,66 @@ pragma solidity ^0.8.24;
import "../../../utils/Imports.sol";
import "../../../utils/SmartAccountTestLab.t.sol";

contract TestERC4337Account_ValidateUserOp is Test, SmartAccountTestLab {
/// @title Test suite for testing onlyEntryPoint modifier in Nexus contracts under ERC4337 standards.
contract TestERC4337Account_OnlyEntryPoint is Test, SmartAccountTestLab {
Nexus public account;
MockValidator public validator;
address public userAddress;

/// Setup environment for each test case
function setUp() public {
init();
BOB_ACCOUNT.addDeposit{ value: 1 ether }(); // Fund the account to cover potential transaction fees
}

function test_ValidateUserOp_ValidOperation() public {
// Initialize a user operation with a valid setup
/// Verifies that a valid operation passes validation when invoked from the EntryPoint.
function test_ValidUserOpValidation_FromEntryPoint() public {
// Arrange a valid user operation
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = buildPackedUserOp(userAddress, getNonce(address(BOB_ACCOUNT), address(VALIDATOR_MODULE)));
userOps[0] = buildPackedUserOp(BOB.addr, getNonce(address(BOB_ACCOUNT), address(VALIDATOR_MODULE)));
bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]);
userOps[0].signature = signMessage(BOB, userOpHash);
userOps[0].signature = signMessage(BOB, userOpHash); // Sign operation with valid signer

// Act: Validate the operation from the entry point, expecting it to succeed
startPrank(address(ENTRYPOINT));
// Attempt to validate the user operation, expecting success
uint256 res = BOB_ACCOUNT.validateUserOp(userOps[0], userOpHash, 10);
uint256 res = BOB_ACCOUNT.validateUserOp(userOps[0], userOpHash, 0);
stopPrank();

// Assert that the operation is validated successfully
assertTrue(res == 0, "Valid operation should pass validation");
}

/// Ensures that operations fail validation when invoked from an unauthorized sender.
function test_UserOpValidation_FailsFromNonEntryPoint() public {
// Setup a valid user operation, but simulate calling from a non-entry point address
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = buildPackedUserOp(userAddress, getNonce(address(BOB_ACCOUNT), address(VALIDATOR_MODULE)));
bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]);
userOps[0].signature = signMessage(BOB, userOpHash); // Still correctly signed

// Act: Attempt to validate the operation from a non-entry point address
startPrank(address(BOB_ACCOUNT));
vm.expectRevert(abi.encodeWithSelector(AccountAccessUnauthorized.selector));
uint256 res = BOB_ACCOUNT.validateUserOp(userOps[0], userOpHash, 0);
stopPrank();

// Assert that the operation fails validation due to incorrect sender
}

function test_ValidateUserOp_InvalidSignature() public {
startPrank(address(ENTRYPOINT));
// Initialize a user operation with a valid nonce but signed by an incorrect signer
/// Tests that the operation fails validation when the signature is invalid.
function test_UserOpValidation_FailsWithInvalidSignature() public {
// Arrange a user operation with incorrect signature
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = buildPackedUserOp(userAddress, getNonce(address(BOB_ACCOUNT), address(VALIDATOR_MODULE)));
bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]);
userOps[0].signature = signMessage(ALICE, userOpHash); // Incorrect signer simulated
userOps[0].signature = signMessage(ALICE, userOpHash); // Incorrect signer

// Act: Validate the operation from the entry point
startPrank(address(ENTRYPOINT));
// Attempt to validate the user operation, expecting failure due to invalid signature
uint256 res = BOB_ACCOUNT.validateUserOp(userOps[0], userOpHash, 0);
assertTrue(res == 1, "Operation with invalid signature should fail validation");
stopPrank();

// Assert that the operation fails validation due to invalid signature
assertTrue(res == 1, "Operation with invalid signature should fail validation");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,115 @@ pragma solidity ^0.8.24;
import "../../../utils/Imports.sol";
import "../../../utils/SmartAccountTestLab.t.sol";

contract TestERC4337Account_ValidateUserOp is Test, SmartAccountTestLab {
Nexus public account;
MockValidator public validator;
address public userAddress;

contract TestERC4337Account_OnlyEntryPointOrSelf is Test, SmartAccountTestLab {
function setUp() public {
init();
BOB_ACCOUNT.addDeposit{ value: 1 ether }(); // Ensure BOB_ACCOUNT has ether for operations requiring ETH transfers.
}

function test_ExecuteUserOp_Valid_FromEntryPoint() public {
Execution[] memory execution = new Execution[](1);
execution[0] = Execution(address(BOB_ACCOUNT), 0, "");
PackedUserOperation[] memory userOps = preparePackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, execution);
// Simulate EntryPoint processing the operation
ENTRYPOINT.handleOps(userOps, payable(BOB.addr));
}

function test_ValidateUserOp_ValidOperation() public {
// Initialize a user operation with a valid setup
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = buildPackedUserOp(userAddress, getNonce(address(BOB_ACCOUNT), address(VALIDATOR_MODULE)));
bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]);
userOps[0].signature = signMessage(BOB, userOpHash);
function test_ExecuteUserOp_Invalid_FromNonEntryPoint() public {
startPrank(ALICE.addr);
Execution[] memory execution = new Execution[](1);
execution[0] = Execution(address(BOB_ACCOUNT), 0, "");
// This should fail as ALICE is not the EntryPoint
PackedUserOperation[] memory userOps = preparePackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, execution);

vm.expectRevert(abi.encodeWithSelector(AccountAccessUnauthorized.selector));

// Attempt to validate the user operation, expecting success
BOB_ACCOUNT.executeUserOp(userOps[0], bytes32(0)); // Example usage, ensure this matches actual expected call
stopPrank();
}

function test_InstallModule_FromEntryPoint() public {
startPrank(address(ENTRYPOINT));
uint256 res = BOB_ACCOUNT.validateUserOp(userOps[0], userOpHash, 10);
assertTrue(res == 0, "Valid operation should pass validation");
// Attempt to install a module from the EntryPoint, should succeed
BOB_ACCOUNT.installModule(2, address(EXECUTOR_MODULE), "");
stopPrank();
}

function test_ValidateUserOp_InvalidSignature() public {
// Initialize a user operation with a valid nonce but signed by an incorrect signer
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = buildPackedUserOp(userAddress, getNonce(address(BOB_ACCOUNT), address(VALIDATOR_MODULE)));
bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]);
userOps[0].signature = signMessage(ALICE, userOpHash); // Incorrect signer simulated
function test_InstallModule_FromSelf() public {
// Install module from the account itself, should succeed
startPrank(address(BOB_ACCOUNT));
BOB_ACCOUNT.installModule(2, address(EXECUTOR_MODULE), "");
}

function test_UninstallModule_FromNonEntryPointOrSelf() public {
startPrank(ALICE.addr);
// This call should fail because ALICE is neither EntryPoint nor the BOB_ACCOUNT itself
vm.expectRevert(abi.encodeWithSelector(AccountAccessUnauthorized.selector));
BOB_ACCOUNT.uninstallModule(2, address(EXECUTOR_MODULE), new bytes(0));
stopPrank();
}

function test_WithdrawDeposit_ToAuthorizedAddress() public {
startPrank(address(ENTRYPOINT));
// Attempt to validate the user operation, expecting failure due to invalid signature
uint256 res = BOB_ACCOUNT.validateUserOp(userOps[0], userOpHash, 0);
assertTrue(res == 1, "Operation with invalid signature should fail validation");
// EntryPoint attempts to withdraw funds to a specified address, should succeed
BOB_ACCOUNT.withdrawDepositTo(BOB.addr, 0.5 ether);
stopPrank();
}

function test_WithdrawDeposit_FromSelf() public {
// The account itself initiates withdrawal, should succeed
startPrank(address(BOB_ACCOUNT));
BOB_ACCOUNT.withdrawDepositTo(BOB.addr, 0.5 ether);
}

function test_WithdrawDeposit_FromUnauthorizedAddress() public {
startPrank(ALICE.addr);
// Attempting to withdraw from an unauthorized address (not EntryPoint or self), should fail
vm.expectRevert(abi.encodeWithSelector(AccountAccessUnauthorized.selector));
BOB_ACCOUNT.withdrawDepositTo(BOB.addr, 0.5 ether);
stopPrank();
}

function test_ExecuteViaExecutor_WithdrawDepositTo() public {
// Install Executor module first
startPrank(address(ENTRYPOINT));
BOB_ACCOUNT.installModule(2, address(EXECUTOR_MODULE), "");
stopPrank();
uint256 depositBefore = BOB_ACCOUNT.getDeposit();
// Prepare the call data for the withdrawDepositTo function
bytes memory callData = abi.encodeWithSelector(BOB_ACCOUNT.withdrawDepositTo.selector, BOB.addr, 0.5 ether);

// Set up the Execution structure to use the installed executor module
Execution[] memory executions = new Execution[](1);
executions[0] = Execution(address(BOB_ACCOUNT), 0, callData);

// Use the executor module to perform the operation via BOB_ACCOUNT
EXECUTOR_MODULE.executeBatchViaAccount(BOB_ACCOUNT, executions);
uint256 depositAfter = BOB_ACCOUNT.getDeposit();

assertEq(depositAfter, depositBefore - 0.5 ether, "Deposit should be reduced by 0.5 ether");
}

function test_WithdrawDeposit_ToAuthorizedAddress_WithUserOps() public {
uint256 depositBefore = BOB_ACCOUNT.getDeposit();

Execution[] memory executions = new Execution[](1);
bytes memory callData = abi.encodeWithSelector(BOB_ACCOUNT.withdrawDepositTo.selector, BOB.addr, 0.5 ether);
executions[0] = Execution(address(BOB_ACCOUNT), 0, callData);

PackedUserOperation[] memory userOps = preparePackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, executions);
ENTRYPOINT.handleOps(userOps, payable(BOB.addr));

uint256 depositAfter = BOB_ACCOUNT.getDeposit();
assertApproxEqAbs(depositAfter, depositBefore - 0.5 ether, 0.0001 ether, "Deposit should be reduced by 0.5 ether");
}

function test_InstallModule_FromEntryPoint_WithUserOps() public {
Execution[] memory executions = new Execution[](1);
bytes memory callData = abi.encodeWithSelector(BOB_ACCOUNT.installModule.selector, 2, address(EXECUTOR_MODULE), "");
executions[0] = Execution(address(BOB_ACCOUNT), 0, callData);

PackedUserOperation[] memory userOps = preparePackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, executions);
ENTRYPOINT.handleOps(userOps, payable(BOB.addr));
}
}
Loading

0 comments on commit 6a0ff5d

Please sign in to comment.