From 933ceaaef63a5fbd881b281208bc222e5f0c7b0a Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Mon, 15 Dec 2025 12:30:22 -0300 Subject: [PATCH 1/2] evm: support 7702 delegator --- .gitmodules | 6 ++ package.json | 3 +- .../smart-accounts/SmartAccountsHandler.sol | 43 +++++++++- .../EIP7702StatelessDeleGatorMock.sol | 12 +++ packages/evm/hardhat.config.ts | 15 +++- packages/evm/lib/delegation-framework | 1 + packages/evm/lib/openzeppelin-contracts | 1 + packages/evm/package.json | 6 +- packages/evm/remappings.txt | 2 + packages/evm/test/helpers/delegations.ts | 62 ++++++++++++++ .../SmartAccountsHandler.test.ts | 80 +++++++++++++++++++ yarn.lock | 5 -- 12 files changed, 221 insertions(+), 15 deletions(-) create mode 100644 .gitmodules create mode 100644 packages/evm/contracts/test/smart-accounts/EIP7702StatelessDeleGatorMock.sol create mode 160000 packages/evm/lib/delegation-framework create mode 160000 packages/evm/lib/openzeppelin-contracts create mode 100644 packages/evm/remappings.txt create mode 100644 packages/evm/test/helpers/delegations.ts diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..87bb52d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "packages/evm/lib/delegation-framework"] + path = packages/evm/lib/delegation-framework + url = https://github.com/MetaMask/delegation-framework.git +[submodule "packages/evm/lib/openzeppelin-contracts"] + path = packages/evm/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts.git diff --git a/package.json b/package.json index 36c2565..83c92f4 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ }, "workspaces": { "packages": [ - "packages/**" + "packages/evm", + "packages/svm" ] } } diff --git a/packages/evm/contracts/smart-accounts/SmartAccountsHandler.sol b/packages/evm/contracts/smart-accounts/SmartAccountsHandler.sol index b482da3..5a9780f 100644 --- a/packages/evm/contracts/smart-accounts/SmartAccountsHandler.sol +++ b/packages/evm/contracts/smart-accounts/SmartAccountsHandler.sol @@ -17,6 +17,9 @@ pragma solidity ^0.8.20; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import '@openzeppelin/contracts/utils/Address.sol'; +import { EIP7702StatelessDeleGator } from 'delegation-framework/EIP7702/EIP7702StatelessDeleGator.sol'; +import { IDelegationManager, ModeCode } from 'delegation-framework/interfaces/IDelegationManager.sol'; + import '../interfaces/ISafe.sol'; import '../interfaces/ISmartAccount.sol'; import '../interfaces/ISmartAccountsHandler.sol'; @@ -54,11 +57,12 @@ contract SmartAccountsHandler is ISmartAccountsHandler { // solhint-disable-next-line avoid-low-level-calls if (_isMimicSmartAccount(account)) return ISmartAccount(account).call(target, data, value); if (_isSafe(account)) return _callSafe(account, target, data, value); + if (_isEIP7702StatelessDeleGator(account)) return _callEIP7702StatelessDeleGator(account, target, data, value); revert SmartAccountsHandlerUnsupportedAccount(account); } /** - * @dev Performs a transfer from a safe + * @dev Performs a transfer from a Gnosis Safe */ function _transferSafe(address account, address token, address to, uint256 amount) internal { Denominations.isNativeToken(token) @@ -67,7 +71,7 @@ contract SmartAccountsHandler is ISmartAccountsHandler { } /** - * @dev Performs a call from a safe + * @dev Performs a call from a Gnosis Safe */ function _callSafe(address account, address target, bytes memory data, uint256 value) internal @@ -85,6 +89,29 @@ contract SmartAccountsHandler is ISmartAccountsHandler { : Address.verifyCallResultFromTarget(target, success, result); } + /** + * @dev Performs a call from a EIP7702StatelessDeleGator + */ + function _callEIP7702StatelessDeleGator(address account, address target, bytes memory data, uint256 value) + internal + returns (bytes memory) + { + (bytes memory permissionContext, bytes memory callData) = abi.decode(data, (bytes, bytes)); + + bytes[] memory permissionContexts = new bytes[](1); + permissionContexts[0] = permissionContext; + + ModeCode[] memory modes = new ModeCode[](1); + modes[0] = ModeCode.wrap(bytes32(0)); + + bytes[] memory executions = new bytes[](1); + executions[0] = abi.encodePacked(target, value, callData); + + IDelegationManager delegationManager = EIP7702StatelessDeleGator(payable(account)).delegationManager(); + delegationManager.redeemDelegations(permissionContexts, modes, executions); + return new bytes(0); + } + /** * @dev Tells whether an account is a Mimic smart account * @param account Address of the account being queried @@ -108,4 +135,16 @@ contract SmartAccountsHandler is ISmartAccountsHandler { return false; } } + + /** + * @dev Tells whether an account is an EIP7702StatelessDeleGator + * @param account Address of the account being queried + */ + function _isEIP7702StatelessDeleGator(address account) internal view returns (bool) { + try EIP7702StatelessDeleGator(payable(account)).delegationManager() returns (IDelegationManager) { + return true; + } catch { + return false; + } + } } diff --git a/packages/evm/contracts/test/smart-accounts/EIP7702StatelessDeleGatorMock.sol b/packages/evm/contracts/test/smart-accounts/EIP7702StatelessDeleGatorMock.sol new file mode 100644 index 0000000..e7635e1 --- /dev/null +++ b/packages/evm/contracts/test/smart-accounts/EIP7702StatelessDeleGatorMock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { DelegationManager } from 'delegation-framework/DelegationManager.sol'; +import { IEntryPoint, EIP7702StatelessDeleGator } from 'delegation-framework/EIP7702/EIP7702StatelessDeleGator.sol'; + +contract EIP7702StatelessDeleGatorMock is EIP7702StatelessDeleGator { + constructor(address owner) EIP7702StatelessDeleGator(new DelegationManager(owner), IEntryPoint(address(0))) { + // solhint-disable-previous-line no-empty-blocks + } +} diff --git a/packages/evm/hardhat.config.ts b/packages/evm/hardhat.config.ts index 28aea2c..0af58e6 100644 --- a/packages/evm/hardhat.config.ts +++ b/packages/evm/hardhat.config.ts @@ -8,8 +8,17 @@ dotenv.config() const config: HardhatUserConfig = { plugins: [hardhatVerify, hardhatToolboxMochaEthersPlugin], solidity: { - profiles: { - default: { + compilers: [ + { + version: '0.8.23', + settings: { + optimizer: { + enabled: true, + runs: 1000, + }, + }, + }, + { version: '0.8.28', settings: { optimizer: { @@ -18,7 +27,7 @@ const config: HardhatUserConfig = { }, }, }, - }, + ], }, networks: { optimism: { diff --git a/packages/evm/lib/delegation-framework b/packages/evm/lib/delegation-framework new file mode 160000 index 0000000..bfbdf97 --- /dev/null +++ b/packages/evm/lib/delegation-framework @@ -0,0 +1 @@ +Subproject commit bfbdf9795a976833ed2fa000baf42fbb83958b03 diff --git a/packages/evm/lib/openzeppelin-contracts b/packages/evm/lib/openzeppelin-contracts new file mode 160000 index 0000000..e4f7021 --- /dev/null +++ b/packages/evm/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 diff --git a/packages/evm/package.json b/packages/evm/package.json index 944a9df..1c5a4e8 100644 --- a/packages/evm/package.json +++ b/packages/evm/package.json @@ -13,9 +13,6 @@ "lint:typescript": "eslint . --ext .ts", "test": "hardhat test" }, - "dependencies": { - "@openzeppelin/contracts": "5.3.0" - }, "devDependencies": { "@mimicprotocol/sdk": "0.0.1-rc.20", "@nomicfoundation/hardhat-ethers": "^4.0.0-next.23", @@ -43,7 +40,8 @@ }, "eslintIgnore": [ "types", - "artifacts" + "artifacts", + "lib" ], "eslintConfig": { "extends": "eslint-config-mimic" diff --git a/packages/evm/remappings.txt b/packages/evm/remappings.txt new file mode 100644 index 0000000..8f75e29 --- /dev/null +++ b/packages/evm/remappings.txt @@ -0,0 +1,2 @@ +delegation-framework/=lib/delegation-framework/src/ +@openzeppelin/=lib/openzeppelin-contracts/ diff --git a/packages/evm/test/helpers/delegations.ts b/packages/evm/test/helpers/delegations.ts new file mode 100644 index 0000000..bf7e606 --- /dev/null +++ b/packages/evm/test/helpers/delegations.ts @@ -0,0 +1,62 @@ +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types' + +const { ethers } = await network.connect() + +import { network } from 'hardhat' + +const ROOT_AUTHORITY = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + +const DOMAIN = { + name: 'DelegationManager', + version: '1', + chainId: 0, + verifyingContract: '', +} + +const DELEGATION_712_TYPES = { + Caveat: [ + { name: 'enforcer', type: 'address' }, + { name: 'terms', type: 'bytes' }, + ], + Delegation: [ + { name: 'delegate', type: 'address' }, + { name: 'delegator', type: 'address' }, + { name: 'authority', type: 'bytes32' }, + { name: 'caveats', type: 'Caveat[]' }, + { name: 'salt', type: 'uint256' }, + ], +} as const + +const DELEGATION_ABI_TYPE = + 'tuple(address delegate,address delegator,bytes32 authority,tuple(address enforcer,bytes terms,bytes args)[] caveats,uint256 salt,bytes signature)' + +export async function signDelegation( + user: HardhatEthersSigner, + delegate: string, + delegationManager: string +): Promise { + const chainId = (await ethers.provider.getNetwork()).chainId + const delegation = { + delegate, + delegator: user.address, + authority: ROOT_AUTHORITY, + caveats: [], + salt: 1n, + signature: '0x', + } + + delegation.signature = await user.signTypedData( + { ...DOMAIN, chainId, verifyingContract: delegationManager }, + DELEGATION_712_TYPES, + { + delegate: delegation.delegate, + delegator: delegation.delegator, + authority: delegation.authority, + caveats: [], + salt: delegation.salt, + } + ) + + const abi = ethers.AbiCoder.defaultAbiCoder() + return abi.encode([`${DELEGATION_ABI_TYPE}[]`], [[delegation]]) +} diff --git a/packages/evm/test/smart-accounts/SmartAccountsHandler.test.ts b/packages/evm/test/smart-accounts/SmartAccountsHandler.test.ts index 382be61..9958bb8 100644 --- a/packages/evm/test/smart-accounts/SmartAccountsHandler.test.ts +++ b/packages/evm/test/smart-accounts/SmartAccountsHandler.test.ts @@ -6,12 +6,14 @@ import { network } from 'hardhat' import { CallMock, + EIP7702StatelessDeleGatorMock, SafeMock, SmartAccount7702, SmartAccountContract, SmartAccountsHandler, TokenMock, } from '../../types/ethers-contracts/index.js' +import { signDelegation } from '../helpers/delegations.js' const { ethers } = await network.connect() @@ -21,6 +23,7 @@ describe('SmartAccountsHandler', () => { let handler: SmartAccountsHandler, smartAccountContract: SmartAccountContract, smartAccount7702: SmartAccount7702, + smartAccountDelegator: EIP7702StatelessDeleGatorMock, safe: SafeMock let owner: HardhatEthersSigner, user: HardhatEthersSigner @@ -33,6 +36,7 @@ describe('SmartAccountsHandler', () => { handler = await ethers.deployContract('SmartAccountsHandler') smartAccount7702 = await ethers.deployContract('SmartAccount7702', [handler]) smartAccountContract = await ethers.deployContract('SmartAccountContract', [handler, owner]) + smartAccountDelegator = await ethers.deployContract('EIP7702StatelessDeleGatorMock', [owner]) safe = await ethers.deployContract('SafeMock') }) @@ -381,6 +385,82 @@ describe('SmartAccountsHandler', () => { }) }) }) + + context('when the account is a 7702 Stateless Delegator', () => { + let authorization: Authorization + + beforeEach('sign authorization', async () => { + authorization = await user.authorize({ address: smartAccountDelegator }) + }) + + context('when the inner call succeeds', () => { + let callData: string + + beforeEach('encode call data', async () => { + callData = callMock.interface.encodeFunctionData('call') + }) + + const itExecutesTheCallWithValue = (value: bigint) => { + it('executes a call', async () => { + const delegationManager = await smartAccountDelegator.delegationManager() + const permissionContext = await signDelegation(user, handler.target, delegationManager) + const data = ethers.AbiCoder.defaultAbiCoder().encode(['bytes', 'bytes'], [permissionContext, callData]) + + const tx = await handler.call(smartAccountDelegator, callMock, data, value, { + authorizationList: [authorization], + }) + + const receipt = await tx.wait() + + const events = await callMock.queryFilter( + callMock.filters.CallReceived(), + receipt!.blockNumber, + receipt!.blockNumber + ) + + expect(events).to.have.lengthOf(1) + expect(events[0].args.sender).to.equal(user) + expect(events[0].args.value).to.equal(value) + }) + } + + context('when sending no value', () => { + const value = 0n + + itExecutesTheCallWithValue(value) + }) + + context('when sending value', () => { + const value = 1n + + beforeEach('fund smart account', async () => { + await owner.sendTransaction({ to: safe, value, data: '0x' }) + }) + + itExecutesTheCallWithValue(value) + }) + }) + + context('when the inner call fails', () => { + let callData: string + + beforeEach('encode call data', () => { + callData = callMock.interface.encodeFunctionData('callError') + }) + + it('bubbles the error', async () => { + const delegationManager = await smartAccountDelegator.delegationManager() + const permissionContext = await signDelegation(user, handler.target, delegationManager) + const data = ethers.AbiCoder.defaultAbiCoder().encode(['bytes', 'bytes'], [permissionContext, callData]) + + const promise = handler.call(smartAccountDelegator, callMock, data, 0, { + authorizationList: [authorization], + }) + + await expect(promise).to.be.revertedWithCustomError(callMock, 'CallError') + }) + }) + }) }) context('when the account is not supported', () => { diff --git a/yarn.lock b/yarn.lock index 9081310..3e458d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -779,11 +779,6 @@ "@nomicfoundation/solidity-analyzer-linux-x64-musl" "0.1.2" "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.2" -"@openzeppelin/contracts@5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.3.0.tgz#0a90ce16f5c855e3c8239691f1722cd4999ae741" - integrity sha512-zj/KGoW7zxWUE8qOI++rUM18v+VeLTTzKs/DJFkSzHpQFPD/jKKF0TrMxBfGLl3kpdELCNccvB3zmofSzm4nlA== - "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" From ed841dd64e64cb48b24d12ca73572ab53890d4c0 Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Mon, 15 Dec 2025 14:59:21 -0300 Subject: [PATCH 2/2] chore: update gh action --- .github/workflows/ci.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61efa14..c7083d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Set up Node.js + - name: Set up Node uses: actions/setup-node@v3 with: node-version-file: ".nvmrc" @@ -57,13 +57,23 @@ jobs: needs: find-changed-packages if: ${{ contains(needs.find-changed-packages.outputs.packages, 'evm') }} steps: - - name: Checkout - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - name: Checkout (with submodules) + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + - name: Update submodules + run: git submodule update --init --recursive + - name: Set up Node + uses: actions/setup-node@v3 with: node-version-file: ".nvmrc" - name: Install run: yarn workspace @mimicprotocol/contracts-evm install + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Forge install + run: cd packages/evm/lib/delegation-framework && forge build - name: Build run: yarn workspace @mimicprotocol/contracts-evm build - name: Test @@ -77,7 +87,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Set up Node.js + - name: Set up Node uses: actions/setup-node@v4 with: node-version: "22.15.1"