Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
},
"workspaces": {
"packages": [
"packages/**"
"packages/evm",
"packages/svm"
]
}
}
43 changes: 41 additions & 2 deletions packages/evm/contracts/smart-accounts/SmartAccountsHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
15 changes: 12 additions & 3 deletions packages/evm/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -18,7 +27,7 @@ const config: HardhatUserConfig = {
},
},
},
},
],
},
networks: {
ethereum: {
Expand Down
1 change: 1 addition & 0 deletions packages/evm/lib/delegation-framework
Submodule delegation-framework added at bfbdf9
1 change: 1 addition & 0 deletions packages/evm/lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at e4f702
6 changes: 2 additions & 4 deletions packages/evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -43,7 +40,8 @@
},
"eslintIgnore": [
"types",
"artifacts"
"artifacts",
"lib"
],
"eslintConfig": {
"extends": "eslint-config-mimic"
Expand Down
2 changes: 2 additions & 0 deletions packages/evm/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
delegation-framework/=lib/delegation-framework/src/
@openzeppelin/=lib/openzeppelin-contracts/
62 changes: 62 additions & 0 deletions packages/evm/test/helpers/delegations.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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]])
}
80 changes: 80 additions & 0 deletions packages/evm/test/smart-accounts/SmartAccountsHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

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()

Expand All @@ -21,6 +23,7 @@
let handler: SmartAccountsHandler,
smartAccountContract: SmartAccountContract,
smartAccount7702: SmartAccount7702,
smartAccountDelegator: EIP7702StatelessDeleGatorMock,
safe: SafeMock
let owner: HardhatEthersSigner, user: HardhatEthersSigner

Expand All @@ -33,6 +36,7 @@
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')
})

Expand Down Expand Up @@ -381,6 +385,82 @@
})
})
})

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,

Check warning on line 417 in packages/evm/test/smart-accounts/SmartAccountsHandler.test.ts

View workflow job for this annotation

GitHub Actions / lint (evm)

Forbidden non-null assertion
receipt!.blockNumber

Check warning on line 418 in packages/evm/test/smart-accounts/SmartAccountsHandler.test.ts

View workflow job for this annotation

GitHub Actions / lint (evm)

Forbidden non-null assertion
)

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', () => {
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading