├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── audits └── Tokenbound - Zellic Audit Report.pdf ├── foundry.toml ├── package.json ├── remappings.txt ├── script ├── DeployAccountV3.s.sol ├── publish.sh └── verify-package-json-in-sync.js ├── src ├── AccountGuardian.sol ├── AccountProxy.sol ├── AccountV3.sol ├── AccountV3Upgradable.sol ├── abstract │ ├── ERC4337Account.sol │ ├── ERC6551Account.sol │ ├── Lockable.sol │ ├── Overridable.sol │ ├── Permissioned.sol │ ├── Signatory.sol │ └── execution │ │ ├── BaseExecutor.sol │ │ ├── BatchExecutor.sol │ │ ├── ERC6551Executor.sol │ │ ├── NestedAccountExecutor.sol │ │ ├── SandboxExecutor.sol │ │ └── TokenboundExecutor.sol ├── cross-chain │ └── FxChildExecutor.sol ├── interfaces │ ├── IAccountGuardian.sol │ └── ISandboxExecutor.sol ├── lib │ ├── LibExecutor.sol │ ├── LibSandbox.sol │ └── OPAddressAliasHelper.sol ├── package.json └── utils │ └── Errors.sol └── test ├── Account.t.sol ├── AccountCrossChain.t.sol ├── AccountERC1155.t.sol ├── AccountERC20.t.sol ├── AccountERC4337.t.sol ├── AccountERC721.t.sol ├── AccountETH.t.sol ├── AccountOverrides.t.sol ├── AccountPermissions.t.sol └── mocks ├── MockAccountUpgradable.sol ├── MockERC1155.sol ├── MockERC20.sol ├── MockERC721.sol ├── MockExecutor.sol ├── MockReverter.sol ├── MockSandboxExecutor.sol └── MockSigner.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [pull_request] 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv --no-match-test testCannotOverflowContext 34 | forge test -vvv --match-test testCannotOverflowContext --code-size-limit 24576 35 | id: test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # ignore all broadcast logs 11 | broadcast/ 12 | 13 | # Dotenv file 14 | .env 15 | node_modules 16 | pnpm-lock.yaml 17 | package-lock.json 18 | yarn.lock 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/account-abstraction"] 5 | path = lib/account-abstraction 6 | url = https://github.com/eth-infinitism/account-abstraction 7 | [submodule "lib/erc6551"] 8 | path = lib/erc6551 9 | url = https://github.com/erc6551/reference 10 | [submodule "lib/openzeppelin-contracts"] 11 | path = lib/openzeppelin-contracts 12 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 13 | [submodule "lib/multicall-authenticated"] 14 | path = lib/multicall-authenticated 15 | url = https://github.com/jaydenwindle/multicall-authenticated 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tokenbound Account Contracts 2 | 3 | This repository contains an opinionated [ERC-6551](https://eips.ethereum.org/EIPS/eip-6551) account implementation. The smart contracts are written in Solidity using the [Foundry](https://book.getfoundry.sh/) development framework. 4 | 5 | **This project is under active development and may undergo changes until ERC-6551 is finalized.** For the most recently deployed version of these contracts, see the [v0.3.1](https://github.com/tokenbound/contracts/releases/tag/v0.3.1) release. We recommend this version for any production usage. 6 | 7 | ## Contracts 8 | 9 | The `src/` directory contains the main contracts for the project: 10 | 11 | - `Account.sol`: This contract is the main ERC-6551 account implementation. It includes functionalities for executing a low-level call against an account if the caller is authorized to make calls, setting the implementation address for a given function call, granting a given caller execution permissions, locking the account until a certain timestamp, and more. 12 | 13 | - `AccountGuardian.sol`: This contract manages upgrade and cross-chain execution settings for accounts. It includes functionalities for setting trusted implementations and executors. 14 | 15 | - `AccountProxy.sol`: This contract is an ERC-1967 proxy which enables account upgradability. It includes functionalities for initializing and getting the implementation of the contract. 16 | 17 | ## Using as a Dependency 18 | 19 | ### Foundry 20 | 21 | If you want to use `tokenbound/contracts` as a dependency in another project, you can add it using `forge install`: 22 | 23 | ```sh 24 | forge install tokenbound=tokenbound/contracts 25 | ``` 26 | 27 | This will add `tokenbound/contracts` as a git submodule in your project. For more information on managing dependencies, refer to the [Foundry dependencies guide](https://github.com/foundry-rs/book/blob/master/src/projects/dependencies.md). 28 | 29 | ### Hardhat 30 | 31 | ```sh 32 | npm install @tokenbound/contracts 33 | ``` 34 | 35 | and use, for example, as 36 | 37 | ``` 38 | import "@tokenbound/contracts/AccountV3.sol"; 39 | ``` 40 | 41 | ## Development Setup 42 | 43 | You will need to have Foundry installed on your system. Please refer to the [Foundry installation guide](https://github.com/foundry-rs/book/blob/master/src/getting-started/installation.md) for detailed instructions. 44 | 45 | To use this repository, first clone it: 46 | 47 | ```sh 48 | git clone https://github.com/tokenbound/contracts.git 49 | cd contracts 50 | ``` 51 | 52 | Then, install the dependencies: 53 | 54 | ```sh 55 | forge install 56 | ``` 57 | 58 | This will install the submodule dependencies that are in the project. 59 | 60 | ## Running Tests 61 | 62 | To run the tests, use the `forge test` command: 63 | 64 | ```sh 65 | forge test 66 | ``` 67 | 68 | For more information on writing and running tests, refer to the [Foundry testing guide](https://github.com/foundry-rs/book/blob/master/src/forge/writing-tests.md). 69 | 70 | ## Contributing 71 | 72 | Contributions are welcome and appreciated! Please make sure to run the tests before submitting a pull request. 73 | -------------------------------------------------------------------------------- /audits/Tokenbound - Zellic Audit Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokenbound/contracts/bce75f985558fad3d06ee1b86f224f0cfb783631/audits/Tokenbound - Zellic Audit Report.pdf -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | solc_version = "0.8.17" 7 | optimizer = true 8 | optimizer_runs = 200 9 | 10 | [fmt] 11 | line_length = 100 12 | 13 | [rpc_endpoints] 14 | goerli = "${GOERLI_RPC_URL}" 15 | sepolia = "${SEPOLIA_RPC_URL}" 16 | mumbai = "${MUMBAI_RPC_URL}" 17 | mainnet = "${MAINNET_RPC_URL}" 18 | polygon = "${POLYGON_RPC_URL}" 19 | 20 | [etherscan] 21 | goerli = { key = "${ETHERSCAN_API_KEY}" } 22 | sepolia = { key = "${ETHERSCAN_API_KEY}" } 23 | mumbai = { key = "${POLYGONSCAN_API_KEY}" } 24 | mainnet = { key = "${ETHERSCAN_API_KEY}" } 25 | polygon = { key = "${POLYGONSCAN_API_KEY}" } 26 | 27 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tokenbound/contracts", 3 | "version": "0.3.1-beta.0", 4 | "scripts": { 5 | "compile": "forge test", 6 | "format": "forge fmt", 7 | "lint": "solhint -w 0 'contracts/**/*.sol'", 8 | "prepublishOnly": "echo 'ERROR: Use script/publish.sh to publish' && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/tokenbound/contracts.git" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @account-abstraction/=lib/account-abstraction/ 2 | erc6551/=lib/erc6551/src/ 3 | ds-test/=lib/forge-std/lib/ds-test/src/ 4 | erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ 5 | forge-std/=lib/forge-std/src/ 6 | @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ 7 | multicall-authenticated/=lib/multicall-authenticated/src/ 8 | -------------------------------------------------------------------------------- /script/DeployAccountV3.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | import "@openzeppelin/contracts/utils/Create2.sol"; 7 | import "@openzeppelin/contracts/utils/Strings.sol"; 8 | 9 | import "../src/AccountGuardian.sol"; 10 | import "../src/AccountV3Upgradable.sol"; 11 | import "../src/AccountProxy.sol"; 12 | 13 | contract DeployAccountV3 is Script { 14 | function run() external { 15 | bytes32 salt = 0x6551655165516551655165516551655165516551655165516551655165516551; 16 | address factory = 0x4e59b44847b379578588920cA78FbF26c0B4956C; 17 | 18 | address tokenboundSafe = 0x781b6A527482828bB04F33563797d4b696ddF328; 19 | address erc4337EntryPoint = 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789; 20 | address multicallForwarder = 0xcA1167915584462449EE5b4Ea51c37fE81eCDCCD; 21 | address erc6551Registry = 0x000000006551c19487814612e58FE06813775758; 22 | 23 | address guardian = Create2.computeAddress( 24 | salt, 25 | keccak256( 26 | abi.encodePacked(type(AccountGuardian).creationCode, abi.encode(tokenboundSafe)) 27 | ), 28 | factory 29 | ); 30 | address implementation = Create2.computeAddress( 31 | salt, 32 | keccak256( 33 | abi.encodePacked( 34 | type(AccountV3Upgradable).creationCode, 35 | abi.encode(erc4337EntryPoint, multicallForwarder, erc6551Registry, guardian) 36 | ) 37 | ), 38 | factory 39 | ); 40 | address proxy = Create2.computeAddress( 41 | salt, 42 | keccak256( 43 | abi.encodePacked( 44 | type(AccountProxy).creationCode, abi.encode(guardian, implementation) 45 | ) 46 | ), 47 | factory 48 | ); 49 | 50 | // Deploy AccountGuardian 51 | if (guardian.code.length == 0) { 52 | vm.startBroadcast(); 53 | new AccountGuardian{salt: salt}(tokenboundSafe); 54 | vm.stopBroadcast(); 55 | 56 | console.log("AccountGuardian:", guardian, "(deployed)"); 57 | } else { 58 | console.log("AccountGuardian:", guardian, "(exists)"); 59 | } 60 | 61 | // Deploy Account implementation 62 | if (implementation.code.length == 0) { 63 | vm.startBroadcast(); 64 | new AccountV3Upgradable{salt: salt}( 65 | erc4337EntryPoint, 66 | multicallForwarder, 67 | erc6551Registry, 68 | guardian 69 | ); 70 | vm.stopBroadcast(); 71 | 72 | console.log("AccountV3Upgradable:", implementation, "(deployed)"); 73 | } else { 74 | console.log("AccountV3Upgradable:", implementation, "(exists)"); 75 | } 76 | 77 | // Deploy AccountProxy 78 | if (proxy.code.length == 0) { 79 | vm.startBroadcast(); 80 | new AccountProxy{salt: salt}(guardian, implementation); 81 | vm.stopBroadcast(); 82 | 83 | console.log("AccountProxy:", proxy, "(deployed)"); 84 | } else { 85 | console.log("AccountProxy:", proxy, "(exists)"); 86 | } 87 | 88 | console.log("\nVerification Commands:\n"); 89 | console.log( 90 | "AccountGuardian: forge verify-contract --num-of-optimizations 200 --chain-id", 91 | block.chainid, 92 | guardian, 93 | string.concat( 94 | "src/AccountGuardian.sol:AccountGuardian --constructor-args $(cast abi-encode \"constructor(address)\" ", 95 | Strings.toHexString(tokenboundSafe), 96 | ")\n" 97 | ) 98 | ); 99 | console.log( 100 | "AccountV3Upgradable: forge verify-contract --num-of-optimizations 200 --chain-id", 101 | block.chainid, 102 | implementation, 103 | string.concat( 104 | "src/AccountV3Upgradable.sol:AccountV3Upgradable --constructor-args $(cast abi-encode \"constructor(address,address,address,address)\" ", 105 | Strings.toHexString(erc4337EntryPoint), 106 | " ", 107 | Strings.toHexString(multicallForwarder), 108 | " ", 109 | Strings.toHexString(erc6551Registry), 110 | " ", 111 | Strings.toHexString(guardian), 112 | ")\n" 113 | ) 114 | ); 115 | console.log( 116 | "AccountProxy: forge verify-contract --num-of-optimizations 200 --chain-id", 117 | block.chainid, 118 | proxy, 119 | string.concat( 120 | "src/AccountProxy.sol:AccountProxy --constructor-args $(cast abi-encode \"constructor(address,address)\" ", 121 | Strings.toHexString(guardian), 122 | " ", 123 | Strings.toHexString(implementation), 124 | ")\n" 125 | ) 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /script/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cp README.md src/README.md 4 | cd src 5 | if [[ "$1" == "alpha" || "$1" == "beta" ]]; then 6 | npm publish --tag $1 7 | else 8 | npm publish 9 | fi 10 | rm README.md 11 | cd .. 12 | -------------------------------------------------------------------------------- /script/verify-package-json-in-sync.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pkg = require("../package.json"); 4 | const pkgc = require("../src/package.json"); 5 | 6 | if (pkg.version !== pkgc.version) { 7 | console.error("package.json and contracts/package.json are out of sync"); 8 | process.exit(1); 9 | } 10 | -------------------------------------------------------------------------------- /src/AccountGuardian.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/access/Ownable2Step.sol"; 5 | 6 | /** 7 | * @dev Manages upgrade and cross-chain execution settings for accounts 8 | */ 9 | contract AccountGuardian is Ownable2Step { 10 | /** 11 | * @dev mapping from implementation => is trusted 12 | */ 13 | mapping(address => bool) public isTrustedImplementation; 14 | 15 | /** 16 | * @dev mapping from cross-chain executor => is trusted 17 | */ 18 | mapping(address => bool) public isTrustedExecutor; 19 | 20 | event TrustedImplementationUpdated(address implementation, bool trusted); 21 | event TrustedExecutorUpdated(address executor, bool trusted); 22 | 23 | constructor(address owner) { 24 | _transferOwnership(owner); 25 | } 26 | 27 | /** 28 | * @dev Sets a given implementation address as trusted, allowing accounts to upgrade to this 29 | * implementation 30 | */ 31 | function setTrustedImplementation(address implementation, bool trusted) external onlyOwner { 32 | isTrustedImplementation[implementation] = trusted; 33 | emit TrustedImplementationUpdated(implementation, trusted); 34 | } 35 | 36 | /** 37 | * @dev Sets a given cross-chain executor address as trusted, allowing it to relay operations to 38 | * accounts on non-native chains 39 | */ 40 | function setTrustedExecutor(address executor, bool trusted) external onlyOwner { 41 | isTrustedExecutor[executor] = trusted; 42 | emit TrustedExecutorUpdated(executor, trusted); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/AccountProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; 5 | import "@openzeppelin/contracts/proxy/Proxy.sol"; 6 | 7 | import "./interfaces/IAccountGuardian.sol"; 8 | import "./utils/Errors.sol"; 9 | 10 | contract AccountProxy is Proxy, ERC1967Upgrade { 11 | address immutable guardian; 12 | address immutable initialImplementation; 13 | 14 | constructor(address _guardian, address _initialImplementation) { 15 | if (_guardian == address(0) || _initialImplementation == address(0)) { 16 | revert InvalidImplementation(); 17 | } 18 | guardian = _guardian; 19 | initialImplementation = _initialImplementation; 20 | } 21 | 22 | function initialize(address implementation) external { 23 | if (implementation != initialImplementation) { 24 | if (!IAccountGuardian(guardian).isTrustedImplementation(implementation)) { 25 | revert InvalidImplementation(); 26 | } 27 | } 28 | if (ERC1967Upgrade._getImplementation() != address(0)) revert AlreadyInitialized(); 29 | ERC1967Upgrade._upgradeTo(implementation); 30 | } 31 | 32 | function _implementation() internal view override returns (address) { 33 | return ERC1967Upgrade._getImplementation(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/AccountV3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; 5 | import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; 7 | 8 | import "erc6551/lib/ERC6551AccountLib.sol"; 9 | 10 | import "./abstract/Lockable.sol"; 11 | import "./abstract/Overridable.sol"; 12 | import "./abstract/Permissioned.sol"; 13 | import "./abstract/ERC6551Account.sol"; 14 | import "./abstract/ERC4337Account.sol"; 15 | import "./abstract/execution/TokenboundExecutor.sol"; 16 | 17 | import "./lib/OPAddressAliasHelper.sol"; 18 | 19 | import "./interfaces/IAccountGuardian.sol"; 20 | 21 | /** 22 | * @title Tokenbound ERC-6551 Account Implementation 23 | */ 24 | contract AccountV3 is 25 | ERC721Holder, 26 | ERC1155Holder, 27 | Lockable, 28 | Overridable, 29 | Permissioned, 30 | ERC6551Account, 31 | ERC4337Account, 32 | TokenboundExecutor 33 | { 34 | IAccountGuardian immutable guardian; 35 | 36 | /** 37 | * @param entryPoint_ The ERC-4337 EntryPoint address 38 | * @param multicallForwarder The MulticallForwarder address 39 | * @param erc6551Registry The ERC-6551 Registry address 40 | * @param _guardian The AccountGuardian address 41 | */ 42 | constructor( 43 | address entryPoint_, 44 | address multicallForwarder, 45 | address erc6551Registry, 46 | address _guardian 47 | ) ERC4337Account(entryPoint_) TokenboundExecutor(multicallForwarder, erc6551Registry) { 48 | guardian = IAccountGuardian(_guardian); 49 | } 50 | 51 | /** 52 | * @notice Called whenever this account received Ether 53 | * 54 | * @dev Can be overriden via Overridable 55 | */ 56 | receive() external payable override { 57 | _handleOverride(); 58 | } 59 | 60 | /** 61 | * @notice Called whenever the calldata function selector does not match a defined function 62 | * 63 | * @dev Can be overriden via Overridable 64 | */ 65 | fallback() external payable { 66 | _handleOverride(); 67 | } 68 | 69 | /** 70 | * @notice Returns the owner of the token this account is bound to (if available) 71 | * 72 | * @dev Returns zero address if token is on a foreign chain or token contract does not exist 73 | * 74 | * @return address The address which owns the token this account is bound to 75 | */ 76 | function owner() public view virtual returns (address) { 77 | (uint256 chainId, address tokenContract, uint256 tokenId) = ERC6551AccountLib.token(); 78 | return _tokenOwner(chainId, tokenContract, tokenId); 79 | } 80 | 81 | /** 82 | * @notice Returns whether a given ERC165 interface ID is supported 83 | * 84 | * @dev Can be overriden via Overridable except for base interfaces. 85 | * 86 | * @param interfaceId The interface ID to query for 87 | * @return bool True if the interface is supported, false otherwise 88 | */ 89 | function supportsInterface(bytes4 interfaceId) 90 | public 91 | view 92 | virtual 93 | override(ERC1155Receiver, ERC6551Account, ERC6551Executor) 94 | returns (bool) 95 | { 96 | bool interfaceSupported = super.supportsInterface(interfaceId); 97 | 98 | if (interfaceSupported) return true; 99 | 100 | _handleOverrideStatic(); 101 | 102 | return false; 103 | } 104 | 105 | /** 106 | * @dev called whenever an ERC-721 token is received. Can be overriden via Overridable. Reverts 107 | * if token being received is the token the account is bound to. 108 | */ 109 | function onERC721Received(address, address, uint256 tokenId, bytes memory) 110 | public 111 | virtual 112 | override 113 | returns (bytes4) 114 | { 115 | (uint256 chainId, address tokenContract, uint256 _tokenId) = ERC6551AccountLib.token(); 116 | 117 | if (msg.sender == tokenContract && tokenId == _tokenId && chainId == block.chainid) { 118 | revert OwnershipCycle(); 119 | } 120 | 121 | _handleOverride(); 122 | 123 | return this.onERC721Received.selector; 124 | } 125 | 126 | /** 127 | * @dev called whenever an ERC-1155 token is received. Can be overriden via Overridable. 128 | */ 129 | function onERC1155Received(address, address, uint256, uint256, bytes memory) 130 | public 131 | virtual 132 | override 133 | returns (bytes4) 134 | { 135 | _handleOverride(); 136 | return this.onERC1155Received.selector; 137 | } 138 | 139 | /** 140 | * @dev called whenever a batch of ERC-1155 tokens are received. Can be overriden via Overridable. 141 | */ 142 | function onERC1155BatchReceived( 143 | address, 144 | address, 145 | uint256[] memory, 146 | uint256[] memory, 147 | bytes memory 148 | ) public virtual override returns (bytes4) { 149 | _handleOverride(); 150 | return this.onERC1155BatchReceived.selector; 151 | } 152 | 153 | /** 154 | * @notice Returns whether a given account is authorized to sign on behalf of this account 155 | * 156 | * @param signer The address to query authorization for 157 | * @return True if the signer is valid, false otherwise 158 | */ 159 | function _isValidSigner(address signer, bytes memory) 160 | internal 161 | view 162 | virtual 163 | override 164 | returns (bool) 165 | { 166 | (uint256 chainId, address tokenContract, uint256 tokenId) = ERC6551AccountLib.token(); 167 | 168 | // Single level accuont owner is valid signer 169 | address _owner = _tokenOwner(chainId, tokenContract, tokenId); 170 | if (signer == _owner) return true; 171 | 172 | // Root owner of accuont tree is valid signer 173 | address _rootOwner = _rootTokenOwner(_owner, chainId, tokenContract, tokenId); 174 | if (signer == _rootOwner) return true; 175 | 176 | // Accounts granted permission by root owner are valid signers 177 | return hasPermission(signer, _rootOwner); 178 | } 179 | 180 | /** 181 | * Determines if a given hash and signature are valid for this account 182 | * @param hash Hash of signed data 183 | * @param signature ECDSA signature or encoded contract signature (v=0) 184 | */ 185 | function _isValidSignature(bytes32 hash, bytes calldata signature) 186 | internal 187 | view 188 | virtual 189 | override(ERC4337Account, Signatory) 190 | returns (bool) 191 | { 192 | uint8 v = uint8(signature[64]); 193 | address signer; 194 | 195 | // Smart contract signature 196 | if (v == 0) { 197 | // Signer address encoded in r 198 | signer = address(uint160(uint256(bytes32(signature[:32])))); 199 | 200 | // Allow recursive signature verification 201 | if (!_isValidSigner(signer, "") && signer != address(this)) { 202 | return false; 203 | } 204 | 205 | // Signature offset encoded in s 206 | bytes calldata _signature = signature[uint256(bytes32(signature[32:64])):]; 207 | 208 | return SignatureChecker.isValidERC1271SignatureNow(signer, hash, _signature); 209 | } 210 | 211 | ECDSA.RecoverError _error; 212 | (signer, _error) = ECDSA.tryRecover(hash, signature); 213 | 214 | if (_error != ECDSA.RecoverError.NoError) return false; 215 | 216 | return _isValidSigner(signer, ""); 217 | } 218 | 219 | /** 220 | * @notice Returns whether a given account is authorized to execute transactions on behalf of 221 | * this account 222 | * 223 | * @param executor The address to query authorization for 224 | * @return True if the executor is authorized, false otherwise 225 | */ 226 | function _isValidExecutor(address executor) internal view virtual override returns (bool) { 227 | // Allow execution from ERC-4337 EntryPoint 228 | if (executor == address(entryPoint())) return true; 229 | 230 | (uint256 chainId, address tokenContract, uint256 tokenId) = ERC6551AccountLib.token(); 231 | 232 | // Allow cross chain execution 233 | if (chainId != block.chainid) { 234 | // Allow execution from L1 account on OPStack chains 235 | if (OPAddressAliasHelper.undoL1ToL2Alias(_msgSender()) == address(this)) { 236 | return true; 237 | } 238 | 239 | // Allow execution from trusted cross chain bridges 240 | if (guardian.isTrustedExecutor(executor)) return true; 241 | } 242 | 243 | // Allow execution from owner 244 | address _owner = _tokenOwner(chainId, tokenContract, tokenId); 245 | if (executor == _owner) return true; 246 | 247 | // Allow execution from root owner of account tree 248 | address _rootOwner = _rootTokenOwner(_owner, chainId, tokenContract, tokenId); 249 | if (executor == _rootOwner) return true; 250 | 251 | // Allow execution from permissioned account 252 | if (hasPermission(executor, _rootOwner)) return true; 253 | 254 | return false; 255 | } 256 | 257 | /** 258 | * @dev Updates account state based on previous state and msg.data 259 | */ 260 | function _updateState() internal virtual { 261 | _state = uint256(keccak256(abi.encode(_state, _msgData()))); 262 | } 263 | 264 | /** 265 | * @dev Called before executing an operation. Reverts if account is locked. Ensures state is 266 | * updated prior to execution. 267 | */ 268 | function _beforeExecute() internal virtual override { 269 | if (isLocked()) revert AccountLocked(); 270 | _updateState(); 271 | } 272 | 273 | /** 274 | * @dev Called before locking the account. Reverts if account is locked. Updates account state. 275 | */ 276 | function _beforeLock() internal virtual override { 277 | if (isLocked()) revert AccountLocked(); 278 | _updateState(); 279 | } 280 | 281 | /** 282 | * @dev Called before setting overrides on the account. Reverts if account is locked. Updates 283 | * account state. 284 | */ 285 | function _beforeSetOverrides() internal virtual override { 286 | if (isLocked()) revert AccountLocked(); 287 | _updateState(); 288 | } 289 | 290 | /** 291 | * @dev Called before setting permissions on the account. Reverts if account is locked. Updates 292 | * account state. 293 | */ 294 | function _beforeSetPermissions() internal virtual override { 295 | if (isLocked()) revert AccountLocked(); 296 | _updateState(); 297 | } 298 | 299 | /** 300 | * @dev Returns the root owner of an account. If account is not owned by a TBA, returns the 301 | * owner of the NFT bound to this account. If account is owned by a TBA, iterates up token 302 | * ownership tree and returns root owner. 303 | * 304 | * *Security Warning*: the return value of this function can only be trusted if it is also the 305 | * address of the sender (as the code of the NFT contract cannot be trusted). This function 306 | * should therefore only be used for authorization and never authentication. 307 | */ 308 | function _rootTokenOwner(uint256 chainId, address tokenContract, uint256 tokenId) 309 | internal 310 | view 311 | virtual 312 | override(Overridable, Permissioned, Lockable) 313 | returns (address) 314 | { 315 | address _owner = _tokenOwner(chainId, tokenContract, tokenId); 316 | 317 | return _rootTokenOwner(_owner, chainId, tokenContract, tokenId); 318 | } 319 | 320 | /** 321 | * @dev Returns the root owner of an account given a known account owner address (saves an 322 | * additional external call). 323 | */ 324 | function _rootTokenOwner( 325 | address owner_, 326 | uint256 chainId, 327 | address tokenContract, 328 | uint256 tokenId 329 | ) internal view virtual returns (address) { 330 | address _owner = owner_; 331 | 332 | while (ERC6551AccountLib.isERC6551Account(_owner, __self, erc6551Registry)) { 333 | (chainId, tokenContract, tokenId) = IERC6551Account(payable(_owner)).token(); 334 | _owner = _tokenOwner(chainId, tokenContract, tokenId); 335 | } 336 | 337 | return _owner; 338 | } 339 | 340 | /** 341 | * @dev Returns the owner of the token which this account is bound to. Returns the zero address 342 | * if token does not exist on the current chain or if the token contract does not exist 343 | */ 344 | function _tokenOwner(uint256 chainId, address tokenContract, uint256 tokenId) 345 | internal 346 | view 347 | virtual 348 | returns (address) 349 | { 350 | if (chainId != block.chainid) return address(0); 351 | if (tokenContract.code.length == 0) return address(0); 352 | 353 | try IERC721(tokenContract).ownerOf(tokenId) returns (address _owner) { 354 | return _owner; 355 | } catch { 356 | return address(0); 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/AccountV3Upgradable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; 5 | import "./AccountV3.sol"; 6 | 7 | contract AccountV3Upgradable is AccountV3, UUPSUpgradeable { 8 | constructor( 9 | address entryPoint_, 10 | address multicallForwarder, 11 | address erc6551Registry, 12 | address guardian 13 | ) AccountV3(entryPoint_, multicallForwarder, erc6551Registry, guardian) {} 14 | 15 | function _authorizeUpgrade(address implementation) internal virtual override { 16 | if (!guardian.isTrustedImplementation(implementation)) revert InvalidImplementation(); 17 | if (!_isValidExecutor(_msgSender())) revert NotAuthorized(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/abstract/ERC4337Account.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 5 | 6 | import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 7 | import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; 8 | import {BaseAccount as BaseERC4337Account} from 9 | "@account-abstraction/contracts/core/BaseAccount.sol"; 10 | 11 | import "../utils/Errors.sol"; 12 | 13 | /** 14 | * @title ERC-4337 Support 15 | * @dev Implements ERC-4337 account support 16 | */ 17 | abstract contract ERC4337Account is BaseERC4337Account { 18 | using ECDSA for bytes32; 19 | 20 | IEntryPoint immutable _entryPoint; 21 | 22 | constructor(address entryPoint_) { 23 | if (entryPoint_ == address(0)) revert InvalidEntryPoint(); 24 | _entryPoint = IEntryPoint(entryPoint_); 25 | } 26 | 27 | /** 28 | * @dev See {BaseERC4337Account-entryPoint} 29 | */ 30 | function entryPoint() public view override returns (IEntryPoint) { 31 | return _entryPoint; 32 | } 33 | 34 | /** 35 | * @dev See {BaseERC4337Account-_validateSignature} 36 | */ 37 | function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) 38 | internal 39 | view 40 | virtual 41 | override 42 | returns (uint256) 43 | { 44 | if (_isValidSignature(_getUserOpSignatureHash(userOp, userOpHash), userOp.signature)) { 45 | return 0; 46 | } 47 | 48 | return 1; 49 | } 50 | 51 | /** 52 | * @dev Returns the user operation hash that should be signed by the account owner 53 | */ 54 | function _getUserOpSignatureHash(UserOperation calldata, bytes32 userOpHash) 55 | internal 56 | view 57 | virtual 58 | returns (bytes32) 59 | { 60 | return userOpHash.toEthSignedMessageHash(); 61 | } 62 | 63 | function _isValidSignature(bytes32 hash, bytes calldata signature) 64 | internal 65 | view 66 | virtual 67 | returns (bool); 68 | } 69 | -------------------------------------------------------------------------------- /src/abstract/ERC6551Account.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 5 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 6 | 7 | import "erc6551/lib/ERC6551AccountLib.sol"; 8 | import "erc6551/interfaces/IERC6551Account.sol"; 9 | 10 | import "./Signatory.sol"; 11 | 12 | /** 13 | * @title ERC-6551 Account Support 14 | * @dev Implements the ERC-6551 Account interface 15 | */ 16 | abstract contract ERC6551Account is IERC6551Account, ERC165, Signatory { 17 | uint256 _state; 18 | 19 | receive() external payable virtual {} 20 | 21 | /** 22 | * @dev See: {IERC6551Account-isValidSigner} 23 | */ 24 | function isValidSigner(address signer, bytes calldata data) 25 | external 26 | view 27 | returns (bytes4 magicValue) 28 | { 29 | if (_isValidSigner(signer, data)) { 30 | return IERC6551Account.isValidSigner.selector; 31 | } 32 | 33 | return bytes4(0); 34 | } 35 | 36 | /** 37 | * @dev See: {IERC6551Account-token} 38 | */ 39 | function token() 40 | public 41 | view 42 | returns (uint256 chainId, address tokenContract, uint256 tokenId) 43 | { 44 | return ERC6551AccountLib.token(); 45 | } 46 | 47 | /** 48 | * @dev See: {IERC6551Account-state} 49 | */ 50 | function state() public view returns (uint256) { 51 | return _state; 52 | } 53 | 54 | function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { 55 | return 56 | interfaceId == type(IERC6551Account).interfaceId || super.supportsInterface(interfaceId); 57 | } 58 | 59 | /** 60 | * @dev Returns true if a given signer is authorized to use this account 61 | */ 62 | function _isValidSigner(address signer, bytes memory) internal view virtual returns (bool); 63 | } 64 | -------------------------------------------------------------------------------- /src/abstract/Lockable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "erc6551/lib/ERC6551AccountLib.sol"; 5 | 6 | import "../utils/Errors.sol"; 7 | 8 | /** 9 | * @title Account Lock 10 | * @dev Allows the root owner of a token bound account to lock access to an account until a 11 | * certain timestamp 12 | */ 13 | abstract contract Lockable { 14 | /** 15 | * @notice The timestamp at which this account will be unlocked 16 | */ 17 | uint256 public lockedUntil; 18 | 19 | event LockUpdated(uint256 lockedUntil); 20 | 21 | /** 22 | * @dev Locks the account until a certain timestamp 23 | * 24 | * @param _lockedUntil The time at which this account will no longer be locke 25 | */ 26 | function lock(uint256 _lockedUntil) external virtual { 27 | (uint256 chainId, address tokenContract, uint256 tokenId) = ERC6551AccountLib.token(); 28 | address _owner = _rootTokenOwner(chainId, tokenContract, tokenId); 29 | 30 | if (_owner == address(0)) revert NotAuthorized(); 31 | if (msg.sender != _owner) revert NotAuthorized(); 32 | 33 | if (_lockedUntil > block.timestamp + 365 days) { 34 | revert ExceedsMaxLockTime(); 35 | } 36 | 37 | _beforeLock(); 38 | 39 | lockedUntil = _lockedUntil; 40 | 41 | emit LockUpdated(_lockedUntil); 42 | } 43 | 44 | /** 45 | * @dev Returns the current lock status of the account as a boolean 46 | */ 47 | function isLocked() public view virtual returns (bool) { 48 | return lockedUntil > block.timestamp; 49 | } 50 | 51 | function _rootTokenOwner(uint256 chainId, address tokenContract, uint256 tokenId) 52 | internal 53 | view 54 | virtual 55 | returns (address); 56 | 57 | function _beforeLock() internal virtual {} 58 | } 59 | -------------------------------------------------------------------------------- /src/abstract/Overridable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "erc6551/lib/ERC6551AccountLib.sol"; 5 | 6 | import "../utils/Errors.sol"; 7 | import "../lib/LibSandbox.sol"; 8 | 9 | /** 10 | * @title Account Overrides 11 | * @dev Allows the root owner of a token bound account to override the implementation of a given 12 | * function selector on the account. Overrides are keyed by the root owner address, so will be 13 | * disabled upon transfer of the token which owns this account tree. 14 | */ 15 | abstract contract Overridable { 16 | /** 17 | * @dev mapping from owner => selector => implementation 18 | */ 19 | mapping(address => mapping(bytes4 => address)) public overrides; 20 | 21 | event OverrideUpdated(address owner, bytes4 selector, address implementation); 22 | 23 | /** 24 | * @dev Sets the implementation address for a given array of function selectors. Can only be 25 | * called by the root owner of the account 26 | * 27 | * @param selectors Array of selectors to override 28 | * @param implementations Array of implementation address corresponding to selectors 29 | */ 30 | function setOverrides(bytes4[] calldata selectors, address[] calldata implementations) 31 | external 32 | virtual 33 | { 34 | (uint256 chainId, address tokenContract, uint256 tokenId) = ERC6551AccountLib.token(); 35 | address _owner = _rootTokenOwner(chainId, tokenContract, tokenId); 36 | 37 | if (_owner == address(0)) revert NotAuthorized(); 38 | if (msg.sender != _owner) revert NotAuthorized(); 39 | 40 | _beforeSetOverrides(); 41 | 42 | address sandbox = LibSandbox.sandbox(address(this)); 43 | if (sandbox.code.length == 0) LibSandbox.deploy(address(this)); 44 | 45 | uint256 length = selectors.length; 46 | 47 | if (implementations.length != length) revert InvalidInput(); 48 | 49 | for (uint256 i = 0; i < length; i++) { 50 | overrides[_owner][selectors[i]] = implementations[i]; 51 | emit OverrideUpdated(_owner, selectors[i], implementations[i]); 52 | } 53 | } 54 | 55 | /** 56 | * @dev Calls into the implementation address using sandbox if override is set for the current 57 | * function selector. If an implementation is defined, this funciton will either revert or 58 | * return with the return value of the implementation 59 | */ 60 | function _handleOverride() internal virtual { 61 | (uint256 chainId, address tokenContract, uint256 tokenId) = ERC6551AccountLib.token(); 62 | address _owner = _rootTokenOwner(chainId, tokenContract, tokenId); 63 | 64 | address implementation = overrides[_owner][msg.sig]; 65 | 66 | if (implementation != address(0)) { 67 | address sandbox = LibSandbox.sandbox(address(this)); 68 | (bool success, bytes memory result) = 69 | sandbox.call(abi.encodePacked(implementation, msg.data, msg.sender)); 70 | assembly { 71 | if iszero(success) { revert(add(result, 32), mload(result)) } 72 | return(add(result, 32), mload(result)) 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * @dev Static calls into the implementation addressif override is set for the current function 79 | * selector. If an implementation is defined, this funciton will either revert or return with 80 | * the return value of the implementation 81 | */ 82 | function _handleOverrideStatic() internal view virtual { 83 | (uint256 chainId, address tokenContract, uint256 tokenId) = ERC6551AccountLib.token(); 84 | address _owner = _rootTokenOwner(chainId, tokenContract, tokenId); 85 | 86 | address implementation = overrides[_owner][msg.sig]; 87 | 88 | if (implementation != address(0)) { 89 | (bool success, bytes memory result) = implementation.staticcall(msg.data); 90 | assembly { 91 | if iszero(success) { revert(add(result, 32), mload(result)) } 92 | return(add(result, 32), mload(result)) 93 | } 94 | } 95 | } 96 | 97 | function _beforeSetOverrides() internal virtual {} 98 | 99 | function _rootTokenOwner(uint256 chainId, address tokenContract, uint256 tokenId) 100 | internal 101 | view 102 | virtual 103 | returns (address); 104 | } 105 | -------------------------------------------------------------------------------- /src/abstract/Permissioned.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "erc6551/lib/ERC6551AccountLib.sol"; 5 | import "../utils/Errors.sol"; 6 | 7 | /** 8 | * @title Account Permissions 9 | * @dev Allows the root owner of a token bound account to allow another account to execute 10 | * operations from this account. Permissions are keyed by the root owner address, so will be 11 | * disabled upon transfer of the token which owns this account tree. 12 | */ 13 | abstract contract Permissioned { 14 | /** 15 | * @dev mapping from owner => caller => has permissions 16 | */ 17 | mapping(address => mapping(address => bool)) public permissions; 18 | 19 | event PermissionUpdated(address owner, address caller, bool hasPermission); 20 | 21 | /** 22 | * @dev Grants or revokes execution permissions for a given array of callers on this account. 23 | * Can only be called by the root owner of the account 24 | * 25 | * @param callers Array of callers to grant permissions to 26 | * @param _permissions Array of booleans, true if execution permissions should be granted, 27 | * false if permissions should be revoked 28 | */ 29 | function setPermissions(address[] calldata callers, bool[] calldata _permissions) 30 | external 31 | virtual 32 | { 33 | (uint256 chainId, address tokenContract, uint256 tokenId) = ERC6551AccountLib.token(); 34 | address _owner = _rootTokenOwner(chainId, tokenContract, tokenId); 35 | 36 | if (_owner == address(0)) revert NotAuthorized(); 37 | if (msg.sender != _owner) revert NotAuthorized(); 38 | 39 | _beforeSetPermissions(); 40 | 41 | uint256 length = callers.length; 42 | 43 | if (_permissions.length != length) revert InvalidInput(); 44 | 45 | for (uint256 i = 0; i < length; i++) { 46 | permissions[_owner][callers[i]] = _permissions[i]; 47 | emit PermissionUpdated(_owner, callers[i], _permissions[i]); 48 | } 49 | } 50 | 51 | /** 52 | * @dev Returns true if caller has permissions to act on behalf of owner 53 | * 54 | * @param caller Address to query permissions for 55 | * @param owner Root owner address for which to query permissions 56 | */ 57 | function hasPermission(address caller, address owner) internal view returns (bool) { 58 | return permissions[owner][caller]; 59 | } 60 | 61 | function _beforeSetPermissions() internal virtual {} 62 | 63 | function _rootTokenOwner(uint256 chainId, address tokenContract, uint256 tokenId) 64 | internal 65 | view 66 | virtual 67 | returns (address); 68 | } 69 | -------------------------------------------------------------------------------- /src/abstract/Signatory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/interfaces/IERC1271.sol"; 5 | 6 | /** 7 | * @title Signatory 8 | * @dev Implements ERC-1271 signature verification 9 | */ 10 | abstract contract Signatory is IERC1271 { 11 | /** 12 | * @dev See {IERC1721-isValidSignature} 13 | */ 14 | function isValidSignature(bytes32 hash, bytes calldata signature) 15 | external 16 | view 17 | returns (bytes4 magicValue) 18 | { 19 | if (_isValidSignature(hash, signature)) { 20 | return IERC1271.isValidSignature.selector; 21 | } 22 | 23 | return bytes4(0); 24 | } 25 | 26 | function _isValidSignature(bytes32 hash, bytes calldata signature) 27 | internal 28 | view 29 | virtual 30 | returns (bool); 31 | } 32 | -------------------------------------------------------------------------------- /src/abstract/execution/BaseExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/utils/Context.sol"; 5 | 6 | import "./SandboxExecutor.sol"; 7 | 8 | /** 9 | * @title Base Executor 10 | * @dev Base configuration for all executors 11 | */ 12 | abstract contract BaseExecutor is Context, SandboxExecutor { 13 | function _beforeExecute() internal virtual {} 14 | 15 | function _isValidExecutor(address executor) internal view virtual returns (bool); 16 | } 17 | -------------------------------------------------------------------------------- /src/abstract/execution/BatchExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../../utils/Errors.sol"; 5 | 6 | import "./BaseExecutor.sol"; 7 | 8 | /** 9 | * @title Batch Executor 10 | * @dev Allows multiple operations to be executed from this account in a single transaction 11 | */ 12 | abstract contract BatchExecutor is BaseExecutor { 13 | struct Operation { 14 | address to; 15 | uint256 value; 16 | bytes data; 17 | uint8 operation; 18 | } 19 | 20 | /** 21 | * @notice Executes a batch of operations if the caller is authorized 22 | * @param operations Operations to execute 23 | */ 24 | function executeBatch(Operation[] calldata operations) 25 | external 26 | payable 27 | returns (bytes[] memory) 28 | { 29 | if (!_isValidExecutor(_msgSender())) revert NotAuthorized(); 30 | 31 | _beforeExecute(); 32 | 33 | uint256 length = operations.length; 34 | bytes[] memory results = new bytes[](length); 35 | 36 | for (uint256 i = 0; i < length; i++) { 37 | results[i] = LibExecutor._execute( 38 | operations[i].to, operations[i].value, operations[i].data, operations[i].operation 39 | ); 40 | } 41 | 42 | return results; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/abstract/execution/ERC6551Executor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; 5 | import "@openzeppelin/contracts/utils/Context.sol"; 6 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 7 | 8 | import "erc6551/interfaces/IERC6551Executable.sol"; 9 | import "erc6551/interfaces/IERC6551Account.sol"; 10 | import "erc6551/lib/ERC6551AccountLib.sol"; 11 | 12 | import "../../utils/Errors.sol"; 13 | import "../../lib/LibExecutor.sol"; 14 | import "../../lib/LibSandbox.sol"; 15 | import "./SandboxExecutor.sol"; 16 | import "./BaseExecutor.sol"; 17 | 18 | /** 19 | * @title ERC-6551 Executor 20 | * @dev Basic executor which implements the IERC6551Executable execution interface 21 | */ 22 | abstract contract ERC6551Executor is IERC6551Executable, ERC165, BaseExecutor { 23 | /** 24 | * Executes a low-level operation from this account if the caller is a valid executor 25 | * 26 | * @param to Account to operate on 27 | * @param value Value to send with operation 28 | * @param data Encoded calldata of operation 29 | * @param operation Operation type (0=CALL, 1=DELEGATECALL, 2=CREATE, 3=CREATE2) 30 | */ 31 | function execute(address to, uint256 value, bytes calldata data, uint8 operation) 32 | external 33 | payable 34 | virtual 35 | returns (bytes memory) 36 | { 37 | if (!_isValidExecutor(_msgSender())) revert NotAuthorized(); 38 | 39 | _beforeExecute(); 40 | 41 | return LibExecutor._execute(to, value, data, operation); 42 | } 43 | 44 | function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { 45 | return interfaceId == type(IERC6551Executable).interfaceId 46 | || super.supportsInterface(interfaceId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/abstract/execution/NestedAccountExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 5 | import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; 6 | 7 | import "erc6551/interfaces/IERC6551Executable.sol"; 8 | import "erc6551/interfaces/IERC6551Account.sol"; 9 | import "erc6551/lib/ERC6551AccountLib.sol"; 10 | 11 | import "../../utils/Errors.sol"; 12 | import "../../lib/LibExecutor.sol"; 13 | import "../../lib/LibSandbox.sol"; 14 | import "./SandboxExecutor.sol"; 15 | import "./BaseExecutor.sol"; 16 | 17 | import "../Lockable.sol"; 18 | 19 | /** 20 | * @title Nested Account Executor 21 | * @dev Allows the root owner of a nested token bound account to execute transactions directly 22 | * against the nested account, even if intermediate accounts have not been created. 23 | */ 24 | abstract contract NestedAccountExecutor is BaseExecutor { 25 | address immutable __self = address(this); 26 | address public immutable erc6551Registry; 27 | 28 | struct ERC6551AccountInfo { 29 | bytes32 salt; 30 | address tokenContract; 31 | uint256 tokenId; 32 | } 33 | 34 | constructor(address _erc6551Registry) { 35 | if (_erc6551Registry == address(0)) revert InvalidERC6551Registry(); 36 | erc6551Registry = _erc6551Registry; 37 | } 38 | 39 | /** 40 | * Executes a low-level operation from this account if the caller is a valid signer on the 41 | * parent TBA specified in the proof 42 | * 43 | * @param to Account to operate on 44 | * @param value Value to send with operation 45 | * @param data Encoded calldata of operation 46 | * @param operation Operation type (0=CALL, 1=DELEGATECALL, 2=CREATE, 3=CREATE2) 47 | * @param proof An array of ERC-6551 account information specifying the ownership path from this 48 | * account to its parent 49 | */ 50 | function executeNested( 51 | address to, 52 | uint256 value, 53 | bytes calldata data, 54 | uint8 operation, 55 | ERC6551AccountInfo[] calldata proof 56 | ) external payable returns (bytes memory) { 57 | uint256 length = proof.length; 58 | address current = _msgSender(); 59 | 60 | ERC6551AccountInfo calldata accountInfo; 61 | for (uint256 i = 0; i < length; i++) { 62 | accountInfo = proof[i]; 63 | address tokenContract = accountInfo.tokenContract; 64 | uint256 tokenId = accountInfo.tokenId; 65 | 66 | address next = ERC6551AccountLib.computeAddress( 67 | erc6551Registry, __self, accountInfo.salt, block.chainid, tokenContract, tokenId 68 | ); 69 | 70 | if (tokenContract.code.length == 0) revert InvalidAccountProof(); 71 | 72 | if (next.code.length > 0) { 73 | if (Lockable(next).isLocked()) revert AccountLocked(); 74 | } 75 | 76 | try IERC721(tokenContract).ownerOf(tokenId) returns (address _owner) { 77 | if (_owner != current) revert InvalidAccountProof(); 78 | current = next; 79 | } catch { 80 | revert InvalidAccountProof(); 81 | } 82 | } 83 | 84 | if (!_isValidExecutor(current)) revert NotAuthorized(); 85 | 86 | _beforeExecute(); 87 | 88 | return LibExecutor._execute(to, value, data, operation); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/abstract/execution/SandboxExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/utils/Create2.sol"; 5 | 6 | import "../../interfaces/ISandboxExecutor.sol"; 7 | import "../../utils/Errors.sol"; 8 | import "../../lib/LibSandbox.sol"; 9 | import "../../lib/LibExecutor.sol"; 10 | 11 | /** 12 | * @title Sandbox Executor 13 | * @dev Allows the sandbox contract for an account to execute low-level operations 14 | */ 15 | abstract contract SandboxExecutor is ISandboxExecutor { 16 | /** 17 | * @dev Ensures that a given caller is the sandbox for this account 18 | */ 19 | function _requireFromSandbox() internal view { 20 | if (msg.sender != LibSandbox.sandbox(address(this))) revert NotAuthorized(); 21 | } 22 | 23 | /** 24 | * @dev Allows the sandbox contract to execute low-level calls from this account 25 | */ 26 | function extcall(address to, uint256 value, bytes calldata data) 27 | external 28 | returns (bytes memory result) 29 | { 30 | _requireFromSandbox(); 31 | return LibExecutor._call(to, value, data); 32 | } 33 | 34 | /** 35 | * @dev Allows the sandbox contract to create contracts on behalf of this account 36 | */ 37 | function extcreate(uint256 value, bytes calldata bytecode) external returns (address) { 38 | _requireFromSandbox(); 39 | 40 | return LibExecutor._create(value, bytecode); 41 | } 42 | 43 | /** 44 | * @dev Allows the sandbox contract to create deterministic contracts on behalf of this account 45 | */ 46 | function extcreate2(uint256 value, bytes32 salt, bytes calldata bytecode) 47 | external 48 | returns (address) 49 | { 50 | _requireFromSandbox(); 51 | return LibExecutor._create2(value, salt, bytecode); 52 | } 53 | 54 | /** 55 | * @dev Allows arbitrary storage reads on this account from external contracts 56 | */ 57 | function extsload(bytes32 slot) external view returns (bytes32 value) { 58 | assembly { 59 | value := sload(slot) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/abstract/execution/TokenboundExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; 5 | 6 | import "erc6551/interfaces/IERC6551Executable.sol"; 7 | import "erc6551/interfaces/IERC6551Account.sol"; 8 | import "erc6551/lib/ERC6551AccountLib.sol"; 9 | 10 | import "../../utils/Errors.sol"; 11 | import "../../lib/LibExecutor.sol"; 12 | import "../../lib/LibSandbox.sol"; 13 | import "./ERC6551Executor.sol"; 14 | import "./BatchExecutor.sol"; 15 | import "./NestedAccountExecutor.sol"; 16 | 17 | /** 18 | * @title Tokenbound Executor 19 | * @dev Enables basic ERC-6551 execution as well as batch, nested, and mult-account execution 20 | */ 21 | abstract contract TokenboundExecutor is 22 | ERC6551Executor, 23 | BatchExecutor, 24 | NestedAccountExecutor, 25 | ERC2771Context 26 | { 27 | constructor(address multicallForwarder, address _erc6551Registry) 28 | ERC2771Context(multicallForwarder) 29 | NestedAccountExecutor(_erc6551Registry) 30 | { 31 | if (multicallForwarder == address(0)) revert InvalidMulticallForwarder(); 32 | } 33 | 34 | function _msgSender() 35 | internal 36 | view 37 | virtual 38 | override(Context, ERC2771Context) 39 | returns (address sender) 40 | { 41 | return super._msgSender(); 42 | } 43 | 44 | function _msgData() 45 | internal 46 | view 47 | virtual 48 | override(Context, ERC2771Context) 49 | returns (bytes calldata) 50 | { 51 | return super._msgData(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/cross-chain/FxChildExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | interface IFxMessageProcessor { 5 | function processMessageFromRoot(uint256 stateId, address rootMessageSender, bytes calldata data) 6 | external; 7 | } 8 | 9 | contract FxChildExecutor is IFxMessageProcessor { 10 | address public immutable fxChild; 11 | 12 | event Executed(bool success, bytes data); 13 | 14 | error InvalidSender(); 15 | 16 | constructor(address _fxChild) { 17 | fxChild = _fxChild; 18 | } 19 | 20 | function processMessageFromRoot(uint256, address rootMessageSender, bytes calldata data) 21 | external 22 | { 23 | if (msg.sender != fxChild) revert InvalidSender(); 24 | (bool success, bytes memory result) = rootMessageSender.call(data); 25 | emit Executed(success, result); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/interfaces/IAccountGuardian.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | interface IAccountGuardian { 5 | function setTrustedImplementation(address implementation, bool trusted) external; 6 | 7 | function setTrustedExecutor(address executor, bool trusted) external; 8 | 9 | function defaultImplementation() external view returns (address); 10 | 11 | function isTrustedImplementation(address implementation) external view returns (bool); 12 | 13 | function isTrustedExecutor(address implementation) external view returns (bool); 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/ISandboxExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | interface ISandboxExecutor { 5 | function extcall(address to, uint256 value, bytes calldata data) 6 | external 7 | returns (bytes memory result); 8 | 9 | function extcreate(uint256 value, bytes calldata data) external returns (address); 10 | 11 | function extcreate2(uint256 value, bytes32 salt, bytes calldata bytecode) 12 | external 13 | returns (address); 14 | 15 | function extsload(bytes32 slot) external view returns (bytes32 value); 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/LibExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../utils/Errors.sol"; 5 | import "./LibSandbox.sol"; 6 | 7 | library LibExecutor { 8 | uint8 constant OP_CALL = 0; 9 | uint8 constant OP_DELEGATECALL = 1; 10 | uint8 constant OP_CREATE = 2; 11 | uint8 constant OP_CREATE2 = 3; 12 | 13 | function _execute(address to, uint256 value, bytes calldata data, uint8 operation) 14 | internal 15 | returns (bytes memory) 16 | { 17 | if (operation == OP_CALL) return _call(to, value, data); 18 | if (operation == OP_DELEGATECALL) { 19 | address sandbox = LibSandbox.sandbox(address(this)); 20 | if (sandbox.code.length == 0) LibSandbox.deploy(address(this)); 21 | return _call(sandbox, value, abi.encodePacked(to, data)); 22 | } 23 | if (operation == OP_CREATE) return abi.encodePacked(_create(value, data)); 24 | if (operation == OP_CREATE2) { 25 | bytes32 salt = bytes32(data[:32]); 26 | bytes calldata bytecode = data[32:]; 27 | return abi.encodePacked(_create2(value, salt, bytecode)); 28 | } 29 | 30 | revert InvalidOperation(); 31 | } 32 | 33 | function _call(address to, uint256 value, bytes memory data) 34 | internal 35 | returns (bytes memory result) 36 | { 37 | bool success; 38 | (success, result) = to.call{value: value}(data); 39 | 40 | if (!success) { 41 | assembly { 42 | revert(add(result, 32), mload(result)) 43 | } 44 | } 45 | } 46 | 47 | function _create(uint256 value, bytes memory data) internal returns (address created) { 48 | bytes memory bytecode = data; 49 | 50 | assembly { 51 | created := create(value, add(bytecode, 0x20), mload(bytecode)) 52 | } 53 | 54 | if (created == address(0)) revert ContractCreationFailed(); 55 | } 56 | 57 | function _create2(uint256 value, bytes32 salt, bytes calldata data) 58 | internal 59 | returns (address created) 60 | { 61 | bytes memory bytecode = data; 62 | 63 | assembly { 64 | created := create2(value, add(bytecode, 0x20), mload(bytecode), salt) 65 | } 66 | 67 | if (created == address(0)) revert ContractCreationFailed(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/LibSandbox.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/utils/Create2.sol"; 5 | 6 | library LibSandbox { 7 | bytes public constant header = hex"604380600d600039806000f3fe73"; 8 | bytes public constant footer = 9 | hex"3314601d573d3dfd5b363d3d373d3d6014360360143d5160601c5af43d6000803e80603e573d6000fd5b3d6000f3"; 10 | 11 | function bytecode(address owner) internal pure returns (bytes memory) { 12 | return abi.encodePacked(header, owner, footer); 13 | } 14 | 15 | function sandbox(address owner) internal view returns (address) { 16 | return 17 | Create2.computeAddress(keccak256("org.tokenbound.sandbox"), keccak256(bytecode(owner))); 18 | } 19 | 20 | function deploy(address owner) internal { 21 | Create2.deploy(0, keccak256("org.tokenbound.sandbox"), bytecode(owner)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/OPAddressAliasHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | // Source: https://github.com/ethereum-optimism/optimism/blob/96562692558e5c3851899488bcebe51fbe3b7f09/packages/contracts-bedrock/src/vendor/AddressAliasHelper.sol 5 | library OPAddressAliasHelper { 6 | uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); 7 | 8 | /// @notice Utility function that converts the address in the L1 that submitted a tx to 9 | /// the inbox to the msg.sender viewed in the L2 10 | /// @param l1Address the address in the L1 that triggered the tx to L2 11 | /// @return l2Address L2 address as viewed in msg.sender 12 | function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { 13 | unchecked { 14 | l2Address = address(uint160(l1Address) + offset); 15 | } 16 | } 17 | 18 | /// @notice Utility function that converts the msg.sender viewed in the L2 to the 19 | /// address in the L1 that submitted a tx to the inbox 20 | /// @param l2Address L2 address as viewed in msg.sender 21 | /// @return l1Address the address in the L1 that triggered the tx to L2 22 | function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { 23 | unchecked { 24 | l1Address = address(uint160(l2Address) - offset); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tokenbound/contracts", 3 | "version": "0.3.1-beta.0", 4 | "files": [ 5 | "**/*.sol", 6 | "README.md" 7 | ], 8 | "description": "Test TokenBound implementation", 9 | "scripts": { 10 | "prepublishOnly": "../script/verify-package-json-in-sync.js" 11 | }, 12 | "peerDependencies": { 13 | "@account-abstraction/contracts": "^0.6.0", 14 | "@openzeppelin/contracts": "^4.9.3", 15 | "erc6551": "^0.3.1" 16 | }, 17 | "publishConfig": { 18 | "access": "public" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/Errors.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | error InvalidOperation(); 5 | error ContractCreationFailed(); 6 | error NotAuthorized(); 7 | error InvalidInput(); 8 | error ExceedsMaxLockTime(); 9 | error AccountLocked(); 10 | error InvalidAccountProof(); 11 | error InvalidGuardian(); 12 | error InvalidImplementation(); 13 | error AlreadyInitialized(); 14 | error InvalidEntryPoint(); 15 | error InvalidMulticallForwarder(); 16 | error InvalidERC6551Registry(); 17 | error OwnershipCycle(); 18 | -------------------------------------------------------------------------------- /test/Account.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "@openzeppelin/contracts/utils/Create2.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 9 | import "@openzeppelin/contracts/proxy/Clones.sol"; 10 | 11 | import "erc6551/ERC6551Registry.sol"; 12 | import "erc6551/interfaces/IERC6551Account.sol"; 13 | import "erc6551/interfaces/IERC6551Executable.sol"; 14 | 15 | import "multicall-authenticated/Multicall3.sol"; 16 | 17 | import "../src/AccountV3.sol"; 18 | import "../src/AccountV3Upgradable.sol"; 19 | import "../src/AccountGuardian.sol"; 20 | import "../src/AccountProxy.sol"; 21 | 22 | import "./mocks/MockERC721.sol"; 23 | import "./mocks/MockSigner.sol"; 24 | import "./mocks/MockExecutor.sol"; 25 | import "./mocks/MockSandboxExecutor.sol"; 26 | import "./mocks/MockReverter.sol"; 27 | import "./mocks/MockAccountUpgradable.sol"; 28 | 29 | contract AccountTest is Test { 30 | Multicall3 forwarder; 31 | AccountV3 implementation; 32 | AccountV3Upgradable upgradableImplementation; 33 | AccountProxy proxy; 34 | ERC6551Registry public registry; 35 | AccountGuardian public guardian; 36 | 37 | MockERC721 public tokenCollection; 38 | 39 | function setUp() public { 40 | registry = new ERC6551Registry(); 41 | 42 | forwarder = new Multicall3(); 43 | guardian = new AccountGuardian(address(this)); 44 | implementation = new AccountV3( 45 | address(1), address(forwarder), address(registry), address(guardian) 46 | ); 47 | upgradableImplementation = new AccountV3Upgradable( 48 | address(1), address(forwarder), address(registry), address(guardian) 49 | ); 50 | proxy = new AccountProxy(address(guardian), address(upgradableImplementation)); 51 | 52 | tokenCollection = new MockERC721(); 53 | 54 | // mint tokenId 1 during setup for accurate cold call gas measurement 55 | uint256 tokenId = 1; 56 | address user1 = vm.addr(1); 57 | tokenCollection.mint(user1, tokenId); 58 | } 59 | 60 | function testNonOwnerCallsFail() public { 61 | uint256 tokenId = 1; 62 | address user2 = vm.addr(2); 63 | 64 | address accountAddress = registry.createAccount( 65 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 66 | ); 67 | 68 | vm.deal(accountAddress, 1 ether); 69 | 70 | AccountV3 account = AccountV3(payable(accountAddress)); 71 | 72 | // should fail if user2 tries to use account 73 | vm.prank(user2); 74 | vm.expectRevert(NotAuthorized.selector); 75 | account.execute(payable(user2), 0.1 ether, "", LibExecutor.OP_CALL); 76 | 77 | // should fail if user2 tries to use account 78 | vm.prank(user2); 79 | vm.expectRevert(NotAuthorized.selector); 80 | account.execute(payable(user2), 0.1 ether, "", LibExecutor.OP_CALL); 81 | } 82 | 83 | function testAccountOwnershipTransfer() public { 84 | uint256 tokenId = 1; 85 | address user1 = vm.addr(1); 86 | address user2 = vm.addr(2); 87 | 88 | address accountAddress = registry.createAccount( 89 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 90 | ); 91 | 92 | vm.deal(accountAddress, 1 ether); 93 | 94 | AccountV3 account = AccountV3(payable(accountAddress)); 95 | 96 | // should succeed with original owner 97 | vm.prank(user1); 98 | account.execute(payable(user1), 0.1 ether, "", LibExecutor.OP_CALL); 99 | 100 | // should fail if user2 tries to use account 101 | vm.prank(user2); 102 | vm.expectRevert(NotAuthorized.selector); 103 | account.execute(payable(user2), 0.1 ether, "", LibExecutor.OP_CALL); 104 | 105 | vm.prank(user1); 106 | tokenCollection.safeTransferFrom(user1, user2, tokenId); 107 | 108 | // should succeed now that user2 is owner 109 | vm.prank(user2); 110 | account.execute(payable(user2), 0.1 ether, "", LibExecutor.OP_CALL); 111 | 112 | assertEq(user2.balance, 0.1 ether); 113 | } 114 | 115 | function testSignatureVerification() public { 116 | uint256 tokenId = 1; 117 | 118 | address accountAddress = registry.createAccount( 119 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 120 | ); 121 | 122 | AccountV3 account = AccountV3(payable(accountAddress)); 123 | 124 | bytes32 hash = keccak256("This is a signed message"); 125 | (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(1, hash); 126 | 127 | // ECDSA signature 128 | bytes memory signature1 = abi.encodePacked(r1, s1, v1); 129 | bytes4 returnValue = account.isValidSignature(hash, signature1); 130 | assertEq(returnValue, IERC1271.isValidSignature.selector); 131 | 132 | MockSigner mockSigner = new MockSigner(); 133 | 134 | address[] memory callers = new address[](1); 135 | callers[0] = address(mockSigner); 136 | bool[] memory _permissions = new bool[](1); 137 | _permissions[0] = true; 138 | 139 | vm.prank(vm.addr(1)); 140 | account.setPermissions(callers, _permissions); 141 | 142 | // ERC-1271 signature 143 | bytes memory contractSignature = abi.encodePacked( 144 | uint256(uint160(address(mockSigner))), uint256(65), uint8(0), signature1 145 | ); 146 | returnValue = account.isValidSignature(hash, contractSignature); 147 | assertEq(returnValue, IERC1271.isValidSignature.selector); 148 | 149 | // ERC-1271 signature invalid 150 | _permissions[0] = false; 151 | vm.prank(vm.addr(1)); 152 | account.setPermissions(callers, _permissions); 153 | returnValue = account.isValidSignature(hash, contractSignature); 154 | assertEq(returnValue, bytes4(0)); 155 | 156 | // Recursive account signature 157 | bytes memory recursiveSignature = 158 | abi.encodePacked(uint256(uint160(address(account))), uint256(65), uint8(0), signature1); 159 | returnValue = account.isValidSignature(hash, recursiveSignature); 160 | assertEq(returnValue, IERC1271.isValidSignature.selector); 161 | } 162 | 163 | function testSignatureVerificationFailsInvalidSigner() public { 164 | uint256 tokenId = 1; 165 | 166 | address accountAddress = registry.createAccount( 167 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 168 | ); 169 | 170 | AccountV3 account = AccountV3(payable(accountAddress)); 171 | 172 | bytes32 hash = keccak256("This is a signed message"); 173 | 174 | (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(2, hash); 175 | bytes memory signature2 = abi.encodePacked(r2, s2, v2); 176 | 177 | bytes4 returnValue2 = account.isValidSignature(hash, signature2); 178 | 179 | assertFalse(returnValue2 == IERC1271.isValidSignature.selector); 180 | } 181 | 182 | function testAccountLocksAndUnlocks() public { 183 | uint256 tokenId = 1; 184 | address user1 = vm.addr(1); 185 | address user2 = vm.addr(2); 186 | 187 | address accountAddress = registry.createAccount( 188 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 189 | ); 190 | 191 | vm.deal(accountAddress, 1 ether); 192 | 193 | AccountV3 account = AccountV3(payable(accountAddress)); 194 | 195 | // cannot lock account if invalid signer 196 | vm.prank(user2); 197 | vm.expectRevert(NotAuthorized.selector); 198 | account.lock(1 days); 199 | 200 | // cannot be locked for more than 365 days 201 | vm.prank(user1); 202 | vm.expectRevert(ExceedsMaxLockTime.selector); 203 | account.lock(366 days); 204 | 205 | uint256 state = account.state(); 206 | 207 | // lock account for 10 days 208 | uint256 unlockTimestamp = block.timestamp + 10 days; 209 | vm.prank(user1); 210 | account.lock(unlockTimestamp); 211 | 212 | // locking account should change state 213 | assertTrue(state != account.state()); 214 | 215 | assertEq(account.isLocked(), true); 216 | 217 | // transaction should revert if account is locked 218 | vm.prank(user1); 219 | vm.expectRevert(AccountLocked.selector); 220 | account.execute(payable(user1), 1 ether, "", LibExecutor.OP_CALL); 221 | 222 | // fallback calls should revert if account is locked 223 | vm.prank(user1); 224 | (bool success, bytes memory result) = 225 | accountAddress.call(abi.encodeWithSignature("customFunction()")); 226 | 227 | console.log(success); 228 | console.logBytes(result); 229 | 230 | // setOverrides calls should revert if account is locked 231 | { 232 | bytes4[] memory selectors = new bytes4[](1); 233 | selectors[0] = IERC721Receiver.onERC721Received.selector; 234 | address[] memory implementations = new address[](1); 235 | implementations[0] = vm.addr(1337); 236 | vm.prank(user1); 237 | vm.expectRevert(AccountLocked.selector); 238 | account.setOverrides(selectors, implementations); 239 | } 240 | 241 | // lock calls should revert if account is locked 242 | vm.prank(user1); 243 | vm.expectRevert(AccountLocked.selector); 244 | account.lock(0); 245 | 246 | // signing should fail if account is locked 247 | { 248 | bytes32 hash = keccak256("This is a signed message"); 249 | (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(2, hash); 250 | bytes memory signature1 = abi.encodePacked(r1, s1, v1); 251 | bytes4 returnValue = account.isValidSignature(hash, signature1); 252 | assertEq(returnValue, 0); 253 | } 254 | 255 | // warp to timestamp after account is unlocked 256 | vm.warp(unlockTimestamp + 1 days); 257 | 258 | // transaction succeed now that account lock has expired 259 | vm.prank(user1); 260 | account.execute(payable(user1), 1 ether, "", 0); 261 | assertEq(user1.balance, 1 ether); 262 | 263 | // signing should now that account lock has expired 264 | bytes32 hashAfterUnlock = keccak256("This is a signed message"); 265 | (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(1, hashAfterUnlock); 266 | bytes memory signature2 = abi.encodePacked(r2, s2, v2); 267 | bytes4 returnValue1 = account.isValidSignature(hashAfterUnlock, signature2); 268 | assertEq(returnValue1, IERC1271.isValidSignature.selector); 269 | } 270 | 271 | function testExecuteCallRevert() public { 272 | uint256 tokenId = 1; 273 | address user1 = vm.addr(1); 274 | 275 | address accountAddress = registry.createAccount( 276 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 277 | ); 278 | 279 | vm.deal(accountAddress, 1 ether); 280 | 281 | AccountV3 account = AccountV3(payable(accountAddress)); 282 | 283 | MockReverter mockReverter = new MockReverter(); 284 | 285 | vm.prank(user1); 286 | vm.expectRevert(MockReverter.MockError.selector); 287 | account.execute(payable(address(mockReverter)), 0, abi.encodeWithSignature("fail()"), 0); 288 | } 289 | 290 | function testExecuteInvalidOperation() public { 291 | uint256 tokenId = 1; 292 | address user1 = vm.addr(1); 293 | 294 | address accountAddress = registry.createAccount( 295 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 296 | ); 297 | 298 | vm.deal(accountAddress, 1 ether); 299 | 300 | AccountV3 account = AccountV3(payable(accountAddress)); 301 | 302 | vm.prank(user1); 303 | vm.expectRevert(InvalidOperation.selector); 304 | account.execute(vm.addr(2), 0.1 ether, "", type(uint8).max); 305 | } 306 | 307 | function testExecuteCreate() public { 308 | uint256 tokenId = 1; 309 | address user1 = vm.addr(1); 310 | address user2 = vm.addr(2); 311 | 312 | address accountAddress = registry.createAccount( 313 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 314 | ); 315 | 316 | AccountV3 account = AccountV3(payable(accountAddress)); 317 | 318 | uint256 state = account.state(); 319 | 320 | // should succeed when called by owner 321 | vm.prank(user1); 322 | bytes memory result = account.execute(address(0), 0, type(MockERC721).creationCode, 2); 323 | 324 | address deployedContract = address(uint160(uint256(bytes32(result)) >> 96)); 325 | 326 | // batch execution should change state 327 | assertTrue(state != account.state()); 328 | 329 | assertTrue(deployedContract.code.length > 0); 330 | 331 | // should fail when called by non-owner 332 | vm.prank(user2); 333 | vm.expectRevert(NotAuthorized.selector); 334 | account.execute(address(0), 0, type(MockERC721).creationCode, 2); 335 | } 336 | 337 | function testExecuteCreate2() public { 338 | uint256 tokenId = 1; 339 | address user1 = vm.addr(1); 340 | address user2 = vm.addr(2); 341 | 342 | address accountAddress = registry.createAccount( 343 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 344 | ); 345 | 346 | AccountV3 account = AccountV3(payable(accountAddress)); 347 | 348 | address computedContract = Create2.computeAddress( 349 | keccak256("salt"), keccak256(type(MockERC721).creationCode), accountAddress 350 | ); 351 | bytes memory payload = abi.encodePacked(keccak256("salt"), type(MockERC721).creationCode); 352 | 353 | uint256 state = account.state(); 354 | 355 | // should succeed when called by owner 356 | vm.prank(user1); 357 | bytes memory result = account.execute(address(0), 0, payload, 3); 358 | 359 | address deployedContract = address(uint160(uint256(bytes32(result)) >> 96)); 360 | 361 | // batch execution should change state 362 | assertTrue(state != account.state()); 363 | 364 | assertEq(computedContract, deployedContract); 365 | assertTrue(deployedContract.code.length > 0); 366 | 367 | // should fail when called by non-owner 368 | vm.prank(user2); 369 | vm.expectRevert(NotAuthorized.selector); 370 | account.execute(address(0), 0, payload, 2); 371 | } 372 | 373 | function testExecuteBatch() public { 374 | uint256 tokenId = 1; 375 | address user1 = vm.addr(1); 376 | address user2 = vm.addr(2); 377 | 378 | address accountAddress = registry.createAccount( 379 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 380 | ); 381 | 382 | vm.deal(accountAddress, 1 ether); 383 | 384 | AccountV3 account = AccountV3(payable(accountAddress)); 385 | 386 | BatchExecutor.Operation[] memory operations = new BatchExecutor.Operation[](3); 387 | operations[0] = BatchExecutor.Operation(vm.addr(2), 0.1 ether, "", 0); 388 | operations[1] = BatchExecutor.Operation(vm.addr(3), 0.1 ether, "", 0); 389 | operations[2] = BatchExecutor.Operation(vm.addr(4), 0.1 ether, "", 0); 390 | 391 | uint256 state = account.state(); 392 | 393 | // should succeed when called by owner 394 | vm.prank(user1); 395 | account.executeBatch(operations); 396 | 397 | // batch execution should change state 398 | assertTrue(state != account.state()); 399 | 400 | assertEq(vm.addr(2).balance, 0.1 ether); 401 | assertEq(vm.addr(3).balance, 0.1 ether); 402 | assertEq(vm.addr(4).balance, 0.1 ether); 403 | 404 | // should fail when called by non-owner 405 | vm.prank(user2); 406 | vm.expectRevert(NotAuthorized.selector); 407 | account.executeBatch(operations); 408 | } 409 | 410 | function testExecuteNested() public { 411 | uint256 tokenId = 1; 412 | address user1 = vm.addr(1); 413 | address user2 = vm.addr(2); 414 | 415 | address accountAddress = registry.createAccount( 416 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 417 | ); 418 | 419 | tokenCollection.mint(accountAddress, 2); 420 | 421 | // Account for tokenId 2 not deployed 422 | address accountAddress2 = 423 | registry.account(address(implementation), 0, block.chainid, address(tokenCollection), 2); 424 | 425 | tokenCollection.mint(accountAddress2, 3); 426 | 427 | address accountAddress3 = registry.createAccount( 428 | address(implementation), 0, block.chainid, address(tokenCollection), 3 429 | ); 430 | 431 | vm.deal(accountAddress3, 1 ether); 432 | 433 | console.log("accounts"); 434 | console.log(vm.addr(1), accountAddress, accountAddress2, accountAddress3); 435 | 436 | AccountV3 nestedAccount = AccountV3(payable(accountAddress3)); 437 | 438 | NestedAccountExecutor.ERC6551AccountInfo[] memory proof = 439 | new NestedAccountExecutor.ERC6551AccountInfo[](2); 440 | proof[0] = NestedAccountExecutor.ERC6551AccountInfo(0, address(tokenCollection), 1); 441 | proof[1] = NestedAccountExecutor.ERC6551AccountInfo(0, address(tokenCollection), 2); 442 | 443 | uint256 state = nestedAccount.state(); 444 | 445 | // should succeed when called by owner 446 | vm.prank(user1); 447 | nestedAccount.executeNested(vm.addr(2), 0.1 ether, "", 0, proof); 448 | 449 | // nested execution should change state 450 | assertTrue(state != nestedAccount.state()); 451 | 452 | assertEq(vm.addr(2).balance, 0.1 ether); 453 | 454 | // should fail when called by non-owner 455 | vm.prank(user2); 456 | vm.expectRevert(InvalidAccountProof.selector); 457 | nestedAccount.executeNested(vm.addr(2), 0.1 ether, "", 0, proof); 458 | } 459 | 460 | function testExecuteForwarder() public { 461 | uint256 tokenId = 1; 462 | address user1 = vm.addr(1); 463 | address user2 = vm.addr(2); 464 | 465 | address accountAddress = registry.createAccount( 466 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 467 | ); 468 | 469 | tokenCollection.mint(user1, 2); 470 | 471 | address accountAddress2 = registry.createAccount( 472 | address(implementation), 0, block.chainid, address(tokenCollection), 2 473 | ); 474 | 475 | tokenCollection.mint(user1, 3); 476 | 477 | address accountAddress3 = registry.createAccount( 478 | address(implementation), 0, block.chainid, address(tokenCollection), 3 479 | ); 480 | 481 | vm.deal(accountAddress, 1 ether); 482 | vm.deal(accountAddress2, 1 ether); 483 | vm.deal(accountAddress3, 1 ether); 484 | 485 | Multicall3.Call3[] memory calls = new Multicall3.Call3[](3); 486 | calls[0] = Multicall3.Call3( 487 | accountAddress, 488 | false, 489 | abi.encodeWithSignature( 490 | "execute(address,uint256,bytes,uint8)", 491 | vm.addr(2), 492 | 0.1 ether, 493 | "", 494 | LibExecutor.OP_CALL 495 | ) 496 | ); 497 | calls[1] = Multicall3.Call3( 498 | accountAddress2, 499 | false, 500 | abi.encodeWithSignature( 501 | "execute(address,uint256,bytes,uint8)", 502 | vm.addr(2), 503 | 0.1 ether, 504 | "", 505 | LibExecutor.OP_CALL 506 | ) 507 | ); 508 | calls[2] = Multicall3.Call3( 509 | accountAddress3, 510 | false, 511 | abi.encodeWithSignature( 512 | "execute(address,uint256,bytes,uint8)", 513 | vm.addr(2), 514 | 0.1 ether, 515 | "", 516 | LibExecutor.OP_CALL 517 | ) 518 | ); 519 | 520 | vm.prank(user1); 521 | Multicall3.Result[] memory results = forwarder.aggregate3(calls); 522 | for (uint256 i = 0; i < results.length; i++) { 523 | assertTrue(results[i].success); 524 | assertEq(results[i].returnData, abi.encode(new bytes(0))); 525 | } 526 | 527 | assertEq(user2.balance, 0.3 ether); 528 | 529 | // should fail when called by non-owner 530 | vm.prank(user2); 531 | vm.expectRevert("Multicall3: call failed"); 532 | results = forwarder.aggregate3(calls); 533 | 534 | // balance should not have changed 535 | assertEq(user2.balance, 0.3 ether); 536 | 537 | for (uint256 i = 0; i < results.length; i++) { 538 | assertFalse(results[i].success); 539 | assertEq(bytes4(results[i].returnData), NotAuthorized.selector); 540 | } 541 | } 542 | 543 | function testExecuteSandbox() public { 544 | uint256 tokenId = 1; 545 | address user1 = vm.addr(1); 546 | 547 | address accountAddress = registry.createAccount( 548 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 549 | ); 550 | 551 | vm.deal(accountAddress, 1 ether); 552 | 553 | AccountV3 account = AccountV3(payable(accountAddress)); 554 | 555 | vm.expectRevert(NotAuthorized.selector); 556 | account.extcall(vm.addr(2), 0.1 ether, ""); 557 | 558 | vm.expectRevert(NotAuthorized.selector); 559 | account.extcreate(0, type(MockERC721).creationCode); 560 | 561 | vm.expectRevert(NotAuthorized.selector); 562 | account.extcreate2(0, keccak256("salt"), type(MockERC721).creationCode); 563 | 564 | MockSandboxExecutor mockSandboxExecutor = new MockSandboxExecutor(); 565 | 566 | vm.prank(user1); 567 | vm.expectRevert(MockReverter.MockError.selector); 568 | account.execute( 569 | address(mockSandboxExecutor), 570 | 0, 571 | abi.encodeWithSignature("fail()"), 572 | LibExecutor.OP_DELEGATECALL 573 | ); 574 | 575 | vm.prank(user1); 576 | bytes memory result = account.execute( 577 | address(mockSandboxExecutor), 578 | 0, 579 | abi.encodeWithSignature("customFunction()"), 580 | LibExecutor.OP_DELEGATECALL 581 | ); 582 | 583 | assertEq(uint256(bytes32(result)), 12345); 584 | 585 | vm.prank(user1); 586 | result = account.execute( 587 | address(mockSandboxExecutor), 588 | 0, 589 | abi.encodeWithSignature("sentEther(address,uint256)", vm.addr(2), 0.1 ether), 590 | LibExecutor.OP_DELEGATECALL 591 | ); 592 | 593 | assertEq(accountAddress.balance, 0.9 ether); 594 | assertEq(vm.addr(2).balance, 0.1 ether); 595 | 596 | vm.prank(user1); 597 | result = account.execute( 598 | address(mockSandboxExecutor), 599 | 0, 600 | abi.encodeWithSignature("createNFT()"), 601 | LibExecutor.OP_DELEGATECALL 602 | ); 603 | address deployedNFT = address(uint160(uint256(bytes32(result)))); 604 | 605 | assertTrue(deployedNFT.code.length > 0); 606 | 607 | vm.prank(user1); 608 | result = account.execute( 609 | address(mockSandboxExecutor), 610 | 0, 611 | abi.encodeWithSignature("createNFTDeterministic()"), 612 | LibExecutor.OP_DELEGATECALL 613 | ); 614 | deployedNFT = address(uint160(uint256(bytes32(result)))); 615 | 616 | assertTrue(deployedNFT.code.length > 0); 617 | 618 | vm.prank(user1); 619 | result = account.execute( 620 | address(mockSandboxExecutor), 621 | 0, 622 | abi.encodeWithSignature("getSlot0()"), 623 | LibExecutor.OP_DELEGATECALL 624 | ); 625 | assertEq(uint256(bytes32(result)), 0); 626 | } 627 | 628 | function testAccountOwnerIsNullIfContextNotSet() public { 629 | address accountClone = Clones.clone(address(implementation)); 630 | 631 | assertEq(AccountV3(payable(accountClone)).owner(), address(0)); 632 | } 633 | 634 | function testEIP165Support() public { 635 | uint256 tokenId = 1; 636 | 637 | address accountAddress = registry.createAccount( 638 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 639 | ); 640 | 641 | vm.deal(accountAddress, 1 ether); 642 | 643 | AccountV3 account = AccountV3(payable(accountAddress)); 644 | 645 | assertEq(account.supportsInterface(0x6faff5f1), true); // IERC6551Account 646 | assertEq(account.supportsInterface(0x51945447), true); // IERC6551Executable 647 | assertEq(account.supportsInterface(type(IERC1155Receiver).interfaceId), true); 648 | assertEq(account.supportsInterface(type(IERC165).interfaceId), true); 649 | } 650 | 651 | function testAccountUpgrade() public { 652 | uint256 tokenId = 1; 653 | address user1 = vm.addr(1); 654 | 655 | address accountAddress = registry.createAccount( 656 | address(proxy), 0, block.chainid, address(tokenCollection), tokenId 657 | ); 658 | 659 | AccountProxy(payable(accountAddress)).initialize(address(upgradableImplementation)); 660 | AccountV3Upgradable account = AccountV3Upgradable(payable(accountAddress)); 661 | 662 | MockAccountUpgradable upgradedImplementation = new MockAccountUpgradable( 663 | address(1), 664 | address(1), 665 | address(1), 666 | address(1) 667 | ); 668 | 669 | vm.expectRevert(InvalidImplementation.selector); 670 | vm.prank(user1); 671 | account.upgradeTo(address(upgradedImplementation)); 672 | 673 | guardian.setTrustedImplementation(address(upgradedImplementation), true); 674 | 675 | vm.prank(user1); 676 | account.upgradeTo(address(upgradedImplementation)); 677 | uint256 returnValue = MockAccountUpgradable(payable(accountAddress)).customFunction(); 678 | 679 | assertEq(returnValue, 12345); 680 | } 681 | 682 | function testProxyZeroAddressInit() public { 683 | vm.expectRevert(InvalidImplementation.selector); 684 | new AccountProxy(address(1), address(0)); 685 | vm.expectRevert(InvalidImplementation.selector); 686 | new AccountProxy(address(0), address(1)); 687 | } 688 | } 689 | -------------------------------------------------------------------------------- /test/AccountCrossChain.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "@openzeppelin/contracts/utils/Create2.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 9 | import "@openzeppelin/contracts/proxy/Clones.sol"; 10 | 11 | import "erc6551/ERC6551Registry.sol"; 12 | import "erc6551/interfaces/IERC6551Account.sol"; 13 | import "erc6551/interfaces/IERC6551Executable.sol"; 14 | 15 | import "../src/AccountV3.sol"; 16 | import "../src/AccountV3Upgradable.sol"; 17 | import "../src/AccountGuardian.sol"; 18 | import "../src/AccountProxy.sol"; 19 | 20 | import "../src/lib/OPAddressAliasHelper.sol"; 21 | 22 | import "./mocks/MockERC721.sol"; 23 | import "./mocks/MockSigner.sol"; 24 | import "./mocks/MockExecutor.sol"; 25 | import "./mocks/MockSandboxExecutor.sol"; 26 | import "./mocks/MockReverter.sol"; 27 | import "./mocks/MockAccountUpgradable.sol"; 28 | 29 | contract AccountTest is Test { 30 | AccountV3 implementation; 31 | AccountV3Upgradable upgradableImplementation; 32 | ERC6551Registry public registry; 33 | AccountGuardian public guardian; 34 | 35 | MockERC721 public tokenCollection; 36 | 37 | uint256 fork1; 38 | uint256 fork2; 39 | 40 | function setUp() public { 41 | fork1 = vm.createFork("https://ethereum.publicnode.com"); 42 | fork2 = vm.createFork("https://optimism.publicnode.com"); 43 | 44 | registry = new ERC6551Registry(); 45 | 46 | guardian = new AccountGuardian(address(this)); 47 | implementation = new AccountV3(address(1), address(1), address(registry), address(guardian)); 48 | 49 | vm.makePersistent(address(registry)); 50 | vm.makePersistent(address(guardian)); 51 | vm.makePersistent(address(implementation)); 52 | 53 | // collection only exists on fork1 54 | vm.selectFork(fork1); 55 | tokenCollection = new MockERC721(); 56 | uint256 tokenId = 1; 57 | address user1 = vm.addr(1); 58 | tokenCollection.mint(user1, tokenId); 59 | } 60 | 61 | function testCrossChainCalls() public { 62 | uint256 tokenId = 1; 63 | address user1 = vm.addr(1); 64 | address crossChainExecutor = vm.addr(2); 65 | 66 | // create account on fork1 67 | vm.selectFork(fork1); 68 | assertEq(tokenCollection.ownerOf(tokenId), user1); 69 | uint256 chainId = block.chainid + 1; 70 | address accountAddress = registry.createAccount( 71 | address(implementation), 0, chainId, address(tokenCollection), tokenId 72 | ); 73 | 74 | // create non-native account on fork2 75 | vm.selectFork(fork2); 76 | assertEq(address(tokenCollection).code.length, 0); 77 | assertFalse(chainId == block.chainid); 78 | registry.createAccount( 79 | address(implementation), 0, chainId, address(tokenCollection), tokenId 80 | ); 81 | 82 | vm.deal(accountAddress, 1 ether); 83 | 84 | AccountV3 account = AccountV3(payable(accountAddress)); 85 | 86 | vm.prank(crossChainExecutor); 87 | vm.expectRevert(NotAuthorized.selector); 88 | account.execute(user1, 0.1 ether, "", 0); 89 | assertEq(user1.balance, 0 ether); 90 | 91 | guardian.setTrustedExecutor(crossChainExecutor, true); 92 | 93 | vm.prank(crossChainExecutor); 94 | account.execute(user1, 0.1 ether, "", 0); 95 | assertEq(user1.balance, 0.1 ether); 96 | 97 | address notCrossChainExecutor = vm.addr(3); 98 | vm.prank(notCrossChainExecutor); 99 | vm.expectRevert(NotAuthorized.selector); 100 | account.execute(user1, 0.1 ether, "", 0); 101 | assertEq(user1.balance, 0.1 ether); 102 | 103 | address nativeAccountAddress = registry.createAccount( 104 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 105 | ); 106 | 107 | AccountV3 nativeAccount = AccountV3(payable(nativeAccountAddress)); 108 | 109 | vm.prank(crossChainExecutor); 110 | vm.expectRevert(NotAuthorized.selector); 111 | nativeAccount.execute(user1, 0.1 ether, "", 0); 112 | assertEq(user1.balance, 0.1 ether); 113 | } 114 | 115 | function testCrossChainCallsOPStack() public { 116 | uint256 tokenId = 1; 117 | address user1 = vm.addr(1); 118 | 119 | // create account on fork1 120 | vm.selectFork(fork1); 121 | assertEq(tokenCollection.ownerOf(tokenId), user1); 122 | uint256 chainId = block.chainid + 1; 123 | address accountAddress = registry.createAccount( 124 | address(implementation), 0, chainId, address(tokenCollection), tokenId 125 | ); 126 | 127 | // create non-native account on fork2 128 | vm.selectFork(fork2); 129 | assertEq(address(tokenCollection).code.length, 0); 130 | assertFalse(chainId == block.chainid); 131 | registry.createAccount( 132 | address(implementation), 0, chainId, address(tokenCollection), tokenId 133 | ); 134 | 135 | vm.deal(accountAddress, 1 ether); 136 | 137 | AccountV3 account = AccountV3(payable(accountAddress)); 138 | 139 | // fork1 owner cannot access account 140 | vm.prank(vm.addr(1)); 141 | vm.expectRevert(NotAuthorized.selector); 142 | account.execute(user1, 0.1 ether, "", 0); 143 | assertEq(user1.balance, 0 ether); 144 | 145 | // account can access account via optimism portal 146 | vm.prank(OPAddressAliasHelper.applyL1ToL2Alias(accountAddress)); 147 | account.execute(user1, 0.1 ether, "", 0); 148 | assertEq(user1.balance, 0.1 ether); 149 | 150 | address nativeAccountAddress = registry.createAccount( 151 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 152 | ); 153 | 154 | AccountV3 nativeAccount = AccountV3(payable(nativeAccountAddress)); 155 | 156 | // portal cannot be used to access native OP accounts 157 | vm.prank(OPAddressAliasHelper.applyL1ToL2Alias(accountAddress)); 158 | vm.expectRevert(NotAuthorized.selector); 159 | nativeAccount.execute(user1, 0.1 ether, "", 0); 160 | assertEq(user1.balance, 0.1 ether); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /test/AccountERC1155.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/proxy/Clones.sol"; 8 | 9 | import "erc6551/ERC6551Registry.sol"; 10 | import "erc6551/interfaces/IERC6551Account.sol"; 11 | 12 | import "../src/AccountV3.sol"; 13 | import "../src/AccountGuardian.sol"; 14 | 15 | import "./mocks/MockERC721.sol"; 16 | import "./mocks/MockERC1155.sol"; 17 | import "./mocks/MockExecutor.sol"; 18 | 19 | contract AccountERC1155Test is Test { 20 | MockERC1155 public dummyERC1155; 21 | 22 | AccountV3 implementation; 23 | ERC6551Registry public registry; 24 | 25 | MockERC721 public tokenCollection; 26 | 27 | function setUp() public { 28 | dummyERC1155 = new MockERC1155(); 29 | 30 | implementation = new AccountV3(address(1), address(1), address(1), address(1)); 31 | registry = new ERC6551Registry(); 32 | 33 | tokenCollection = new MockERC721(); 34 | } 35 | 36 | function testTransferERC1155PreDeploy(uint256 tokenId) public { 37 | address user1 = vm.addr(1); 38 | 39 | address computedAccountInstance = registry.account( 40 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 41 | ); 42 | 43 | tokenCollection.mint(user1, tokenId); 44 | assertEq(tokenCollection.ownerOf(tokenId), user1); 45 | 46 | dummyERC1155.mint(computedAccountInstance, 1, 10); 47 | 48 | assertEq(dummyERC1155.balanceOf(computedAccountInstance, 1), 10); 49 | 50 | address accountAddress = registry.createAccount( 51 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 52 | ); 53 | 54 | AccountV3 account = AccountV3(payable(accountAddress)); 55 | 56 | bytes memory erc1155TransferCall = abi.encodeWithSignature( 57 | "safeTransferFrom(address,address,uint256,uint256,bytes)", account, user1, 1, 10, "" 58 | ); 59 | vm.prank(user1); 60 | account.execute(payable(address(dummyERC1155)), 0, erc1155TransferCall, 0); 61 | 62 | assertEq(dummyERC1155.balanceOf(accountAddress, 1), 0); 63 | assertEq(dummyERC1155.balanceOf(user1, 1), 10); 64 | } 65 | 66 | function testTransferERC1155PostDeploy(uint256 tokenId) public { 67 | address user1 = vm.addr(1); 68 | 69 | address accountAddress = registry.createAccount( 70 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 71 | ); 72 | 73 | tokenCollection.mint(user1, tokenId); 74 | assertEq(tokenCollection.ownerOf(tokenId), user1); 75 | 76 | dummyERC1155.mint(accountAddress, 1, 10); 77 | 78 | assertEq(dummyERC1155.balanceOf(accountAddress, 1), 10); 79 | 80 | AccountV3 account = AccountV3(payable(accountAddress)); 81 | 82 | bytes memory erc1155TransferCall = abi.encodeWithSignature( 83 | "safeTransferFrom(address,address,uint256,uint256,bytes)", account, user1, 1, 10, "" 84 | ); 85 | vm.prank(user1); 86 | account.execute(payable(address(dummyERC1155)), 0, erc1155TransferCall, 0); 87 | 88 | assertEq(dummyERC1155.balanceOf(accountAddress, 1), 0); 89 | assertEq(dummyERC1155.balanceOf(user1, 1), 10); 90 | } 91 | 92 | function testBatchTransferERC1155(uint256 tokenId) public { 93 | address user1 = vm.addr(1); 94 | 95 | address accountAddress = registry.createAccount( 96 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 97 | ); 98 | 99 | tokenCollection.mint(user1, tokenId); 100 | assertEq(tokenCollection.ownerOf(tokenId), user1); 101 | 102 | dummyERC1155.mint(user1, 1, 10); 103 | dummyERC1155.mint(user1, 2, 10); 104 | 105 | uint256[] memory ids = new uint256[](2); 106 | ids[0] = 1; 107 | ids[1] = 2; 108 | uint256[] memory amounts = new uint256[](2); 109 | amounts[0] = 10; 110 | amounts[1] = 10; 111 | 112 | vm.prank(user1); 113 | dummyERC1155.safeBatchTransferFrom(user1, accountAddress, ids, amounts, ""); 114 | 115 | assertEq(dummyERC1155.balanceOf(accountAddress, 1), 10); 116 | assertEq(dummyERC1155.balanceOf(accountAddress, 2), 10); 117 | assertEq(dummyERC1155.balanceOf(user1, 1), 0); 118 | assertEq(dummyERC1155.balanceOf(user1, 2), 0); 119 | } 120 | 121 | function testOverrideERC1155Receiver(uint256 tokenId) public { 122 | address user1 = vm.addr(1); 123 | 124 | tokenCollection.mint(user1, tokenId); 125 | assertEq(tokenCollection.ownerOf(tokenId), user1); 126 | 127 | address accountAddress = registry.createAccount( 128 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 129 | ); 130 | 131 | AccountV3 account = AccountV3(payable(accountAddress)); 132 | 133 | MockExecutor mockExecutor = new MockExecutor(); 134 | 135 | // set overrides on account 136 | bytes4[] memory selectors = new bytes4[](1); 137 | selectors[0] = bytes4( 138 | abi.encodeWithSignature("onERC1155Received(address,address,uint256,uint256,bytes)") 139 | ); 140 | address[] memory implementations = new address[](1); 141 | implementations[0] = address(mockExecutor); 142 | vm.prank(user1); 143 | account.setOverrides(selectors, implementations); 144 | 145 | vm.expectRevert("ERC1155: ERC1155Receiver rejected tokens"); 146 | dummyERC1155.mint(accountAddress, 1, 10); 147 | } 148 | 149 | function testOverrideERC1155BatchReceiver(uint256 tokenId) public { 150 | address user1 = vm.addr(1); 151 | 152 | tokenCollection.mint(user1, tokenId); 153 | assertEq(tokenCollection.ownerOf(tokenId), user1); 154 | 155 | address accountAddress = registry.createAccount( 156 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 157 | ); 158 | 159 | AccountV3 account = AccountV3(payable(accountAddress)); 160 | 161 | MockExecutor mockExecutor = new MockExecutor(); 162 | 163 | // set overrides on account 164 | bytes4[] memory selectors = new bytes4[](1); 165 | selectors[0] = bytes4( 166 | abi.encodeWithSignature( 167 | "onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)" 168 | ) 169 | ); 170 | address[] memory implementations = new address[](1); 171 | implementations[0] = address(mockExecutor); 172 | vm.prank(user1); 173 | account.setOverrides(selectors, implementations); 174 | 175 | dummyERC1155.mint(user1, 1, 10); 176 | dummyERC1155.mint(user1, 2, 10); 177 | 178 | uint256[] memory ids = new uint256[](2); 179 | ids[0] = 1; 180 | ids[1] = 2; 181 | uint256[] memory amounts = new uint256[](2); 182 | amounts[0] = 10; 183 | amounts[1] = 10; 184 | 185 | vm.expectRevert("ERC1155: ERC1155Receiver rejected tokens"); 186 | vm.prank(user1); 187 | dummyERC1155.safeBatchTransferFrom(user1, accountAddress, ids, amounts, ""); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /test/AccountERC20.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/proxy/Clones.sol"; 8 | 9 | import "erc6551/ERC6551Registry.sol"; 10 | import "erc6551/interfaces/IERC6551Account.sol"; 11 | 12 | import "../src/AccountV3.sol"; 13 | import "../src/AccountGuardian.sol"; 14 | 15 | import "./mocks/MockERC721.sol"; 16 | import "./mocks/MockERC20.sol"; 17 | 18 | contract AccountERC20Test is Test { 19 | MockERC20 public dummyERC20; 20 | 21 | AccountV3 implementation; 22 | ERC6551Registry public registry; 23 | 24 | MockERC721 public tokenCollection; 25 | 26 | function setUp() public { 27 | dummyERC20 = new MockERC20(); 28 | 29 | implementation = new AccountV3(address(1), address(1), address(1), address(1)); 30 | registry = new ERC6551Registry(); 31 | 32 | tokenCollection = new MockERC721(); 33 | } 34 | 35 | function testTransferERC20PreDeploy(uint256 tokenId) public { 36 | address user1 = vm.addr(1); 37 | 38 | address computedAccountInstance = registry.account( 39 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 40 | ); 41 | 42 | tokenCollection.mint(user1, tokenId); 43 | assertEq(tokenCollection.ownerOf(tokenId), user1); 44 | 45 | dummyERC20.mint(computedAccountInstance, 1 ether); 46 | 47 | assertEq(dummyERC20.balanceOf(computedAccountInstance), 1 ether); 48 | 49 | address accountAddress = registry.createAccount( 50 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 51 | ); 52 | 53 | AccountV3 account = AccountV3(payable(accountAddress)); 54 | 55 | bytes memory erc20TransferCall = 56 | abi.encodeWithSignature("transfer(address,uint256)", user1, 1 ether); 57 | vm.prank(user1); 58 | account.execute(payable(address(dummyERC20)), 0, erc20TransferCall, 0); 59 | 60 | assertEq(dummyERC20.balanceOf(accountAddress), 0); 61 | assertEq(dummyERC20.balanceOf(user1), 1 ether); 62 | } 63 | 64 | function testTransferERC20PostDeploy(uint256 tokenId) public { 65 | address user1 = vm.addr(1); 66 | 67 | address accountAddress = registry.createAccount( 68 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 69 | ); 70 | 71 | tokenCollection.mint(user1, tokenId); 72 | assertEq(tokenCollection.ownerOf(tokenId), user1); 73 | 74 | dummyERC20.mint(accountAddress, 1 ether); 75 | 76 | assertEq(dummyERC20.balanceOf(accountAddress), 1 ether); 77 | 78 | AccountV3 account = AccountV3(payable(accountAddress)); 79 | 80 | bytes memory erc20TransferCall = 81 | abi.encodeWithSignature("transfer(address,uint256)", user1, 1 ether); 82 | vm.prank(user1); 83 | account.execute(payable(address(dummyERC20)), 0, erc20TransferCall, 0); 84 | 85 | assertEq(dummyERC20.balanceOf(accountAddress), 0); 86 | assertEq(dummyERC20.balanceOf(user1), 1 ether); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/AccountERC4337.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | import "@openzeppelin/contracts/proxy/Clones.sol"; 9 | 10 | import "@account-abstraction/contracts/core/EntryPoint.sol"; 11 | 12 | import "erc6551/ERC6551Registry.sol"; 13 | import "erc6551/interfaces/IERC6551Account.sol"; 14 | 15 | import "../src/AccountV3.sol"; 16 | import "../src/AccountGuardian.sol"; 17 | 18 | import "./mocks/MockERC721.sol"; 19 | 20 | contract AccountERC4337Test is Test { 21 | using ECDSA for bytes32; 22 | 23 | AccountV3 implementation; 24 | ERC6551Registry public registry; 25 | IEntryPoint public entryPoint; 26 | 27 | MockERC721 public tokenCollection; 28 | 29 | function setUp() public { 30 | entryPoint = new EntryPoint(); 31 | implementation = new AccountV3(address(entryPoint), address(1), address(1), address(1)); 32 | registry = new ERC6551Registry(); 33 | 34 | tokenCollection = new MockERC721(); 35 | } 36 | 37 | function testReturnsEntryPoint() public { 38 | address accountAddress = registry.createAccount( 39 | address(implementation), 0, block.chainid, address(tokenCollection), 1 40 | ); 41 | 42 | assertEq(address(AccountV3(payable(accountAddress)).entryPoint()), address(entryPoint)); 43 | } 44 | 45 | function test4337CallCreateAccount() public { 46 | uint256 tokenId = 1; 47 | address user1 = vm.addr(1); 48 | address user2 = vm.addr(2); 49 | 50 | tokenCollection.mint(user1, tokenId); 51 | assertEq(tokenCollection.ownerOf(tokenId), user1); 52 | 53 | address accountAddress = registry.account( 54 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 55 | ); 56 | 57 | bytes memory initCode = abi.encodePacked( 58 | address(registry), 59 | abi.encodeWithSignature( 60 | "createAccount(address,bytes32,uint256,address,uint256)", 61 | address(implementation), 62 | 0, 63 | block.chainid, 64 | address(tokenCollection), 65 | tokenId 66 | ) 67 | ); 68 | 69 | bytes memory callData = 70 | abi.encodeWithSignature("execute(address,uint256,bytes,uint8)", user2, 0.1 ether, "", 0); 71 | 72 | UserOperation memory op = UserOperation({ 73 | sender: accountAddress, 74 | nonce: 0, 75 | initCode: initCode, 76 | callData: callData, 77 | callGasLimit: 1000000, 78 | verificationGasLimit: 1000000, 79 | preVerificationGas: 1000000, 80 | maxFeePerGas: block.basefee + 10, 81 | maxPriorityFeePerGas: 10, 82 | paymasterAndData: "", 83 | signature: "" 84 | }); 85 | 86 | bytes32 opHash = entryPoint.getUserOpHash(op); 87 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, opHash.toEthSignedMessageHash()); 88 | 89 | bytes memory signature = abi.encodePacked(r, s, v); 90 | op.signature = signature; 91 | 92 | vm.deal(accountAddress, 1 ether); 93 | 94 | UserOperation[] memory ops = new UserOperation[](1); 95 | ops[0] = op; 96 | 97 | assertEq(entryPoint.getNonce(accountAddress, 0), 0); 98 | entryPoint.handleOps(ops, payable(user1)); 99 | assertEq(entryPoint.getNonce(accountAddress, 0), 1); 100 | 101 | assertEq(user2.balance, 0.1 ether); 102 | assertTrue(accountAddress.balance < 0.9 ether); 103 | } 104 | 105 | function test4337CallExistingAccount() public { 106 | uint256 tokenId = 1; 107 | address user1 = vm.addr(1); 108 | address user2 = vm.addr(2); 109 | 110 | tokenCollection.mint(user1, tokenId); 111 | assertEq(tokenCollection.ownerOf(tokenId), user1); 112 | 113 | address accountAddress = registry.createAccount( 114 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 115 | ); 116 | 117 | bytes memory callData = 118 | abi.encodeWithSignature("execute(address,uint256,bytes,uint8)", user2, 0.1 ether, "", 0); 119 | 120 | UserOperation memory op = UserOperation({ 121 | sender: accountAddress, 122 | nonce: 0, 123 | initCode: "", 124 | callData: callData, 125 | callGasLimit: 1000000, 126 | verificationGasLimit: 1000000, 127 | preVerificationGas: 1000000, 128 | maxFeePerGas: block.basefee + 10, 129 | maxPriorityFeePerGas: 10, 130 | paymasterAndData: "", 131 | signature: "" 132 | }); 133 | 134 | bytes32 opHash = entryPoint.getUserOpHash(op); 135 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, opHash.toEthSignedMessageHash()); 136 | 137 | bytes memory signature = abi.encodePacked(r, s, v); 138 | op.signature = signature; 139 | 140 | vm.deal(accountAddress, 1 ether); 141 | 142 | UserOperation[] memory ops = new UserOperation[](1); 143 | ops[0] = op; 144 | 145 | assertEq(entryPoint.getNonce(accountAddress, 0), 0); 146 | entryPoint.handleOps(ops, payable(user1)); 147 | assertEq(entryPoint.getNonce(accountAddress, 0), 1); 148 | 149 | assertEq(user2.balance, 0.1 ether); 150 | assertTrue(accountAddress.balance < 0.9 ether); 151 | } 152 | 153 | function test4337CallRevertsInvalidSignature() public { 154 | uint256 tokenId = 1; 155 | address user1 = vm.addr(1); 156 | address user2 = vm.addr(2); 157 | 158 | tokenCollection.mint(user1, tokenId); 159 | assertEq(tokenCollection.ownerOf(tokenId), user1); 160 | 161 | address accountAddress = registry.createAccount( 162 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 163 | ); 164 | 165 | bytes memory callData = 166 | abi.encodeWithSignature("execute(address,uint256,bytes,uint8)", user2, 0.1 ether, "", 0); 167 | 168 | UserOperation memory op = UserOperation({ 169 | sender: accountAddress, 170 | nonce: 0, 171 | initCode: "", 172 | callData: callData, 173 | callGasLimit: 1000000, 174 | verificationGasLimit: 1000000, 175 | preVerificationGas: 1000000, 176 | maxFeePerGas: block.basefee + 10, 177 | maxPriorityFeePerGas: 10, 178 | paymasterAndData: "", 179 | signature: "" 180 | }); 181 | 182 | bytes32 opHash = entryPoint.getUserOpHash(op); 183 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, opHash.toEthSignedMessageHash()); 184 | 185 | // invalidate signature 186 | bytes memory signature = abi.encodePacked(r, s, v + 1); 187 | op.signature = signature; 188 | 189 | vm.deal(accountAddress, 1 ether); 190 | 191 | UserOperation[] memory ops = new UserOperation[](1); 192 | ops[0] = op; 193 | 194 | vm.expectRevert(); 195 | entryPoint.handleOps(ops, payable(user1)); 196 | 197 | assertEq(accountAddress.balance, 1 ether); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /test/AccountERC721.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/proxy/Clones.sol"; 8 | 9 | import "erc6551/ERC6551Registry.sol"; 10 | import "erc6551/interfaces/IERC6551Account.sol"; 11 | 12 | import "../src/AccountV3.sol"; 13 | import "../src/AccountGuardian.sol"; 14 | 15 | import "./mocks/MockERC721.sol"; 16 | import "./mocks/MockExecutor.sol"; 17 | 18 | contract AccountERC721Test is Test { 19 | MockERC721 public dummyERC721; 20 | 21 | AccountV3 implementation; 22 | ERC6551Registry public registry; 23 | 24 | MockERC721 public tokenCollection; 25 | 26 | function setUp() public { 27 | dummyERC721 = new MockERC721(); 28 | 29 | implementation = new AccountV3(address(1), address(1), address(1), address(1)); 30 | registry = new ERC6551Registry(); 31 | 32 | tokenCollection = new MockERC721(); 33 | } 34 | 35 | function testTransferERC721PreDeploy(uint256 tokenId) public { 36 | address user1 = vm.addr(1); 37 | 38 | address computedAccountInstance = registry.account( 39 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 40 | ); 41 | 42 | tokenCollection.mint(user1, tokenId); 43 | assertEq(tokenCollection.ownerOf(tokenId), user1); 44 | 45 | dummyERC721.mint(computedAccountInstance, 1); 46 | 47 | assertEq(dummyERC721.balanceOf(computedAccountInstance), 1); 48 | assertEq(dummyERC721.ownerOf(1), computedAccountInstance); 49 | 50 | address accountAddress = registry.createAccount( 51 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 52 | ); 53 | 54 | AccountV3 account = AccountV3(payable(accountAddress)); 55 | 56 | bytes memory erc721TransferCall = abi.encodeWithSignature( 57 | "safeTransferFrom(address,address,uint256)", accountAddress, user1, 1 58 | ); 59 | vm.prank(user1); 60 | account.execute(payable(address(dummyERC721)), 0, erc721TransferCall, 0); 61 | 62 | assertEq(dummyERC721.balanceOf(address(account)), 0); 63 | assertEq(dummyERC721.balanceOf(user1), 1); 64 | assertEq(dummyERC721.ownerOf(1), user1); 65 | } 66 | 67 | function testTransferERC721PostDeploy(uint256 tokenId) public { 68 | address user1 = vm.addr(1); 69 | 70 | address accountAddress = registry.createAccount( 71 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 72 | ); 73 | 74 | tokenCollection.mint(user1, tokenId); 75 | assertEq(tokenCollection.ownerOf(tokenId), user1); 76 | 77 | dummyERC721.mint(accountAddress, 1); 78 | 79 | assertEq(dummyERC721.balanceOf(accountAddress), 1); 80 | assertEq(dummyERC721.ownerOf(1), accountAddress); 81 | 82 | AccountV3 account = AccountV3(payable(accountAddress)); 83 | 84 | bytes memory erc721TransferCall = 85 | abi.encodeWithSignature("safeTransferFrom(address,address,uint256)", account, user1, 1); 86 | vm.prank(user1); 87 | account.execute(payable(address(dummyERC721)), 0, erc721TransferCall, 0); 88 | 89 | assertEq(dummyERC721.balanceOf(accountAddress), 0); 90 | assertEq(dummyERC721.balanceOf(user1), 1); 91 | assertEq(dummyERC721.ownerOf(1), user1); 92 | } 93 | 94 | function testCannotOwnSelf() public { 95 | address owner = vm.addr(1); 96 | uint256 tokenId = 100; 97 | bytes32 salt = bytes32(uint256(200)); 98 | 99 | tokenCollection.mint(owner, tokenId); 100 | 101 | vm.prank(owner, owner); 102 | address account = registry.createAccount( 103 | address(implementation), salt, block.chainid, address(tokenCollection), tokenId 104 | ); 105 | 106 | vm.prank(owner); 107 | vm.expectRevert(OwnershipCycle.selector); 108 | tokenCollection.safeTransferFrom(owner, account, tokenId); 109 | } 110 | 111 | function testOverrideERC721Receiver(uint256 tokenId) public { 112 | address user1 = vm.addr(1); 113 | 114 | tokenCollection.mint(user1, tokenId); 115 | assertEq(tokenCollection.ownerOf(tokenId), user1); 116 | 117 | address accountAddress = registry.createAccount( 118 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 119 | ); 120 | 121 | AccountV3 account = AccountV3(payable(accountAddress)); 122 | 123 | MockExecutor mockExecutor = new MockExecutor(); 124 | 125 | // set overrides on account 126 | bytes4[] memory selectors = new bytes4[](1); 127 | selectors[0] = 128 | bytes4(abi.encodeWithSignature("onERC721Received(address,address,uint256,bytes)")); 129 | address[] memory implementations = new address[](1); 130 | implementations[0] = address(mockExecutor); 131 | vm.prank(user1); 132 | account.setOverrides(selectors, implementations); 133 | 134 | vm.expectRevert("ERC721: transfer to non ERC721Receiver implementer"); 135 | dummyERC721.mint(accountAddress, 1); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/AccountETH.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/proxy/Clones.sol"; 8 | 9 | import "erc6551/ERC6551Registry.sol"; 10 | import "erc6551/interfaces/IERC6551Account.sol"; 11 | 12 | import "../src/AccountV3.sol"; 13 | import "../src/AccountGuardian.sol"; 14 | 15 | import "./mocks/MockERC721.sol"; 16 | 17 | contract AccountETHTest is Test { 18 | AccountV3 implementation; 19 | ERC6551Registry public registry; 20 | 21 | MockERC721 public tokenCollection; 22 | 23 | function setUp() public { 24 | implementation = new AccountV3(address(1), address(1), address(1), address(1)); 25 | registry = new ERC6551Registry(); 26 | 27 | tokenCollection = new MockERC721(); 28 | } 29 | 30 | function testTransferETHPreDeploy() public { 31 | uint256 tokenId = 1; 32 | address user1 = vm.addr(1); 33 | vm.deal(user1, 0.2 ether); 34 | 35 | // get address that account will be deployed to (before token is minted) 36 | address accountAddress = registry.account( 37 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 38 | ); 39 | 40 | // mint token for account to user1 41 | tokenCollection.mint(user1, tokenId); 42 | 43 | assertEq(tokenCollection.ownerOf(tokenId), user1); 44 | 45 | // send ETH from user1 to account (prior to account deployment) 46 | vm.prank(user1); 47 | (bool sent,) = accountAddress.call{value: 0.2 ether}(""); 48 | assertTrue(sent); 49 | 50 | assertEq(accountAddress.balance, 0.2 ether); 51 | 52 | // deploy account contract (from a different wallet) 53 | address createdAccountInstance = registry.createAccount( 54 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 55 | ); 56 | 57 | assertEq(accountAddress, createdAccountInstance); 58 | 59 | AccountV3 account = AccountV3(payable(accountAddress)); 60 | 61 | // user1 executes transaction to send ETH from account 62 | vm.prank(user1); 63 | account.execute(payable(user1), 0.1 ether, "", 0); 64 | 65 | // success! 66 | assertEq(accountAddress.balance, 0.1 ether); 67 | assertEq(user1.balance, 0.1 ether); 68 | } 69 | 70 | function testTransferETHPostDeploy(uint256 tokenId) public { 71 | address user1 = vm.addr(1); 72 | vm.deal(user1, 0.2 ether); 73 | 74 | address accountAddress = registry.createAccount( 75 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 76 | ); 77 | 78 | tokenCollection.mint(user1, tokenId); 79 | 80 | assertEq(tokenCollection.ownerOf(tokenId), user1); 81 | 82 | vm.prank(user1); 83 | (bool sent,) = accountAddress.call{value: 0.2 ether}(""); 84 | assertTrue(sent); 85 | 86 | assertEq(accountAddress.balance, 0.2 ether); 87 | 88 | AccountV3 account = AccountV3(payable(accountAddress)); 89 | 90 | vm.prank(user1); 91 | account.execute(payable(user1), 0.1 ether, "", 0); 92 | 93 | assertEq(accountAddress.balance, 0.1 ether); 94 | assertEq(user1.balance, 0.1 ether); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/AccountOverrides.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "@openzeppelin/contracts/utils/Create2.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 9 | import "@openzeppelin/contracts/proxy/Clones.sol"; 10 | 11 | import "erc6551/ERC6551Registry.sol"; 12 | import "erc6551/interfaces/IERC6551Account.sol"; 13 | import "erc6551/interfaces/IERC6551Executable.sol"; 14 | 15 | import "../src/AccountV3.sol"; 16 | import "../src/AccountV3Upgradable.sol"; 17 | import "../src/AccountGuardian.sol"; 18 | import "../src/AccountProxy.sol"; 19 | 20 | import "./mocks/MockERC721.sol"; 21 | import "./mocks/MockSigner.sol"; 22 | import "./mocks/MockExecutor.sol"; 23 | import "./mocks/MockSandboxExecutor.sol"; 24 | import "./mocks/MockReverter.sol"; 25 | import "./mocks/MockAccountUpgradable.sol"; 26 | 27 | contract AccountTest is Test { 28 | AccountV3 implementation; 29 | ERC6551Registry public registry; 30 | 31 | MockERC721 public tokenCollection; 32 | 33 | function setUp() public { 34 | registry = new ERC6551Registry(); 35 | 36 | implementation = new AccountV3(address(1), address(1), address(registry), address(1)); 37 | 38 | tokenCollection = new MockERC721(); 39 | 40 | // mint tokenId 1 during setup for accurate cold call gas measurement 41 | uint256 tokenId = 1; 42 | address user1 = vm.addr(1); 43 | tokenCollection.mint(user1, tokenId); 44 | } 45 | 46 | function testCustomOverridesFallback() public { 47 | uint256 tokenId = 1; 48 | address user1 = vm.addr(1); 49 | 50 | address accountAddress = registry.createAccount( 51 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 52 | ); 53 | 54 | vm.deal(accountAddress, 1 ether); 55 | 56 | AccountV3 account = AccountV3(payable(accountAddress)); 57 | 58 | MockExecutor mockExecutor = new MockExecutor(); 59 | 60 | // calls succeed with noop if override is undefined 61 | (bool success, bytes memory result) = 62 | accountAddress.call(abi.encodeWithSignature("customFunction()")); 63 | assertEq(success, true); 64 | assertEq(result, ""); 65 | 66 | uint256 state = account.state(); 67 | 68 | // set overrides on account 69 | bytes4[] memory selectors = new bytes4[](2); 70 | selectors[0] = bytes4(abi.encodeWithSignature("customFunction()")); 71 | selectors[1] = bytes4(abi.encodeWithSignature("fail()")); 72 | address[] memory implementations = new address[](2); 73 | implementations[0] = address(mockExecutor); 74 | implementations[1] = address(mockExecutor); 75 | vm.prank(user1); 76 | account.setOverrides(selectors, implementations); 77 | 78 | assertTrue(state != account.state()); 79 | 80 | // execution module handles fallback calls 81 | assertEq(MockExecutor(accountAddress).customFunction(), 12345); 82 | 83 | // execution bubbles up errors on revert 84 | vm.expectRevert(MockReverter.MockError.selector); 85 | MockExecutor(accountAddress).fail(); 86 | } 87 | 88 | function testCustomOverridesSupportsInterface() public { 89 | uint256 tokenId = 1; 90 | address user1 = vm.addr(1); 91 | 92 | address accountAddress = registry.createAccount( 93 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 94 | ); 95 | 96 | vm.deal(accountAddress, 1 ether); 97 | 98 | AccountV3 account = AccountV3(payable(accountAddress)); 99 | 100 | assertEq(account.supportsInterface(type(IERC1155Receiver).interfaceId), true); 101 | assertEq(account.supportsInterface(0x12345678), false); 102 | 103 | MockExecutor mockExecutor = new MockExecutor(); 104 | 105 | // set overrides on account 106 | bytes4[] memory selectors = new bytes4[](1); 107 | selectors[0] = bytes4(abi.encodeWithSignature("supportsInterface(bytes4)")); 108 | address[] memory implementations = new address[](1); 109 | implementations[0] = address(mockExecutor); 110 | vm.prank(user1); 111 | account.setOverrides(selectors, implementations); 112 | 113 | // override handles extra interface support 114 | assertEq(AccountV3(payable(accountAddress)).supportsInterface(0x12345678), true); 115 | // cannot override default interfaces 116 | assertEq( 117 | AccountV3(payable(accountAddress)).supportsInterface(type(IERC1155Receiver).interfaceId), 118 | true 119 | ); 120 | } 121 | 122 | function testCustomOverridesNested() public { 123 | uint256 tokenId = 1; 124 | uint256 tokenId2 = 2; 125 | address user1 = vm.addr(1); 126 | 127 | address accountAddress = registry.createAccount( 128 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 129 | ); 130 | 131 | tokenCollection.mint(accountAddress, tokenId2); 132 | 133 | address accountAddress2 = registry.createAccount( 134 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId2 135 | ); 136 | 137 | vm.deal(accountAddress2, 1 ether); 138 | 139 | AccountV3 account = AccountV3(payable(accountAddress2)); 140 | 141 | MockExecutor mockExecutor = new MockExecutor(); 142 | 143 | // calls succeed with noop if override is undefined 144 | (bool success, bytes memory result) = 145 | accountAddress.call(abi.encodeWithSignature("customFunction()")); 146 | assertEq(success, true); 147 | assertEq(result, ""); 148 | 149 | uint256 state = account.state(); 150 | 151 | // set overrides on account 152 | bytes4[] memory selectors = new bytes4[](2); 153 | selectors[0] = bytes4(abi.encodeWithSignature("customFunction()")); 154 | selectors[1] = bytes4(abi.encodeWithSignature("fail()")); 155 | address[] memory implementations = new address[](2); 156 | implementations[0] = address(mockExecutor); 157 | implementations[1] = address(mockExecutor); 158 | vm.prank(user1); 159 | account.setOverrides(selectors, implementations); 160 | 161 | assertTrue(state != account.state()); 162 | 163 | // execution module handles fallback calls 164 | assertEq(MockExecutor(accountAddress2).customFunction(), 12345); 165 | 166 | // execution bubbles up errors on revert 167 | vm.expectRevert(MockReverter.MockError.selector); 168 | MockExecutor(accountAddress2).fail(); 169 | 170 | // overrides should be reset on root token transfer 171 | vm.prank(user1); 172 | tokenCollection.safeTransferFrom(user1, vm.addr(3), tokenId); 173 | (success, result) = accountAddress.call(abi.encodeWithSignature("customFunction()")); 174 | assertEq(success, true); 175 | assertEq(result, ""); 176 | (success, result) = accountAddress.call(abi.encodeWithSignature("fail()")); 177 | assertEq(success, true); 178 | assertEq(result, ""); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /test/AccountPermissions.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "@openzeppelin/contracts/utils/Create2.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 9 | import "@openzeppelin/contracts/proxy/Clones.sol"; 10 | 11 | import "erc6551/ERC6551Registry.sol"; 12 | import "erc6551/interfaces/IERC6551Account.sol"; 13 | import "erc6551/interfaces/IERC6551Executable.sol"; 14 | 15 | import "../src/AccountV3.sol"; 16 | import "../src/AccountV3Upgradable.sol"; 17 | import "../src/AccountGuardian.sol"; 18 | import "../src/AccountProxy.sol"; 19 | 20 | import "./mocks/MockERC721.sol"; 21 | import "./mocks/MockSigner.sol"; 22 | import "./mocks/MockExecutor.sol"; 23 | import "./mocks/MockSandboxExecutor.sol"; 24 | import "./mocks/MockReverter.sol"; 25 | import "./mocks/MockAccountUpgradable.sol"; 26 | 27 | contract AccountTest is Test { 28 | AccountV3 implementation; 29 | ERC6551Registry public registry; 30 | 31 | MockERC721 public tokenCollection; 32 | 33 | function setUp() public { 34 | registry = new ERC6551Registry(); 35 | 36 | implementation = new AccountV3(address(1), address(1), address(registry), address(1)); 37 | 38 | tokenCollection = new MockERC721(); 39 | 40 | // mint tokenId 1 during setup for accurate cold call gas measurement 41 | uint256 tokenId = 1; 42 | address user1 = vm.addr(1); 43 | tokenCollection.mint(user1, tokenId); 44 | } 45 | 46 | function testCustomPermissions() public { 47 | uint256 tokenId = 1; 48 | address user1 = vm.addr(1); 49 | address user2 = vm.addr(2); 50 | 51 | address accountAddress = registry.createAccount( 52 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 53 | ); 54 | 55 | vm.deal(accountAddress, 1 ether); 56 | 57 | AccountV3 account = AccountV3(payable(accountAddress)); 58 | 59 | assertTrue(account.isValidSigner(user2, "") != IERC6551Account.isValidSigner.selector); 60 | 61 | address[] memory callers = new address[](1); 62 | callers[0] = address(user2); 63 | bool[] memory _permissions = new bool[](1); 64 | _permissions[0] = true; 65 | vm.prank(user1); 66 | account.setPermissions(callers, _permissions); 67 | 68 | assertEq(account.isValidSigner(user2, ""), IERC6551Account.isValidSigner.selector); 69 | 70 | vm.prank(user2); 71 | account.execute(user2, 0.1 ether, "", 0); 72 | 73 | assertEq(user2.balance, 0.1 ether); 74 | } 75 | 76 | function testCustomPermissionsNested() public { 77 | uint256 tokenId = 1; 78 | uint256 tokenId2 = 2; 79 | address user1 = vm.addr(1); 80 | address user2 = vm.addr(2); 81 | 82 | address accountAddress = registry.createAccount( 83 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId 84 | ); 85 | 86 | tokenCollection.mint(accountAddress, tokenId2); 87 | 88 | address accountAddress2 = registry.createAccount( 89 | address(implementation), 0, block.chainid, address(tokenCollection), tokenId2 90 | ); 91 | 92 | vm.deal(accountAddress2, 1 ether); 93 | 94 | AccountV3 account = AccountV3(payable(accountAddress2)); 95 | 96 | assertTrue(account.isValidSigner(user2, "") != IERC6551Account.isValidSigner.selector); 97 | 98 | address[] memory callers = new address[](1); 99 | callers[0] = address(user2); 100 | bool[] memory _permissions = new bool[](1); 101 | _permissions[0] = true; 102 | vm.prank(user1); 103 | account.setPermissions(callers, _permissions); 104 | 105 | assertEq(account.isValidSigner(user2, ""), IERC6551Account.isValidSigner.selector); 106 | 107 | vm.prank(user2); 108 | account.execute(user2, 0.1 ether, "", 0); 109 | 110 | assertEq(user2.balance, 0.1 ether); 111 | 112 | // Permissions should reset when root token is transferred 113 | vm.prank(user1); 114 | tokenCollection.safeTransferFrom(user1, vm.addr(3), tokenId); 115 | assertTrue(account.isValidSigner(user2, "") != IERC6551Account.isValidSigner.selector); 116 | vm.prank(user2); 117 | vm.expectRevert(NotAuthorized.selector); 118 | account.execute(user2, 0.1 ether, "", 0); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/mocks/MockAccountUpgradable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../../src/AccountV3.sol"; 5 | import "../../src/AccountV3Upgradable.sol"; 6 | 7 | contract MockAccountUpgradable is AccountV3Upgradable { 8 | constructor( 9 | address entryPoint_, 10 | address multicallForwarder, 11 | address erc6551Registry, 12 | address guardian 13 | ) AccountV3Upgradable(entryPoint_, multicallForwarder, erc6551Registry, guardian) {} 14 | 15 | function customFunction() external pure returns (uint256) { 16 | return 12345; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/mocks/MockERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; 5 | 6 | contract MockERC1155 is ERC1155 { 7 | constructor() ERC1155("http://MockERC1155.com") {} 8 | 9 | function mint(address to, uint256 tokenId, uint256 amount) external { 10 | _mint(to, tokenId, amount, ""); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/mocks/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20 { 7 | constructor() ERC20("MockERC20", "T20") {} 8 | 9 | function mint(address to, uint256 amount) external { 10 | _mint(to, amount); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/mocks/MockERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | 6 | contract MockERC721 is ERC721 { 7 | constructor() ERC721("MockERC721", "M721") {} 8 | 9 | function mint(address to, uint256 tokenId) external { 10 | _safeMint(to, tokenId); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/mocks/MockExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "./MockReverter.sol"; 5 | import "@openzeppelin/contracts/interfaces/IERC1271.sol"; 6 | 7 | contract MockExecutor is MockReverter { 8 | function customFunction() external pure returns (uint256) { 9 | return 12345; 10 | } 11 | 12 | function supportsInterface(bytes4 interfaceId) external pure returns (bool) { 13 | return interfaceId == 0x12345678; 14 | } 15 | 16 | function onERC721Received(address, address, uint256, bytes memory) 17 | public 18 | pure 19 | returns (bytes4) 20 | { 21 | return bytes4(""); 22 | } 23 | 24 | function onERC1155Received(address, address, uint256, uint256, bytes memory) 25 | public 26 | pure 27 | returns (bytes4) 28 | { 29 | return bytes4(""); 30 | } 31 | 32 | function onERC1155BatchReceived( 33 | address, 34 | address, 35 | uint256[] memory, 36 | uint256[] memory, 37 | bytes memory 38 | ) public pure returns (bytes4) { 39 | return bytes4(""); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/mocks/MockReverter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | contract MockReverter { 5 | error MockError(); 6 | 7 | function fail() external pure returns (uint256) { 8 | revert MockError(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/mocks/MockSandboxExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../../src/interfaces/ISandboxExecutor.sol"; 5 | import "./MockReverter.sol"; 6 | import "./MockERC721.sol"; 7 | import "@openzeppelin/contracts/interfaces/IERC1271.sol"; 8 | 9 | contract MockSandboxExecutor is MockReverter { 10 | function customFunction() external pure returns (uint256) { 11 | return 12345; 12 | } 13 | 14 | function sentEther(address to, uint256 value) external returns (bytes memory) { 15 | return ISandboxExecutor(msg.sender).extcall(to, value, ""); 16 | } 17 | 18 | function createNFT() external returns (address) { 19 | return ISandboxExecutor(msg.sender).extcreate(0, type(MockERC721).creationCode); 20 | } 21 | 22 | function createNFTDeterministic() external returns (address) { 23 | return ISandboxExecutor(msg.sender).extcreate2( 24 | 0, keccak256("salt"), type(MockERC721).creationCode 25 | ); 26 | } 27 | 28 | function getSlot0() external view returns (bytes32) { 29 | return ISandboxExecutor(msg.sender).extsload(bytes32(0)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/mocks/MockSigner.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/interfaces/IERC1271.sol"; 5 | 6 | contract MockSigner is IERC1271 { 7 | function isValidSignature(bytes32, bytes calldata) external pure returns (bytes4 magicValue) { 8 | return IERC1271.isValidSignature.selector; 9 | } 10 | } 11 | --------------------------------------------------------------------------------