├── .gitignore ├── README.md ├── contracts ├── Account.sol └── Paymaster.sol ├── hardhat.config.js ├── package-lock.json ├── package.json └── scripts ├── deploy.js ├── deposit.js ├── execute.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | # Hardhat files 5 | /cache 6 | /artifacts 7 | 8 | # TypeChain files 9 | /typechain 10 | /typechain-types 11 | 12 | # solidity-coverage files 13 | /coverage 14 | /coverage.json 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smart Accounts Hardhat Project 2 | 3 | This is the code built in the "Account Abstraction From Scratch Course": 4 | 5 | - Part 1: https://youtu.be/NM04uxcCOEw 6 | - Part 2: https://youtu.be/2LGpEobxIBA 7 | - Part 3: https://youtu.be/vj2gklqLRSA 8 | - Part 4: https://youtu.be/AY4jI0GXKBc 9 | -------------------------------------------------------------------------------- /contracts/Account.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | import "@account-abstraction/contracts/core/EntryPoint.sol"; 5 | import "@account-abstraction/contracts/interfaces/IAccount.sol"; 6 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 7 | import "@openzeppelin/contracts/utils/Create2.sol"; 8 | 9 | contract Account is IAccount { 10 | uint256 public count; 11 | address public owner; 12 | 13 | constructor(address _owner) { 14 | owner = _owner; 15 | } 16 | 17 | function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256) 18 | external 19 | view 20 | returns (uint256 validationData) 21 | { 22 | address recovered = ECDSA.recover(ECDSA.toEthSignedMessageHash(userOpHash), userOp.signature); 23 | 24 | return owner == recovered ? 0 : 1; 25 | } 26 | 27 | function execute() external { 28 | count++; 29 | } 30 | } 31 | 32 | contract AccountFactory { 33 | function createAccount(address owner) external returns (address) { 34 | bytes32 salt = bytes32(uint256(uint160(owner))); 35 | bytes memory creationCode = type(Account).creationCode; 36 | bytes memory bytecode = abi.encodePacked(creationCode, abi.encode(owner)); 37 | 38 | address addr = Create2.computeAddress(salt, keccak256(bytecode)); 39 | uint256 codeSize = addr.code.length; 40 | if (codeSize > 0) { 41 | return addr; 42 | } 43 | 44 | return deploy(salt, bytecode); 45 | } 46 | 47 | function deploy(bytes32 salt, bytes memory bytecode) internal returns (address addr) { 48 | require(bytecode.length != 0, "Create2: bytecode length is zero"); 49 | /// @solidity memory-safe-assembly 50 | assembly { 51 | addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) 52 | } 53 | require(addr != address(0), "Create2: Failed on deploy"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/Paymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | import "@account-abstraction/contracts/interfaces/IPaymaster.sol"; 5 | 6 | contract Paymaster is IPaymaster { 7 | function validatePaymasterUserOp(UserOperation calldata, bytes32, uint256) 8 | external 9 | pure 10 | returns (bytes memory context, uint256 validationData) 11 | { 12 | context = new bytes(0); 13 | validationData = 0; 14 | } 15 | 16 | /** 17 | * post-operation handler. 18 | * Must verify sender is the entryPoint 19 | * @param mode enum with the following options: 20 | * opSucceeded - user operation succeeded. 21 | * opReverted - user op reverted. still has to pay for gas. 22 | * postOpReverted - user op succeeded, but caused postOp (in mode=opSucceeded) to revert. 23 | * Now this is the 2nd call, after user's op was deliberately reverted. 24 | * @param context - the context value returned by validatePaymasterUserOp 25 | * @param actualGasCost - actual gas used so far (without this postOp call). 26 | */ 27 | function postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) external {} 28 | } 29 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox"); 2 | require("dotenv").config(); 3 | 4 | /** @type import('hardhat/config').HardhatUserConfig */ 5 | module.exports = { 6 | defaultNetwork: "arb", 7 | networks: { 8 | arb: { 9 | url: process.env.RPC_URL, 10 | accounts: [process.env.PRIVATE_KEY], 11 | }, 12 | }, 13 | solidity: { 14 | version: "0.8.19", 15 | settings: { 16 | optimizer: { 17 | enabled: true, 18 | runs: 1000, 19 | }, 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smart-accounts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@account-abstraction/contracts": "^0.6.0", 14 | "@openzeppelin/contracts": "^4.2.0", 15 | "dotenv": "^16.4.1", 16 | "hardhat": "^2.19.4" 17 | }, 18 | "devDependencies": { 19 | "@nomicfoundation/hardhat-toolbox": "^4.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | 3 | async function main() { 4 | const af = await hre.ethers.deployContract("AccountFactory"); 5 | 6 | await af.waitForDeployment(); 7 | 8 | console.log(`AF deployed to ${af.target}`); 9 | 10 | // const pm = await hre.ethers.deployContract("Paymaster"); 11 | 12 | // await pm.waitForDeployment(); 13 | 14 | // console.log(`PM deployed to ${pm.target}`); 15 | } 16 | 17 | main().catch((error) => { 18 | console.error(error); 19 | process.exitCode = 1; 20 | }); 21 | -------------------------------------------------------------------------------- /scripts/deposit.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | 3 | const EP_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; 4 | const PM_ADDRESS = "0xB54b8F3bb679a746E6b75805c9019EFF14AFE043"; 5 | 6 | async function main() { 7 | const entryPoint = await hre.ethers.getContractAt("EntryPoint", EP_ADDRESS); 8 | 9 | await entryPoint.depositTo(PM_ADDRESS, { 10 | value: hre.ethers.parseEther(".2"), 11 | }); 12 | 13 | console.log("deposit was successful!"); 14 | } 15 | 16 | main().catch((error) => { 17 | console.error(error); 18 | process.exitCode = 1; 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/execute.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | 3 | const FACTORY_ADDRESS = "0x11e23674D8CaA5a9bf60B36F2c5C5531cAae7112"; 4 | const EP_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; 5 | const PM_ADDRESS = "0xb643131F6Ac3Bc501df4F2C3392a28e0Dd403626"; 6 | 7 | async function main() { 8 | const entryPoint = await hre.ethers.getContractAt("EntryPoint", EP_ADDRESS); 9 | 10 | const AccountFactory = await hre.ethers.getContractFactory("AccountFactory"); 11 | const [signer0, signer1] = await hre.ethers.getSigners(); 12 | const address0 = await signer0.getAddress(); 13 | let initCode = 14 | FACTORY_ADDRESS + 15 | AccountFactory.interface 16 | .encodeFunctionData("createAccount", [address0]) 17 | .slice(2); 18 | 19 | let sender; 20 | try { 21 | await entryPoint.getSenderAddress(initCode); 22 | } catch (ex) { 23 | sender = "0x" + ex.data.slice(-40); 24 | } 25 | 26 | const code = await ethers.provider.getCode(sender); 27 | if (code !== "0x") { 28 | initCode = "0x"; 29 | } 30 | 31 | console.log({ sender }); 32 | 33 | const Account = await hre.ethers.getContractFactory("Account"); 34 | const userOp = { 35 | sender, // smart account address 36 | nonce: "0x" + (await entryPoint.getNonce(sender, 0)).toString(16), 37 | initCode, 38 | callData: Account.interface.encodeFunctionData("execute"), 39 | paymasterAndData: PM_ADDRESS, 40 | signature: 41 | "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c", 42 | }; 43 | 44 | const { preVerificationGas, verificationGasLimit, callGasLimit } = 45 | await ethers.provider.send("eth_estimateUserOperationGas", [ 46 | userOp, 47 | EP_ADDRESS, 48 | ]); 49 | 50 | userOp.preVerificationGas = preVerificationGas; 51 | userOp.verificationGasLimit = verificationGasLimit; 52 | userOp.callGasLimit = callGasLimit; 53 | 54 | const { maxFeePerGas } = await ethers.provider.getFeeData(); 55 | userOp.maxFeePerGas = "0x" + maxFeePerGas.toString(16); 56 | 57 | const maxPriorityFeePerGas = await ethers.provider.send( 58 | "rundler_maxPriorityFeePerGas" 59 | ); 60 | userOp.maxPriorityFeePerGas = maxPriorityFeePerGas; 61 | 62 | const userOpHash = await entryPoint.getUserOpHash(userOp); 63 | userOp.signature = await signer0.signMessage(hre.ethers.getBytes(userOpHash)); 64 | 65 | const opHash = await ethers.provider.send("eth_sendUserOperation", [ 66 | userOp, 67 | EP_ADDRESS, 68 | ]); 69 | 70 | setTimeout(async () => { 71 | const { transactionHash } = await ethers.provider.send( 72 | "eth_getUserOperationByHash", 73 | [opHash] 74 | ); 75 | 76 | console.log(transactionHash); 77 | }, 5000); 78 | } 79 | 80 | main().catch((error) => { 81 | console.error(error); 82 | process.exitCode = 1; 83 | }); 84 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | 3 | const ACCOUNT_ADDR = "0xd434fe29413880281134b9d552426c950d4e2440"; 4 | 5 | async function main() { 6 | const account = await hre.ethers.getContractAt("Account", ACCOUNT_ADDR); 7 | const count = await account.count(); 8 | console.log(count); 9 | } 10 | 11 | main().catch((error) => { 12 | console.error(error); 13 | process.exitCode = 1; 14 | }); 15 | --------------------------------------------------------------------------------