Skip to content

Commit

Permalink
draft without proofreading/edits
Browse files Browse the repository at this point in the history
  • Loading branch information
nikbhintade committed Sep 28, 2024
1 parent 4ef7d93 commit 2fe8a51
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 5 deletions.
107 changes: 106 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,109 @@ Right now we only have to think about and know what only 2 fields: calldata, and

### validateUserOp Function

validateUserOp function validates the UserOperation and some extra things but as we have seen in `BaseAccount` that is already implemented and part of it which validates the signature i.e. `_validateSignature` function is remaining and expected to be implemented by the developer of the account contract.
`validateUserOp` function validates the UserOperation and some extra things but as we have seen in `BaseAccount` that is already implemented and part of it which validates the signature i.e. `_validateSignature` function is remaining and expected to be implemented by the developer of the account contract.

We are going to use ECDSA signature to verify the UserOperation is signed by the owner of the account contract. If signer and owner is same, we return `SIG_VALIDATION_SUCCESS` else `SIG_VALIDATION_FAILED`.

To verify ECDSA signature we can use ecrecover but using library from openzeppelin is better way to do it as it removes lot of extra work that we might had to do when using ecrecover directly. We will install openzeppelin and then start with writing the function.

```bash
forge install OpenZeppelin/[email protected] --no-commit
```

You should see following message in the terminal if OpenZeppelin is installed correctly.

```bash
Installed openzeppelin-contracts v5.0.2
```

Now we can write `_validateSignature`, the function signature for this function is already defined let’s take that and override it.

```solidity
function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash)
internal
view
override
returns (uint256) {}
```

First step is to create the signed message hash using userOpHash which will be used to get address of signer. To get the address we are going to use recover function which takes signature and hashed message.

```solidity
bytes32 digest = MessageHashUtils.toEthSignedMessageHash(userOpHash);
address messageSigner = ECDSA.recover(digest, userOp.signature);
```

Now that we have `messageSigner`. We can compare that with `i_owner` if they are same then we will return `SIG_VALIDATION_SUCCESS`, if not then `SIG_VALIDATION_FAILED`.

```solidity
if (messageSigner == i_owner) {
return SIG_VALIDATION_SUCCESS;
} else {
return SIG_VALIDATION_FAILED;
}
```

At end the `_validateSignature` function will look like following.

```solidity
function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash)
internal
view
override
returns (uint256)
{
bytes32 digest = MessageHashUtils.toEthSignedMessageHash(userOpHash);
address messageSigner = ECDSA.recover(digest, userOp.signature);
if (messageSigner == i_owner) {
return SIG_VALIDATION_SUCCESS;
} else {
return SIG_VALIDATION_FAILED;
}
}
```

### execute Function

This function will be for account contract to interact with other accounts and contracts. The name of the this function doesn’t matter as the `EntryPoint` contract directly executes calldata. When sending UserOperation, the user or dapp user has been using need to construct that calldata.

For this contract, the function will be called execute and will take address of contract/account to be called, value to be sent with call and calldata for the call as the arguments.

```solidity
function execute(address dest, uint256 value, bytes calldata funcCallData) external {}
```

As this function can sent assets and perform action on behalf of the contract, we need to restrict access to this function. To achieve this we will use function that is available in `BaseAccount`, **`_requireFromEntryPoint`.**

Then just do the call and check if call succeeded, if fails we will revert with a `SimpleAccount__CallFailed` error which we will define at the top of the contract.

```solidity
error SimpleAccount__CallFailed();
```

Let’s write rest of the function. Here is how it looks like.

```solidity
function execute(address dest, uint256 value, bytes calldata funcCallData) external {
_requireFromEntryPoint();
(bool success, bytes memory result) = dest.call{value: value}(funcCallData);
if (!success) {
revert SimpleAccount__CallFailed(result);
}
}
```

With this function finished, we have completed our simple account which is ERC-4337 compliant and can be used with `EntryPoint` and any bundler.

## Final Thoughts

We covered some of the concept that we needed to understand to develop a simple account contract that satisfies requirements of ERC-4337. This article also walks through each function in that contract and libraries which we use like account-abstraction from eth-infinitism and ECDSA from OpenZeppelin.

We also covered a bunch of concepts from ERC-4337 like bundlers, entrypoint, etc. We still don’t know a lot about them except for basics but let’s go over them as we need them.

## Next Steps

In the next article, we will test this contract and learn more intersting things about UserOperation like how to generate UserOperation hash via endpoint. If you want you can start writing tests right now and try to see if our contract is working as except.

I hope you liked this article and will follow the rest of the articles in this series.
6 changes: 3 additions & 3 deletions src/SimpleAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract SimpleAccount is BaseAccount {
error SimpleAccount__NotFromEntryPoint();
error SimpleAccount__CallFailed(bytes result);
error SimpleAccount__CallFailed();

IEntryPoint private immutable i_entryPoint;
address private immutable i_owner;
Expand Down Expand Up @@ -39,9 +39,9 @@ contract SimpleAccount is BaseAccount {

function execute(address dest, uint256 value, bytes calldata funcCallData) external {
_requireFromEntryPoint();
(bool success, bytes memory result) = dest.call{value: value}(funcCallData);
(bool success,) = dest.call{value: value}(funcCallData);
if (!success) {
revert SimpleAccount__CallFailed(result);
revert SimpleAccount__CallFailed();
}
}

Expand Down
2 changes: 1 addition & 1 deletion test/SimpleAccount.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ contract SimpleAccountTest is Test {
uint256 valueToSend = 1 ether;

vm.prank(address(entryPoint));
vm.expectRevert(abi.encodeWithSelector(SimpleAccount.SimpleAccount__CallFailed.selector, bytes("")));
vm.expectRevert(SimpleAccount.SimpleAccount__CallFailed.selector);

simpleAccountHarness.execute(address(failTestContract), valueToSend, "");
}
Expand Down

0 comments on commit 2fe8a51

Please sign in to comment.