From 5e820760461c45189b2c113039dae57995511cf6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 10:49:35 +0100 Subject: [PATCH 01/66] Add P256 implementation and testing --- contracts/utils/cryptography/P256.sol | 297 ++++++++++++++++++++++++++ test/utils/cryptography/P256.t.sol | 28 +++ test/utils/cryptography/P256.test.js | 56 +++++ 3 files changed, 381 insertions(+) create mode 100644 contracts/utils/cryptography/P256.sol create mode 100644 test/utils/cryptography/P256.t.sol create mode 100644 test/utils/cryptography/P256.test.js diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol new file mode 100644 index 00000000000..7fd72f084e5 --- /dev/null +++ b/contracts/utils/cryptography/P256.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.20; + +import { Math } from "../math/Math.sol"; + +/** + * @dev Implementation of secp256r1 verification and recovery functions. + * + * Based on + * - https://github.com/itsobvioustech/aa-passkeys-wallet/blob/main/src/Secp256r1.sol + * Which is heavily inspired from + * - https://github.com/maxrobot/elliptic-solidity/blob/master/contracts/Secp256r1.sol + * - https://github.com/tdrerup/elliptic-curve-solidity/blob/master/contracts/curves/EllipticCurve.sol + */ +library P256 { + struct JPoint { + uint256 x; + uint256 y; + uint256 z; + } + + uint256 constant gx = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296; + uint256 constant gy = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5; + uint256 constant pp = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF; + uint256 constant nn = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551; + uint256 constant aa = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC; + uint256 constant bb = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B; + uint256 constant pp2 = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFD; + uint256 constant nn2 = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC63254F; + uint256 constant pp1div4 = 0x3fffffffc0000000400000000000000000000000400000000000000000000000; + + /** + * @dev signature verification + * @param Qx - public key coordinate X + * @param Qy - public key coordinate Y + * @param r - signature half R + * @param s - signature half S + * @param e - hashed message + */ + function verify(uint256 Qx, uint256 Qy, uint256 r, uint256 s, uint256 e) internal view returns (bool) { + if (r == 0 || r >= nn || s == 0 || s >= nn || !isOnCurve(Qx, Qy)) return false; + + JPoint[16] memory points = _preComputeJacobianPoints(Qx, Qy); + uint256 w = _invModN(s); + uint256 u1 = mulmod(e, w, nn); + uint256 u2 = mulmod(r, w, nn); + (uint256 x, ) = _jMultShamir(points, u1, u2); + return (x == r); + } + + /** + * @dev public key recovery + * @param r - signature half R + * @param s - signature half S + * @param v - signature recovery param + * @param e - hashed message + */ + function recovery(uint256 r, uint256 s, uint8 v, uint256 e) internal view returns (uint256, uint256) { + if (r == 0 || r >= nn || s == 0 || s >= nn || v > 1) return (0, 0); + + uint256 rx = r; + uint256 ry2 = addmod(mulmod(addmod(mulmod(rx, rx, pp), aa, pp), rx, pp), bb, pp); // weierstrass equation y² = x³ + a.x + b + uint256 ry = Math.modExp(ry2, pp1div4, pp); // This formula for sqrt work because pp ≡ 3 (mod 4) + if (mulmod(ry, ry, pp) != ry2) return (0, 0); // Sanity check + if (ry % 2 != v % 2) ry = pp - ry; + + JPoint[16] memory points = _preComputeJacobianPoints(rx, ry); + uint256 w = _invModN(r); + uint256 u1 = mulmod(nn - (e % nn), w, nn); + uint256 u2 = mulmod(s, w, nn); + (uint256 x, uint256 y) = _jMultShamir(points, u1, u2); + return (x, y); + } + + /** + * @dev address recovery + * @param r - signature half R + * @param s - signature half S + * @param v - signature recovery param + * @param e - hashed message + */ + function recoveryAddress(uint256 r, uint256 s, uint8 v, uint256 e) internal view returns (address) { + (uint256 Qx, uint256 Qy) = recovery(r, s, v, e); + return getAddress(Qx, Qy); + } + + /** + * @dev derivate public key + * @param privateKey - private key + */ + function getPublicKey(uint256 privateKey) internal view returns (uint256, uint256) { + (uint256 x, uint256 y, uint256 z) = _jMult(gx, gy, 1, privateKey); + return _affineFromJacobian(x, y, z); + } + + /** + * @dev Hash public key into an address + * @param Qx - public key coordinate X + * @param Qy - public key coordinate Y + */ + function getAddress(uint256 Qx, uint256 Qy) internal pure returns (address result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, Qx) + mstore(0x20, Qy) + result := keccak256(0x00, 0x40) + } + } + + /** + * @dev check if a point is on the curve. + */ + function isOnCurve(uint256 x, uint256 y) internal pure returns (bool result) { + /// @solidity memory-safe-assembly + assembly { + let p := pp + let lhs := mulmod(y, y, p) + let rhs := addmod(mulmod(addmod(mulmod(x, x, p), aa, p), x, p), bb, p) + result := eq(lhs, rhs) + } + } + + /** + * @dev Reduce from jacobian to affine coordinates + * @param jx - jacobian coordinate x + * @param jy - jacobian coordinate y + * @param jz - jacobian coordinate z + * @return ax - affine coordiante x + * @return ay - affine coordiante y + */ + function _affineFromJacobian(uint256 jx, uint256 jy, uint256 jz) private view returns (uint256 ax, uint256 ay) { + if (jz == 0) return (0, 0); + uint256 zinv = _invModP(jz); + uint256 zzinv = mulmod(zinv, zinv, pp); + uint256 zzzinv = mulmod(zzinv, zinv, pp); + ax = mulmod(jx, zzinv, pp); + ay = mulmod(jy, zzzinv, pp); + } + + /** + * @dev Point addition on the jacobian coordinates + * https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates + */ + function _jAdd(uint256 x1, uint256 y1, uint256 z1, uint256 x2, uint256 y2, uint256 z2) private pure returns (uint256 x3, uint256 y3, uint256 z3) { + if (z1 == 0) { + return (x2, y2, z2); + } + if (z2 == 0) { + return (x1, y1, z1); + } + /// @solidity memory-safe-assembly + assembly { + let p := pp + let zz1 := mulmod(z1, z1, p) // zz1 = z1² + let zz2 := mulmod(z2, z2, p) // zz2 = z2² + let u1 := mulmod(x1, zz2, p) // u1 = x1*z2² + let u2 := mulmod(x2, zz1, p) // u2 = x2*z1² + let s1 := mulmod(y1, mulmod(zz2, z2, p), p) // s1 = y1*z2³ + let s2 := mulmod(y2, mulmod(zz1, z1, p), p) // s2 = y2*z1³ + let h := addmod(u2, sub(p, u1), p) // h = u2-u1 + let hh := mulmod(h, h, p) // h² + let hhh := mulmod(h, hh, p) // h³ + let r := addmod(s2, sub(p, s1), p) // r = s2-s1 + + // x' = r²-h³-2*u1*h² + x3 := addmod(addmod(mulmod(r, r, p), sub(p, hhh), p), sub(p, mulmod(2, mulmod(u1, hh, p), p)), p) + // y' = r*(u1*h²-x')-s1*h³ + y3 := addmod(mulmod(r,addmod(mulmod(u1, hh, p), sub(p, x3), p), p), sub(p, mulmod(s1, hhh, p)), p) + // z' = h*z1*z2 + z3 := mulmod(h, mulmod(z1, z2, p), p) + } + } + + /** + * @dev Point doubling on the jacobian coordinates + * https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates + */ + function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 x2, uint256 y2, uint256 z2) { + /// @solidity memory-safe-assembly + assembly { + let p := pp + let yy := mulmod(y, y, p) + let zz := mulmod(z, z, p) + let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y² + let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(aa, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z⁴ + + // x' = m²-2*s + x2 := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) + // y' = m*(s-x')-8*y⁴ + y2 := addmod(mulmod(m, addmod(s, sub(p, x2), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p) + // z' = 2*y*z + z2 := mulmod(2, mulmod(y, z, p), p) + } + } + + /** + * @dev Point multiplication on the jacobian coordinates + */ + function _jMult(uint256 x, uint256 y, uint256 z, uint256 k) private pure returns (uint256 x2, uint256 y2, uint256 z2) { + unchecked { + for (uint256 i = 0; i < 256; ++i) { + if (z > 0) { + (x2, y2, z2) = _jDouble(x2, y2, z2); + } + if (k >> 255 > 0) { + (x2, y2, z2) = _jAdd(x2, y2, z2, x, y, z); + } + k <<= 1; + } + } + } + + /** + * @dev Compute P·u1 + Q·u2 using the precomputed points for P and Q (see {_preComputeJacobianPoints}). + * + * Uses Strauss Shamir trick for EC multiplication + * https://stackoverflow.com/questions/50993471/ec-scalar-multiplication-with-strauss-shamir-method + * we optimise on this a bit to do with 2 bits at a time rather than a single bit + * the individual points for a single pass are precomputed + * overall this reduces the number of additions while keeping the same number of doublings + */ + function _jMultShamir(JPoint[16] memory points, uint256 u1, uint256 u2) private view returns (uint256, uint256) { + uint256 x = 0; + uint256 y = 0; + uint256 z = 0; + unchecked { + for (uint256 i = 0; i < 128; ++i) { + if (z > 0) { + (x, y, z) = _jDouble(x, y, z); + (x, y, z) = _jDouble(x, y, z); + } + // Read 2 bits of u1, and 2 bits of u2. Combining the two give a lookup index in the table. + uint256 pos = (u1 >> 252 & 0xc) | (u2 >> 254 & 0x3); + if (pos > 0) { + (x, y, z) = _jAdd(x, y, z, points[pos].x, points[pos].y, points[pos].z); + } + u1 <<= 2; + u2 <<= 2; + } + } + return _affineFromJacobian(x, y, z); + } + + /** + * @dev Precompute a matrice of usefull jacobian points associated to a given P. This can be seen as a 4x4 matrix + * that contains combinaison of P and G (generator) up to 3 times each. See table bellow: + * + * ┌────┬─────────────────────┐ + * │ i │ 0 1 2 3 │ + * ├────┼─────────────────────┤ + * │ 0 │ 0 p 2p 3p │ + * │ 4 │ g g+p g+2p g+3p │ + * │ 8 │ 2g 2g+p 2g+2p 2g+3p │ + * │ 12 │ 3g 3g+p 3g+2p 3g+3p │ + * └────┴─────────────────────┘ + */ + function _preComputeJacobianPoints(uint256 px, uint256 py) private pure returns (JPoint[16] memory points) { + points[0x00] = JPoint(0, 0, 0); + points[0x01] = JPoint(px, py, 1); + points[0x04] = JPoint(gx, gy, 1); + points[0x02] = _jDoublePoint(points[0x01]); + points[0x08] = _jDoublePoint(points[0x04]); + points[0x03] = _jAddPoint(points[0x01], points[0x02]); + points[0x05] = _jAddPoint(points[0x01], points[0x04]); + points[0x06] = _jAddPoint(points[0x02], points[0x04]); + points[0x07] = _jAddPoint(points[0x03], points[0x04]); + points[0x09] = _jAddPoint(points[0x01], points[0x08]); + points[0x0a] = _jAddPoint(points[0x02], points[0x08]); + points[0x0b] = _jAddPoint(points[0x03], points[0x08]); + points[0x0c] = _jAddPoint(points[0x04], points[0x08]); + points[0x0d] = _jAddPoint(points[0x01], points[0x0c]); + points[0x0e] = _jAddPoint(points[0x02], points[0x0c]); + points[0x0f] = _jAddPoint(points[0x03], points[0x0C]); + } + + function _jAddPoint(JPoint memory p1, JPoint memory p2) private pure returns (JPoint memory) { + (uint256 x, uint256 y, uint256 z) = _jAdd(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z); + return JPoint(x, y, z); + } + + function _jDoublePoint(JPoint memory p) private pure returns (JPoint memory) { + (uint256 x, uint256 y, uint256 z) = _jDouble(p.x, p.y, p.z); + return JPoint(x, y, z); + } + + /** + *@dev From Fermat's little theorem https://en.wikipedia.org/wiki/Fermat%27s_little_theorem: + * `a**(p-1) ≡ 1 mod p`. This means that `a**(p-2)` is an inverse of a in Fp. + */ + function _invModN(uint256 value) private view returns (uint256) { + return Math.modExp(value, nn2, nn); + } + + function _invModP(uint256 value) private view returns (uint256) { + return Math.modExp(value, pp2, pp); + } +} diff --git a/test/utils/cryptography/P256.t.sol b/test/utils/cryptography/P256.t.sol new file mode 100644 index 00000000000..00bc2127165 --- /dev/null +++ b/test/utils/cryptography/P256.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; + +contract P256Test is Test { + /// forge-config: default.fuzz.runs = 256 + function testVerify(uint256 seed, bytes32 digest) public { + uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.nn - 1); + + (uint256 x, uint256 y) = P256.getPublicKey(privateKey); + (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); + assertTrue(P256.verify(x, y, uint256(r), uint256(s), uint256(digest))); + } + + /// forge-config: default.fuzz.runs = 256 + function testRecover(uint256 seed, bytes32 digest) public { + uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.nn - 1); + + (uint256 x, uint256 y) = P256.getPublicKey(privateKey); + (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); + (uint256 qx0, uint256 qy0) = P256.recovery(uint256(r), uint256(s), 0, uint256(digest)); + (uint256 qx1, uint256 qy1) = P256.recovery(uint256(r), uint256(s), 1, uint256(digest)); + assertTrue((qx0 == x && qy0 == y) || (qx1 == x && qy1 == y)); + } +} \ No newline at end of file diff --git a/test/utils/cryptography/P256.test.js b/test/utils/cryptography/P256.test.js new file mode 100644 index 00000000000..d96880c339a --- /dev/null +++ b/test/utils/cryptography/P256.test.js @@ -0,0 +1,56 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { secp256r1 } = require('@noble/curves/p256'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const prepareSignature = ( + privateKey = secp256r1.utils.randomPrivateKey(), + messageHash = ethers.hexlify(ethers.randomBytes(0x20)) +) => { + const publicKey = [ + secp256r1.getPublicKey(privateKey, false).slice(0x01, 0x21), + secp256r1.getPublicKey(privateKey, false).slice(0x21, 0x41), + ].map(ethers.hexlify) + const { r, s, recovery } = secp256r1.sign(messageHash.replace(/0x/, ''), privateKey); + const signature = [ r, s ].map(v => ethers.toBeHex(v, 0x20)); + return { privateKey, publicKey, signature, recovery, messageHash }; +}; + +describe('P256', function () { + async function fixture() { + return { mock: await ethers.deployContract('$P256') }; + } + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture), prepareSignature()); + }); + + it('derivate public from private', async function () { + expect(await this.mock.$getPublicKey(ethers.toBigInt(this.privateKey))).to.deep.equal(this.publicKey); + }); + + Array(10).fill().forEach((_, i, {length}) => { + it(`confirm valid signature (run ${i + 1}/${length})`, async function () { + expect(await this.mock.$verify(...this.publicKey, ...this.signature, this.messageHash)).to.be.true; + }); + + it(`recover public key (run ${i + 1}/${length})`, async function () { + expect(await this.mock.$recovery(...this.signature, this.recovery, this.messageHash)).to.deep.equal(this.publicKey); + }); + }); + + it('reject signature with flipped public key coordinates ([x,y] >> [y,x])', async function () { + this.publicKey.reverse(); + expect(await this.mock.$verify(...this.publicKey, ...this.signature, this.messageHash)).to.be.false; + }); + + it('reject signature with flipped signature values ([r,s] >> [s,r])', async function () { + this.signature.reverse(); + expect(await this.mock.$verify(...this.publicKey, ...this.signature, this.messageHash)).to.be.false; + }); + + it('reject signature with invalid message hash', async function () { + var invalidMessageHash = ethers.hexlify(ethers.randomBytes(32)); + expect(await this.mock.$verify(...this.publicKey, ...this.signature, invalidMessageHash)).to.be.false; + }); +}); \ No newline at end of file From da0f27ecab2fc940804f80ce4b1b5fc504add751 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 10:49:48 +0100 Subject: [PATCH 02/66] enable optimizations by default --- hardhat.config.js | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/hardhat.config.js b/hardhat.config.js index 230cca5e215..4006ce19c0a 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -1,12 +1,12 @@ /// ENVVAR -// - COMPILE_VERSION: compiler version (default: 0.8.20) -// - SRC: contracts folder to compile (default: contracts) -// - COMPILE_MODE: production modes enables optimizations (default: development) -// - IR: enable IR compilation (default: false) -// - COVERAGE: enable coverage report -// - ENABLE_GAS_REPORT: enable gas report -// - COINMARKETCAP: coinmarkercat api key for USD value in gas report -// - CI: output gas report to file instead of stdout +// - COMPILER: compiler version (default: 0.8.20) +// - SRC: contracts folder to compile (default: contracts) +// - RUNS: number of optimization runs (default: 200) +// - IR: enable IR compilation (default: false) +// - COVERAGE: enable coverage report (default: false) +// - GAS: enable gas report (default: false) +// - COINMARKETCAP: coinmarketcap api key for USD value in gas report +// - CI: output gas report to file instead of stdout const fs = require('fs'); const path = require('path'); @@ -25,11 +25,10 @@ const { argv } = require('yargs/yargs')() type: 'string', default: 'contracts', }, - mode: { - alias: 'compileMode', - type: 'string', - choices: ['production', 'development'], - default: 'development', + runs: { + alias: 'optimizationRuns', + type: 'number', + default: 200, }, ir: { alias: 'enableIR', @@ -64,9 +63,6 @@ for (const f of fs.readdirSync(path.join(__dirname, 'hardhat'))) { require(path.join(__dirname, 'hardhat', f)); } -const withOptimizations = argv.gas || argv.coverage || argv.compileMode === 'production'; -const allowUnlimitedContractSize = argv.gas || argv.coverage || argv.compileMode === 'development'; - /** * @type import('hardhat/config').HardhatUserConfig */ @@ -75,10 +71,10 @@ module.exports = { version: argv.compiler, settings: { optimizer: { - enabled: withOptimizations, - runs: 200, + enabled: true, + runs: argv.runs, }, - viaIR: withOptimizations && argv.ir, + viaIR: argv.ir, outputSelection: { '*': { '*': ['storageLayout'] } }, }, }, @@ -88,14 +84,14 @@ module.exports = { 'initcode-size': 'off', }, '*': { - 'code-size': withOptimizations, + 'code-size': true, 'unused-param': !argv.coverage, // coverage causes unused-param warnings default: 'error', }, }, networks: { hardhat: { - allowUnlimitedContractSize, + allowUnlimitedContractSize: argv.gas || argv.coverage, initialBaseFeePerGas: argv.coverage ? 0 : undefined, }, }, From aa59c670831365dacf846bac13eccb1dd6d24269 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 11:10:57 +0100 Subject: [PATCH 03/66] test recovering address --- test/utils/cryptography/P256.test.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/utils/cryptography/P256.test.js b/test/utils/cryptography/P256.test.js index d96880c339a..1323bfc95de 100644 --- a/test/utils/cryptography/P256.test.js +++ b/test/utils/cryptography/P256.test.js @@ -29,14 +29,17 @@ describe('P256', function () { expect(await this.mock.$getPublicKey(ethers.toBigInt(this.privateKey))).to.deep.equal(this.publicKey); }); - Array(10).fill().forEach((_, i, {length}) => { - it(`confirm valid signature (run ${i + 1}/${length})`, async function () { - expect(await this.mock.$verify(...this.publicKey, ...this.signature, this.messageHash)).to.be.true; - }); - - it(`recover public key (run ${i + 1}/${length})`, async function () { - expect(await this.mock.$recovery(...this.signature, this.recovery, this.messageHash)).to.deep.equal(this.publicKey); - }); + it('verify valid signature', async function () { + expect(await this.mock.$verify(...this.publicKey, ...this.signature, this.messageHash)).to.be.true; + }); + + it('recover public key', async function () { + expect(await this.mock.$recovery(...this.signature, this.recovery, this.messageHash)).to.deep.equal(this.publicKey); + }); + + it('recover address', async function () { + const address = ethers.getAddress(ethers.keccak256(ethers.concat(this.publicKey)).slice(-40)); + expect(await this.mock.$recoveryAddress(...this.signature, this.recovery, this.messageHash)).to.equal(address); }); it('reject signature with flipped public key coordinates ([x,y] >> [y,x])', async function () { From 951294787f58d4aea2e2f3d17b2caad382dc4635 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 11:17:54 +0100 Subject: [PATCH 04/66] improved testing --- test/utils/cryptography/P256.test.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/test/utils/cryptography/P256.test.js b/test/utils/cryptography/P256.test.js index 1323bfc95de..c2e2452013b 100644 --- a/test/utils/cryptography/P256.test.js +++ b/test/utils/cryptography/P256.test.js @@ -11,9 +11,10 @@ const prepareSignature = ( secp256r1.getPublicKey(privateKey, false).slice(0x01, 0x21), secp256r1.getPublicKey(privateKey, false).slice(0x21, 0x41), ].map(ethers.hexlify) + const address = ethers.getAddress(ethers.keccak256(ethers.concat(publicKey)).slice(-40)); const { r, s, recovery } = secp256r1.sign(messageHash.replace(/0x/, ''), privateKey); const signature = [ r, s ].map(v => ethers.toBeHex(v, 0x20)); - return { privateKey, publicKey, signature, recovery, messageHash }; + return { address, privateKey, publicKey, signature, recovery, messageHash }; }; describe('P256', function () { @@ -38,22 +39,25 @@ describe('P256', function () { }); it('recover address', async function () { - const address = ethers.getAddress(ethers.keccak256(ethers.concat(this.publicKey)).slice(-40)); - expect(await this.mock.$recoveryAddress(...this.signature, this.recovery, this.messageHash)).to.equal(address); + expect(await this.mock.$recoveryAddress(...this.signature, this.recovery, this.messageHash)).to.equal(this.address); }); it('reject signature with flipped public key coordinates ([x,y] >> [y,x])', async function () { - this.publicKey.reverse(); - expect(await this.mock.$verify(...this.publicKey, ...this.signature, this.messageHash)).to.be.false; + const reversedPublicKey = Array.from(this.publicKey).reverse(); + expect(await this.mock.$verify(...reversedPublicKey, ...this.signature, this.messageHash)).to.be.false; }); it('reject signature with flipped signature values ([r,s] >> [s,r])', async function () { - this.signature.reverse(); - expect(await this.mock.$verify(...this.publicKey, ...this.signature, this.messageHash)).to.be.false; + const reversedSignature = Array.from(this.signature).reverse(); + expect(await this.mock.$verify(...this.publicKey, ...reversedSignature, this.messageHash)).to.be.false; + expect(await this.mock.$recovery(...reversedSignature, this.recovery, this.messageHash)).to.not.deep.equal(this.publicKey); + expect(await this.mock.$recoveryAddress(...reversedSignature, this.recovery, this.messageHash)).to.not.equal(this.address); }); it('reject signature with invalid message hash', async function () { - var invalidMessageHash = ethers.hexlify(ethers.randomBytes(32)); + const invalidMessageHash = ethers.hexlify(ethers.randomBytes(32)); expect(await this.mock.$verify(...this.publicKey, ...this.signature, invalidMessageHash)).to.be.false; + expect(await this.mock.$recovery(...this.signature, this.recovery, invalidMessageHash)).to.not.deep.equal(this.publicKey); + expect(await this.mock.$recoveryAddress(...this.signature, this.recovery, invalidMessageHash)).to.not.equal(this.address); }); }); \ No newline at end of file From a60bf48463536a913ebb677d90edf641c81e0812 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 11:38:32 +0100 Subject: [PATCH 05/66] spelling --- contracts/utils/cryptography/P256.sol | 8 ++++---- test/utils/cryptography/P256.t.sol | 2 +- test/utils/cryptography/P256.test.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol index 7fd72f084e5..c3888ea0be5 100644 --- a/contracts/utils/cryptography/P256.sol +++ b/contracts/utils/cryptography/P256.sol @@ -125,8 +125,8 @@ library P256 { * @param jx - jacobian coordinate x * @param jy - jacobian coordinate y * @param jz - jacobian coordinate z - * @return ax - affine coordiante x - * @return ay - affine coordiante y + * @return ax - affine coordinate x + * @return ay - affine coordinate y */ function _affineFromJacobian(uint256 jx, uint256 jy, uint256 jz) private view returns (uint256 ax, uint256 ay) { if (jz == 0) return (0, 0); @@ -242,8 +242,8 @@ library P256 { } /** - * @dev Precompute a matrice of usefull jacobian points associated to a given P. This can be seen as a 4x4 matrix - * that contains combinaison of P and G (generator) up to 3 times each. See table bellow: + * @dev Precompute a matrice of useful jacobian points associated to a given P. This can be seen as a 4x4 matrix + * that contains combinaison of P and G (generator) up to 3 times each. See table below: * * ┌────┬─────────────────────┐ * │ i │ 0 1 2 3 │ diff --git a/test/utils/cryptography/P256.t.sol b/test/utils/cryptography/P256.t.sol index 00bc2127165..b5b00084fd8 100644 --- a/test/utils/cryptography/P256.t.sol +++ b/test/utils/cryptography/P256.t.sol @@ -25,4 +25,4 @@ contract P256Test is Test { (uint256 qx1, uint256 qy1) = P256.recovery(uint256(r), uint256(s), 1, uint256(digest)); assertTrue((qx0 == x && qy0 == y) || (qx1 == x && qy1 == y)); } -} \ No newline at end of file +} diff --git a/test/utils/cryptography/P256.test.js b/test/utils/cryptography/P256.test.js index c2e2452013b..f9be534140a 100644 --- a/test/utils/cryptography/P256.test.js +++ b/test/utils/cryptography/P256.test.js @@ -60,4 +60,4 @@ describe('P256', function () { expect(await this.mock.$recovery(...this.signature, this.recovery, invalidMessageHash)).to.not.deep.equal(this.publicKey); expect(await this.mock.$recoveryAddress(...this.signature, this.recovery, invalidMessageHash)).to.not.equal(this.address); }); -}); \ No newline at end of file +}); From 918502683dccddf1e93f644dd22f531900dcf19e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 11:39:10 +0100 Subject: [PATCH 06/66] fix lint --- contracts/utils/cryptography/P256.sol | 22 +++++++++++++++++----- test/utils/cryptography/P256.test.js | 22 +++++++++++++++------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol index c3888ea0be5..0901e8ad271 100644 --- a/contracts/utils/cryptography/P256.sol +++ b/contracts/utils/cryptography/P256.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.20; -import { Math } from "../math/Math.sol"; +import {Math} from "../math/Math.sol"; /** * @dev Implementation of secp256r1 verification and recovery functions. @@ -141,7 +141,14 @@ library P256 { * @dev Point addition on the jacobian coordinates * https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates */ - function _jAdd(uint256 x1, uint256 y1, uint256 z1, uint256 x2, uint256 y2, uint256 z2) private pure returns (uint256 x3, uint256 y3, uint256 z3) { + function _jAdd( + uint256 x1, + uint256 y1, + uint256 z1, + uint256 x2, + uint256 y2, + uint256 z2 + ) private pure returns (uint256 x3, uint256 y3, uint256 z3) { if (z1 == 0) { return (x2, y2, z2); } @@ -165,7 +172,7 @@ library P256 { // x' = r²-h³-2*u1*h² x3 := addmod(addmod(mulmod(r, r, p), sub(p, hhh), p), sub(p, mulmod(2, mulmod(u1, hh, p), p)), p) // y' = r*(u1*h²-x')-s1*h³ - y3 := addmod(mulmod(r,addmod(mulmod(u1, hh, p), sub(p, x3), p), p), sub(p, mulmod(s1, hhh, p)), p) + y3 := addmod(mulmod(r, addmod(mulmod(u1, hh, p), sub(p, x3), p), p), sub(p, mulmod(s1, hhh, p)), p) // z' = h*z1*z2 z3 := mulmod(h, mulmod(z1, z2, p), p) } @@ -196,7 +203,12 @@ library P256 { /** * @dev Point multiplication on the jacobian coordinates */ - function _jMult(uint256 x, uint256 y, uint256 z, uint256 k) private pure returns (uint256 x2, uint256 y2, uint256 z2) { + function _jMult( + uint256 x, + uint256 y, + uint256 z, + uint256 k + ) private pure returns (uint256 x2, uint256 y2, uint256 z2) { unchecked { for (uint256 i = 0; i < 256; ++i) { if (z > 0) { @@ -230,7 +242,7 @@ library P256 { (x, y, z) = _jDouble(x, y, z); } // Read 2 bits of u1, and 2 bits of u2. Combining the two give a lookup index in the table. - uint256 pos = (u1 >> 252 & 0xc) | (u2 >> 254 & 0x3); + uint256 pos = ((u1 >> 252) & 0xc) | ((u2 >> 254) & 0x3); if (pos > 0) { (x, y, z) = _jAdd(x, y, z, points[pos].x, points[pos].y, points[pos].z); } diff --git a/test/utils/cryptography/P256.test.js b/test/utils/cryptography/P256.test.js index f9be534140a..e0a479cf6c5 100644 --- a/test/utils/cryptography/P256.test.js +++ b/test/utils/cryptography/P256.test.js @@ -5,15 +5,15 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const prepareSignature = ( privateKey = secp256r1.utils.randomPrivateKey(), - messageHash = ethers.hexlify(ethers.randomBytes(0x20)) + messageHash = ethers.hexlify(ethers.randomBytes(0x20)), ) => { const publicKey = [ secp256r1.getPublicKey(privateKey, false).slice(0x01, 0x21), secp256r1.getPublicKey(privateKey, false).slice(0x21, 0x41), - ].map(ethers.hexlify) + ].map(ethers.hexlify); const address = ethers.getAddress(ethers.keccak256(ethers.concat(publicKey)).slice(-40)); const { r, s, recovery } = secp256r1.sign(messageHash.replace(/0x/, ''), privateKey); - const signature = [ r, s ].map(v => ethers.toBeHex(v, 0x20)); + const signature = [r, s].map(v => ethers.toBeHex(v, 0x20)); return { address, privateKey, publicKey, signature, recovery, messageHash }; }; @@ -50,14 +50,22 @@ describe('P256', function () { it('reject signature with flipped signature values ([r,s] >> [s,r])', async function () { const reversedSignature = Array.from(this.signature).reverse(); expect(await this.mock.$verify(...this.publicKey, ...reversedSignature, this.messageHash)).to.be.false; - expect(await this.mock.$recovery(...reversedSignature, this.recovery, this.messageHash)).to.not.deep.equal(this.publicKey); - expect(await this.mock.$recoveryAddress(...reversedSignature, this.recovery, this.messageHash)).to.not.equal(this.address); + expect(await this.mock.$recovery(...reversedSignature, this.recovery, this.messageHash)).to.not.deep.equal( + this.publicKey, + ); + expect(await this.mock.$recoveryAddress(...reversedSignature, this.recovery, this.messageHash)).to.not.equal( + this.address, + ); }); it('reject signature with invalid message hash', async function () { const invalidMessageHash = ethers.hexlify(ethers.randomBytes(32)); expect(await this.mock.$verify(...this.publicKey, ...this.signature, invalidMessageHash)).to.be.false; - expect(await this.mock.$recovery(...this.signature, this.recovery, invalidMessageHash)).to.not.deep.equal(this.publicKey); - expect(await this.mock.$recoveryAddress(...this.signature, this.recovery, invalidMessageHash)).to.not.equal(this.address); + expect(await this.mock.$recovery(...this.signature, this.recovery, invalidMessageHash)).to.not.deep.equal( + this.publicKey, + ); + expect(await this.mock.$recoveryAddress(...this.signature, this.recovery, invalidMessageHash)).to.not.equal( + this.address, + ); }); }); From 025e36053cfa8c7a2fe1907cafc7b88ca18a060c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 11:44:14 +0100 Subject: [PATCH 07/66] expose imports tick --- contracts/mocks/ExposeImports.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 contracts/mocks/ExposeImports.sol diff --git a/contracts/mocks/ExposeImports.sol b/contracts/mocks/ExposeImports.sol new file mode 100644 index 00000000000..28d275038d3 --- /dev/null +++ b/contracts/mocks/ExposeImports.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {P256} from "../utils/cryptography/P256.sol"; + +abstract contract ExposeImports { + // This will be transpiled, causing all the imports above to be transpiled when running the upgradeable tests. + // This trick is necessary for testing libraries such as Panic.sol (which are not imported by any other transpiled + // contracts and would otherwise not be exposed). +} \ No newline at end of file From 803e7359000fea56c4bdd6599813540ec5030009 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 11:53:17 +0100 Subject: [PATCH 08/66] fix lint --- contracts/mocks/ExposeImports.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/mocks/ExposeImports.sol b/contracts/mocks/ExposeImports.sol index 28d275038d3..2be6ba46ae2 100644 --- a/contracts/mocks/ExposeImports.sol +++ b/contracts/mocks/ExposeImports.sol @@ -8,4 +8,4 @@ abstract contract ExposeImports { // This will be transpiled, causing all the imports above to be transpiled when running the upgradeable tests. // This trick is necessary for testing libraries such as Panic.sol (which are not imported by any other transpiled // contracts and would otherwise not be exposed). -} \ No newline at end of file +} From 57fcecd9cce50550ddc51c135c567fc717f74594 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 12:05:38 +0100 Subject: [PATCH 09/66] fix lint --- contracts/utils/cryptography/P256.sol | 95 ++++++++++++++------------- test/utils/cryptography/P256.t.sol | 4 +- 2 files changed, 53 insertions(+), 46 deletions(-) diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol index 0901e8ad271..ecaab7fa75e 100644 --- a/contracts/utils/cryptography/P256.sol +++ b/contracts/utils/cryptography/P256.sol @@ -7,7 +7,7 @@ import {Math} from "../math/Math.sol"; * @dev Implementation of secp256r1 verification and recovery functions. * * Based on - * - https://github.com/itsobvioustech/aa-passkeys-wallet/blob/main/src/Secp256r1.sol + * - https://github.com/itsobvioustech/A-passkeys-wallet/blob/main/src/Secp256r1.sol * Which is heavily inspired from * - https://github.com/maxrobot/elliptic-solidity/blob/master/contracts/Secp256r1.sol * - https://github.com/tdrerup/elliptic-curve-solidity/blob/master/contracts/curves/EllipticCurve.sol @@ -19,31 +19,38 @@ library P256 { uint256 z; } - uint256 constant gx = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296; - uint256 constant gy = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5; - uint256 constant pp = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF; - uint256 constant nn = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551; - uint256 constant aa = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC; - uint256 constant bb = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B; - uint256 constant pp2 = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFD; - uint256 constant nn2 = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC63254F; - uint256 constant pp1div4 = 0x3fffffffc0000000400000000000000000000000400000000000000000000000; + /// @dev Generator (x component) + uint256 internal constant GX = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296; + /// @dev Generator (y component) + uint256 internal constant GY = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5; + /// @dev P (size of the field) + uint256 internal constant P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF; + /// @dev N (order of the field) + uint256 internal constant N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551; + /// @dev A parameter of the weierstrass equation + uint256 internal constant A = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC; + /// @dev B parameter of the weierstrass equation + uint256 internal constant B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B; + + uint256 private constant P2 = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFD; + uint256 private constant N2 = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC63254F; + uint256 private constant P1DIV4 = 0x3fffffffc0000000400000000000000000000000400000000000000000000000; /** * @dev signature verification - * @param Qx - public key coordinate X - * @param Qy - public key coordinate Y + * @param qx - public key coordinate X + * @param qy - public key coordinate Y * @param r - signature half R * @param s - signature half S * @param e - hashed message */ - function verify(uint256 Qx, uint256 Qy, uint256 r, uint256 s, uint256 e) internal view returns (bool) { - if (r == 0 || r >= nn || s == 0 || s >= nn || !isOnCurve(Qx, Qy)) return false; + function verify(uint256 qx, uint256 qy, uint256 r, uint256 s, uint256 e) internal view returns (bool) { + if (r == 0 || r >= N || s == 0 || s >= N || !isOnCurve(qx, qy)) return false; - JPoint[16] memory points = _preComputeJacobianPoints(Qx, Qy); + JPoint[16] memory points = _preComputeJacobianPoints(qx, qy); uint256 w = _invModN(s); - uint256 u1 = mulmod(e, w, nn); - uint256 u2 = mulmod(r, w, nn); + uint256 u1 = mulmod(e, w, N); + uint256 u2 = mulmod(r, w, N); (uint256 x, ) = _jMultShamir(points, u1, u2); return (x == r); } @@ -56,18 +63,18 @@ library P256 { * @param e - hashed message */ function recovery(uint256 r, uint256 s, uint8 v, uint256 e) internal view returns (uint256, uint256) { - if (r == 0 || r >= nn || s == 0 || s >= nn || v > 1) return (0, 0); + if (r == 0 || r >= N || s == 0 || s >= N || v > 1) return (0, 0); uint256 rx = r; - uint256 ry2 = addmod(mulmod(addmod(mulmod(rx, rx, pp), aa, pp), rx, pp), bb, pp); // weierstrass equation y² = x³ + a.x + b - uint256 ry = Math.modExp(ry2, pp1div4, pp); // This formula for sqrt work because pp ≡ 3 (mod 4) - if (mulmod(ry, ry, pp) != ry2) return (0, 0); // Sanity check - if (ry % 2 != v % 2) ry = pp - ry; + uint256 ry2 = addmod(mulmod(addmod(mulmod(rx, rx, P), A, P), rx, P), B, P); // weierstrass equation y² = x³ + a.x + b + uint256 ry = Math.modExp(ry2, P1DIV4, P); // This formula for sqrt work because P ≡ 3 (mod 4) + if (mulmod(ry, ry, P) != ry2) return (0, 0); // Sanity check + if (ry % 2 != v % 2) ry = P - ry; JPoint[16] memory points = _preComputeJacobianPoints(rx, ry); uint256 w = _invModN(r); - uint256 u1 = mulmod(nn - (e % nn), w, nn); - uint256 u2 = mulmod(s, w, nn); + uint256 u1 = mulmod(N - (e % N), w, N); + uint256 u2 = mulmod(s, w, N); (uint256 x, uint256 y) = _jMultShamir(points, u1, u2); return (x, y); } @@ -80,8 +87,8 @@ library P256 { * @param e - hashed message */ function recoveryAddress(uint256 r, uint256 s, uint8 v, uint256 e) internal view returns (address) { - (uint256 Qx, uint256 Qy) = recovery(r, s, v, e); - return getAddress(Qx, Qy); + (uint256 qx, uint256 qy) = recovery(r, s, v, e); + return getAddress(qx, qy); } /** @@ -89,20 +96,20 @@ library P256 { * @param privateKey - private key */ function getPublicKey(uint256 privateKey) internal view returns (uint256, uint256) { - (uint256 x, uint256 y, uint256 z) = _jMult(gx, gy, 1, privateKey); + (uint256 x, uint256 y, uint256 z) = _jMult(GX, GY, 1, privateKey); return _affineFromJacobian(x, y, z); } /** * @dev Hash public key into an address - * @param Qx - public key coordinate X - * @param Qy - public key coordinate Y + * @param qx - public key coordinate X + * @param qy - public key coordinate Y */ - function getAddress(uint256 Qx, uint256 Qy) internal pure returns (address result) { + function getAddress(uint256 qx, uint256 qy) internal pure returns (address result) { /// @solidity memory-safe-assembly assembly { - mstore(0x00, Qx) - mstore(0x20, Qy) + mstore(0x00, qx) + mstore(0x20, qy) result := keccak256(0x00, 0x40) } } @@ -113,9 +120,9 @@ library P256 { function isOnCurve(uint256 x, uint256 y) internal pure returns (bool result) { /// @solidity memory-safe-assembly assembly { - let p := pp + let p := P let lhs := mulmod(y, y, p) - let rhs := addmod(mulmod(addmod(mulmod(x, x, p), aa, p), x, p), bb, p) + let rhs := addmod(mulmod(addmod(mulmod(x, x, p), A, p), x, p), B, p) result := eq(lhs, rhs) } } @@ -131,10 +138,10 @@ library P256 { function _affineFromJacobian(uint256 jx, uint256 jy, uint256 jz) private view returns (uint256 ax, uint256 ay) { if (jz == 0) return (0, 0); uint256 zinv = _invModP(jz); - uint256 zzinv = mulmod(zinv, zinv, pp); - uint256 zzzinv = mulmod(zzinv, zinv, pp); - ax = mulmod(jx, zzinv, pp); - ay = mulmod(jy, zzzinv, pp); + uint256 zzinv = mulmod(zinv, zinv, P); + uint256 zzzinv = mulmod(zzinv, zinv, P); + ax = mulmod(jx, zzinv, P); + ay = mulmod(jy, zzzinv, P); } /** @@ -157,7 +164,7 @@ library P256 { } /// @solidity memory-safe-assembly assembly { - let p := pp + let p := P let zz1 := mulmod(z1, z1, p) // zz1 = z1² let zz2 := mulmod(z2, z2, p) // zz2 = z2² let u1 := mulmod(x1, zz2, p) // u1 = x1*z2² @@ -185,11 +192,11 @@ library P256 { function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 x2, uint256 y2, uint256 z2) { /// @solidity memory-safe-assembly assembly { - let p := pp + let p := P let yy := mulmod(y, y, p) let zz := mulmod(z, z, p) let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y² - let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(aa, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z⁴ + let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(A, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z⁴ // x' = m²-2*s x2 := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) @@ -269,7 +276,7 @@ library P256 { function _preComputeJacobianPoints(uint256 px, uint256 py) private pure returns (JPoint[16] memory points) { points[0x00] = JPoint(0, 0, 0); points[0x01] = JPoint(px, py, 1); - points[0x04] = JPoint(gx, gy, 1); + points[0x04] = JPoint(GX, GY, 1); points[0x02] = _jDoublePoint(points[0x01]); points[0x08] = _jDoublePoint(points[0x04]); points[0x03] = _jAddPoint(points[0x01], points[0x02]); @@ -300,10 +307,10 @@ library P256 { * `a**(p-1) ≡ 1 mod p`. This means that `a**(p-2)` is an inverse of a in Fp. */ function _invModN(uint256 value) private view returns (uint256) { - return Math.modExp(value, nn2, nn); + return Math.modExp(value, N2, N); } function _invModP(uint256 value) private view returns (uint256) { - return Math.modExp(value, pp2, pp); + return Math.modExp(value, P2, P); } } diff --git a/test/utils/cryptography/P256.t.sol b/test/utils/cryptography/P256.t.sol index b5b00084fd8..279633d8835 100644 --- a/test/utils/cryptography/P256.t.sol +++ b/test/utils/cryptography/P256.t.sol @@ -8,7 +8,7 @@ import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; contract P256Test is Test { /// forge-config: default.fuzz.runs = 256 function testVerify(uint256 seed, bytes32 digest) public { - uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.nn - 1); + uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.N - 1); (uint256 x, uint256 y) = P256.getPublicKey(privateKey); (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); @@ -17,7 +17,7 @@ contract P256Test is Test { /// forge-config: default.fuzz.runs = 256 function testRecover(uint256 seed, bytes32 digest) public { - uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.nn - 1); + uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.N - 1); (uint256 x, uint256 y) = P256.getPublicKey(privateKey); (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); From 4dae298bd4860ed71bff38c71e2be56eb3db99d9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 12:07:52 +0100 Subject: [PATCH 10/66] add changeset --- .changeset/odd-lobsters-wash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/odd-lobsters-wash.md diff --git a/.changeset/odd-lobsters-wash.md b/.changeset/odd-lobsters-wash.md new file mode 100644 index 00000000000..185362ae415 --- /dev/null +++ b/.changeset/odd-lobsters-wash.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`P256`: Add a library for verification/recovery of Secp256r1 (Aka P256) signatures. From 6cf039d6aa92f7631d205703b0a2e20d88c97618 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 12:13:10 +0100 Subject: [PATCH 11/66] improve doc --- contracts/utils/cryptography/P256.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol index ecaab7fa75e..779426ef441 100644 --- a/contracts/utils/cryptography/P256.sol +++ b/contracts/utils/cryptography/P256.sol @@ -25,7 +25,7 @@ library P256 { uint256 internal constant GY = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5; /// @dev P (size of the field) uint256 internal constant P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF; - /// @dev N (order of the field) + /// @dev N (order of G) uint256 internal constant N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551; /// @dev A parameter of the weierstrass equation uint256 internal constant A = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC; From c094fa1af8e88e5fa7d0d889a10c00aa4d14ea69 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 12:34:15 +0100 Subject: [PATCH 12/66] add envvar to force allowUnlimitedContractSize --- .github/workflows/checks.yml | 2 ++ hardhat.config.js | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 30b72e2ea02..7d6c9d0a94d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -66,6 +66,8 @@ jobs: run: bash scripts/upgradeable/transpile.sh - name: Run tests run: npm run test + env: + UNLIMITED: true - name: Check linearisation of the inheritance graph run: npm run test:inheritance - name: Check storage layout diff --git a/hardhat.config.js b/hardhat.config.js index 4006ce19c0a..e9d395e5593 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -3,6 +3,7 @@ // - SRC: contracts folder to compile (default: contracts) // - RUNS: number of optimization runs (default: 200) // - IR: enable IR compilation (default: false) +// - UNLIMITED: allow deployment of contracts larger than 24k (default: false) // - COVERAGE: enable coverage report (default: false) // - GAS: enable gas report (default: false) // - COINMARKETCAP: coinmarketcap api key for USD value in gas report @@ -35,6 +36,11 @@ const { argv } = require('yargs/yargs')() type: 'boolean', default: false, }, + unlimited: { + alias: 'allowUnlimitedContractSize', + type: 'boolean', + default: false + }, // Extra modules coverage: { type: 'boolean', @@ -91,7 +97,7 @@ module.exports = { }, networks: { hardhat: { - allowUnlimitedContractSize: argv.gas || argv.coverage, + allowUnlimitedContractSize: argv.gas || argv.coverage || argv.unlimited, initialBaseFeePerGas: argv.coverage ? 0 : undefined, }, }, From 20a03dfd47d60ab818d922e41cf63ba6837e77f0 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 13:34:24 +0100 Subject: [PATCH 13/66] fix lint --- hardhat.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.js b/hardhat.config.js index e9d395e5593..04f15212e93 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -39,7 +39,7 @@ const { argv } = require('yargs/yargs')() unlimited: { alias: 'allowUnlimitedContractSize', type: 'boolean', - default: false + default: false, }, // Extra modules coverage: { From 15f1a6bbe11ee3de0c9a41e185fd7d8fdec593ec Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Feb 2024 13:37:43 +0100 Subject: [PATCH 14/66] fix stack too deep error in coverage --- hardhat.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/hardhat.config.js b/hardhat.config.js index 04f15212e93..67d8f50f7a7 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -79,6 +79,7 @@ module.exports = { optimizer: { enabled: true, runs: argv.runs, + details: { yul: true }, }, viaIR: argv.ir, outputSelection: { '*': { '*': ['storageLayout'] } }, From e2040e423ae4fe6b86a174fa4fb2bee0ff186e15 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 13 Feb 2024 10:39:48 +0100 Subject: [PATCH 15/66] reoder arguments to match ecrecover and EIP-7212 --- contracts/utils/cryptography/P256.sol | 26 +++++++++++------------ test/utils/cryptography/P256.t.sol | 10 ++++----- test/utils/cryptography/P256.test.js | 30 ++++++++++++++++++--------- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol index 779426ef441..31760c6d1e6 100644 --- a/contracts/utils/cryptography/P256.sol +++ b/contracts/utils/cryptography/P256.sol @@ -38,18 +38,18 @@ library P256 { /** * @dev signature verification - * @param qx - public key coordinate X - * @param qy - public key coordinate Y + * @param h - hashed message * @param r - signature half R * @param s - signature half S - * @param e - hashed message + * @param qx - public key coordinate X + * @param qy - public key coordinate Y */ - function verify(uint256 qx, uint256 qy, uint256 r, uint256 s, uint256 e) internal view returns (bool) { + function verify(uint256 h, uint256 r, uint256 s, uint256 qx, uint256 qy) internal view returns (bool) { if (r == 0 || r >= N || s == 0 || s >= N || !isOnCurve(qx, qy)) return false; JPoint[16] memory points = _preComputeJacobianPoints(qx, qy); uint256 w = _invModN(s); - uint256 u1 = mulmod(e, w, N); + uint256 u1 = mulmod(h, w, N); uint256 u2 = mulmod(r, w, N); (uint256 x, ) = _jMultShamir(points, u1, u2); return (x == r); @@ -57,12 +57,12 @@ library P256 { /** * @dev public key recovery + * @param h - hashed message + * @param v - signature recovery param * @param r - signature half R * @param s - signature half S - * @param v - signature recovery param - * @param e - hashed message */ - function recovery(uint256 r, uint256 s, uint8 v, uint256 e) internal view returns (uint256, uint256) { + function recovery(uint256 h, uint8 v, uint256 r, uint256 s) internal view returns (uint256, uint256) { if (r == 0 || r >= N || s == 0 || s >= N || v > 1) return (0, 0); uint256 rx = r; @@ -73,7 +73,7 @@ library P256 { JPoint[16] memory points = _preComputeJacobianPoints(rx, ry); uint256 w = _invModN(r); - uint256 u1 = mulmod(N - (e % N), w, N); + uint256 u1 = mulmod(N - (h % N), w, N); uint256 u2 = mulmod(s, w, N); (uint256 x, uint256 y) = _jMultShamir(points, u1, u2); return (x, y); @@ -81,13 +81,13 @@ library P256 { /** * @dev address recovery + * @param h - hashed message + * @param v - signature recovery param * @param r - signature half R * @param s - signature half S - * @param v - signature recovery param - * @param e - hashed message */ - function recoveryAddress(uint256 r, uint256 s, uint8 v, uint256 e) internal view returns (address) { - (uint256 qx, uint256 qy) = recovery(r, s, v, e); + function recoveryAddress(uint256 h, uint8 v, uint256 r, uint256 s) internal view returns (address) { + (uint256 qx, uint256 qy) = recovery(h, v, r, s); return getAddress(qx, qy); } diff --git a/test/utils/cryptography/P256.t.sol b/test/utils/cryptography/P256.t.sol index 279633d8835..736ea223bd4 100644 --- a/test/utils/cryptography/P256.t.sol +++ b/test/utils/cryptography/P256.t.sol @@ -6,23 +6,23 @@ import {Test} from "forge-std/Test.sol"; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; contract P256Test is Test { - /// forge-config: default.fuzz.runs = 256 + /// forge-config: default.fuzz.runs = 512 function testVerify(uint256 seed, bytes32 digest) public { uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.N - 1); (uint256 x, uint256 y) = P256.getPublicKey(privateKey); (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); - assertTrue(P256.verify(x, y, uint256(r), uint256(s), uint256(digest))); + assertTrue(P256.verify(uint256(digest), uint256(r), uint256(s), x, y)); } - /// forge-config: default.fuzz.runs = 256 + /// forge-config: default.fuzz.runs = 512 function testRecover(uint256 seed, bytes32 digest) public { uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.N - 1); (uint256 x, uint256 y) = P256.getPublicKey(privateKey); (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); - (uint256 qx0, uint256 qy0) = P256.recovery(uint256(r), uint256(s), 0, uint256(digest)); - (uint256 qx1, uint256 qy1) = P256.recovery(uint256(r), uint256(s), 1, uint256(digest)); + (uint256 qx0, uint256 qy0) = P256.recovery(uint256(digest), 0, uint256(r), uint256(s)); + (uint256 qx1, uint256 qy1) = P256.recovery(uint256(digest), 1, uint256(r), uint256(s)); assertTrue((qx0 == x && qy0 == y) || (qx1 == x && qy1 == y)); } } diff --git a/test/utils/cryptography/P256.test.js b/test/utils/cryptography/P256.test.js index e0a479cf6c5..fd1c58857e9 100644 --- a/test/utils/cryptography/P256.test.js +++ b/test/utils/cryptography/P256.test.js @@ -31,40 +31,50 @@ describe('P256', function () { }); it('verify valid signature', async function () { - expect(await this.mock.$verify(...this.publicKey, ...this.signature, this.messageHash)).to.be.true; + expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.true; }); it('recover public key', async function () { - expect(await this.mock.$recovery(...this.signature, this.recovery, this.messageHash)).to.deep.equal(this.publicKey); + expect(await this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.deep.equal(this.publicKey); }); it('recover address', async function () { - expect(await this.mock.$recoveryAddress(...this.signature, this.recovery, this.messageHash)).to.equal(this.address); + expect(await this.mock.$recoveryAddress(this.messageHash, this.recovery, ...this.signature)).to.equal(this.address); }); it('reject signature with flipped public key coordinates ([x,y] >> [y,x])', async function () { const reversedPublicKey = Array.from(this.publicKey).reverse(); - expect(await this.mock.$verify(...reversedPublicKey, ...this.signature, this.messageHash)).to.be.false; + expect(await this.mock.$verify(this.messageHash, ...this.signature, ...reversedPublicKey)).to.be.false; }); it('reject signature with flipped signature values ([r,s] >> [s,r])', async function () { const reversedSignature = Array.from(this.signature).reverse(); - expect(await this.mock.$verify(...this.publicKey, ...reversedSignature, this.messageHash)).to.be.false; - expect(await this.mock.$recovery(...reversedSignature, this.recovery, this.messageHash)).to.not.deep.equal( + expect(await this.mock.$verify(this.messageHash, ...reversedSignature, ...this.publicKey)).to.be.false; + expect(await this.mock.$recovery(this.messageHash, this.recovery, ...reversedSignature)).to.not.deep.equal( this.publicKey, ); - expect(await this.mock.$recoveryAddress(...reversedSignature, this.recovery, this.messageHash)).to.not.equal( + expect(await this.mock.$recoveryAddress(this.messageHash, this.recovery, ...reversedSignature)).to.not.equal( this.address, ); }); it('reject signature with invalid message hash', async function () { const invalidMessageHash = ethers.hexlify(ethers.randomBytes(32)); - expect(await this.mock.$verify(...this.publicKey, ...this.signature, invalidMessageHash)).to.be.false; - expect(await this.mock.$recovery(...this.signature, this.recovery, invalidMessageHash)).to.not.deep.equal( + expect(await this.mock.$verify(invalidMessageHash, ...this.signature, ...this.publicKey)).to.be.false; + expect(await this.mock.$recovery(invalidMessageHash, this.recovery, ...this.signature)).to.not.deep.equal( this.publicKey, ); - expect(await this.mock.$recoveryAddress(...this.signature, this.recovery, invalidMessageHash)).to.not.equal( + expect(await this.mock.$recoveryAddress(invalidMessageHash, this.recovery, ...this.signature)).to.not.equal( + this.address, + ); + }); + + it('fail to recover signature with invalid recovery bit', async function () { + const invalidRecovery = 1 - this.recovery; + expect(await this.mock.$recovery(this.messageHash, invalidRecovery, ...this.signature)).to.not.deep.equal( + this.publicKey, + ); + expect(await this.mock.$recoveryAddress(this.messageHash, invalidRecovery, ...this.signature)).to.not.equal( this.address, ); }); From 695b73234c9bde0e0ac3a901d8c7bcd316176938 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 13 Mar 2024 15:00:14 +0100 Subject: [PATCH 16/66] reduce diff --- contracts/mocks/ExposeImports.sol | 11 ----------- contracts/mocks/Stateless.sol | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 contracts/mocks/ExposeImports.sol diff --git a/contracts/mocks/ExposeImports.sol b/contracts/mocks/ExposeImports.sol deleted file mode 100644 index 2be6ba46ae2..00000000000 --- a/contracts/mocks/ExposeImports.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {P256} from "../utils/cryptography/P256.sol"; - -abstract contract ExposeImports { - // This will be transpiled, causing all the imports above to be transpiled when running the upgradeable tests. - // This trick is necessary for testing libraries such as Panic.sol (which are not imported by any other transpiled - // contracts and would otherwise not be exposed). -} diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 56f5b4c6610..8053acc1085 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -24,6 +24,7 @@ import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; import {Math} from "../utils/math/Math.sol"; import {MerkleProof} from "../utils/cryptography/MerkleProof.sol"; import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol"; +import {P256} from "../utils/cryptography/P256.sol"; import {SafeCast} from "../utils/math/SafeCast.sol"; import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol"; import {ShortStrings} from "../utils/ShortStrings.sol"; From f36f183be3df9b1be3bfe8fc76831e5bec97b6a5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 Apr 2024 18:16:40 +0200 Subject: [PATCH 17/66] Start initial work on ERC4337 interface and helpers --- contracts/abstraction/UserOperationUtils.sol | 109 +++++++++++++++++++ contracts/interfaces/IERC4337.sol | 47 ++++++++ 2 files changed, 156 insertions(+) create mode 100644 contracts/abstraction/UserOperationUtils.sol create mode 100644 contracts/interfaces/IERC4337.sol diff --git a/contracts/abstraction/UserOperationUtils.sol b/contracts/abstraction/UserOperationUtils.sol new file mode 100644 index 00000000000..0d2228f3308 --- /dev/null +++ b/contracts/abstraction/UserOperationUtils.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../interfaces/IERC4337.sol"; +import {Math} from "../utils/math/Math.sol"; + +// TODO: move that to a dedicated file in `contracts/utils/math` ? +library Unpack { + function split(bytes32 packed) internal pure returns (uint256 high128, uint256 low128) { + return (uint128(bytes16(packed)), uint128(uint256(packed))); + } + + function high(bytes32 packed) internal pure returns (uint256) { + return uint256(packed) >> 128; + } + + function low(bytes32 packed) internal pure returns (uint256) { + return uint128(uint256(packed)); + } +} + +library UserOperationUtils { + using Unpack for bytes32; + + uint256 public constant PAYMASTER_VALIDATION_GAS_OFFSET = 20; + uint256 public constant PAYMASTER_POSTOP_GAS_OFFSET = 36; + uint256 public constant PAYMASTER_DATA_OFFSET = 52; + + // Need to fuzz this against `userOp.sender` + function getSender(PackedUserOperation calldata userOp) internal pure returns (address) { + address data; + assembly { + data := calldataload(userOp) + } + return address(uint160(data)); + } + + function getMaxPriorityFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return userOp.gasFees.high(); + } + + function getMaxFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return userOp.gasFees.low(); + } + + function getGasPrice(PackedUserOperation calldata userOp) internal view returns (uint256) { + unchecked { + (uint256 maxPriorityFeePerGas, uint256 maxFeePerGas) = userOp.gasFees.split(); + return + maxFeePerGas == maxPriorityFeePerGas + ? maxFeePerGas + : Math.min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); + } + } + + function getVerificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return userOp.accountGasLimits.high(); + } + + function getCallGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return userOp.accountGasLimits.low(); + } + + function getPaymasterVerificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_POSTOP_GAS_OFFSET])); + } + + function getPostOpGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET:PAYMASTER_DATA_OFFSET])); + } + + function getPaymasterStaticFields( + bytes calldata paymasterAndData + ) internal pure returns (address paymaster, uint256 validationGasLimit, uint256 postOpGasLimit) { + return ( + address(bytes20(paymasterAndData[:PAYMASTER_VALIDATION_GAS_OFFSET])), + uint128(bytes16(paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_POSTOP_GAS_OFFSET])), + uint128(bytes16(paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET:PAYMASTER_DATA_OFFSET])) + ); + } + + function encode(PackedUserOperation calldata userOp) internal pure returns (bytes memory ret) { + return + abi.encode( + userOp.sender, + userOp.nonce, + calldataKeccak(userOp.initCode), + calldataKeccak(userOp.callData), + userOp.accountGasLimits, + userOp.preVerificationGas, + userOp.gasFees, + calldataKeccak(userOp.paymasterAndData) + ); + } + + function hash(PackedUserOperation calldata userOp) internal pure returns (bytes32) { + return keccak256(encode(userOp)); + } + + function calldataKeccak(bytes calldata data) private pure returns (bytes32 ret) { + assembly ("memory-safe") { + let ptr := mload(0x40) + let len := data.length + calldatacopy(ptr, data.offset, len) + ret := keccak256(ptr, len) + } + } +} diff --git a/contracts/interfaces/IERC4337.sol b/contracts/interfaces/IERC4337.sol new file mode 100644 index 00000000000..51146667ccc --- /dev/null +++ b/contracts/interfaces/IERC4337.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/* +struct UserOperation { + address sender; // The account making the operation + uint256 nonce; // Anti-replay parameter (see “Semi-abstracted Nonce Support” ) + address factory; // account factory, only for new accounts + bytes factoryData; // data for account factory (only if account factory exists) + bytes callData; // The data to pass to the sender during the main execution call + uint256 callGasLimit; // The amount of gas to allocate the main execution call + uint256 verificationGasLimit; // The amount of gas to allocate for the verification step + uint256 preVerificationGas; // Extra gas to pay the bunder + uint256 maxFeePerGas; // Maximum fee per gas (similar to EIP-1559 max_fee_per_gas) + uint256 maxPriorityFeePerGas; // Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas) + address paymaster; // Address of paymaster contract, (or empty, if account pays for itself) + uint256 paymasterVerificationGasLimit; // The amount of gas to allocate for the paymaster validation code + uint256 paymasterPostOpGasLimit; // The amount of gas to allocate for the paymaster post-operation code + bytes paymasterData; // Data for paymaster (only if paymaster exists) + bytes signature; // Data passed into the account to verify authorization +} +*/ + +struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; // concatenation of factory address and factoryData (or empty) + bytes callData; + bytes32 accountGasLimits; // concatenation of verificationGas (16 bytes) and callGas (16 bytes) + uint256 preVerificationGas; + bytes32 gasFees; // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes) + bytes paymasterAndData; // concatenation of paymaster fields (or empty) + bytes signature; +} + +interface IAccount { + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256 validationData); +} + +interface IAccountExecute { + function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; +} From a1532d0b6b9830c65f5d636b49c911cd146c30b8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 Apr 2024 18:38:22 +0200 Subject: [PATCH 18/66] Packing library --- contracts/abstraction/UserOperationUtils.sol | 34 ++++++-------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/contracts/abstraction/UserOperationUtils.sol b/contracts/abstraction/UserOperationUtils.sol index 0d2228f3308..cbdd97f0154 100644 --- a/contracts/abstraction/UserOperationUtils.sol +++ b/contracts/abstraction/UserOperationUtils.sol @@ -4,28 +4,14 @@ pragma solidity ^0.8.20; import {PackedUserOperation} from "../interfaces/IERC4337.sol"; import {Math} from "../utils/math/Math.sol"; - -// TODO: move that to a dedicated file in `contracts/utils/math` ? -library Unpack { - function split(bytes32 packed) internal pure returns (uint256 high128, uint256 low128) { - return (uint128(bytes16(packed)), uint128(uint256(packed))); - } - - function high(bytes32 packed) internal pure returns (uint256) { - return uint256(packed) >> 128; - } - - function low(bytes32 packed) internal pure returns (uint256) { - return uint128(uint256(packed)); - } -} +import {Packing} from "../utils/Packing.sol"; library UserOperationUtils { - using Unpack for bytes32; + using Packing for *; - uint256 public constant PAYMASTER_VALIDATION_GAS_OFFSET = 20; - uint256 public constant PAYMASTER_POSTOP_GAS_OFFSET = 36; - uint256 public constant PAYMASTER_DATA_OFFSET = 52; + uint256 internal constant PAYMASTER_VALIDATION_GAS_OFFSET = 20; + uint256 internal constant PAYMASTER_POSTOP_GAS_OFFSET = 36; + uint256 internal constant PAYMASTER_DATA_OFFSET = 52; // Need to fuzz this against `userOp.sender` function getSender(PackedUserOperation calldata userOp) internal pure returns (address) { @@ -37,16 +23,16 @@ library UserOperationUtils { } function getMaxPriorityFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.gasFees.high(); + return userOp.gasFees.asUint128x2().high(); } function getMaxFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.gasFees.low(); + return userOp.gasFees.asUint128x2().low(); } function getGasPrice(PackedUserOperation calldata userOp) internal view returns (uint256) { unchecked { - (uint256 maxPriorityFeePerGas, uint256 maxFeePerGas) = userOp.gasFees.split(); + (uint256 maxPriorityFeePerGas, uint256 maxFeePerGas) = userOp.gasFees.asUint128x2().split(); return maxFeePerGas == maxPriorityFeePerGas ? maxFeePerGas @@ -55,11 +41,11 @@ library UserOperationUtils { } function getVerificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.accountGasLimits.high(); + return userOp.accountGasLimits.asUint128x2().high(); } function getCallGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.accountGasLimits.low(); + return userOp.accountGasLimits.asUint128x2().low(); } function getPaymasterVerificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { From 00e0eea76d5049f30b5b7be368e5c69c6d67a396 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 5 Apr 2024 16:31:37 +0200 Subject: [PATCH 19/66] reorder UserOperationUtils.sol --- contracts/abstraction/UserOperationUtils.sol | 90 +++++++++----------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/contracts/abstraction/UserOperationUtils.sol b/contracts/abstraction/UserOperationUtils.sol index cbdd97f0154..9c8476fade7 100644 --- a/contracts/abstraction/UserOperationUtils.sol +++ b/contracts/abstraction/UserOperationUtils.sol @@ -9,12 +9,26 @@ import {Packing} from "../utils/Packing.sol"; library UserOperationUtils { using Packing for *; - uint256 internal constant PAYMASTER_VALIDATION_GAS_OFFSET = 20; - uint256 internal constant PAYMASTER_POSTOP_GAS_OFFSET = 36; - uint256 internal constant PAYMASTER_DATA_OFFSET = 52; + function hash(PackedUserOperation calldata userOp) internal pure returns (bytes32) { + return keccak256(encode(userOp)); + } + + function encode(PackedUserOperation calldata userOp) internal pure returns (bytes memory ret) { + return + abi.encode( + userOp.sender, + userOp.nonce, + _keccak(userOp.initCode), + _keccak(userOp.callData), + userOp.accountGasLimits, + userOp.preVerificationGas, + userOp.gasFees, + _keccak(userOp.paymasterAndData) + ); + } // Need to fuzz this against `userOp.sender` - function getSender(PackedUserOperation calldata userOp) internal pure returns (address) { + function sender(PackedUserOperation calldata userOp) internal pure returns (address) { address data; assembly { data := calldataload(userOp) @@ -22,69 +36,43 @@ library UserOperationUtils { return address(uint160(data)); } - function getMaxPriorityFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.gasFees.asUint128x2().high(); + function verificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return userOp.accountGasLimits.asUint128x2().first(); } - function getMaxFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.gasFees.asUint128x2().low(); + function callGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return userOp.accountGasLimits.asUint128x2().second(); } - function getGasPrice(PackedUserOperation calldata userOp) internal view returns (uint256) { - unchecked { - (uint256 maxPriorityFeePerGas, uint256 maxFeePerGas) = userOp.gasFees.asUint128x2().split(); - return - maxFeePerGas == maxPriorityFeePerGas - ? maxFeePerGas - : Math.min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); - } + function maxPriorityFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return userOp.gasFees.asUint128x2().first(); } - function getVerificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.accountGasLimits.asUint128x2().high(); + function maxFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return userOp.gasFees.asUint128x2().second(); } - function getCallGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.accountGasLimits.asUint128x2().low(); - } - - function getPaymasterVerificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_POSTOP_GAS_OFFSET])); - } - - function getPostOpGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return uint128(bytes16(userOp.paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET:PAYMASTER_DATA_OFFSET])); + function gasPrice(PackedUserOperation calldata userOp) internal view returns (uint256) { + unchecked { + // Following values are "per gas" + (uint256 maxPriorityFee, uint256 maxFee) = userOp.gasFees.asUint128x2().split(); + return maxFee == maxPriorityFee ? maxFee : Math.min(maxFee, maxPriorityFee + block.basefee); + } } - function getPaymasterStaticFields( - bytes calldata paymasterAndData - ) internal pure returns (address paymaster, uint256 validationGasLimit, uint256 postOpGasLimit) { - return ( - address(bytes20(paymasterAndData[:PAYMASTER_VALIDATION_GAS_OFFSET])), - uint128(bytes16(paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_POSTOP_GAS_OFFSET])), - uint128(bytes16(paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET:PAYMASTER_DATA_OFFSET])) - ); + function paymaster(PackedUserOperation calldata userOp) internal pure returns (address) { + return address(bytes20(userOp.paymasterAndData[0:20])); } - function encode(PackedUserOperation calldata userOp) internal pure returns (bytes memory ret) { - return - abi.encode( - userOp.sender, - userOp.nonce, - calldataKeccak(userOp.initCode), - calldataKeccak(userOp.callData), - userOp.accountGasLimits, - userOp.preVerificationGas, - userOp.gasFees, - calldataKeccak(userOp.paymasterAndData) - ); + function paymasterVerificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return uint128(bytes16(userOp.paymasterAndData[20:36])); } - function hash(PackedUserOperation calldata userOp) internal pure returns (bytes32) { - return keccak256(encode(userOp)); + function paymasterPostOpGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { + return uint128(bytes16(userOp.paymasterAndData[36:52])); } - function calldataKeccak(bytes calldata data) private pure returns (bytes32 ret) { + function _keccak(bytes calldata data) private pure returns (bytes32 ret) { assembly ("memory-safe") { let ptr := mload(0x40) let len := data.length From 8fa363e5ee8ebc1c927a0c2a5cef4e3f420010fc Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 8 Apr 2024 15:50:53 +0200 Subject: [PATCH 20/66] 4337 account wip --- contracts/abstraction/Account.sol | 76 ++++++++++++++++++++ contracts/abstraction/SimpleAccount.sol | 51 +++++++++++++ contracts/abstraction/UserOperationUtils.sol | 24 +------ contracts/interfaces/IERC4337.sol | 29 ++++++++ 4 files changed, 159 insertions(+), 21 deletions(-) create mode 100644 contracts/abstraction/Account.sol create mode 100644 contracts/abstraction/SimpleAccount.sol diff --git a/contracts/abstraction/Account.sol b/contracts/abstraction/Account.sol new file mode 100644 index 00000000000..06cf488494d --- /dev/null +++ b/contracts/abstraction/Account.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation, IAccount, IEntryPoint} from "../interfaces/IERC4337.sol"; +import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol"; +import {SignatureChecker} from "../utils/cryptography/SignatureChecker.sol"; +import {SafeCast} from "../utils/math/SafeCast.sol"; + +abstract contract Account is IAccount { + using SafeCast for bool; + + error AccountEntryPointRestricted(); + error AccountUserRestricted(); + error AccountInvalidBatchLength(); + + // Modifiers + modifier onlyEntryPoint() { + if (msg.sender != address(entryPoint())) { + revert AccountEntryPointRestricted(); + } + _; + } + + modifier onlyAuthorizedOrSelf() { + if (msg.sender != address(this) && !_isAuthorized(msg.sender)) { + revert AccountUserRestricted(); + } + _; + } + + // Virtual pure (not implemented) hooks + function entryPoint() public view virtual returns (IEntryPoint); + + function _isAuthorized(address) internal view virtual returns (bool); + + // Public interface + function getNonce() public view virtual returns (uint256) { + return entryPoint().getNonce(address(this), 0); + } + + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external virtual override onlyEntryPoint returns (uint256 validationData) { + validationData = _validateSignature(userOp, userOpHash); + _validateNonce(userOp.nonce); + _payPrefund(missingAccountFunds); + } + + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual returns (uint256 validationData) { + return + (_isAuthorized(userOp.sender) && + SignatureChecker.isValidSignatureNow( + userOp.sender, + MessageHashUtils.toEthSignedMessageHash(userOpHash), + userOp.signature + )).toUint(); + } + + function _validateNonce(uint256 nonce) internal view virtual { + // TODO ? + } + + function _payPrefund(uint256 missingAccountFunds) internal virtual { + if (missingAccountFunds != 0) { + (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}(""); + success; + //ignore failure (its EntryPoint's job to verify, not account.) + } + } +} diff --git a/contracts/abstraction/SimpleAccount.sol b/contracts/abstraction/SimpleAccount.sol new file mode 100644 index 00000000000..ae7f89cd6a1 --- /dev/null +++ b/contracts/abstraction/SimpleAccount.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation, IEntryPoint} from "../interfaces/IERC4337.sol"; +import {Account} from "./Account.sol"; +import {Ownable} from "../access/Ownable.sol"; +import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "../token/ERC1155/utils/ERC1155Holder.sol"; +import {Address} from "../utils/Address.sol"; + +contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder { + IEntryPoint private immutable _entryPoint; + + constructor(IEntryPoint entryPoint_, address initialOwner) Ownable(initialOwner) { + _entryPoint = entryPoint_; + } + + receive() external payable {} + + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + function _isAuthorized(address user) internal view virtual override returns (bool) { + return user == owner(); + } + + function execute(address target, uint256 value, bytes calldata data) external onlyAuthorizedOrSelf { + _call(target, value, data); + } + + function executeBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata calldatas + ) external onlyAuthorizedOrSelf { + if (targets.length != calldatas.length || (values.length != 0 && values.length != targets.length)) { + revert AccountInvalidBatchLength(); + } + + for (uint256 i = 0; i < targets.length; ++i) { + _call(targets[i], (values.length == 0 ? 0 : values[i]), calldatas[i]); + } + } + + function _call(address target, uint256 value, bytes memory data) internal { + (bool success, bytes memory returndata) = target.call{value: value}(data); + Address.verifyCallResult(success, returndata); + } +} diff --git a/contracts/abstraction/UserOperationUtils.sol b/contracts/abstraction/UserOperationUtils.sol index 9c8476fade7..4005b3c24d3 100644 --- a/contracts/abstraction/UserOperationUtils.sol +++ b/contracts/abstraction/UserOperationUtils.sol @@ -18,24 +18,15 @@ library UserOperationUtils { abi.encode( userOp.sender, userOp.nonce, - _keccak(userOp.initCode), - _keccak(userOp.callData), + keccak256(userOp.initCode), + keccak256(userOp.callData), userOp.accountGasLimits, userOp.preVerificationGas, userOp.gasFees, - _keccak(userOp.paymasterAndData) + keccak256(userOp.paymasterAndData) ); } - // Need to fuzz this against `userOp.sender` - function sender(PackedUserOperation calldata userOp) internal pure returns (address) { - address data; - assembly { - data := calldataload(userOp) - } - return address(uint160(data)); - } - function verificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { return userOp.accountGasLimits.asUint128x2().first(); } @@ -71,13 +62,4 @@ library UserOperationUtils { function paymasterPostOpGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { return uint128(bytes16(userOp.paymasterAndData[36:52])); } - - function _keccak(bytes calldata data) private pure returns (bytes32 ret) { - assembly ("memory-safe") { - let ptr := mload(0x40) - let len := data.length - calldatacopy(ptr, data.offset, len) - ret := keccak256(ptr, len) - } - } } diff --git a/contracts/interfaces/IERC4337.sol b/contracts/interfaces/IERC4337.sol index 51146667ccc..b8375304f9a 100644 --- a/contracts/interfaces/IERC4337.sol +++ b/contracts/interfaces/IERC4337.sol @@ -34,6 +34,35 @@ struct PackedUserOperation { bytes signature; } +interface IAggregator { + function validateSignatures(PackedUserOperation[] calldata userOps, bytes calldata signature) external view; + + function validateUserOpSignature( + PackedUserOperation calldata userOp + ) external view returns (bytes memory sigForUserOp); + + function aggregateSignatures( + PackedUserOperation[] calldata userOps + ) external view returns (bytes memory aggregatesSignature); +} + +interface IEntryPoint { + struct UserOpsPerAggregator { + PackedUserOperation[] userOps; + IAggregator aggregator; + bytes signature; + } + + function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external; + + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) external; + + function getNonce(address sender, uint192 key) external view returns (uint256 nonce); +} + interface IAccount { function validateUserOp( PackedUserOperation calldata userOp, From 9309f716262ceb360f119830deb7ca6a5aed27e7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 10 Apr 2024 16:23:38 +0200 Subject: [PATCH 21/66] wip --- contracts/abstraction/EntryPoint.sol | 392 +++++++++++++++++++ contracts/abstraction/UserOperationUtils.sol | 115 ++++-- contracts/utils/Packing.sol | 61 +++ 3 files changed, 541 insertions(+), 27 deletions(-) create mode 100644 contracts/abstraction/EntryPoint.sol diff --git a/contracts/abstraction/EntryPoint.sol b/contracts/abstraction/EntryPoint.sol new file mode 100644 index 00000000000..1cfffa03bbb --- /dev/null +++ b/contracts/abstraction/EntryPoint.sol @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { + PackedUserOperation, + IEntryPoint, + IAggregator, + IAccount, + IAccountExecute +} from "../interfaces/IERC4337.sol"; + +import {UserOperationUtils} from "./UserOperationUtils.sol"; +import {Address} from "../utils/Address.sol"; +import {Packing} from "../utils/Packing.sol"; +import {ReentrancyGuardTransient} from "../utils/ReentrancyGuardTransient.sol"; + + + + + + + +contract KeydNonces { + /** + * @dev The nonce used for an `account` is not the expected current nonce. + */ + error InvalidAccountNonce(address account, uint256 currentNonce); + + mapping(address => mapping(uint192 => uint64)) private _nonce; + + function getNonce(address owner, uint192 key) public view virtual returns (uint256) { + return (uint256(key) << 64) | _nonce[owner][key]; + } + + function _useNonce(address owner, uint192 key) internal virtual returns (uint64) { + // TODO: use unchecked here ? + return _nonce[owner][key]++; + } + + function _useCheckedNonce(address owner, uint256 keyNonce) internal { + _useCheckedNonce(owner, uint192(keyNonce >> 64), uint64(keyNonce)); + } + + function _useCheckedNonce(address owner, uint192 key, uint64 nonce) internal virtual { + uint256 current = _useNonce(owner, key); + if (nonce != current) { + revert InvalidAccountNonce(owner, current); + } + } +} + + + + + + +contract EntryPoint is IEntryPoint, ReentrancyGuardTransient, KeydNonces { + using UserOperationUtils for *; + using Packing for *; + + struct UserOpInfo { + UserOperationUtils.MemoryUserOp mUserOp; + bytes32 userOpHash; + uint256 prefund; + uint256 preOpGas; + bytes context; + } + + + + + function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external nonReentrant { + UserOpInfo[] memory opInfos = new UserOpInfo[](ops.length); + + for (uint256 i = 0; i < ops.length; ++i) { + ( + uint256 validationData, + uint256 pmValidationData + ) = _validatePrepayment(ops[i], opInfos[i]); + + _validateAccountAndPaymasterValidationData( + validationData, + pmValidationData, + address(0) + ); + } + + uint256 collected = 0; + for (uint256 i = 0; i < ops.length; ++i) { + collected += _executeUserOp(ops[i], opInfos[i]); + } + + Address.sendValue(beneficiary, collected); + } + + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) external nonReentrant { + uint256 totalOps = 0; + for (uint256 i = 0; i < opsPerAggregator.length; ++i) { + PackedUserOperation[] calldata ops = opsPerAggregator[i].userOps; + IAggregator aggregator = opsPerAggregator[i].aggregator; + + //address(1) is special marker of "signature error" + require(address(aggregator) != address(1), "AA96 invalid aggregator"); + + if (address(aggregator) != address(0)) { + // solhint-disable-next-line no-empty-blocks + try aggregator.validateSignatures(ops, opsPerAggregator[i].signature) + {} + catch + { + revert("SignatureValidationFailed"); + // revert SignatureValidationFailed(address(aggregator)); + } + } + totalOps += ops.length; + } + + UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); + + uint256 opIndex = 0; + for (uint256 a = 0; a < opsPerAggregator.length; ++a) { + PackedUserOperation[] calldata ops = opsPerAggregator[a].userOps; + IAggregator aggregator = opsPerAggregator[a].aggregator; + + for (uint256 i = 0; i < ops.length; ++i) { + ( + uint256 validationData, + uint256 paymasterValidationData + ) = _validatePrepayment(ops[i], opInfos[opIndex]); + + _validateAccountAndPaymasterValidationData( + validationData, + paymasterValidationData, + address(aggregator) + ); + opIndex++; + } + } + + uint256 collected = 0; + + opIndex = 0; + for (uint256 a = 0; a < opsPerAggregator.length; ++a) { + PackedUserOperation[] calldata ops = opsPerAggregator[a].userOps; + + for (uint256 i = 0; i < ops.length; ++i) { + collected += _executeUserOp(ops[i], opInfos[opIndex]); + opIndex++; + } + } + + Address.sendValue(beneficiary, collected); + } + + + + function getNonce(address owner, uint192 key) public view virtual override(IEntryPoint, KeydNonces) returns (uint256) { + return super.getNonce(owner, key); + } + + + + + + + + + + function _validatePrepayment(PackedUserOperation calldata userOp, UserOpInfo memory outOpInfo) + internal + returns (uint256 validationData, uint256 paymasterValidationData) + { + unchecked { + uint256 preGas = gasleft(); + + outOpInfo.mUserOp.load(userOp); + outOpInfo.userOpHash = userOp.hash(); + + // Validate all numeric values in userOp are well below 128 bit, so they can safely be added + // and multiplied without causing overflow. + uint256 maxGasValues = + outOpInfo.mUserOp.preVerificationGas | + outOpInfo.mUserOp.verificationGasLimit | + outOpInfo.mUserOp.callGasLimit | + outOpInfo.mUserOp.paymasterVerificationGasLimit | + outOpInfo.mUserOp.paymasterPostOpGasLimit | + outOpInfo.mUserOp.maxFeePerGas | + outOpInfo.mUserOp.maxPriorityFeePerGas; + require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); + + validationData = _validateAccountPrepayment(userOp, outOpInfo); + _useCheckedNonce(outOpInfo.mUserOp.sender, outOpInfo.mUserOp.nonce); + + require (preGas - gasleft() <= outOpInfo.mUserOp.verificationGasLimit, "AA26 over verificationGasLimit"); + + if (outOpInfo.mUserOp.paymaster != address(0)) { + // (outOpInfo.mUserOp.context, paymasterValidationData) = _validatePaymasterPrepayment( + // opIndex, + // userOp, + // outOpInfo, + // requiredPreFund + // ); + } else { + paymasterValidationData = 0; + } + + outOpInfo.prefund = outOpInfo.mUserOp.requiredPrefund(); + outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; + } + } + + function _validateAccountPrepayment(PackedUserOperation calldata userOp, UserOpInfo memory /*outOpInfo*/) + internal + returns (uint256 validationData) + { + + unchecked { + address sender = userOp.createSenderIfNeeded(); + address paymaster = userOp.paymaster(); + uint256 missingAccountFunds = 0; + + // uint256 requiredPrefund = outOpInfo.mUserOp.requiredPrefund(); + if (paymaster == address(0)) { + //TODO + // uint256 bal = balanceOf(sender); + // missingAccountFunds = bal > requiredPrefund ? 0 : requiredPrefund - bal; // TODO: use select + } + + try IAccount(sender).validateUserOp{ gas: userOp.verificationGasLimit() }(userOp, userOp.hash(), missingAccountFunds) returns (uint256 _validationData) { + validationData = _validationData; + } catch (bytes memory /*returndata*/) { + // TODO return bombing? + // revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); + revert('Reverted'); + } + + if (paymaster == address(0)) { + // TODO + // DepositInfo storage senderInfo = deposits[sender]; + // uint256 deposit = senderInfo.deposit; + // if (requiredPrefund > deposit) { + // revert FailedOp(opIndex, "AA21 didn't pay prefund"); + // } + // senderInfo.deposit = deposit - requiredPrefund; + } + } + } + + function _validateAccountAndPaymasterValidationData(uint256 validationData, uint256 paymasterValidationData, address expectedAggregator) + internal + view + { + (address aggregator, bool outOfTimeRange) = _parseValidationData(validationData); + + require(expectedAggregator == aggregator, "AA24 signature error"); + require(!outOfTimeRange, "AA22 expired or not due"); + // pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. + // Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation). + address pmAggregator; + (pmAggregator, outOfTimeRange) = _parseValidationData(paymasterValidationData); + + require(pmAggregator == address(0), "AA34 signature error"); + require(!outOfTimeRange, "AA32 paymaster expired or not due"); + } + + function _parseValidationData(uint256 validationData) + internal + view + returns (address aggregator, bool outOfTimeRange) + { + return validationData == 0 + ? (address(0), false) + : ( + validationData.asAddressUint48x2().first(), + block.timestamp > validationData.asAddressUint48x2().second() || block.timestamp < validationData.asAddressUint48x2().third() + ); + } + + function _executeUserOp(PackedUserOperation calldata userOp, UserOpInfo memory opInfo) + internal + returns (uint256 collected) + { + // uint256 preGas = gasleft(); + + uint256 saveFreePtr; + assembly ("memory-safe") { + saveFreePtr := mload(0x40) + } + + bytes memory innerCall = bytes4(userOp.callData[0:4]) == IAccountExecute.executeUserOp.selector + ? abi.encodeCall(this.innerHandleOp, (abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)), opInfo, opInfo.context)) + : abi.encodeCall(this.innerHandleOp, (userOp.callData, opInfo, opInfo.context)); + + (bool success, bytes memory returndata) = address(this).call(innerCall); + if (success && returndata.length >= 0x20) { + collected = abi.decode(returndata, (uint256)); + } else { + // bytes32 innerRevertCode = abi.decode(returndata, (bytes32)); + + // TODO + // if (innerRevertCode == INNER_OUT_OF_GAS) { + // // handleOps was called with gas limit too low. abort entire bundle. + // //can only be caused by bundler (leaving not enough gas for inner call) + // revert FailedOp(opIndex, "AA95 out of gas"); + // } else if (innerRevertCode == INNER_REVERT_LOW_PREFUND) { + // // innerCall reverted on prefund too low. treat entire prefund as "gas cost" + // uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + // uint256 actualGasCost = opInfo.prefund; + // emitPrefundTooLow(opInfo); + // emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); + // collected = actualGasCost; + // } else { + // emit PostOpRevertReason( + // opInfo.userOpHash, + // opInfo.mUserOp.sender, + // opInfo.mUserOp.nonce, + // Exec.getReturnData(REVERT_REASON_MAX_LEN) + // ); + + // uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + // collected = _postExecution( + // IPaymaster.PostOpMode.postOpReverted, + // opInfo, + // opInfo.context, + // actualGas + // ); + // } + } + + assembly ("memory-safe") { + mstore(0x40, saveFreePtr) + } + } + + + + + + + + function innerHandleOp(bytes memory callData, UserOpInfo memory opInfo, bytes calldata context) + external + returns (uint256 actualGasCost) + { + //TODO + // uint256 preGas = gasleft(); + + // require(msg.sender == address(this), "AA92 internal call only"); + // uint256 callGasLimit = opInfo.mUserOp.callGasLimit; + // unchecked { + // // handleOps was called with gas limit too low. abort entire bundle. + // if ( + // gasleft() * 63 / 64 < + // callGasLimit + + // opInfo.mUserOp.paymasterPostOpGasLimit + + // // INNER_GAS_OVERHEAD // TODO + // 10_000 + // ) { + // assembly ("memory-safe") { + // mstore(0, INNER_OUT_OF_GAS) + // revert(0, 32) + // } + // } + // } + + // IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; + // if (callData.length > 0) { + // bool success = Exec.call(opInfo.mUserOp.sender, 0, callData, callGasLimit); + // if (!success) { + // bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); + // if (result.length > 0) { + // emit UserOperationRevertReason( + // opInfo.userOpHash, + // opInfo.mUserOp.sender, + // opInfo.mUserOp.nonce, + // result + // ); + // } + // mode = IPaymaster.PostOpMode.opReverted; + // } + // } + + // unchecked { + // uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + // return _postExecution(mode, opInfo, context, actualGas); + // } + } +} \ No newline at end of file diff --git a/contracts/abstraction/UserOperationUtils.sol b/contracts/abstraction/UserOperationUtils.sol index 4005b3c24d3..f44c8ae1ebc 100644 --- a/contracts/abstraction/UserOperationUtils.sol +++ b/contracts/abstraction/UserOperationUtils.sol @@ -9,57 +9,118 @@ import {Packing} from "../utils/Packing.sol"; library UserOperationUtils { using Packing for *; - function hash(PackedUserOperation calldata userOp) internal pure returns (bytes32) { - return keccak256(encode(userOp)); + /// Calldata + function hash(PackedUserOperation calldata self) internal pure returns (bytes32) { + return keccak256(encode(self)); } - function encode(PackedUserOperation calldata userOp) internal pure returns (bytes memory ret) { + function encode(PackedUserOperation calldata self) internal pure returns (bytes memory ret) { return abi.encode( - userOp.sender, - userOp.nonce, - keccak256(userOp.initCode), - keccak256(userOp.callData), - userOp.accountGasLimits, - userOp.preVerificationGas, - userOp.gasFees, - keccak256(userOp.paymasterAndData) + self.sender, + self.nonce, + keccak256(self.initCode), + keccak256(self.callData), + self.accountGasLimits, + self.preVerificationGas, + self.gasFees, + keccak256(self.paymasterAndData) ); } - function verificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.accountGasLimits.asUint128x2().first(); + function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return self.accountGasLimits.asUint128x2().first(); } - function callGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.accountGasLimits.asUint128x2().second(); + function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return self.accountGasLimits.asUint128x2().second(); } - function maxPriorityFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.gasFees.asUint128x2().first(); + function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { + return self.gasFees.asUint128x2().first(); } - function maxFeePerGas(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return userOp.gasFees.asUint128x2().second(); + function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { + return self.gasFees.asUint128x2().second(); } - function gasPrice(PackedUserOperation calldata userOp) internal view returns (uint256) { + function gasPrice(PackedUserOperation calldata self) internal view returns (uint256) { unchecked { // Following values are "per gas" - (uint256 maxPriorityFee, uint256 maxFee) = userOp.gasFees.asUint128x2().split(); + (uint256 maxPriorityFee, uint256 maxFee) = self.gasFees.asUint128x2().split(); return maxFee == maxPriorityFee ? maxFee : Math.min(maxFee, maxPriorityFee + block.basefee); } } - function paymaster(PackedUserOperation calldata userOp) internal pure returns (address) { - return address(bytes20(userOp.paymasterAndData[0:20])); + function paymaster(PackedUserOperation calldata self) internal pure returns (address) { + return address(bytes20(self.paymasterAndData[0:20])); } - function paymasterVerificationGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return uint128(bytes16(userOp.paymasterAndData[20:36])); + function paymasterVerificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(bytes16(self.paymasterAndData[20:36])); } - function paymasterPostOpGasLimit(PackedUserOperation calldata userOp) internal pure returns (uint256) { - return uint128(bytes16(userOp.paymasterAndData[36:52])); + function paymasterPostOpGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(bytes16(self.paymasterAndData[36:52])); + } + + function createSenderIfNeeded(PackedUserOperation calldata userOp) internal returns (address) { + address sender = userOp.sender; + + if (sender.code.length == 0) { + require(userOp.initCode.length >= 20, "Missing init code"); + + (bool success, bytes memory returndata) = address(bytes20(userOp.initCode[0:20])).call{ gas: verificationGasLimit(userOp) }(userOp.initCode[20:]); + + require(success && returndata.length >= 0x20, "InitCode failed or OOG"); + require(sender == abi.decode(returndata, (address)), "InitCode must return sender"); + require(sender.code.length != 0, "InitCode must create sender"); + + // TODO - emit event + } + return sender; + } + + /// Memory + struct MemoryUserOp { + address sender; + uint256 nonce; + uint256 verificationGasLimit; + uint256 callGasLimit; + uint256 paymasterVerificationGasLimit; + uint256 paymasterPostOpGasLimit; + uint256 preVerificationGas; + address paymaster; + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + } + + function load(MemoryUserOp memory self, PackedUserOperation calldata source) internal pure { + self.sender = source.sender; + self.nonce = source.nonce; + (self.verificationGasLimit, self.callGasLimit) = source.accountGasLimits.asUint128x2().split(); + self.preVerificationGas = source.preVerificationGas; + (self.maxPriorityFeePerGas, self.maxFeePerGas) = source.gasFees.asUint128x2().split(); + + if (source.paymasterAndData.length > 0) { + require(source.paymasterAndData.length >= 52, "AA93 invalid paymasterAndData"); + self.paymaster = paymaster(source); + self.paymasterVerificationGasLimit = paymasterVerificationGasLimit(source); + self.paymasterPostOpGasLimit = paymasterPostOpGasLimit(source); + } else { + self.paymaster = address(0); + self.paymasterVerificationGasLimit = 0; + self.paymasterPostOpGasLimit = 0; + } + } + + function requiredPrefund(MemoryUserOp memory self) internal pure returns (uint256) { + return ( + self.verificationGasLimit + + self.callGasLimit + + self.paymasterVerificationGasLimit + + self.paymasterPostOpGasLimit + + self.preVerificationGas + ) * self.maxFeePerGas; } } diff --git a/contracts/utils/Packing.sol b/contracts/utils/Packing.sol index 1929f9b59bf..6bea1979b8c 100644 --- a/contracts/utils/Packing.sol +++ b/contracts/utils/Packing.sol @@ -13,11 +13,21 @@ library Packing { return Uint128x2.wrap(self); } + /// @dev Cast a bytes32 into a Uint128x2 + function asUint128x2(uint256 self) internal pure returns (Uint128x2) { + return Uint128x2.wrap(bytes32(self)); + } + /// @dev Cast a Uint128x2 into a bytes32 function asBytes32(Uint128x2 self) internal pure returns (bytes32) { return Uint128x2.unwrap(self); } + /// @dev Cast a Uint128x2 into a bytes32 + function asUint256(Uint128x2 self) internal pure returns (uint256) { + return uint256(Uint128x2.unwrap(self)); + } + /// @dev Pack two uint128 into a Uint128x2 function pack(uint128 first128, uint128 second128) internal pure returns (Uint128x2) { return Uint128x2.wrap(bytes32(bytes16(first128)) | bytes32(uint256(second128))); @@ -37,4 +47,55 @@ library Packing { function second(Uint128x2 self) internal pure returns (uint128) { return uint128(uint256(Uint128x2.unwrap(self))); } + + type AddressUint48x2 is bytes32; + + /// @dev Cast a bytes32 into a AddressUint48x2 + function asAddressUint48x2(bytes32 self) internal pure returns (AddressUint48x2) { + return AddressUint48x2.wrap(self); + } + + /// @dev Cast a uint256 into a AddressUint48x2 + function asAddressUint48x2(uint256 self) internal pure returns (AddressUint48x2) { + return AddressUint48x2.wrap(bytes32(self)); + } + + /// @dev Cast a AddressUint48x2 into a bytes32 + function asBytes32(AddressUint48x2 self) internal pure returns (bytes32) { + return AddressUint48x2.unwrap(self); + } + + /// @dev Cast a AddressUint48x2 into a uint256 + function asUint256(AddressUint48x2 self) internal pure returns (uint256) { + return uint256(AddressUint48x2.unwrap(self)); + } + + /// @dev Pack two uint128 into a AddressUint48x2 + function pack(address first160, uint48 second48, uint48 third48) internal pure returns (AddressUint48x2) { + return AddressUint48x2.wrap(bytes32( + uint256(uint160(first160)) | + uint256(second48) << 160 | + uint256(third48) << 208 + )); + } + + /// @dev Split a AddressUint48x2 into two uint128 + function split(AddressUint48x2 self) internal pure returns (address, uint48, uint48) { + return (first(self), second(self), third(self)); + } + + /// @dev Get the first element (address) of a AddressUint48x2 + function first(AddressUint48x2 self) internal pure returns (address) { + return address(uint160(uint256(AddressUint48x2.unwrap(self)))); + } + + /// @dev Get the second element (uint48) of a AddressUint48x2 + function second(AddressUint48x2 self) internal pure returns (uint48) { + return uint48(uint256(AddressUint48x2.unwrap(self)) >> 160); + } + + /// @dev Get the second element (uint48) of a AddressUint48x2 + function third(AddressUint48x2 self) internal pure returns (uint48) { + return uint48(uint256(AddressUint48x2.unwrap(self)) >> 208); + } } From 1616a96d5e302d85543cd272ff45e2f2886ee09e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 11 Apr 2024 17:03:54 +0200 Subject: [PATCH 22/66] wip --- contracts/abstraction/ERC4337Utils.sol | 247 +++++++ contracts/abstraction/EntryPoint.sol | 706 +++++++++++-------- contracts/abstraction/StakeManager.sol | 101 +++ contracts/abstraction/UserOperationUtils.sol | 126 ---- contracts/interfaces/IERC4337.sol | 54 +- contracts/utils/Call.sol | 72 ++ contracts/utils/Memory.sol | 22 + contracts/utils/NoncesWithKey.sol | 40 ++ contracts/utils/Packing.sol | 61 -- 9 files changed, 956 insertions(+), 473 deletions(-) create mode 100644 contracts/abstraction/ERC4337Utils.sol create mode 100644 contracts/abstraction/StakeManager.sol delete mode 100644 contracts/abstraction/UserOperationUtils.sol create mode 100644 contracts/utils/Call.sol create mode 100644 contracts/utils/Memory.sol create mode 100644 contracts/utils/NoncesWithKey.sol diff --git a/contracts/abstraction/ERC4337Utils.sol b/contracts/abstraction/ERC4337Utils.sol new file mode 100644 index 00000000000..cdb7357db9f --- /dev/null +++ b/contracts/abstraction/ERC4337Utils.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IEntryPoint, PackedUserOperation} from "../interfaces/IERC4337.sol"; +import {Math} from "../utils/math/Math.sol"; +import {Call} from "../utils/Call.sol"; +import {Packing} from "../utils/Packing.sol"; + +library ERC4337Utils { + using Packing for *; + /* + * For simulation purposes, validateUserOp (and validatePaymasterUserOp) + * return this value on success. + */ + uint256 internal constant SIG_VALIDATION_SUCCESS = 0; + + /* + * For simulation purposes, validateUserOp (and validatePaymasterUserOp) + * must return this value in case of signature failure, instead of revert. + */ + uint256 internal constant SIG_VALIDATION_FAILED = 1; + + // Create sender from initcode + function createSender(bytes calldata initCode, uint256 gas) internal returns (address sender) { + return + Call.call(address(bytes20(initCode[0:20])), 0, initCode[20:], gas) && Call.getReturnDataSize() >= 0x20 + ? abi.decode(Call.getReturnData(0x20), (address)) + : address(0); + } + + // Validation data + function parseValidationData( + uint256 validationData + ) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil) { + aggregator = address(uint160(validationData)); + validUntil = uint48(validationData >> 160); + validAfter = uint48(validationData >> 208); + if (validUntil == 0) validUntil = type(uint48).max; + } + + function packValidationData( + address aggregator, + uint48 validAfter, + uint48 validUntil + ) internal pure returns (uint256) { + return uint160(aggregator) | (uint256(validUntil) << 160) | (uint256(validAfter) << 208); + } + + function packValidationData(bool sigFailed, uint48 validUntil, uint48 validAfter) internal pure returns (uint256) { + return + (sigFailed ? SIG_VALIDATION_FAILED : SIG_VALIDATION_SUCCESS) | + (uint256(validUntil) << 160) | + (uint256(validAfter) << 208); + } + + function getValidationData(uint256 validationData) internal view returns (address aggregator, bool outOfTimeRange) { + if (validationData == 0) { + return (address(0), false); + } else { + (address agregator, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData); + return (agregator, block.timestamp > validUntil || block.timestamp < validAfter); + } + } + + /* + enum ErrorCodes { + AA10_SENDER_ALREADY_CONSTRUCTED, + AA13_INITCODE_FAILLED, + AA14_INITCODE_WRONG_SENDER, + AA15_INITCODE_NO_DEPLOYMENT, + // Account + AA21_MISSING_FUNDS, + AA22_EXPIRED_OR_NOT_DUE, + AA23_REVERTED, + AA24_SIGNATURE_ERROR, + AA25_INVALID_NONCE, + AA26_OVER_VERIFICATION_GAS_LIMIT, + // Paymaster + AA31_MISSING_FUNDS, + AA32_EXPIRED_OR_NOT_DUE, + AA33_REVERTED, + AA34_SIGNATURE_ERROR, + AA36_OVER_VERIFICATION_GAS_LIMIT, + // other + AA95_OUT_OF_GAS + } + + function toString(ErrorCodes err) internal pure returns (string memory) { + if (err == ErrorCodes.AA10_SENDER_ALREADY_CONSTRUCTED) { + return "AA10 sender already constructed"; + } else if (err == ErrorCodes.AA13_INITCODE_FAILLED) { + return "AA13 initCode failed or OOG"; + } else if (err == ErrorCodes.AA14_INITCODE_WRONG_SENDER) { + return "AA14 initCode must return sender"; + } else if (err == ErrorCodes.AA15_INITCODE_NO_DEPLOYMENT) { + return "AA15 initCode must create sender"; + } else if (err == ErrorCodes.AA21_MISSING_FUNDS) { + return "AA21 didn't pay prefund"; + } else if (err == ErrorCodes.AA22_EXPIRED_OR_NOT_DUE) { + return "AA22 expired or not due"; + } else if (err == ErrorCodes.AA23_REVERTED) { + return "AA23 reverted"; + } else if (err == ErrorCodes.AA24_SIGNATURE_ERROR) { + return "AA24 signature error"; + } else if (err == ErrorCodes.AA25_INVALID_NONCE) { + return "AA25 invalid account nonce"; + } else if (err == ErrorCodes.AA26_OVER_VERIFICATION_GAS_LIMIT) { + return "AA26 over verificationGasLimit"; + } else if (err == ErrorCodes.AA31_MISSING_FUNDS) { + return "AA31 paymaster deposit too low"; + } else if (err == ErrorCodes.AA32_EXPIRED_OR_NOT_DUE) { + return "AA32 paymaster expired or not due"; + } else if (err == ErrorCodes.AA33_REVERTED) { + return "AA33 reverted"; + } else if (err == ErrorCodes.AA34_SIGNATURE_ERROR) { + return "AA34 signature error"; + } else if (err == ErrorCodes.AA36_OVER_VERIFICATION_GAS_LIMIT) { + return "AA36 over paymasterVerificationGasLimit"; + } else if (err == ErrorCodes.AA95_OUT_OF_GAS) { + return "AA95 out of gas"; + } else { + return "Unknown error code"; + } + } + + function failedOp(uint256 index, ErrorCodes err) internal pure { + revert IEntryPoint.FailedOp(index, toString(err)); + } + + function failedOp(uint256 index, ErrorCodes err, bytes memory extraData) internal pure { + revert IEntryPoint.FailedOpWithRevert(index, toString(err), extraData); + } + */ + + // Packed user operation + function hash(PackedUserOperation calldata self) internal pure returns (bytes32) { + return keccak256(encode(self)); + } + + function encode(PackedUserOperation calldata self) internal pure returns (bytes memory ret) { + return + abi.encode( + self.sender, + self.nonce, + keccak256(self.initCode), + keccak256(self.callData), + self.accountGasLimits, + self.preVerificationGas, + self.gasFees, + keccak256(self.paymasterAndData) + ); + } + + function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return self.accountGasLimits.asUint128x2().first(); + } + + function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return self.accountGasLimits.asUint128x2().second(); + } + + function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { + return self.gasFees.asUint128x2().first(); + } + + function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { + return self.gasFees.asUint128x2().second(); + } + + function gasPrice(PackedUserOperation calldata self) internal view returns (uint256) { + unchecked { + // Following values are "per gas" + (uint256 maxPriorityFee, uint256 maxFee) = self.gasFees.asUint128x2().split(); + return maxFee == maxPriorityFee ? maxFee : Math.min(maxFee, maxPriorityFee + block.basefee); + } + } + + function paymaster(PackedUserOperation calldata self) internal pure returns (address) { + return address(bytes20(self.paymasterAndData[0:20])); + } + + function paymasterVerificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(bytes16(self.paymasterAndData[20:36])); + } + + function paymasterPostOpGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(bytes16(self.paymasterAndData[36:52])); + } + + struct UserOpInfo { + address sender; + uint256 nonce; + uint256 verificationGasLimit; + uint256 callGasLimit; + uint256 paymasterVerificationGasLimit; + uint256 paymasterPostOpGasLimit; + uint256 preVerificationGas; + address paymaster; + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + bytes32 userOpHash; + uint256 prefund; + uint256 preOpGas; + bytes context; + } + + function load(UserOpInfo memory self, PackedUserOperation calldata source) internal view { + self.sender = source.sender; + self.nonce = source.nonce; + (self.verificationGasLimit, self.callGasLimit) = source.accountGasLimits.asUint128x2().split(); + self.preVerificationGas = source.preVerificationGas; + (self.maxPriorityFeePerGas, self.maxFeePerGas) = source.gasFees.asUint128x2().split(); + + if (source.paymasterAndData.length > 0) { + require(source.paymasterAndData.length >= 52, "AA93 invalid paymasterAndData"); + self.paymaster = paymaster(source); + self.paymasterVerificationGasLimit = paymasterVerificationGasLimit(source); + self.paymasterPostOpGasLimit = paymasterPostOpGasLimit(source); + } else { + self.paymaster = address(0); + self.paymasterVerificationGasLimit = 0; + self.paymasterPostOpGasLimit = 0; + } + self.userOpHash = keccak256(abi.encode(hash(source), address(this), block.chainid)); + self.prefund = 0; + self.preOpGas = 0; + self.context = ""; + } + + function requiredPrefund(UserOpInfo memory self) internal pure returns (uint256) { + return + (self.verificationGasLimit + + self.callGasLimit + + self.paymasterVerificationGasLimit + + self.paymasterPostOpGasLimit + + self.preVerificationGas) * self.maxFeePerGas; + } + + function gasPrice(UserOpInfo memory self) internal view returns (uint256) { + unchecked { + uint256 maxFee = self.maxFeePerGas; + uint256 maxPriorityFee = self.maxPriorityFeePerGas; + return maxFee == maxPriorityFee ? maxFee : Math.min(maxFee, maxPriorityFee + block.basefee); + } + } +} diff --git a/contracts/abstraction/EntryPoint.sol b/contracts/abstraction/EntryPoint.sol index 1cfffa03bbb..ac697ebc03c 100644 --- a/contracts/abstraction/EntryPoint.sol +++ b/contracts/abstraction/EntryPoint.sol @@ -2,93 +2,142 @@ pragma solidity ^0.8.20; -import { - PackedUserOperation, - IEntryPoint, - IAggregator, - IAccount, - IAccountExecute -} from "../interfaces/IERC4337.sol"; - -import {UserOperationUtils} from "./UserOperationUtils.sol"; -import {Address} from "../utils/Address.sol"; -import {Packing} from "../utils/Packing.sol"; -import {ReentrancyGuardTransient} from "../utils/ReentrancyGuardTransient.sol"; - - - - - +import {IEntryPoint, IEntryPointNonces, IEntryPointStake, IAccount, IAccountExecute, IAggregator, IPaymaster, PackedUserOperation} from "../interfaces/IERC4337.sol"; +import {IERC165} from "../interfaces/IERC165.sol"; +import {ERC165} from "../utils/introspection/ERC165.sol"; +import {Address} from "../utils/Address.sol"; +import {Call} from "../utils/Call.sol"; +import {Memory} from "../utils/Memory.sol"; +import {NoncesWithKey} from "../utils/NoncesWithKey.sol"; +import {ReentrancyGuard} from "../utils/ReentrancyGuard.sol"; +import {ERC4337Utils} from "./ERC4337Utils.sol"; +import {StakeManager} from "./StakeManager.sol"; + +/* + * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. + * Only one instance required on each chain. + */ +contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard, ERC165 { + using ERC4337Utils for *; + + // TODO: move to interface? + event UserOperationEvent( + bytes32 indexed userOpHash, + address indexed sender, + address indexed paymaster, + uint256 nonce, + bool success, + uint256 actualGasCost, + uint256 actualGasUsed + ); + event AccountDeployed(bytes32 indexed userOpHash, address indexed sender, address factory, address paymaster); + event UserOperationRevertReason( + bytes32 indexed userOpHash, + address indexed sender, + uint256 nonce, + bytes revertReason + ); + event PostOpRevertReason(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason); + event UserOperationPrefundTooLow(bytes32 indexed userOpHash, address indexed sender, uint256 nonce); + event BeforeExecution(); + event SignatureAggregatorChanged(address indexed aggregator); + error PostOpReverted(bytes returnData); + error SignatureValidationFailed(address aggregator); + error SenderAddressResult(address sender); + + //compensate for innerHandleOps' emit message and deposit refund. + // allow some slack for future gas price changes. + uint256 private constant INNER_GAS_OVERHEAD = 10000; + bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead"; + bytes32 private constant INNER_REVERT_LOW_PREFUND = hex"deadaa51"; + uint256 private constant REVERT_REASON_MAX_LEN = 2048; + uint256 private constant PENALTY_PERCENT = 10; + + // TODO + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return super.supportsInterface(interfaceId); + // || interfaceId == (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) + // || interfaceId == type(IEntryPoint).interfaceId + // || interfaceId == type(IStakeManager).interfaceId + // || interfaceId == type(INonceManager).interfaceId; + } -contract KeydNonces { /** - * @dev The nonce used for an `account` is not the expected current nonce. + * Execute a user operation. + * @param opIndex - Index into the opInfo array. + * @param userOp - The userOp to execute. + * @param opInfo - The opInfo filled by validatePrepayment for this userOp. + * @return collected - The total amount this userOp paid. */ - error InvalidAccountNonce(address account, uint256 currentNonce); - - mapping(address => mapping(uint192 => uint64)) private _nonce; - - function getNonce(address owner, uint192 key) public view virtual returns (uint256) { - return (uint256(key) << 64) | _nonce[owner][key]; - } - - function _useNonce(address owner, uint192 key) internal virtual returns (uint64) { - // TODO: use unchecked here ? - return _nonce[owner][key]++; - } - - function _useCheckedNonce(address owner, uint256 keyNonce) internal { - _useCheckedNonce(owner, uint192(keyNonce >> 64), uint64(keyNonce)); - } + function _executeUserOp( + uint256 opIndex, + PackedUserOperation calldata userOp, + ERC4337Utils.UserOpInfo memory opInfo + ) internal returns (uint256 collected) { + uint256 preGas = gasleft(); + + // Allocate memory and reset the free memory pointer. Buffer for innerCall is not kept/protected + Memory.FreePtr ptr = Memory.save(); + bytes memory innerCall = userOp.callData.length >= 4 && + bytes4(userOp.callData[0:4]) == IAccountExecute.executeUserOp.selector + ? abi.encodeCall( + this.innerHandleOp, + (abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)), opInfo) + ) + : abi.encodeCall(this.innerHandleOp, (userOp.callData, opInfo)); + Memory.load(ptr); + + bool success = Call.call(address(this), 0, innerCall); + bytes32 result = abi.decode(Call.getReturnDataFixed(0x20), (bytes32)); + + if (success) { + collected = uint256(result); + } else if (result == INNER_OUT_OF_GAS) { + // handleOps was called with gas limit too low. abort entire bundle. + //can only be caused by bundler (leaving not enough gas for inner call) + revert FailedOp(opIndex, "AA95 out of gas"); + } else if (result == INNER_REVERT_LOW_PREFUND) { + // innerCall reverted on prefund too low. treat entire prefund as "gas cost" + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + uint256 actualGasCost = opInfo.prefund; + emit UserOperationPrefundTooLow(opInfo.userOpHash, opInfo.sender, opInfo.nonce); + emit UserOperationEvent( + opInfo.userOpHash, + opInfo.sender, + opInfo.paymaster, + opInfo.nonce, + success, + actualGasCost, + actualGas + ); + collected = actualGasCost; + } else { + emit PostOpRevertReason( + opInfo.userOpHash, + opInfo.sender, + opInfo.nonce, + Call.getReturnData(REVERT_REASON_MAX_LEN) + ); - function _useCheckedNonce(address owner, uint192 key, uint64 nonce) internal virtual { - uint256 current = _useNonce(owner, key); - if (nonce != current) { - revert InvalidAccountNonce(owner, current); + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + collected = _postExecution(IPaymaster.PostOpMode.postOpReverted, opInfo, actualGas); } } -} - - - - - -contract EntryPoint is IEntryPoint, ReentrancyGuardTransient, KeydNonces { - using UserOperationUtils for *; - using Packing for *; - - struct UserOpInfo { - UserOperationUtils.MemoryUserOp mUserOp; - bytes32 userOpHash; - uint256 prefund; - uint256 preOpGas; - bytes context; - } - - - - - function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external nonReentrant { - UserOpInfo[] memory opInfos = new UserOpInfo[](ops.length); + function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) public nonReentrant { + ERC4337Utils.UserOpInfo[] memory opInfos = new ERC4337Utils.UserOpInfo[](ops.length); for (uint256 i = 0; i < ops.length; ++i) { - ( - uint256 validationData, - uint256 pmValidationData - ) = _validatePrepayment(ops[i], opInfos[i]); - - _validateAccountAndPaymasterValidationData( - validationData, - pmValidationData, - address(0) - ); + (uint256 validationData, uint256 pmValidationData) = _validatePrepayment(i, ops[i], opInfos[i]); + _validateAccountAndPaymasterValidationData(i, validationData, pmValidationData, address(0)); } + emit BeforeExecution(); + uint256 collected = 0; for (uint256 i = 0; i < ops.length; ++i) { - collected += _executeUserOp(ops[i], opInfos[i]); + collected += _executeUserOp(i, ops[i], opInfos[i]); } Address.sendValue(beneficiary, collected); @@ -97,7 +146,7 @@ contract EntryPoint is IEntryPoint, ReentrancyGuardTransient, KeydNonces { function handleAggregatedOps( UserOpsPerAggregator[] calldata opsPerAggregator, address payable beneficiary - ) external nonReentrant { + ) public nonReentrant { uint256 totalOps = 0; for (uint256 i = 0; i < opsPerAggregator.length; ++i) { PackedUserOperation[] calldata ops = opsPerAggregator[i].userOps; @@ -105,21 +154,16 @@ contract EntryPoint is IEntryPoint, ReentrancyGuardTransient, KeydNonces { //address(1) is special marker of "signature error" require(address(aggregator) != address(1), "AA96 invalid aggregator"); - if (address(aggregator) != address(0)) { // solhint-disable-next-line no-empty-blocks - try aggregator.validateSignatures(ops, opsPerAggregator[i].signature) - {} - catch - { - revert("SignatureValidationFailed"); - // revert SignatureValidationFailed(address(aggregator)); + try aggregator.validateSignatures(ops, opsPerAggregator[i].signature) {} catch { + revert SignatureValidationFailed(address(aggregator)); } } totalOps += ops.length; } - UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); + ERC4337Utils.UserOpInfo[] memory opInfos = new ERC4337Utils.UserOpInfo[](totalOps); uint256 opIndex = 0; for (uint256 a = 0; a < opsPerAggregator.length; ++a) { @@ -127,12 +171,13 @@ contract EntryPoint is IEntryPoint, ReentrancyGuardTransient, KeydNonces { IAggregator aggregator = opsPerAggregator[a].aggregator; for (uint256 i = 0; i < ops.length; ++i) { - ( - uint256 validationData, - uint256 paymasterValidationData - ) = _validatePrepayment(ops[i], opInfos[opIndex]); - + (uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment( + opIndex, + ops[i], + opInfos[opIndex] + ); _validateAccountAndPaymasterValidationData( + i, validationData, paymasterValidationData, address(aggregator) @@ -141,252 +186,347 @@ contract EntryPoint is IEntryPoint, ReentrancyGuardTransient, KeydNonces { } } - uint256 collected = 0; + emit BeforeExecution(); opIndex = 0; + + uint256 collected = 0; for (uint256 a = 0; a < opsPerAggregator.length; ++a) { PackedUserOperation[] calldata ops = opsPerAggregator[a].userOps; + IAggregator aggregator = opsPerAggregator[a].aggregator; + emit SignatureAggregatorChanged(address(aggregator)); for (uint256 i = 0; i < ops.length; ++i) { - collected += _executeUserOp(ops[i], opInfos[opIndex]); + collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]); opIndex++; } } + emit SignatureAggregatorChanged(address(0)); Address.sendValue(beneficiary, collected); } + /** + * Inner function to handle a UserOperation. + * Must be declared "external" to open a call context, but it can only be called by handleOps. + * @param callData - The callData to execute. + * @param opInfo - The UserOpInfo struct. + * @return actualGasCost - the actual cost in eth this UserOperation paid for gas + */ + function innerHandleOp( + bytes memory callData, + ERC4337Utils.UserOpInfo memory opInfo + ) external returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + require(msg.sender == address(this), "AA92 internal call only"); - - function getNonce(address owner, uint192 key) public view virtual override(IEntryPoint, KeydNonces) returns (uint256) { - return super.getNonce(owner, key); - } - - - - - - - - - - function _validatePrepayment(PackedUserOperation calldata userOp, UserOpInfo memory outOpInfo) - internal - returns (uint256 validationData, uint256 paymasterValidationData) - { unchecked { - uint256 preGas = gasleft(); - - outOpInfo.mUserOp.load(userOp); - outOpInfo.userOpHash = userOp.hash(); + // handleOps was called with gas limit too low. abort entire bundle. + if ((gasleft() * 63) / 64 < opInfo.callGasLimit + opInfo.paymasterPostOpGasLimit + INNER_GAS_OVERHEAD) { + Call.revertWithCode(INNER_OUT_OF_GAS); + } - // Validate all numeric values in userOp are well below 128 bit, so they can safely be added - // and multiplied without causing overflow. - uint256 maxGasValues = - outOpInfo.mUserOp.preVerificationGas | - outOpInfo.mUserOp.verificationGasLimit | - outOpInfo.mUserOp.callGasLimit | - outOpInfo.mUserOp.paymasterVerificationGasLimit | - outOpInfo.mUserOp.paymasterPostOpGasLimit | - outOpInfo.mUserOp.maxFeePerGas | - outOpInfo.mUserOp.maxPriorityFeePerGas; - require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); - - validationData = _validateAccountPrepayment(userOp, outOpInfo); - _useCheckedNonce(outOpInfo.mUserOp.sender, outOpInfo.mUserOp.nonce); - - require (preGas - gasleft() <= outOpInfo.mUserOp.verificationGasLimit, "AA26 over verificationGasLimit"); - - if (outOpInfo.mUserOp.paymaster != address(0)) { - // (outOpInfo.mUserOp.context, paymasterValidationData) = _validatePaymasterPrepayment( - // opIndex, - // userOp, - // outOpInfo, - // requiredPreFund - // ); + IPaymaster.PostOpMode mode; + if (callData.length == 0 || Call.call(opInfo.sender, 0, callData, opInfo.callGasLimit)) { + mode = IPaymaster.PostOpMode.opSucceeded; } else { - paymasterValidationData = 0; + mode = IPaymaster.PostOpMode.opReverted; + // if we get here, that means callData.length > 0 and the Call failed + if (Call.getReturnDataSize() > 0) { + emit UserOperationRevertReason( + opInfo.userOpHash, + opInfo.sender, + opInfo.nonce, + Call.getReturnData(REVERT_REASON_MAX_LEN) + ); + } } - outOpInfo.prefund = outOpInfo.mUserOp.requiredPrefund(); - outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + return _postExecution(mode, opInfo, actualGas); + } + } + + /** + * Create sender smart contract account if init code is provided. + * @param opIndex - The operation index. + * @param opInfo - The operation info. + * @param initCode - The init code for the smart contract account. + */ + function _createSenderIfNeeded( + uint256 opIndex, + ERC4337Utils.UserOpInfo memory opInfo, + bytes calldata initCode + ) internal { + if (initCode.length != 0) { + address sender = opInfo.sender; + if (sender.code.length != 0) revert FailedOp(opIndex, "AA10 sender already constructed"); + + address deployed = ERC4337Utils.createSender(initCode, opInfo.verificationGasLimit); + if (deployed == address(0)) revert FailedOp(opIndex, "AA13 initCode failed or OOG"); + else if (deployed != sender) revert FailedOp(opIndex, "AA14 initCode must return sender"); + else if (deployed.code.length == 0) revert FailedOp(opIndex, "AA15 initCode must create sender"); + + emit AccountDeployed(opInfo.userOpHash, sender, address(bytes20(initCode[0:20])), opInfo.paymaster); } } - function _validateAccountPrepayment(PackedUserOperation calldata userOp, UserOpInfo memory /*outOpInfo*/) - internal - returns (uint256 validationData) - { + function getSenderAddress(bytes calldata initCode) public { + revert SenderAddressResult(ERC4337Utils.createSender(initCode, gasleft())); + } + /** + * Call account.validateUserOp. + * Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. + * Decrement account's deposit if needed. + * @param opIndex - The operation index. + * @param op - The user operation. + * @param opInfo - The operation info. + * @param requiredPrefund - The required prefund amount. + */ + function _validateAccountPrepayment( + uint256 opIndex, + PackedUserOperation calldata op, + ERC4337Utils.UserOpInfo memory opInfo, + uint256 requiredPrefund + ) internal returns (uint256 validationData) { unchecked { - address sender = userOp.createSenderIfNeeded(); - address paymaster = userOp.paymaster(); - uint256 missingAccountFunds = 0; + address sender = opInfo.sender; + address paymaster = opInfo.paymaster; + uint256 verificationGasLimit = opInfo.verificationGasLimit; + + _createSenderIfNeeded(opIndex, opInfo, op.initCode); - // uint256 requiredPrefund = outOpInfo.mUserOp.requiredPrefund(); + uint256 missingAccountFunds = 0; if (paymaster == address(0)) { - //TODO - // uint256 bal = balanceOf(sender); - // missingAccountFunds = bal > requiredPrefund ? 0 : requiredPrefund - bal; // TODO: use select + uint256 balance = balanceOf(sender); + if (requiredPrefund > balance) { + missingAccountFunds = requiredPrefund - balance; + } } - try IAccount(sender).validateUserOp{ gas: userOp.verificationGasLimit() }(userOp, userOp.hash(), missingAccountFunds) returns (uint256 _validationData) { + try + IAccount(sender).validateUserOp{gas: verificationGasLimit}(op, opInfo.userOpHash, missingAccountFunds) + returns (uint256 _validationData) { validationData = _validationData; - } catch (bytes memory /*returndata*/) { - // TODO return bombing? - // revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN)); - revert('Reverted'); + } catch { + revert FailedOpWithRevert(opIndex, "AA23 reverted", Call.getReturnData(REVERT_REASON_MAX_LEN)); } if (paymaster == address(0)) { - // TODO - // DepositInfo storage senderInfo = deposits[sender]; - // uint256 deposit = senderInfo.deposit; - // if (requiredPrefund > deposit) { - // revert FailedOp(opIndex, "AA21 didn't pay prefund"); - // } - // senderInfo.deposit = deposit - requiredPrefund; + uint256 balance = balanceOf(sender); + if (requiredPrefund > balance) { + revert FailedOp(opIndex, "AA21 didn't pay prefund"); + } else if (requiredPrefund > 0) { + _decrementDeposit(sender, requiredPrefund); + } } } } - function _validateAccountAndPaymasterValidationData(uint256 validationData, uint256 paymasterValidationData, address expectedAggregator) - internal - view - { - (address aggregator, bool outOfTimeRange) = _parseValidationData(validationData); - - require(expectedAggregator == aggregator, "AA24 signature error"); - require(!outOfTimeRange, "AA22 expired or not due"); - // pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. - // Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation). - address pmAggregator; - (pmAggregator, outOfTimeRange) = _parseValidationData(paymasterValidationData); + /** + * In case the request has a paymaster: + * - Validate paymaster has enough deposit. + * - Call paymaster.validatePaymasterUserOp. + * - Revert with proper FailedOp in case paymaster reverts. + * - Decrement paymaster's deposit. + * @param opIndex - The operation index. + * @param op - The user operation. + * @param opInfo - The operation info. + * @param requiredPrefund - The required prefund amount. + */ + function _validatePaymasterPrepayment( + uint256 opIndex, + PackedUserOperation calldata op, + ERC4337Utils.UserOpInfo memory opInfo, + uint256 requiredPrefund + ) internal returns (bytes memory context, uint256 validationData) { + unchecked { + uint256 preGas = gasleft(); - require(pmAggregator == address(0), "AA34 signature error"); - require(!outOfTimeRange, "AA32 paymaster expired or not due"); - } + address paymaster = opInfo.paymaster; + uint256 verificationGasLimit = opInfo.paymasterVerificationGasLimit; - function _parseValidationData(uint256 validationData) - internal - view - returns (address aggregator, bool outOfTimeRange) - { - return validationData == 0 - ? (address(0), false) - : ( - validationData.asAddressUint48x2().first(), - block.timestamp > validationData.asAddressUint48x2().second() || block.timestamp < validationData.asAddressUint48x2().third() - ); - } + uint256 balance = balanceOf(paymaster); + if (requiredPrefund > balance) { + revert FailedOp(opIndex, "AA31 paymaster deposit too low"); + } else if (requiredPrefund > 0) { + _decrementDeposit(paymaster, requiredPrefund); + } - function _executeUserOp(PackedUserOperation calldata userOp, UserOpInfo memory opInfo) - internal - returns (uint256 collected) - { - // uint256 preGas = gasleft(); + try + IPaymaster(paymaster).validatePaymasterUserOp{gas: verificationGasLimit}( + op, + opInfo.userOpHash, + requiredPrefund + ) + returns (bytes memory _context, uint256 _validationData) { + context = _context; + validationData = _validationData; + } catch { + revert FailedOpWithRevert(opIndex, "AA33 reverted", Call.getReturnData(REVERT_REASON_MAX_LEN)); + } - uint256 saveFreePtr; - assembly ("memory-safe") { - saveFreePtr := mload(0x40) + if (preGas - gasleft() > verificationGasLimit) { + revert FailedOp(opIndex, "AA36 over paymasterVerificationGasLimit"); + } } + } - bytes memory innerCall = bytes4(userOp.callData[0:4]) == IAccountExecute.executeUserOp.selector - ? abi.encodeCall(this.innerHandleOp, (abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)), opInfo, opInfo.context)) - : abi.encodeCall(this.innerHandleOp, (userOp.callData, opInfo, opInfo.context)); - - (bool success, bytes memory returndata) = address(this).call(innerCall); - if (success && returndata.length >= 0x20) { - collected = abi.decode(returndata, (uint256)); - } else { - // bytes32 innerRevertCode = abi.decode(returndata, (bytes32)); - - // TODO - // if (innerRevertCode == INNER_OUT_OF_GAS) { - // // handleOps was called with gas limit too low. abort entire bundle. - // //can only be caused by bundler (leaving not enough gas for inner call) - // revert FailedOp(opIndex, "AA95 out of gas"); - // } else if (innerRevertCode == INNER_REVERT_LOW_PREFUND) { - // // innerCall reverted on prefund too low. treat entire prefund as "gas cost" - // uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - // uint256 actualGasCost = opInfo.prefund; - // emitPrefundTooLow(opInfo); - // emitUserOperationEvent(opInfo, false, actualGasCost, actualGas); - // collected = actualGasCost; - // } else { - // emit PostOpRevertReason( - // opInfo.userOpHash, - // opInfo.mUserOp.sender, - // opInfo.mUserOp.nonce, - // Exec.getReturnData(REVERT_REASON_MAX_LEN) - // ); - - // uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - // collected = _postExecution( - // IPaymaster.PostOpMode.postOpReverted, - // opInfo, - // opInfo.context, - // actualGas - // ); - // } + /** + * Revert if either account validationData or paymaster validationData is expired. + * @param opIndex - The operation index. + * @param validationData - The account validationData. + * @param paymasterValidationData - The paymaster validationData. + * @param expectedAggregator - The expected aggregator. + */ + function _validateAccountAndPaymasterValidationData( + uint256 opIndex, + uint256 validationData, + uint256 paymasterValidationData, + address expectedAggregator + ) internal view { + (address aggregator, bool aggregatorOutOfTimeRange) = validationData.getValidationData(); + if (aggregator != expectedAggregator) { + revert FailedOp(opIndex, "AA24 signature error"); + } else if (aggregatorOutOfTimeRange) { + revert FailedOp(opIndex, "AA22 expired or not due"); } - - assembly ("memory-safe") { - mstore(0x40, saveFreePtr) + // pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. + // Non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation). + (address pmAggregator, bool pmAggregatorOutOfTimeRange) = paymasterValidationData.getValidationData(); + if (pmAggregator != address(0)) { + revert FailedOp(opIndex, "AA34 signature error"); + } else if (pmAggregatorOutOfTimeRange) { + revert FailedOp(opIndex, "AA32 paymaster expired or not due"); } } + /** + * Validate account and paymaster (if defined) and + * also make sure total validation doesn't exceed verificationGasLimit. + * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) + * @param opIndex - The index of this userOp into the "opInfos" array. + * @param userOp - The userOp to validate. + */ + function _validatePrepayment( + uint256 opIndex, + PackedUserOperation calldata userOp, + ERC4337Utils.UserOpInfo memory outOpInfo + ) internal returns (uint256 validationData, uint256 paymasterValidationData) { + uint256 preGas = gasleft(); + unchecked { + outOpInfo.load(userOp); + // Validate all numeric values in userOp are well below 128 bit, so they can safely be added + // and multiplied without causing overflow. + uint256 maxGasValues = outOpInfo.preVerificationGas | + outOpInfo.verificationGasLimit | + outOpInfo.callGasLimit | + outOpInfo.paymasterVerificationGasLimit | + outOpInfo.paymasterPostOpGasLimit | + outOpInfo.maxFeePerGas | + outOpInfo.maxPriorityFeePerGas; + + if (maxGasValues > type(uint120).max) { + revert FailedOp(opIndex, "AA94 gas values overflow"); + } + uint256 requiredPreFund = outOpInfo.requiredPrefund(); + validationData = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund); + if (!_tryUseNonce(outOpInfo.sender, outOpInfo.nonce)) { + revert FailedOp(opIndex, "AA25 invalid account nonce"); + } + if (preGas - gasleft() > outOpInfo.verificationGasLimit) { + revert FailedOp(opIndex, "AA26 over verificationGasLimit"); + } + if (outOpInfo.paymaster != address(0)) { + (outOpInfo.context, paymasterValidationData) = _validatePaymasterPrepayment( + opIndex, + userOp, + outOpInfo, + requiredPreFund + ); + } - function innerHandleOp(bytes memory callData, UserOpInfo memory opInfo, bytes calldata context) - external - returns (uint256 actualGasCost) - { - //TODO - // uint256 preGas = gasleft(); - - // require(msg.sender == address(this), "AA92 internal call only"); - // uint256 callGasLimit = opInfo.mUserOp.callGasLimit; - // unchecked { - // // handleOps was called with gas limit too low. abort entire bundle. - // if ( - // gasleft() * 63 / 64 < - // callGasLimit + - // opInfo.mUserOp.paymasterPostOpGasLimit + - // // INNER_GAS_OVERHEAD // TODO - // 10_000 - // ) { - // assembly ("memory-safe") { - // mstore(0, INNER_OUT_OF_GAS) - // revert(0, 32) - // } - // } - // } - - // IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; - // if (callData.length > 0) { - // bool success = Exec.call(opInfo.mUserOp.sender, 0, callData, callGasLimit); - // if (!success) { - // bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); - // if (result.length > 0) { - // emit UserOperationRevertReason( - // opInfo.userOpHash, - // opInfo.mUserOp.sender, - // opInfo.mUserOp.nonce, - // result - // ); - // } - // mode = IPaymaster.PostOpMode.opReverted; - // } - // } - - // unchecked { - // uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - // return _postExecution(mode, opInfo, context, actualGas); - // } + outOpInfo.prefund = requiredPreFund; + outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; + } } -} \ No newline at end of file + + /** + * Process post-operation, called just after the callData is executed. + * If a paymaster is defined and its validation returned a non-empty context, its postOp is called. + * The excess amount is refunded to the account (or paymaster - if it was used in the request). + * @param mode - Whether is called from innerHandleOp, or outside (postOpReverted). + * @param opInfo - UserOp fields and info collected during validation. + * @param actualGas - The gas used so far by this user operation. + */ + function _postExecution( + IPaymaster.PostOpMode mode, + ERC4337Utils.UserOpInfo memory opInfo, + uint256 actualGas + ) private returns (uint256 actualGasCost) { + uint256 preGas = gasleft(); + unchecked { + address refundAddress = opInfo.paymaster; + uint256 gasPrice = opInfo.gasPrice(); + + if (refundAddress == address(0)) { + refundAddress = opInfo.sender; + } else if (opInfo.context.length > 0 && mode != IPaymaster.PostOpMode.postOpReverted) { + try + IPaymaster(refundAddress).postOp{gas: opInfo.paymasterPostOpGasLimit}( + mode, + opInfo.context, + actualGas * gasPrice, + gasPrice + ) + {} catch { + revert PostOpReverted(Call.getReturnData(REVERT_REASON_MAX_LEN)); + } + } + actualGas += preGas - gasleft(); + + // Calculating a penalty for unused execution gas + uint256 executionGasLimit = opInfo.callGasLimit + opInfo.paymasterPostOpGasLimit; + uint256 executionGasUsed = actualGas - opInfo.preOpGas; + // this check is required for the gas used within EntryPoint and not covered by explicit gas limits + if (executionGasLimit > executionGasUsed) { + actualGas += ((executionGasLimit - executionGasUsed) * PENALTY_PERCENT) / 100; + } + + actualGasCost = actualGas * gasPrice; + uint256 prefund = opInfo.prefund; + if (prefund < actualGasCost) { + if (mode == IPaymaster.PostOpMode.postOpReverted) { + actualGasCost = prefund; + emit UserOperationPrefundTooLow(opInfo.userOpHash, opInfo.sender, opInfo.nonce); + } else { + Call.revertWithCode(INNER_REVERT_LOW_PREFUND); + } + } else if (prefund > actualGasCost) { + _incrementDeposit(refundAddress, prefund - actualGasCost); + } + emit UserOperationEvent( + opInfo.userOpHash, + opInfo.sender, + opInfo.paymaster, + opInfo.nonce, + mode == IPaymaster.PostOpMode.opSucceeded, + actualGasCost, + actualGas + ); + } + } + + function getNonce( + address owner, + uint192 key + ) public view virtual override(IEntryPointNonces, NoncesWithKey) returns (uint256) { + return super.getNonce(owner, key); + } +} diff --git a/contracts/abstraction/StakeManager.sol b/contracts/abstraction/StakeManager.sol new file mode 100644 index 00000000000..6a8b829d5b2 --- /dev/null +++ b/contracts/abstraction/StakeManager.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.23; + +import {IEntryPointStake} from "../interfaces/IERC4337.sol"; +import {Address} from "../utils/Address.sol"; + +abstract contract StakeManager is IEntryPointStake { + event Deposited(address indexed account, uint256 totalDeposit); + + event Withdrawn(address indexed account, address withdrawAddress, uint256 amount); + + event StakeLocked(address indexed account, uint256 totalStaked, uint256 unstakeDelaySec); + + event StakeUnlocked(address indexed account, uint256 withdrawTime); + + event StakeWithdrawn(address indexed account, address withdrawAddress, uint256 amount); + + struct DepositInfo { + uint256 deposit; + bool staked; + uint112 stake; + uint32 unstakeDelaySec; + uint48 withdrawTime; + } + + struct StakeInfo { + uint256 stake; + uint256 unstakeDelaySec; + } + + mapping(address => DepositInfo) private _deposits; + + receive() external payable { + depositTo(msg.sender); + } + + function balanceOf(address account) public view returns (uint256) { + return _deposits[account].deposit; + } + + function depositTo(address account) public payable virtual { + uint256 newDeposit = _incrementDeposit(account, msg.value); + emit Deposited(account, newDeposit); + } + + function addStake(uint32 unstakeDelaySec) public payable { + DepositInfo storage info = _deposits[msg.sender]; + require(unstakeDelaySec > 0, "must specify unstake delay"); + require(unstakeDelaySec >= info.unstakeDelaySec, "cannot decrease unstake time"); + + uint256 stake = info.stake + msg.value; + require(stake > 0, "no stake specified"); + require(stake <= type(uint112).max, "stake overflow"); + + _deposits[msg.sender] = DepositInfo(info.deposit, true, uint112(stake), unstakeDelaySec, 0); + + emit StakeLocked(msg.sender, stake, unstakeDelaySec); + } + + function unlockStake() public { + DepositInfo storage info = _deposits[msg.sender]; + require(info.unstakeDelaySec != 0, "not staked"); + require(info.staked, "already unstaking"); + + uint48 withdrawTime = uint48(block.timestamp) + info.unstakeDelaySec; + info.withdrawTime = withdrawTime; + info.staked = false; + + emit StakeUnlocked(msg.sender, withdrawTime); + } + + function withdrawStake(address payable withdrawAddress) public { + DepositInfo storage info = _deposits[msg.sender]; + uint256 stake = info.stake; + if (stake > 0) { + require(info.withdrawTime > 0, "must call unlockStake() first"); + require(info.withdrawTime <= block.timestamp, "Stake withdrawal is not due"); + + info.unstakeDelaySec = 0; + info.withdrawTime = 0; + info.stake = 0; + + emit StakeWithdrawn(msg.sender, withdrawAddress, stake); + Address.sendValue(withdrawAddress, stake); + } + } + + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) public { + _deposits[msg.sender].deposit -= withdrawAmount; + emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount); + Address.sendValue(withdrawAddress, withdrawAmount); + } + + function _incrementDeposit(address account, uint256 amount) internal returns (uint256) { + return _deposits[account].deposit += amount; + } + + function _decrementDeposit(address account, uint256 amount) internal returns (uint256) { + return _deposits[account].deposit -= amount; + } +} diff --git a/contracts/abstraction/UserOperationUtils.sol b/contracts/abstraction/UserOperationUtils.sol deleted file mode 100644 index f44c8ae1ebc..00000000000 --- a/contracts/abstraction/UserOperationUtils.sol +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {PackedUserOperation} from "../interfaces/IERC4337.sol"; -import {Math} from "../utils/math/Math.sol"; -import {Packing} from "../utils/Packing.sol"; - -library UserOperationUtils { - using Packing for *; - - /// Calldata - function hash(PackedUserOperation calldata self) internal pure returns (bytes32) { - return keccak256(encode(self)); - } - - function encode(PackedUserOperation calldata self) internal pure returns (bytes memory ret) { - return - abi.encode( - self.sender, - self.nonce, - keccak256(self.initCode), - keccak256(self.callData), - self.accountGasLimits, - self.preVerificationGas, - self.gasFees, - keccak256(self.paymasterAndData) - ); - } - - function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { - return self.accountGasLimits.asUint128x2().first(); - } - - function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { - return self.accountGasLimits.asUint128x2().second(); - } - - function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { - return self.gasFees.asUint128x2().first(); - } - - function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { - return self.gasFees.asUint128x2().second(); - } - - function gasPrice(PackedUserOperation calldata self) internal view returns (uint256) { - unchecked { - // Following values are "per gas" - (uint256 maxPriorityFee, uint256 maxFee) = self.gasFees.asUint128x2().split(); - return maxFee == maxPriorityFee ? maxFee : Math.min(maxFee, maxPriorityFee + block.basefee); - } - } - - function paymaster(PackedUserOperation calldata self) internal pure returns (address) { - return address(bytes20(self.paymasterAndData[0:20])); - } - - function paymasterVerificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { - return uint128(bytes16(self.paymasterAndData[20:36])); - } - - function paymasterPostOpGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { - return uint128(bytes16(self.paymasterAndData[36:52])); - } - - function createSenderIfNeeded(PackedUserOperation calldata userOp) internal returns (address) { - address sender = userOp.sender; - - if (sender.code.length == 0) { - require(userOp.initCode.length >= 20, "Missing init code"); - - (bool success, bytes memory returndata) = address(bytes20(userOp.initCode[0:20])).call{ gas: verificationGasLimit(userOp) }(userOp.initCode[20:]); - - require(success && returndata.length >= 0x20, "InitCode failed or OOG"); - require(sender == abi.decode(returndata, (address)), "InitCode must return sender"); - require(sender.code.length != 0, "InitCode must create sender"); - - // TODO - emit event - } - return sender; - } - - /// Memory - struct MemoryUserOp { - address sender; - uint256 nonce; - uint256 verificationGasLimit; - uint256 callGasLimit; - uint256 paymasterVerificationGasLimit; - uint256 paymasterPostOpGasLimit; - uint256 preVerificationGas; - address paymaster; - uint256 maxFeePerGas; - uint256 maxPriorityFeePerGas; - } - - function load(MemoryUserOp memory self, PackedUserOperation calldata source) internal pure { - self.sender = source.sender; - self.nonce = source.nonce; - (self.verificationGasLimit, self.callGasLimit) = source.accountGasLimits.asUint128x2().split(); - self.preVerificationGas = source.preVerificationGas; - (self.maxPriorityFeePerGas, self.maxFeePerGas) = source.gasFees.asUint128x2().split(); - - if (source.paymasterAndData.length > 0) { - require(source.paymasterAndData.length >= 52, "AA93 invalid paymasterAndData"); - self.paymaster = paymaster(source); - self.paymasterVerificationGasLimit = paymasterVerificationGasLimit(source); - self.paymasterPostOpGasLimit = paymasterPostOpGasLimit(source); - } else { - self.paymaster = address(0); - self.paymasterVerificationGasLimit = 0; - self.paymasterPostOpGasLimit = 0; - } - } - - function requiredPrefund(MemoryUserOp memory self) internal pure returns (uint256) { - return ( - self.verificationGasLimit + - self.callGasLimit + - self.paymasterVerificationGasLimit + - self.paymasterPostOpGasLimit + - self.preVerificationGas - ) * self.maxFeePerGas; - } -} diff --git a/contracts/interfaces/IERC4337.sol b/contracts/interfaces/IERC4337.sol index b8375304f9a..9c80c2564bd 100644 --- a/contracts/interfaces/IERC4337.sol +++ b/contracts/interfaces/IERC4337.sol @@ -46,7 +46,34 @@ interface IAggregator { ) external view returns (bytes memory aggregatesSignature); } -interface IEntryPoint { +interface IEntryPointNonces { + function getNonce(address sender, uint192 key) external view returns (uint256 nonce); +} + +interface IEntryPointStake { + // add a stake to the calling entity + function addStake(uint32 unstakeDelaySec) external payable; + + // unlock the stake (must wait unstakeDelay before can withdraw) + function unlockStake() external; + + // withdraw the unlocked stake + function withdrawStake(address payable withdrawAddress) external; + + // return the deposit of an account + function balanceOf(address account) external view returns (uint256); + + // add to the deposit of the given account + function depositTo(address account) external payable; + + // withdraw from the deposit of the current account + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external; +} + +interface IEntryPoint is IEntryPointNonces, IEntryPointStake { + error FailedOp(uint256 opIndex, string reason); + error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); + struct UserOpsPerAggregator { PackedUserOperation[] userOps; IAggregator aggregator; @@ -59,10 +86,10 @@ interface IEntryPoint { UserOpsPerAggregator[] calldata opsPerAggregator, address payable beneficiary ) external; - - function getNonce(address sender, uint192 key) external view returns (uint256 nonce); } +// TODO: EntryPointSimulation + interface IAccount { function validateUserOp( PackedUserOperation calldata userOp, @@ -74,3 +101,24 @@ interface IAccount { interface IAccountExecute { function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; } + +interface IPaymaster { + enum PostOpMode { + opSucceeded, + opReverted, + postOpReverted + } + + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external returns (bytes memory context, uint256 validationData); + + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) external; +} diff --git a/contracts/utils/Call.sol b/contracts/utils/Call.sol new file mode 100644 index 00000000000..b3892e7266c --- /dev/null +++ b/contracts/utils/Call.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Math} from "./math/Math.sol"; + +/** + * Utility functions helpful when making different kinds of contract calls in Solidity. + */ +library Call { + function call(address to, uint256 value, bytes memory data) internal returns (bool success) { + return call(to, value, data, gasleft()); + } + + function call(address to, uint256 value, bytes memory data, uint256 txGas) internal returns (bool success) { + assembly ("memory-safe") { + success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0) + } + } + + function staticcall(address to, bytes memory data) internal view returns (bool success) { + return staticcall(to, data, gasleft()); + } + + function staticcall(address to, bytes memory data, uint256 txGas) internal view returns (bool success) { + assembly ("memory-safe") { + success := staticcall(txGas, to, add(data, 0x20), mload(data), 0, 0) + } + } + + function delegateCall(address to, bytes memory data) internal returns (bool success) { + return delegateCall(to, data, gasleft()); + } + + function delegateCall(address to, bytes memory data, uint256 txGas) internal returns (bool success) { + assembly ("memory-safe") { + success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0) + } + } + + function getReturnDataSize() internal pure returns (uint256 returnDataSize) { + assembly ("memory-safe") { + returnDataSize := returndatasize() + } + } + + function getReturnData(uint256 maxLen) internal pure returns (bytes memory ptr) { + return getReturnDataFixed(Math.min(maxLen, getReturnDataSize())); + } + + function getReturnDataFixed(uint256 len) internal pure returns (bytes memory ptr) { + assembly ("memory-safe") { + ptr := mload(0x40) + mstore(0x40, add(ptr, add(len, 0x20))) + mstore(ptr, len) + returndatacopy(add(ptr, 0x20), 0, len) + } + } + + function revertWithData(bytes memory returnData) internal pure { + assembly ("memory-safe") { + revert(add(returnData, 0x20), mload(returnData)) + } + } + + function revertWithCode(bytes32 code) internal pure { + assembly ("memory-safe") { + mstore(0, code) + revert(0, 0x20) + } + } +} diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol new file mode 100644 index 00000000000..d1cb7b9ddae --- /dev/null +++ b/contracts/utils/Memory.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev Helper library packing and unpacking multiple values into bytes32 + */ +library Memory { + type FreePtr is bytes32; + + function save() internal pure returns (FreePtr ptr) { + assembly ("memory-safe") { + ptr := mload(0x40) + } + } + + function load(FreePtr ptr) internal pure { + assembly ("memory-safe") { + mstore(0x40, ptr) + } + } +} diff --git a/contracts/utils/NoncesWithKey.sol b/contracts/utils/NoncesWithKey.sol new file mode 100644 index 00000000000..ec4256c69fe --- /dev/null +++ b/contracts/utils/NoncesWithKey.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +contract NoncesWithKey { + /** + * @dev The nonce used for an `account` is not the expected current nonce. + */ + error InvalidAccountNonce(address account, uint256 currentNonce); + + mapping(address => mapping(uint192 => uint64)) private _nonce; + + function getNonce(address owner, uint192 key) public view virtual returns (uint256) { + return (uint256(key) << 64) | _nonce[owner][key]; + } + + function _useNonce(address owner, uint192 key) internal virtual returns (uint64) { + // TODO: use unchecked here? Do we expect 2**64 nonce ever be used for a single owner? + return _nonce[owner][key]++; + } + + function _tryUseNonce(address owner, uint256 keyNonce) internal returns (bool) { + return _tryUseNonce(owner, uint192(keyNonce >> 64), uint64(keyNonce)); + } + + function _tryUseNonce(address owner, uint192 key, uint64 nonce) internal virtual returns (bool) { + return _useNonce(owner, key) == nonce; + } + + function _useNonceOrRevert(address owner, uint256 keyNonce) internal { + _useNonceOrRevert(owner, uint192(keyNonce >> 64), uint64(keyNonce)); + } + + function _useNonceOrRevert(address owner, uint192 key, uint64 nonce) internal virtual { + uint256 current = _useNonce(owner, key); + if (nonce != current) { + revert InvalidAccountNonce(owner, current); + } + } +} diff --git a/contracts/utils/Packing.sol b/contracts/utils/Packing.sol index 6bea1979b8c..1929f9b59bf 100644 --- a/contracts/utils/Packing.sol +++ b/contracts/utils/Packing.sol @@ -13,21 +13,11 @@ library Packing { return Uint128x2.wrap(self); } - /// @dev Cast a bytes32 into a Uint128x2 - function asUint128x2(uint256 self) internal pure returns (Uint128x2) { - return Uint128x2.wrap(bytes32(self)); - } - /// @dev Cast a Uint128x2 into a bytes32 function asBytes32(Uint128x2 self) internal pure returns (bytes32) { return Uint128x2.unwrap(self); } - /// @dev Cast a Uint128x2 into a bytes32 - function asUint256(Uint128x2 self) internal pure returns (uint256) { - return uint256(Uint128x2.unwrap(self)); - } - /// @dev Pack two uint128 into a Uint128x2 function pack(uint128 first128, uint128 second128) internal pure returns (Uint128x2) { return Uint128x2.wrap(bytes32(bytes16(first128)) | bytes32(uint256(second128))); @@ -47,55 +37,4 @@ library Packing { function second(Uint128x2 self) internal pure returns (uint128) { return uint128(uint256(Uint128x2.unwrap(self))); } - - type AddressUint48x2 is bytes32; - - /// @dev Cast a bytes32 into a AddressUint48x2 - function asAddressUint48x2(bytes32 self) internal pure returns (AddressUint48x2) { - return AddressUint48x2.wrap(self); - } - - /// @dev Cast a uint256 into a AddressUint48x2 - function asAddressUint48x2(uint256 self) internal pure returns (AddressUint48x2) { - return AddressUint48x2.wrap(bytes32(self)); - } - - /// @dev Cast a AddressUint48x2 into a bytes32 - function asBytes32(AddressUint48x2 self) internal pure returns (bytes32) { - return AddressUint48x2.unwrap(self); - } - - /// @dev Cast a AddressUint48x2 into a uint256 - function asUint256(AddressUint48x2 self) internal pure returns (uint256) { - return uint256(AddressUint48x2.unwrap(self)); - } - - /// @dev Pack two uint128 into a AddressUint48x2 - function pack(address first160, uint48 second48, uint48 third48) internal pure returns (AddressUint48x2) { - return AddressUint48x2.wrap(bytes32( - uint256(uint160(first160)) | - uint256(second48) << 160 | - uint256(third48) << 208 - )); - } - - /// @dev Split a AddressUint48x2 into two uint128 - function split(AddressUint48x2 self) internal pure returns (address, uint48, uint48) { - return (first(self), second(self), third(self)); - } - - /// @dev Get the first element (address) of a AddressUint48x2 - function first(AddressUint48x2 self) internal pure returns (address) { - return address(uint160(uint256(AddressUint48x2.unwrap(self)))); - } - - /// @dev Get the second element (uint48) of a AddressUint48x2 - function second(AddressUint48x2 self) internal pure returns (uint48) { - return uint48(uint256(AddressUint48x2.unwrap(self)) >> 160); - } - - /// @dev Get the second element (uint48) of a AddressUint48x2 - function third(AddressUint48x2 self) internal pure returns (uint48) { - return uint48(uint256(AddressUint48x2.unwrap(self)) >> 208); - } } From aef216833cae5434d0a598fa841d39a03dc1f304 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 11 Apr 2024 17:22:09 +0200 Subject: [PATCH 23/66] refactor --- contracts/abstraction/EntryPoint.sol | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/abstraction/EntryPoint.sol b/contracts/abstraction/EntryPoint.sol index ac697ebc03c..3ceb9149448 100644 --- a/contracts/abstraction/EntryPoint.sol +++ b/contracts/abstraction/EntryPoint.sol @@ -79,13 +79,15 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard // Allocate memory and reset the free memory pointer. Buffer for innerCall is not kept/protected Memory.FreePtr ptr = Memory.save(); - bytes memory innerCall = userOp.callData.length >= 4 && - bytes4(userOp.callData[0:4]) == IAccountExecute.executeUserOp.selector - ? abi.encodeCall( - this.innerHandleOp, - (abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)), opInfo) + bytes memory innerCall = abi.encodeCall( + this.innerHandleOp, + ( + userOp.callData.length >= 0x04 && bytes4(userOp.callData[0:4]) == IAccountExecute.executeUserOp.selector + ? abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)) + : userOp.callData, + opInfo ) - : abi.encodeCall(this.innerHandleOp, (userOp.callData, opInfo)); + ); Memory.load(ptr); bool success = Call.call(address(this), 0, innerCall); From 04a6fb02080892ae80ae125dda8d66e6a4ced49c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 12 Apr 2024 15:31:21 +0200 Subject: [PATCH 24/66] refactor --- .../abstraction/{ => account}/Account.sol | 8 ++++---- .../{ => account}/SimpleAccount.sol | 10 +++++----- .../{ => entrypoint}/EntryPoint.sol | 19 +++++++++---------- .../{ => entrypoint}/StakeManager.sol | 4 ++-- .../abstraction/{ => utils}/ERC4337Utils.sol | 8 ++++---- 5 files changed, 24 insertions(+), 25 deletions(-) rename contracts/abstraction/{ => account}/Account.sol (87%) rename contracts/abstraction/{ => account}/SimpleAccount.sol (82%) rename contracts/abstraction/{ => entrypoint}/EntryPoint.sol (97%) rename contracts/abstraction/{ => entrypoint}/StakeManager.sol (96%) rename contracts/abstraction/{ => utils}/ERC4337Utils.sol (97%) diff --git a/contracts/abstraction/Account.sol b/contracts/abstraction/account/Account.sol similarity index 87% rename from contracts/abstraction/Account.sol rename to contracts/abstraction/account/Account.sol index 06cf488494d..937c8216b0a 100644 --- a/contracts/abstraction/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.20; -import {PackedUserOperation, IAccount, IEntryPoint} from "../interfaces/IERC4337.sol"; -import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol"; -import {SignatureChecker} from "../utils/cryptography/SignatureChecker.sol"; -import {SafeCast} from "../utils/math/SafeCast.sol"; +import {PackedUserOperation, IAccount, IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {MessageHashUtils} from "../../utils/cryptography/MessageHashUtils.sol"; +import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol"; +import {SafeCast} from "../../utils/math/SafeCast.sol"; abstract contract Account is IAccount { using SafeCast for bool; diff --git a/contracts/abstraction/SimpleAccount.sol b/contracts/abstraction/account/SimpleAccount.sol similarity index 82% rename from contracts/abstraction/SimpleAccount.sol rename to contracts/abstraction/account/SimpleAccount.sol index ae7f89cd6a1..327971f6aab 100644 --- a/contracts/abstraction/SimpleAccount.sol +++ b/contracts/abstraction/account/SimpleAccount.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.20; -import {PackedUserOperation, IEntryPoint} from "../interfaces/IERC4337.sol"; +import {PackedUserOperation, IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {Ownable} from "../../access/Ownable.sol"; +import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; +import {Address} from "../../utils/Address.sol"; import {Account} from "./Account.sol"; -import {Ownable} from "../access/Ownable.sol"; -import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155Holder} from "../token/ERC1155/utils/ERC1155Holder.sol"; -import {Address} from "../utils/Address.sol"; contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder { IEntryPoint private immutable _entryPoint; diff --git a/contracts/abstraction/EntryPoint.sol b/contracts/abstraction/entrypoint/EntryPoint.sol similarity index 97% rename from contracts/abstraction/EntryPoint.sol rename to contracts/abstraction/entrypoint/EntryPoint.sol index 3ceb9149448..070527521ba 100644 --- a/contracts/abstraction/EntryPoint.sol +++ b/contracts/abstraction/entrypoint/EntryPoint.sol @@ -2,16 +2,15 @@ pragma solidity ^0.8.20; -import {IEntryPoint, IEntryPointNonces, IEntryPointStake, IAccount, IAccountExecute, IAggregator, IPaymaster, PackedUserOperation} from "../interfaces/IERC4337.sol"; - -import {IERC165} from "../interfaces/IERC165.sol"; -import {ERC165} from "../utils/introspection/ERC165.sol"; -import {Address} from "../utils/Address.sol"; -import {Call} from "../utils/Call.sol"; -import {Memory} from "../utils/Memory.sol"; -import {NoncesWithKey} from "../utils/NoncesWithKey.sol"; -import {ReentrancyGuard} from "../utils/ReentrancyGuard.sol"; -import {ERC4337Utils} from "./ERC4337Utils.sol"; +import {IEntryPoint, IEntryPointNonces, IEntryPointStake, IAccount, IAccountExecute, IAggregator, IPaymaster, PackedUserOperation} from "../../interfaces/IERC4337.sol"; +import {IERC165} from "../../interfaces/IERC165.sol"; +import {ERC165} from "../../utils/introspection/ERC165.sol"; +import {Address} from "../../utils/Address.sol"; +import {Call} from "../../utils/Call.sol"; +import {Memory} from "../../utils/Memory.sol"; +import {NoncesWithKey} from "../../utils/NoncesWithKey.sol"; +import {ReentrancyGuard} from "../../utils/ReentrancyGuard.sol"; +import {ERC4337Utils} from "./../utils/ERC4337Utils.sol"; import {StakeManager} from "./StakeManager.sol"; /* diff --git a/contracts/abstraction/StakeManager.sol b/contracts/abstraction/entrypoint/StakeManager.sol similarity index 96% rename from contracts/abstraction/StakeManager.sol rename to contracts/abstraction/entrypoint/StakeManager.sol index 6a8b829d5b2..fa42f90d28d 100644 --- a/contracts/abstraction/StakeManager.sol +++ b/contracts/abstraction/entrypoint/StakeManager.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.23; -import {IEntryPointStake} from "../interfaces/IERC4337.sol"; -import {Address} from "../utils/Address.sol"; +import {IEntryPointStake} from "../../interfaces/IERC4337.sol"; +import {Address} from "../../utils/Address.sol"; abstract contract StakeManager is IEntryPointStake { event Deposited(address indexed account, uint256 totalDeposit); diff --git a/contracts/abstraction/ERC4337Utils.sol b/contracts/abstraction/utils/ERC4337Utils.sol similarity index 97% rename from contracts/abstraction/ERC4337Utils.sol rename to contracts/abstraction/utils/ERC4337Utils.sol index cdb7357db9f..541dbda5309 100644 --- a/contracts/abstraction/ERC4337Utils.sol +++ b/contracts/abstraction/utils/ERC4337Utils.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.20; -import {IEntryPoint, PackedUserOperation} from "../interfaces/IERC4337.sol"; -import {Math} from "../utils/math/Math.sol"; -import {Call} from "../utils/Call.sol"; -import {Packing} from "../utils/Packing.sol"; +import {IEntryPoint, PackedUserOperation} from "../../interfaces/IERC4337.sol"; +import {Math} from "../../utils/math/Math.sol"; +import {Call} from "../../utils/Call.sol"; +import {Packing} from "../../utils/Packing.sol"; library ERC4337Utils { using Packing for *; From 0f3c3fa02b8b5bac4bc36e80bdc478a3d535b081 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 12 Apr 2024 16:40:45 +0200 Subject: [PATCH 25/66] entrypoint deploys account --- .../{account => mocks}/SimpleAccount.sol | 2 +- test/abstraction/entrypoint.test.js | 115 ++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) rename contracts/abstraction/{account => mocks}/SimpleAccount.sol (97%) create mode 100644 test/abstraction/entrypoint.test.js diff --git a/contracts/abstraction/account/SimpleAccount.sol b/contracts/abstraction/mocks/SimpleAccount.sol similarity index 97% rename from contracts/abstraction/account/SimpleAccount.sol rename to contracts/abstraction/mocks/SimpleAccount.sol index 327971f6aab..e2e5c1b7799 100644 --- a/contracts/abstraction/account/SimpleAccount.sol +++ b/contracts/abstraction/mocks/SimpleAccount.sol @@ -7,7 +7,7 @@ import {Ownable} from "../../access/Ownable.sol"; import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; import {Address} from "../../utils/Address.sol"; -import {Account} from "./Account.sol"; +import {Account} from "../account/Account.sol"; contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder { IEntryPoint private immutable _entryPoint; diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js new file mode 100644 index 00000000000..c35fe197ae9 --- /dev/null +++ b/test/abstraction/entrypoint.test.js @@ -0,0 +1,115 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const accounts = await ethers.getSigners(); + const entrypoint = await ethers.deployContract('EntryPoint'); + const factory = await ethers.deployContract('$Create2'); + + const makeAA = (user, salt = ethers.randomBytes(32)) => + ethers.getContractFactory('SimpleAccount').then(accountFactory => + accountFactory + .getDeployTransaction(entrypoint, user) + .then(tx => factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) + .then(deployCode => ethers.concat([factory.target, deployCode])) + .then(initCode => + entrypoint + .getSenderAddress(initCode) + .catch(err => err.message.match(/SenderAddressResult\("(?0x[0-9a-zA-Z]{40})"\)/)?.groups?.addr) + .then(sender => Object.assign(accountFactory.attach(sender), { initCode, salt })), + ), + ); + + return { + accounts, + entrypoint, + factory, + makeAA, + }; +} + +describe('EntryPoint', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + it('', async function () { + const user = this.accounts[0]; + const beneficiary = this.accounts[1]; + const sender = await this.makeAA(user); + + expect(await ethers.provider.getCode(sender)).to.equal('0x'); + + await user.sendTransaction({ to: sender, value: ethers.parseEther('1') }); + await expect( + this.entrypoint.handleOps( + [ + { + sender, + nonce: 0n, + initCode: sender.initCode, + callData: '0x', + accountGasLimits: ethers.toBeHex((1000000n << 128n) | 1000000n, 32), // concatenation of verificationGas (16 bytes) and callGas (16 bytes) + preVerificationGas: 1000000n, + gasFees: ethers.toBeHex((100000n << 128n) | 100000n, 32), // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes) + paymasterAndData: '0x', // concatenation of paymaster fields (or empty) + signature: '0x', + }, + ], + beneficiary, + ), + ) + .to.emit(sender, 'OwnershipTransferred') + .withArgs(ethers.ZeroAddress, user) + .to.emit(this.factory, 'return$deploy') + .withArgs(sender) + .to.emit(this.entrypoint, 'AccountDeployed') + .to.emit(this.entrypoint, 'Deposited') + .to.emit(this.entrypoint, 'BeforeExecution') + .to.emit(this.entrypoint, 'UserOperationEvent'); + + expect(await ethers.provider.getCode(sender)).to.not.equal('0x'); + }); + + // describe('base64', function () { + // for (const { title, input, expected } of [ + // { title: 'converts to base64 encoded string with double padding', input: 'test', expected: 'dGVzdA==' }, + // { title: 'converts to base64 encoded string with single padding', input: 'test1', expected: 'dGVzdDE=' }, + // { title: 'converts to base64 encoded string without padding', input: 'test12', expected: 'dGVzdDEy' }, + // { title: 'converts to base64 encoded string (/ case)', input: 'où', expected: 'b/k=' }, + // { title: 'converts to base64 encoded string (+ case)', input: 'zs~1t8', expected: 'enN+MXQ4' }, + // { title: 'empty bytes', input: '', expected: '' }, + // ]) + // it(title, async function () { + // const buffer = Buffer.from(input, 'ascii'); + // expect(await this.mock.$encode(buffer)).to.equal(ethers.encodeBase64(buffer)); + // expect(await this.mock.$encode(buffer)).to.equal(expected); + // }); + // }); + + // describe('base64url', function () { + // for (const { title, input, expected } of [ + // { title: 'converts to base64url encoded string with double padding', input: 'test', expected: 'dGVzdA' }, + // { title: 'converts to base64url encoded string with single padding', input: 'test1', expected: 'dGVzdDE' }, + // { title: 'converts to base64url encoded string without padding', input: 'test12', expected: 'dGVzdDEy' }, + // { title: 'converts to base64url encoded string (_ case)', input: 'où', expected: 'b_k' }, + // { title: 'converts to base64url encoded string (- case)', input: 'zs~1t8', expected: 'enN-MXQ4' }, + // { title: 'empty bytes', input: '', expected: '' }, + // ]) + // it(title, async function () { + // const buffer = Buffer.from(input, 'ascii'); + // expect(await this.mock.$encodeURL(buffer)).to.equal(base64toBase64Url(ethers.encodeBase64(buffer))); + // expect(await this.mock.$encodeURL(buffer)).to.equal(expected); + // }); + // }); + + // it('Encode reads beyond the input buffer into dirty memory', async function () { + // const mock = await ethers.deployContract('Base64Dirty'); + // const buffer32 = ethers.id('example'); + // const buffer31 = buffer32.slice(0, -2); + + // expect(await mock.encode(buffer31)).to.equal(ethers.encodeBase64(buffer31)); + // expect(await mock.encode(buffer32)).to.equal(ethers.encodeBase64(buffer32)); + // }); +}); From 3bf4557353034274133150d4e7313fb2354d3478 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 24 Apr 2024 09:07:15 +0200 Subject: [PATCH 26/66] Update contracts/utils/cryptography/P256.sol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- contracts/utils/cryptography/P256.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol index 31760c6d1e6..e5babdb1e67 100644 --- a/contracts/utils/cryptography/P256.sol +++ b/contracts/utils/cryptography/P256.sol @@ -261,8 +261,8 @@ library P256 { } /** - * @dev Precompute a matrice of useful jacobian points associated to a given P. This can be seen as a 4x4 matrix - * that contains combinaison of P and G (generator) up to 3 times each. See table below: + * @dev Precompute a matrice of useful jacobian points associated with a given P. This can be seen as a 4x4 matrix + * that contains combination of P and G (generator) up to 3 times each. See the table below: * * ┌────┬─────────────────────┐ * │ i │ 0 1 2 3 │ From bba7fa38ea9cd5a00a18438fea2b881c44195b74 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 25 Apr 2024 15:46:23 +0200 Subject: [PATCH 27/66] update pseudocode reference --- contracts/utils/cryptography/P256.sol | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol index e5babdb1e67..6a07db524cd 100644 --- a/contracts/utils/cryptography/P256.sol +++ b/contracts/utils/cryptography/P256.sol @@ -7,7 +7,7 @@ import {Math} from "../math/Math.sol"; * @dev Implementation of secp256r1 verification and recovery functions. * * Based on - * - https://github.com/itsobvioustech/A-passkeys-wallet/blob/main/src/Secp256r1.sol + * - https://github.com/itsobvioustech/aa-passkeys-wallet/blob/main/src/Secp256r1.sol * Which is heavily inspired from * - https://github.com/maxrobot/elliptic-solidity/blob/master/contracts/Secp256r1.sol * - https://github.com/tdrerup/elliptic-curve-solidity/blob/master/contracts/curves/EllipticCurve.sol @@ -146,7 +146,7 @@ library P256 { /** * @dev Point addition on the jacobian coordinates - * https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates + * Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-1998-cmo-2 */ function _jAdd( uint256 x1, @@ -187,7 +187,7 @@ library P256 { /** * @dev Point doubling on the jacobian coordinates - * https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates + * Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-1998-cmo-2 */ function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 x2, uint256 y2, uint256 z2) { /// @solidity memory-safe-assembly @@ -197,10 +197,11 @@ library P256 { let zz := mulmod(z, z, p) let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y² let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(A, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z⁴ + let t := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) // t = m²-2*s - // x' = m²-2*s - x2 := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) - // y' = m*(s-x')-8*y⁴ + // x' = t + x2 := t + // y' = m*(s-t)-8*y⁴ y2 := addmod(mulmod(m, addmod(s, sub(p, x2), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p) // z' = 2*y*z z2 := mulmod(2, mulmod(y, z, p), p) @@ -250,9 +251,7 @@ library P256 { } // Read 2 bits of u1, and 2 bits of u2. Combining the two give a lookup index in the table. uint256 pos = ((u1 >> 252) & 0xc) | ((u2 >> 254) & 0x3); - if (pos > 0) { - (x, y, z) = _jAdd(x, y, z, points[pos].x, points[pos].y, points[pos].z); - } + (x, y, z) = _jAdd(x, y, z, points[pos].x, points[pos].y, points[pos].z); u1 <<= 2; u2 <<= 2; } From 2812ed8f110bd7eaa6e7a1485b3b6911cbdfb797 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 25 Apr 2024 17:18:55 +0200 Subject: [PATCH 28/66] Update contracts/utils/cryptography/P256.sol --- contracts/utils/cryptography/P256.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol index 6a07db524cd..088479f5935 100644 --- a/contracts/utils/cryptography/P256.sol +++ b/contracts/utils/cryptography/P256.sol @@ -251,7 +251,9 @@ library P256 { } // Read 2 bits of u1, and 2 bits of u2. Combining the two give a lookup index in the table. uint256 pos = ((u1 >> 252) & 0xc) | ((u2 >> 254) & 0x3); - (x, y, z) = _jAdd(x, y, z, points[pos].x, points[pos].y, points[pos].z); + if (pos > 0) { + (x, y, z) = _jAdd(x, y, z, points[pos].x, points[pos].y, points[pos].z); + } u1 <<= 2; u2 <<= 2; } From e0ef63b775f54df2bbae5caebc1f00d20fb8378d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 26 Apr 2024 10:02:06 +0200 Subject: [PATCH 29/66] refactor neutral element in jAdd --- contracts/utils/cryptography/P256.sol | 40 ++++++++++++++------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol index 088479f5935..9d42a4656a0 100644 --- a/contracts/utils/cryptography/P256.sol +++ b/contracts/utils/cryptography/P256.sol @@ -155,13 +155,7 @@ library P256 { uint256 x2, uint256 y2, uint256 z2 - ) private pure returns (uint256 x3, uint256 y3, uint256 z3) { - if (z1 == 0) { - return (x2, y2, z2); - } - if (z2 == 0) { - return (x1, y1, z1); - } + ) private pure returns (uint256 rx, uint256 ry, uint256 rz) { /// @solidity memory-safe-assembly assembly { let p := P @@ -177,11 +171,11 @@ library P256 { let r := addmod(s2, sub(p, s1), p) // r = s2-s1 // x' = r²-h³-2*u1*h² - x3 := addmod(addmod(mulmod(r, r, p), sub(p, hhh), p), sub(p, mulmod(2, mulmod(u1, hh, p), p)), p) + rx := addmod(addmod(mulmod(r, r, p), sub(p, hhh), p), sub(p, mulmod(2, mulmod(u1, hh, p), p)), p) // y' = r*(u1*h²-x')-s1*h³ - y3 := addmod(mulmod(r, addmod(mulmod(u1, hh, p), sub(p, x3), p), p), sub(p, mulmod(s1, hhh, p)), p) + ry := addmod(mulmod(r, addmod(mulmod(u1, hh, p), sub(p, rx), p), p), sub(p, mulmod(s1, hhh, p)), p) // z' = h*z1*z2 - z3 := mulmod(h, mulmod(z1, z2, p), p) + rz := mulmod(h, mulmod(z1, z2, p), p) } } @@ -189,7 +183,7 @@ library P256 { * @dev Point doubling on the jacobian coordinates * Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-1998-cmo-2 */ - function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 x2, uint256 y2, uint256 z2) { + function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 rx, uint256 ry, uint256 rz) { /// @solidity memory-safe-assembly assembly { let p := P @@ -200,11 +194,11 @@ library P256 { let t := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) // t = m²-2*s // x' = t - x2 := t + rx := t // y' = m*(s-t)-8*y⁴ - y2 := addmod(mulmod(m, addmod(s, sub(p, x2), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p) + ry := addmod(mulmod(m, addmod(s, sub(p, t), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p) // z' = 2*y*z - z2 := mulmod(2, mulmod(y, z, p), p) + rz := mulmod(2, mulmod(y, z, p), p) } } @@ -216,14 +210,18 @@ library P256 { uint256 y, uint256 z, uint256 k - ) private pure returns (uint256 x2, uint256 y2, uint256 z2) { + ) private pure returns (uint256 rx, uint256 ry, uint256 rz) { unchecked { for (uint256 i = 0; i < 256; ++i) { - if (z > 0) { - (x2, y2, z2) = _jDouble(x2, y2, z2); + if (rz > 0) { + (rx, ry, rz) = _jDouble(rx, ry, rz); } if (k >> 255 > 0) { - (x2, y2, z2) = _jAdd(x2, y2, z2, x, y, z); + if (rz == 0) { + (rx, ry, rz) = (x, y, z); + } else { + (rx, ry, rz) = _jAdd(rx, ry, rz, x, y, z); + } } k <<= 1; } @@ -252,7 +250,11 @@ library P256 { // Read 2 bits of u1, and 2 bits of u2. Combining the two give a lookup index in the table. uint256 pos = ((u1 >> 252) & 0xc) | ((u2 >> 254) & 0x3); if (pos > 0) { - (x, y, z) = _jAdd(x, y, z, points[pos].x, points[pos].y, points[pos].z); + if (z == 0) { + (x, y, z) = (points[pos].x, points[pos].y, points[pos].z); + } else { + (x, y, z) = _jAdd(x, y, z, points[pos].x, points[pos].y, points[pos].z); + } } u1 <<= 2; u2 <<= 2; From a13ad48816027de2b0bb5746738646506b8b9f3c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 26 Apr 2024 15:26:08 +0200 Subject: [PATCH 30/66] refactor entrypoint --- .../abstraction/entrypoint/EntryPoint.sol | 303 ++++++++++-------- .../abstraction/entrypoint/StakeManager.sol | 101 ------ contracts/abstraction/utils/ERC4337Utils.sol | 8 - .../utils/SenderCreationHelper.sol | 40 +++ contracts/interfaces/IERC4337.sol | 18 +- test/abstraction/entrypoint.test.js | 52 +-- 6 files changed, 224 insertions(+), 298 deletions(-) delete mode 100644 contracts/abstraction/entrypoint/StakeManager.sol create mode 100644 contracts/abstraction/utils/SenderCreationHelper.sol diff --git a/contracts/abstraction/entrypoint/EntryPoint.sol b/contracts/abstraction/entrypoint/EntryPoint.sol index 070527521ba..48fdf8e9e59 100644 --- a/contracts/abstraction/entrypoint/EntryPoint.sol +++ b/contracts/abstraction/entrypoint/EntryPoint.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {IEntryPoint, IEntryPointNonces, IEntryPointStake, IAccount, IAccountExecute, IAggregator, IPaymaster, PackedUserOperation} from "../../interfaces/IERC4337.sol"; import {IERC165} from "../../interfaces/IERC165.sol"; +import {ERC20} from "../../token/ERC20/ERC20.sol"; import {ERC165} from "../../utils/introspection/ERC165.sol"; import {Address} from "../../utils/Address.sol"; import {Call} from "../../utils/Call.sol"; @@ -11,15 +12,17 @@ import {Memory} from "../../utils/Memory.sol"; import {NoncesWithKey} from "../../utils/NoncesWithKey.sol"; import {ReentrancyGuard} from "../../utils/ReentrancyGuard.sol"; import {ERC4337Utils} from "./../utils/ERC4337Utils.sol"; -import {StakeManager} from "./StakeManager.sol"; +import {SenderCreationHelper} from "./../utils/SenderCreationHelper.sol"; /* * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. * Only one instance required on each chain. */ -contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard, ERC165 { +contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, NoncesWithKey, ReentrancyGuard { using ERC4337Utils for *; + SenderCreationHelper private immutable _senderCreator = new SenderCreationHelper(); + // TODO: move to interface? event UserOperationEvent( bytes32 indexed userOpHash, @@ -43,7 +46,6 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard event SignatureAggregatorChanged(address indexed aggregator); error PostOpReverted(bytes returnData); error SignatureValidationFailed(address aggregator); - error SenderAddressResult(address sender); //compensate for innerHandleOps' emit message and deposit refund. // allow some slack for future gas price changes. @@ -53,7 +55,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard uint256 private constant REVERT_REASON_MAX_LEN = 2048; uint256 private constant PENALTY_PERCENT = 10; - // TODO + // ERC165: TODO function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return super.supportsInterface(interfaceId); // || interfaceId == (type(IEntryPoint).interfaceId ^ type(IStakeManager).interfaceId ^ type(INonceManager).interfaceId) @@ -62,70 +64,58 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard // || interfaceId == type(INonceManager).interfaceId; } - /** - * Execute a user operation. - * @param opIndex - Index into the opInfo array. - * @param userOp - The userOp to execute. - * @param opInfo - The opInfo filled by validatePrepayment for this userOp. - * @return collected - The total amount this userOp paid. - */ - function _executeUserOp( - uint256 opIndex, - PackedUserOperation calldata userOp, - ERC4337Utils.UserOpInfo memory opInfo - ) internal returns (uint256 collected) { - uint256 preGas = gasleft(); + function getSenderAddress(bytes calldata initCode) public returns (address) { + return _senderCreator.getSenderAddress(initCode); + } - // Allocate memory and reset the free memory pointer. Buffer for innerCall is not kept/protected - Memory.FreePtr ptr = Memory.save(); - bytes memory innerCall = abi.encodeCall( - this.innerHandleOp, - ( - userOp.callData.length >= 0x04 && bytes4(userOp.callData[0:4]) == IAccountExecute.executeUserOp.selector - ? abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)) - : userOp.callData, - opInfo - ) - ); - Memory.load(ptr); + /**************************************************************************************************************** + * IEntryPointStake * + ****************************************************************************************************************/ + receive() external payable { + _mint(msg.sender, msg.value); + } - bool success = Call.call(address(this), 0, innerCall); - bytes32 result = abi.decode(Call.getReturnDataFixed(0x20), (bytes32)); + function balanceOf(address account) public view virtual override(ERC20, IEntryPointStake) returns (uint256) { + return super.balanceOf(account); + } - if (success) { - collected = uint256(result); - } else if (result == INNER_OUT_OF_GAS) { - // handleOps was called with gas limit too low. abort entire bundle. - //can only be caused by bundler (leaving not enough gas for inner call) - revert FailedOp(opIndex, "AA95 out of gas"); - } else if (result == INNER_REVERT_LOW_PREFUND) { - // innerCall reverted on prefund too low. treat entire prefund as "gas cost" - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - uint256 actualGasCost = opInfo.prefund; - emit UserOperationPrefundTooLow(opInfo.userOpHash, opInfo.sender, opInfo.nonce); - emit UserOperationEvent( - opInfo.userOpHash, - opInfo.sender, - opInfo.paymaster, - opInfo.nonce, - success, - actualGasCost, - actualGas - ); - collected = actualGasCost; - } else { - emit PostOpRevertReason( - opInfo.userOpHash, - opInfo.sender, - opInfo.nonce, - Call.getReturnData(REVERT_REASON_MAX_LEN) - ); + function depositTo(address account) public payable virtual { + _mint(account, msg.value); + } - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - collected = _postExecution(IPaymaster.PostOpMode.postOpReverted, opInfo, actualGas); - } + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) public virtual { + _burn(msg.sender, withdrawAmount); + Address.sendValue(withdrawAddress, withdrawAmount); + } + + // TODO: implement + function addStake(uint32 /*unstakeDelaySec*/) public payable virtual { + revert("Stake not Implemented yet"); + } + + // TODO: implement and remove pure + function unlockStake() public pure virtual { + revert("Stake not Implemented yet"); + } + + // TODO: implement and remove pure + function withdrawStake(address payable /*withdrawAddress*/) public pure virtual { + revert("Stake not Implemented yet"); + } + + /**************************************************************************************************************** + * IEntryPointNonces * + ****************************************************************************************************************/ + function getNonce( + address owner, + uint192 key + ) public view virtual override(IEntryPointNonces, NoncesWithKey) returns (uint256) { + return super.getNonce(owner, key); } + /**************************************************************************************************************** + * Handle user operations * + ****************************************************************************************************************/ function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) public nonReentrant { ERC4337Utils.UserOpInfo[] memory opInfos = new ERC4337Utils.UserOpInfo[](ops.length); @@ -207,6 +197,70 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard Address.sendValue(beneficiary, collected); } + /** + * Execute a user operation. + * @param opIndex - Index into the opInfo array. + * @param userOp - The userOp to execute. + * @param opInfo - The opInfo filled by validatePrepayment for this userOp. + * @return collected - The total amount this userOp paid. + */ + function _executeUserOp( + uint256 opIndex, + PackedUserOperation calldata userOp, + ERC4337Utils.UserOpInfo memory opInfo + ) internal returns (uint256 collected) { + uint256 preGas = gasleft(); + + // Allocate memory and reset the free memory pointer. Buffer for innerCall is not kept/protected + Memory.FreePtr ptr = Memory.save(); + bytes memory innerCall = abi.encodeCall( + this.innerHandleOp, + ( + userOp.callData.length >= 0x04 && bytes4(userOp.callData[0:4]) == IAccountExecute.executeUserOp.selector + ? abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)) + : userOp.callData, + opInfo + ) + ); + Memory.load(ptr); + + bool success = Call.call(address(this), 0, innerCall); + bytes32 result = abi.decode(Call.getReturnDataFixed(0x20), (bytes32)); + + if (success) { + collected = uint256(result); + } else if (result == INNER_OUT_OF_GAS) { + // handleOps was called with gas limit too low. abort entire bundle. + //can only be caused by bundler (leaving not enough gas for inner call) + revert FailedOp(opIndex, "AA95 out of gas"); + } else if (result == INNER_REVERT_LOW_PREFUND) { + // innerCall reverted on prefund too low. treat entire prefund as "gas cost" + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + uint256 actualGasCost = opInfo.prefund; + emit UserOperationPrefundTooLow(opInfo.userOpHash, opInfo.sender, opInfo.nonce); + emit UserOperationEvent( + opInfo.userOpHash, + opInfo.sender, + opInfo.paymaster, + opInfo.nonce, + success, + actualGasCost, + actualGas + ); + collected = actualGasCost; + } else { + emit PostOpRevertReason( + opInfo.userOpHash, + opInfo.sender, + opInfo.nonce, + Call.getReturnData(REVERT_REASON_MAX_LEN) + ); + + uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; + collected = _postExecution(IPaymaster.PostOpMode.postOpReverted, opInfo, actualGas); + } + } + /** * Inner function to handle a UserOperation. * Must be declared "external" to open a call context, but it can only be called by handleOps. @@ -263,7 +317,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard address sender = opInfo.sender; if (sender.code.length != 0) revert FailedOp(opIndex, "AA10 sender already constructed"); - address deployed = ERC4337Utils.createSender(initCode, opInfo.verificationGasLimit); + address deployed = _senderCreator.createSender{gas: opInfo.verificationGasLimit}(initCode); if (deployed == address(0)) revert FailedOp(opIndex, "AA13 initCode failed or OOG"); else if (deployed != sender) revert FailedOp(opIndex, "AA14 initCode must return sender"); else if (deployed.code.length == 0) revert FailedOp(opIndex, "AA15 initCode must create sender"); @@ -272,8 +326,59 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard } } - function getSenderAddress(bytes calldata initCode) public { - revert SenderAddressResult(ERC4337Utils.createSender(initCode, gasleft())); + /** + * Validate account and paymaster (if defined) and + * also make sure total validation doesn't exceed verificationGasLimit. + * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) + * @param opIndex - The index of this userOp into the "opInfos" array. + * @param userOp - The userOp to validate. + */ + function _validatePrepayment( + uint256 opIndex, + PackedUserOperation calldata userOp, + ERC4337Utils.UserOpInfo memory outOpInfo + ) internal returns (uint256 validationData, uint256 paymasterValidationData) { + uint256 preGas = gasleft(); + unchecked { + outOpInfo.load(userOp); + + // Validate all numeric values in userOp are well below 128 bit, so they can safely be added + // and multiplied without causing overflow. + uint256 maxGasValues = outOpInfo.preVerificationGas | + outOpInfo.verificationGasLimit | + outOpInfo.callGasLimit | + outOpInfo.paymasterVerificationGasLimit | + outOpInfo.paymasterPostOpGasLimit | + outOpInfo.maxFeePerGas | + outOpInfo.maxPriorityFeePerGas; + + if (maxGasValues > type(uint120).max) { + revert FailedOp(opIndex, "AA94 gas values overflow"); + } + + uint256 requiredPreFund = outOpInfo.requiredPrefund(); + validationData = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund); + + if (!_tryUseNonce(outOpInfo.sender, outOpInfo.nonce)) { + revert FailedOp(opIndex, "AA25 invalid account nonce"); + } + + if (preGas - gasleft() > outOpInfo.verificationGasLimit) { + revert FailedOp(opIndex, "AA26 over verificationGasLimit"); + } + + if (outOpInfo.paymaster != address(0)) { + (outOpInfo.context, paymasterValidationData) = _validatePaymasterPrepayment( + opIndex, + userOp, + outOpInfo, + requiredPreFund + ); + } + + outOpInfo.prefund = requiredPreFund; + outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; + } } /** @@ -319,7 +424,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard if (requiredPrefund > balance) { revert FailedOp(opIndex, "AA21 didn't pay prefund"); } else if (requiredPrefund > 0) { - _decrementDeposit(sender, requiredPrefund); + _burn(sender, requiredPrefund); } } } @@ -352,7 +457,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard if (requiredPrefund > balance) { revert FailedOp(opIndex, "AA31 paymaster deposit too low"); } else if (requiredPrefund > 0) { - _decrementDeposit(paymaster, requiredPrefund); + _burn(paymaster, requiredPrefund); } try @@ -403,61 +508,6 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard } } - /** - * Validate account and paymaster (if defined) and - * also make sure total validation doesn't exceed verificationGasLimit. - * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) - * @param opIndex - The index of this userOp into the "opInfos" array. - * @param userOp - The userOp to validate. - */ - function _validatePrepayment( - uint256 opIndex, - PackedUserOperation calldata userOp, - ERC4337Utils.UserOpInfo memory outOpInfo - ) internal returns (uint256 validationData, uint256 paymasterValidationData) { - uint256 preGas = gasleft(); - unchecked { - outOpInfo.load(userOp); - - // Validate all numeric values in userOp are well below 128 bit, so they can safely be added - // and multiplied without causing overflow. - uint256 maxGasValues = outOpInfo.preVerificationGas | - outOpInfo.verificationGasLimit | - outOpInfo.callGasLimit | - outOpInfo.paymasterVerificationGasLimit | - outOpInfo.paymasterPostOpGasLimit | - outOpInfo.maxFeePerGas | - outOpInfo.maxPriorityFeePerGas; - - if (maxGasValues > type(uint120).max) { - revert FailedOp(opIndex, "AA94 gas values overflow"); - } - - uint256 requiredPreFund = outOpInfo.requiredPrefund(); - validationData = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund); - - if (!_tryUseNonce(outOpInfo.sender, outOpInfo.nonce)) { - revert FailedOp(opIndex, "AA25 invalid account nonce"); - } - - if (preGas - gasleft() > outOpInfo.verificationGasLimit) { - revert FailedOp(opIndex, "AA26 over verificationGasLimit"); - } - - if (outOpInfo.paymaster != address(0)) { - (outOpInfo.context, paymasterValidationData) = _validatePaymasterPrepayment( - opIndex, - userOp, - outOpInfo, - requiredPreFund - ); - } - - outOpInfo.prefund = requiredPreFund; - outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; - } - } - /** * Process post-operation, called just after the callData is executed. * If a paymaster is defined and its validation returned a non-empty context, its postOp is called. @@ -510,7 +560,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard Call.revertWithCode(INNER_REVERT_LOW_PREFUND); } } else if (prefund > actualGasCost) { - _incrementDeposit(refundAddress, prefund - actualGasCost); + _mint(refundAddress, prefund - actualGasCost); } emit UserOperationEvent( opInfo.userOpHash, @@ -523,11 +573,4 @@ contract EntryPoint is IEntryPoint, StakeManager, NoncesWithKey, ReentrancyGuard ); } } - - function getNonce( - address owner, - uint192 key - ) public view virtual override(IEntryPointNonces, NoncesWithKey) returns (uint256) { - return super.getNonce(owner, key); - } } diff --git a/contracts/abstraction/entrypoint/StakeManager.sol b/contracts/abstraction/entrypoint/StakeManager.sol deleted file mode 100644 index fa42f90d28d..00000000000 --- a/contracts/abstraction/entrypoint/StakeManager.sol +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.23; - -import {IEntryPointStake} from "../../interfaces/IERC4337.sol"; -import {Address} from "../../utils/Address.sol"; - -abstract contract StakeManager is IEntryPointStake { - event Deposited(address indexed account, uint256 totalDeposit); - - event Withdrawn(address indexed account, address withdrawAddress, uint256 amount); - - event StakeLocked(address indexed account, uint256 totalStaked, uint256 unstakeDelaySec); - - event StakeUnlocked(address indexed account, uint256 withdrawTime); - - event StakeWithdrawn(address indexed account, address withdrawAddress, uint256 amount); - - struct DepositInfo { - uint256 deposit; - bool staked; - uint112 stake; - uint32 unstakeDelaySec; - uint48 withdrawTime; - } - - struct StakeInfo { - uint256 stake; - uint256 unstakeDelaySec; - } - - mapping(address => DepositInfo) private _deposits; - - receive() external payable { - depositTo(msg.sender); - } - - function balanceOf(address account) public view returns (uint256) { - return _deposits[account].deposit; - } - - function depositTo(address account) public payable virtual { - uint256 newDeposit = _incrementDeposit(account, msg.value); - emit Deposited(account, newDeposit); - } - - function addStake(uint32 unstakeDelaySec) public payable { - DepositInfo storage info = _deposits[msg.sender]; - require(unstakeDelaySec > 0, "must specify unstake delay"); - require(unstakeDelaySec >= info.unstakeDelaySec, "cannot decrease unstake time"); - - uint256 stake = info.stake + msg.value; - require(stake > 0, "no stake specified"); - require(stake <= type(uint112).max, "stake overflow"); - - _deposits[msg.sender] = DepositInfo(info.deposit, true, uint112(stake), unstakeDelaySec, 0); - - emit StakeLocked(msg.sender, stake, unstakeDelaySec); - } - - function unlockStake() public { - DepositInfo storage info = _deposits[msg.sender]; - require(info.unstakeDelaySec != 0, "not staked"); - require(info.staked, "already unstaking"); - - uint48 withdrawTime = uint48(block.timestamp) + info.unstakeDelaySec; - info.withdrawTime = withdrawTime; - info.staked = false; - - emit StakeUnlocked(msg.sender, withdrawTime); - } - - function withdrawStake(address payable withdrawAddress) public { - DepositInfo storage info = _deposits[msg.sender]; - uint256 stake = info.stake; - if (stake > 0) { - require(info.withdrawTime > 0, "must call unlockStake() first"); - require(info.withdrawTime <= block.timestamp, "Stake withdrawal is not due"); - - info.unstakeDelaySec = 0; - info.withdrawTime = 0; - info.stake = 0; - - emit StakeWithdrawn(msg.sender, withdrawAddress, stake); - Address.sendValue(withdrawAddress, stake); - } - } - - function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) public { - _deposits[msg.sender].deposit -= withdrawAmount; - emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount); - Address.sendValue(withdrawAddress, withdrawAmount); - } - - function _incrementDeposit(address account, uint256 amount) internal returns (uint256) { - return _deposits[account].deposit += amount; - } - - function _decrementDeposit(address account, uint256 amount) internal returns (uint256) { - return _deposits[account].deposit -= amount; - } -} diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/abstraction/utils/ERC4337Utils.sol index 541dbda5309..89727cff069 100644 --- a/contracts/abstraction/utils/ERC4337Utils.sol +++ b/contracts/abstraction/utils/ERC4337Utils.sol @@ -21,14 +21,6 @@ library ERC4337Utils { */ uint256 internal constant SIG_VALIDATION_FAILED = 1; - // Create sender from initcode - function createSender(bytes calldata initCode, uint256 gas) internal returns (address sender) { - return - Call.call(address(bytes20(initCode[0:20])), 0, initCode[20:], gas) && Call.getReturnDataSize() >= 0x20 - ? abi.decode(Call.getReturnData(0x20), (address)) - : address(0); - } - // Validation data function parseValidationData( uint256 validationData diff --git a/contracts/abstraction/utils/SenderCreationHelper.sol b/contracts/abstraction/utils/SenderCreationHelper.sol new file mode 100644 index 00000000000..1382cbd45d5 --- /dev/null +++ b/contracts/abstraction/utils/SenderCreationHelper.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Call} from "../../utils/Call.sol"; + +/** + * @dev This is used as an helper by EntryPoint. Because creating an account requires calling an arbitrary (user + * controlled) factory with arbitrary (user controlled) data, its a call that can be used to impersonate the + * entrypoint. To avoid any potential issues, we bounce this operation through this helper. This removed the risk of + * a user using a malicious initCode to impersonate the EntryPoint. + */ +contract SenderCreationHelper { + error SenderAddressResult(address sender); + + function createSender(bytes calldata initCode) public returns (address) { + return + Call.call(address(bytes20(initCode[0:20])), 0, initCode[20:]) && Call.getReturnDataSize() >= 0x20 + ? abi.decode(Call.getReturnData(0x20), (address)) + : address(0); + } + + function createSenderAndRevert(bytes calldata initCode) public returns (address) { + revert SenderAddressResult(createSender(initCode)); + } + + function getSenderAddress(bytes calldata initCode) public returns (address sender) { + try this.createSenderAndRevert(initCode) { + return address(0); // Should not happen + } catch (bytes memory reason) { + if (reason.length != 0x24 || bytes4(reason) != SenderAddressResult.selector) { + return address(0); // Should not happen + } else { + assembly { + sender := mload(add(0x24, reason)) + } + } + } + } +} diff --git a/contracts/interfaces/IERC4337.sol b/contracts/interfaces/IERC4337.sol index 9c80c2564bd..0f681b74aba 100644 --- a/contracts/interfaces/IERC4337.sol +++ b/contracts/interfaces/IERC4337.sol @@ -51,23 +51,17 @@ interface IEntryPointNonces { } interface IEntryPointStake { - // add a stake to the calling entity - function addStake(uint32 unstakeDelaySec) external payable; - - // unlock the stake (must wait unstakeDelay before can withdraw) - function unlockStake() external; - - // withdraw the unlocked stake - function withdrawStake(address payable withdrawAddress) external; - - // return the deposit of an account function balanceOf(address account) external view returns (uint256); - // add to the deposit of the given account function depositTo(address account) external payable; - // withdraw from the deposit of the current account function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external; + + function addStake(uint32 unstakeDelaySec) external payable; + + function unlockStake() external; + + function withdrawStake(address payable withdrawAddress) external; } interface IEntryPoint is IEntryPointNonces, IEntryPointStake { diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js index c35fe197ae9..443bbdec49b 100644 --- a/test/abstraction/entrypoint.test.js +++ b/test/abstraction/entrypoint.test.js @@ -14,9 +14,8 @@ async function fixture() { .then(tx => factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) .then(deployCode => ethers.concat([factory.target, deployCode])) .then(initCode => - entrypoint - .getSenderAddress(initCode) - .catch(err => err.message.match(/SenderAddressResult\("(?0x[0-9a-zA-Z]{40})"\)/)?.groups?.addr) + entrypoint.getSenderAddress + .staticCall(initCode) .then(sender => Object.assign(accountFactory.attach(sender), { initCode, salt })), ), ); @@ -50,8 +49,8 @@ describe('EntryPoint', function () { nonce: 0n, initCode: sender.initCode, callData: '0x', - accountGasLimits: ethers.toBeHex((1000000n << 128n) | 1000000n, 32), // concatenation of verificationGas (16 bytes) and callGas (16 bytes) - preVerificationGas: 1000000n, + accountGasLimits: ethers.toBeHex((2000000n << 128n) | 100000n, 32), // concatenation of verificationGas (16 bytes) and callGas (16 bytes) + preVerificationGas: 100000n, gasFees: ethers.toBeHex((100000n << 128n) | 100000n, 32), // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes) paymasterAndData: '0x', // concatenation of paymaster fields (or empty) signature: '0x', @@ -65,51 +64,10 @@ describe('EntryPoint', function () { .to.emit(this.factory, 'return$deploy') .withArgs(sender) .to.emit(this.entrypoint, 'AccountDeployed') - .to.emit(this.entrypoint, 'Deposited') + .to.emit(this.entrypoint, 'Transfer') // Deposit .to.emit(this.entrypoint, 'BeforeExecution') .to.emit(this.entrypoint, 'UserOperationEvent'); expect(await ethers.provider.getCode(sender)).to.not.equal('0x'); }); - - // describe('base64', function () { - // for (const { title, input, expected } of [ - // { title: 'converts to base64 encoded string with double padding', input: 'test', expected: 'dGVzdA==' }, - // { title: 'converts to base64 encoded string with single padding', input: 'test1', expected: 'dGVzdDE=' }, - // { title: 'converts to base64 encoded string without padding', input: 'test12', expected: 'dGVzdDEy' }, - // { title: 'converts to base64 encoded string (/ case)', input: 'où', expected: 'b/k=' }, - // { title: 'converts to base64 encoded string (+ case)', input: 'zs~1t8', expected: 'enN+MXQ4' }, - // { title: 'empty bytes', input: '', expected: '' }, - // ]) - // it(title, async function () { - // const buffer = Buffer.from(input, 'ascii'); - // expect(await this.mock.$encode(buffer)).to.equal(ethers.encodeBase64(buffer)); - // expect(await this.mock.$encode(buffer)).to.equal(expected); - // }); - // }); - - // describe('base64url', function () { - // for (const { title, input, expected } of [ - // { title: 'converts to base64url encoded string with double padding', input: 'test', expected: 'dGVzdA' }, - // { title: 'converts to base64url encoded string with single padding', input: 'test1', expected: 'dGVzdDE' }, - // { title: 'converts to base64url encoded string without padding', input: 'test12', expected: 'dGVzdDEy' }, - // { title: 'converts to base64url encoded string (_ case)', input: 'où', expected: 'b_k' }, - // { title: 'converts to base64url encoded string (- case)', input: 'zs~1t8', expected: 'enN-MXQ4' }, - // { title: 'empty bytes', input: '', expected: '' }, - // ]) - // it(title, async function () { - // const buffer = Buffer.from(input, 'ascii'); - // expect(await this.mock.$encodeURL(buffer)).to.equal(base64toBase64Url(ethers.encodeBase64(buffer))); - // expect(await this.mock.$encodeURL(buffer)).to.equal(expected); - // }); - // }); - - // it('Encode reads beyond the input buffer into dirty memory', async function () { - // const mock = await ethers.deployContract('Base64Dirty'); - // const buffer32 = ethers.id('example'); - // const buffer31 = buffer32.slice(0, -2); - - // expect(await mock.encode(buffer31)).to.equal(ethers.encodeBase64(buffer31)); - // expect(await mock.encode(buffer32)).to.equal(ethers.encodeBase64(buffer32)); - // }); }); From 342256c925f66d57e6b5f2eaebd5b4c1131a86e9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 26 Apr 2024 17:26:42 +0200 Subject: [PATCH 31/66] erc4337 js helper --- contracts/abstraction/utils/ERC4337Utils.sol | 3 + test/abstraction/entrypoint.test.js | 57 +++------- test/helpers/erc4337.js | 112 +++++++++++++++++++ 3 files changed, 133 insertions(+), 39 deletions(-) create mode 100644 test/helpers/erc4337.js diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/abstraction/utils/ERC4337Utils.sol index 89727cff069..c50696e0768 100644 --- a/contracts/abstraction/utils/ERC4337Utils.sol +++ b/contracts/abstraction/utils/ERC4337Utils.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.20; import {IEntryPoint, PackedUserOperation} from "../../interfaces/IERC4337.sol"; import {Math} from "../../utils/math/Math.sol"; import {Call} from "../../utils/Call.sol"; +import {Memory} from "../../utils/Memory.sol"; import {Packing} from "../../utils/Packing.sol"; library ERC4337Utils { @@ -198,6 +199,7 @@ library ERC4337Utils { } function load(UserOpInfo memory self, PackedUserOperation calldata source) internal view { + Memory.FreePtr ptr = Memory.save(); self.sender = source.sender; self.nonce = source.nonce; (self.verificationGasLimit, self.callGasLimit) = source.accountGasLimits.asUint128x2().split(); @@ -218,6 +220,7 @@ library ERC4337Utils { self.prefund = 0; self.preOpGas = 0; self.context = ""; + Memory.load(ptr); } function requiredPrefund(UserOpInfo memory self) internal pure returns (uint256) { diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js index 443bbdec49b..278cfd184ef 100644 --- a/test/abstraction/entrypoint.test.js +++ b/test/abstraction/entrypoint.test.js @@ -1,30 +1,20 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); + +const { ERC4337Context } = require('../helpers/erc4337'); async function fixture() { const accounts = await ethers.getSigners(); - const entrypoint = await ethers.deployContract('EntryPoint'); - const factory = await ethers.deployContract('$Create2'); - - const makeAA = (user, salt = ethers.randomBytes(32)) => - ethers.getContractFactory('SimpleAccount').then(accountFactory => - accountFactory - .getDeployTransaction(entrypoint, user) - .then(tx => factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) - .then(deployCode => ethers.concat([factory.target, deployCode])) - .then(initCode => - entrypoint.getSenderAddress - .staticCall(initCode) - .then(sender => Object.assign(accountFactory.attach(sender), { initCode, salt })), - ), - ); + const context = new ERC4337Context(); + await context.wait(); return { accounts, - entrypoint, - factory, - makeAA, + context, + entrypoint: context.entrypoint, + factory: context.factory, }; } @@ -36,37 +26,26 @@ describe('EntryPoint', function () { it('', async function () { const user = this.accounts[0]; const beneficiary = this.accounts[1]; - const sender = await this.makeAA(user); + const sender = await this.context.newAccount(user); expect(await ethers.provider.getCode(sender)).to.equal('0x'); await user.sendTransaction({ to: sender, value: ethers.parseEther('1') }); - await expect( - this.entrypoint.handleOps( - [ - { - sender, - nonce: 0n, - initCode: sender.initCode, - callData: '0x', - accountGasLimits: ethers.toBeHex((2000000n << 128n) | 100000n, 32), // concatenation of verificationGas (16 bytes) and callGas (16 bytes) - preVerificationGas: 100000n, - gasFees: ethers.toBeHex((100000n << 128n) | 100000n, 32), // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes) - paymasterAndData: '0x', // concatenation of paymaster fields (or empty) - signature: '0x', - }, - ], - beneficiary, - ), - ) + + const operation = sender.createOp({}, true); + await expect(this.entrypoint.handleOps([operation.packed], beneficiary)) .to.emit(sender, 'OwnershipTransferred') .withArgs(ethers.ZeroAddress, user) .to.emit(this.factory, 'return$deploy') .withArgs(sender) .to.emit(this.entrypoint, 'AccountDeployed') - .to.emit(this.entrypoint, 'Transfer') // Deposit + .withArgs(operation.hash, sender, this.context.factory, ethers.ZeroAddress) + .to.emit(this.entrypoint, 'Transfer') + .withArgs(ethers.ZeroAddress, sender, anyValue) .to.emit(this.entrypoint, 'BeforeExecution') - .to.emit(this.entrypoint, 'UserOperationEvent'); + // BeforeExecution has no args + .to.emit(this.entrypoint, 'UserOperationEvent') + .withArgs(operation.hash, sender, ethers.ZeroAddress, operation.nonce, true, anyValue, anyValue); expect(await ethers.provider.getCode(sender)).to.not.equal('0x'); }); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js new file mode 100644 index 00000000000..0375b271982 --- /dev/null +++ b/test/helpers/erc4337.js @@ -0,0 +1,112 @@ +const { ethers } = require('hardhat'); + +function pack(left, right) { + return ethers.toBeHex((left << 128n) | right, 32); +} + +class ERC4337Context { + constructor() { + this.entrypointAsPromise = ethers.deployContract('EntryPoint'); + this.factoryAsPromise = ethers.deployContract('$Create2'); + this.accountAsPromise = ethers.getContractFactory('SimpleAccount'); + this.chainIdAsPromise = ethers.provider.getNetwork().then(({ chainId }) => chainId); + } + + async wait() { + this.entrypoint = await this.entrypointAsPromise; + this.factory = await this.factoryAsPromise; + this.account = await this.accountAsPromise; + this.chainId = await this.chainIdAsPromise; + return this; + } + + async newAccount(user, salt = ethers.randomBytes(32)) { + await this.wait(); + const initCode = await this.account + .getDeployTransaction(this.entrypoint, user) + .then(tx => this.factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) + .then(deployCode => ethers.concat([this.factory.target, deployCode])); + const instance = await this.entrypoint.getSenderAddress + .staticCall(initCode) + .then(address => this.account.attach(address)); + return new AbstractAccount(instance, initCode, this); + } +} + +class AbstractAccount extends ethers.BaseContract { + constructor(instance, initCode, context) { + super(instance.target, instance.interface, instance.runner, instance.deployTx); + this.address = instance.target; + this.initCode = initCode; + this.context = context; + } + + createOp(params = {}, withInit = false) { + return new UserOperation({ + ...params, + sender: this, + initCode: withInit ? this.initCode : '0x', + }); + } +} + +class UserOperation { + constructor(params) { + this.sender = params.sender; + this.nonce = params.nonce ?? 0n; + this.initCode = params.initCode ?? '0x'; + this.callData = params.callData ?? '0x'; + this.verificationGas = params.verificationGas ?? 2_000_000n; + this.callGas = params.callGas ?? 100_000n; + this.preVerificationGas = params.preVerificationGas ?? 100_000n; + this.maxPriorityFee = params.maxPriorityFee ?? 100_000n; + this.maxFeePerGas = params.maxFeePerGas ?? 100_000n; + this.paymasterAndData = params.paymasterAndData ?? '0x'; + this.signature = params.signature ?? '0x'; + } + + get packed() { + return { + sender: this.sender, + nonce: this.nonce, + initCode: this.initCode, + callData: this.callData, + accountGasLimits: pack(this.verificationGas, this.callGas), + preVerificationGas: this.preVerificationGas, + gasFees: pack(this.maxPriorityFee, this.maxFeePerGas), + paymasterAndData: this.paymasterAndData, + signature: this.signature, + }; + } + + get hash() { + const p = this.packed; + const h = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256', 'uint256', 'uint256'], + [ + p.sender.target, + p.nonce, + ethers.keccak256(p.initCode), + ethers.keccak256(p.callData), + p.accountGasLimits, + p.preVerificationGas, + p.gasFees, + ethers.keccak256(p.paymasterAndData), + ], + ), + ); + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['bytes32', 'address', 'uint256'], + [h, this.sender.context.entrypoint.target, this.sender.context.chainId], + ), + ); + } +} + +module.exports = { + ERC4337Context, + AbstractAccount, + UserOperation, +}; From 30c2d1248034fe1a4f412d8fdf868132cc64a98c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 29 Apr 2024 12:19:15 +0200 Subject: [PATCH 32/66] update 4337 helper --- contracts/abstraction/utils/ERC4337Utils.sol | 114 +++++-------------- test/abstraction/entrypoint.test.js | 112 +++++++++++++----- test/helpers/erc4337.js | 31 +++-- 3 files changed, 130 insertions(+), 127 deletions(-) diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/abstraction/utils/ERC4337Utils.sol index c50696e0768..429d7e84945 100644 --- a/contracts/abstraction/utils/ERC4337Utils.sol +++ b/contracts/abstraction/utils/ERC4337Utils.sol @@ -56,93 +56,37 @@ library ERC4337Utils { } } - /* - enum ErrorCodes { - AA10_SENDER_ALREADY_CONSTRUCTED, - AA13_INITCODE_FAILLED, - AA14_INITCODE_WRONG_SENDER, - AA15_INITCODE_NO_DEPLOYMENT, - // Account - AA21_MISSING_FUNDS, - AA22_EXPIRED_OR_NOT_DUE, - AA23_REVERTED, - AA24_SIGNATURE_ERROR, - AA25_INVALID_NONCE, - AA26_OVER_VERIFICATION_GAS_LIMIT, - // Paymaster - AA31_MISSING_FUNDS, - AA32_EXPIRED_OR_NOT_DUE, - AA33_REVERTED, - AA34_SIGNATURE_ERROR, - AA36_OVER_VERIFICATION_GAS_LIMIT, - // other - AA95_OUT_OF_GAS - } - - function toString(ErrorCodes err) internal pure returns (string memory) { - if (err == ErrorCodes.AA10_SENDER_ALREADY_CONSTRUCTED) { - return "AA10 sender already constructed"; - } else if (err == ErrorCodes.AA13_INITCODE_FAILLED) { - return "AA13 initCode failed or OOG"; - } else if (err == ErrorCodes.AA14_INITCODE_WRONG_SENDER) { - return "AA14 initCode must return sender"; - } else if (err == ErrorCodes.AA15_INITCODE_NO_DEPLOYMENT) { - return "AA15 initCode must create sender"; - } else if (err == ErrorCodes.AA21_MISSING_FUNDS) { - return "AA21 didn't pay prefund"; - } else if (err == ErrorCodes.AA22_EXPIRED_OR_NOT_DUE) { - return "AA22 expired or not due"; - } else if (err == ErrorCodes.AA23_REVERTED) { - return "AA23 reverted"; - } else if (err == ErrorCodes.AA24_SIGNATURE_ERROR) { - return "AA24 signature error"; - } else if (err == ErrorCodes.AA25_INVALID_NONCE) { - return "AA25 invalid account nonce"; - } else if (err == ErrorCodes.AA26_OVER_VERIFICATION_GAS_LIMIT) { - return "AA26 over verificationGasLimit"; - } else if (err == ErrorCodes.AA31_MISSING_FUNDS) { - return "AA31 paymaster deposit too low"; - } else if (err == ErrorCodes.AA32_EXPIRED_OR_NOT_DUE) { - return "AA32 paymaster expired or not due"; - } else if (err == ErrorCodes.AA33_REVERTED) { - return "AA33 reverted"; - } else if (err == ErrorCodes.AA34_SIGNATURE_ERROR) { - return "AA34 signature error"; - } else if (err == ErrorCodes.AA36_OVER_VERIFICATION_GAS_LIMIT) { - return "AA36 over paymasterVerificationGasLimit"; - } else if (err == ErrorCodes.AA95_OUT_OF_GAS) { - return "AA95 out of gas"; - } else { - return "Unknown error code"; - } - } - - function failedOp(uint256 index, ErrorCodes err) internal pure { - revert IEntryPoint.FailedOp(index, toString(err)); - } - - function failedOp(uint256 index, ErrorCodes err, bytes memory extraData) internal pure { - revert IEntryPoint.FailedOpWithRevert(index, toString(err), extraData); - } - */ - // Packed user operation - function hash(PackedUserOperation calldata self) internal pure returns (bytes32) { - return keccak256(encode(self)); + function hash(PackedUserOperation calldata self) internal view returns (bytes32) { + return hash(self, address(this), block.chainid); } - function encode(PackedUserOperation calldata self) internal pure returns (bytes memory ret) { - return + function hash( + PackedUserOperation calldata self, + address entrypoint, + uint256 chainid + ) internal pure returns (bytes32) { + Memory.FreePtr ptr = Memory.save(); + bytes32 result = keccak256( abi.encode( - self.sender, - self.nonce, - keccak256(self.initCode), - keccak256(self.callData), - self.accountGasLimits, - self.preVerificationGas, - self.gasFees, - keccak256(self.paymasterAndData) - ); + keccak256( + abi.encode( + self.sender, + self.nonce, + keccak256(self.initCode), + keccak256(self.callData), + self.accountGasLimits, + self.preVerificationGas, + self.gasFees, + keccak256(self.paymasterAndData) + ) + ), + entrypoint, + chainid + ) + ); + Memory.load(ptr); + return result; } function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { @@ -199,7 +143,6 @@ library ERC4337Utils { } function load(UserOpInfo memory self, PackedUserOperation calldata source) internal view { - Memory.FreePtr ptr = Memory.save(); self.sender = source.sender; self.nonce = source.nonce; (self.verificationGasLimit, self.callGasLimit) = source.accountGasLimits.asUint128x2().split(); @@ -216,11 +159,10 @@ library ERC4337Utils { self.paymasterVerificationGasLimit = 0; self.paymasterPostOpGasLimit = 0; } - self.userOpHash = keccak256(abi.encode(hash(source), address(this), block.chainid)); + self.userOpHash = hash(source); self.prefund = 0; self.preOpGas = 0; self.context = ""; - Memory.load(ptr); } function requiredPrefund(UserOpInfo memory self) internal pure returns (uint256) { diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js index 278cfd184ef..1b796d2e9b6 100644 --- a/test/abstraction/entrypoint.test.js +++ b/test/abstraction/entrypoint.test.js @@ -3,50 +3,100 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); -const { ERC4337Context } = require('../helpers/erc4337'); +const { ERC4337Helper } = require('../helpers/erc4337'); async function fixture() { const accounts = await ethers.getSigners(); - const context = new ERC4337Context(); - await context.wait(); + const helper = new ERC4337Helper(); + await helper.wait(); return { accounts, - context, - entrypoint: context.entrypoint, - factory: context.factory, + helper, + entrypoint: helper.entrypoint, + factory: helper.factory, }; } describe('EntryPoint', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); + + this.user = this.accounts.shift(); + this.beneficiary = this.accounts.shift(); + this.sender = await this.helper.newAccount(this.user); }); - it('', async function () { - const user = this.accounts[0]; - const beneficiary = this.accounts[1]; - const sender = await this.context.newAccount(user); - - expect(await ethers.provider.getCode(sender)).to.equal('0x'); - - await user.sendTransaction({ to: sender, value: ethers.parseEther('1') }); - - const operation = sender.createOp({}, true); - await expect(this.entrypoint.handleOps([operation.packed], beneficiary)) - .to.emit(sender, 'OwnershipTransferred') - .withArgs(ethers.ZeroAddress, user) - .to.emit(this.factory, 'return$deploy') - .withArgs(sender) - .to.emit(this.entrypoint, 'AccountDeployed') - .withArgs(operation.hash, sender, this.context.factory, ethers.ZeroAddress) - .to.emit(this.entrypoint, 'Transfer') - .withArgs(ethers.ZeroAddress, sender, anyValue) - .to.emit(this.entrypoint, 'BeforeExecution') - // BeforeExecution has no args - .to.emit(this.entrypoint, 'UserOperationEvent') - .withArgs(operation.hash, sender, ethers.ZeroAddress, operation.nonce, true, anyValue, anyValue); - - expect(await ethers.provider.getCode(sender)).to.not.equal('0x'); + describe('deploy wallet contract', function () { + it('success: counterfactual funding', async function () { + await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + const operation = await this.sender.createOp({}, true); + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.emit(this.sender, 'OwnershipTransferred') + .withArgs(ethers.ZeroAddress, this.user) + .to.emit(this.factory, 'return$deploy') + .withArgs(this.sender) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.entrypoint, 'Transfer') + .withArgs(ethers.ZeroAddress, this.sender, anyValue) + .to.emit(this.entrypoint, 'BeforeExecution') + // BeforeExecution has no args + .to.emit(this.entrypoint, 'UserOperationEvent') + .withArgs(operation.hash, this.sender, ethers.ZeroAddress, operation.nonce, true, anyValue, anyValue); + + expect(await ethers.provider.getCode(this.sender)).to.not.equal('0x'); + }); + + it.skip('[TODO] success: paymaster funding', async function () { + // TODO: deploy paymaster + // TODO: fund paymaster's account in entrypoint + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + // const operation = await this.sender.createOp({ paymaster: this.user }, true); + // await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + // .to.emit(this.sender, 'OwnershipTransferred') + // .withArgs(ethers.ZeroAddress, this.user) + // .to.emit(this.factory, 'return$deploy') + // .withArgs(this.sender) + // .to.emit(this.entrypoint, 'AccountDeployed') + // .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + // .to.emit(this.entrypoint, 'Transfer') + // .withArgs(ethers.ZeroAddress, this.sender, anyValue) + // .to.emit(this.entrypoint, 'BeforeExecution') + // // BeforeExecution has no args + // .to.emit(this.entrypoint, 'UserOperationEvent') + // .withArgs(operation.hash, this.sender, ethers.ZeroAddress, operation.nonce, true, anyValue, anyValue); + + expect(await ethers.provider.getCode(this.sender)).to.not.equal('0x'); + }); + + it("error: AA21 didn't pay prefund", async function () { + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + const operation = await this.sender.createOp({}, true); + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, "AA21 didn't pay prefund"); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + }); + + it('error: AA25 invalid account nonce', async function () { + await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + const operation = await this.sender.createOp({ nonce: 1n }, true); + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, 'AA25 invalid account nonce'); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + }); }); }); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 0375b271982..37e1c2c9ce5 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -1,10 +1,10 @@ const { ethers } = require('hardhat'); function pack(left, right) { - return ethers.toBeHex((left << 128n) | right, 32); + return ethers.solidityPacked(['uint128', 'uint128'], [left, right]); } -class ERC4337Context { +class ERC4337Helper { constructor() { this.entrypointAsPromise = ethers.deployContract('EntryPoint'); this.factoryAsPromise = ethers.deployContract('$Create2'); @@ -41,19 +41,30 @@ class AbstractAccount extends ethers.BaseContract { this.context = context; } - createOp(params = {}, withInit = false) { - return new UserOperation({ - ...params, - sender: this, - initCode: withInit ? this.initCode : '0x', - }); + async createOp(args = {}, withInit = false) { + const params = Object.assign({ sender: this, initCode: withInit ? this.initCode : '0x' }, args); + // fetch nonce + if (!params.nonce) { + params.nonce = await this.context.entrypointAsPromise.then(entrypoint => entrypoint.getNonce(this, 0)); + } + // prepare paymaster and data + if (ethers.isAddressable(params.paymaster)) { + params.paymaster = await ethers.resolveAddress(params.paymaster); + params.paymasterVerificationGasLimit ??= 100_000n; + params.paymasterPostOpGasLimit ??= 100_000n; + params.paymasterAndData = ethers.solidityPacked( + ['address', 'uint128', 'uint128'], + [params.paymaster, params.paymasterVerificationGasLimit, params.paymasterPostOpGasLimit], + ); + } + return new UserOperation(params); } } class UserOperation { constructor(params) { this.sender = params.sender; - this.nonce = params.nonce ?? 0n; + this.nonce = params.nonce; this.initCode = params.initCode ?? '0x'; this.callData = params.callData ?? '0x'; this.verificationGas = params.verificationGas ?? 2_000_000n; @@ -106,7 +117,7 @@ class UserOperation { } module.exports = { - ERC4337Context, + ERC4337Helper, AbstractAccount, UserOperation, }; From 2f0718890588aee41ba3123e12afbcf8f59b1ea6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 30 Apr 2024 15:30:23 +0200 Subject: [PATCH 33/66] Working on abstract account primitives --- contracts/abstraction/account/Account.sol | 38 +++++------ .../abstraction/account/AccountECDSA.sol | 38 +++++++++++ contracts/abstraction/mocks/SimpleAccount.sol | 8 +-- contracts/abstraction/utils/ERC4337Utils.sol | 8 +-- contracts/mocks/CallReceiverMock.sol | 5 ++ test/abstraction/entrypoint.test.js | 65 +++++++++++++++++-- test/helpers/erc4337.js | 26 ++++++-- 7 files changed, 153 insertions(+), 35 deletions(-) create mode 100644 contracts/abstraction/account/AccountECDSA.sol diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol index 937c8216b0a..7cec9141124 100644 --- a/contracts/abstraction/account/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -3,13 +3,10 @@ pragma solidity ^0.8.20; import {PackedUserOperation, IAccount, IEntryPoint} from "../../interfaces/IERC4337.sol"; -import {MessageHashUtils} from "../../utils/cryptography/MessageHashUtils.sol"; import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol"; -import {SafeCast} from "../../utils/math/SafeCast.sol"; +import {ERC4337Utils} from "./../utils/ERC4337Utils.sol"; abstract contract Account is IAccount { - using SafeCast for bool; - error AccountEntryPointRestricted(); error AccountUserRestricted(); error AccountInvalidBatchLength(); @@ -22,23 +19,32 @@ abstract contract Account is IAccount { _; } - modifier onlyAuthorizedOrSelf() { - if (msg.sender != address(this) && !_isAuthorized(msg.sender)) { + modifier onlyAuthorizedOrEntryPoint() { + if (msg.sender != address(entryPoint()) && !_isAuthorized(msg.sender)) { revert AccountUserRestricted(); } _; } - // Virtual pure (not implemented) hooks + // Hooks function entryPoint() public view virtual returns (IEntryPoint); - function _isAuthorized(address) internal view virtual returns (bool); + function _isAuthorized(address) internal virtual returns (bool); + + function _getSignerAndWindow( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual returns (address, uint48, uint48); // Public interface function getNonce() public view virtual returns (uint256) { return entryPoint().getNonce(address(this), 0); } + function getNonce(uint192 key) public view virtual returns (uint256) { + return entryPoint().getNonce(address(this), key); + } + function validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash, @@ -49,25 +55,19 @@ abstract contract Account is IAccount { _payPrefund(missingAccountFunds); } + // Internal mechanisms function _validateSignature( PackedUserOperation calldata userOp, bytes32 userOpHash ) internal virtual returns (uint256 validationData) { - return - (_isAuthorized(userOp.sender) && - SignatureChecker.isValidSignatureNow( - userOp.sender, - MessageHashUtils.toEthSignedMessageHash(userOpHash), - userOp.signature - )).toUint(); + (address signer, uint48 validAfter, uint48 validUntil) = _getSignerAndWindow(userOp, userOpHash); + return ERC4337Utils.packValidationData(signer != address(0) && _isAuthorized(signer), validAfter, validUntil); } - function _validateNonce(uint256 nonce) internal view virtual { - // TODO ? - } + function _validateNonce(uint256 nonce) internal view virtual {} function _payPrefund(uint256 missingAccountFunds) internal virtual { - if (missingAccountFunds != 0) { + if (missingAccountFunds > 0) { (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}(""); success; //ignore failure (its EntryPoint's job to verify, not account.) diff --git a/contracts/abstraction/account/AccountECDSA.sol b/contracts/abstraction/account/AccountECDSA.sol new file mode 100644 index 00000000000..b9a746c3320 --- /dev/null +++ b/contracts/abstraction/account/AccountECDSA.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../interfaces/IERC4337.sol"; +import {MessageHashUtils} from "../../utils/cryptography/MessageHashUtils.sol"; +import {ECDSA} from "../../utils/cryptography/ECDSA.sol"; +import {Account} from "./Account.sol"; + +abstract contract AccountECDSA is Account { + function _getSignerAndWindow( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (address, uint48, uint48) { + bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); + // This implementation support both "normal" and short signature formats: + // - If signature length is 65, process as "normal" signature (R,S,V) + // - If signature length is 64, process as https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signature] (R,SV) ECDSA signature + // This is safe because the UserOperations include a nonce (which is managed by the entrypoint) for replay protection. + bytes calldata signature = userOp.signature; + if (signature.length == 65) { + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(msgHash, signature); + return (err == ECDSA.RecoverError.NoError ? recovered : address(0), 0, 0); + } else if (signature.length == 64) { + bytes32 r; + bytes32 vs; + /// @solidity memory-safe-assembly + assembly { + r := calldataload(add(signature.offset, 0x20)) + vs := calldataload(add(signature.offset, 0x40)) + } + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(msgHash, r, vs); + return (err == ECDSA.RecoverError.NoError ? recovered : address(0), 0, 0); + } else { + return (address(0), 0, 0); + } + } +} diff --git a/contracts/abstraction/mocks/SimpleAccount.sol b/contracts/abstraction/mocks/SimpleAccount.sol index e2e5c1b7799..e2121769dec 100644 --- a/contracts/abstraction/mocks/SimpleAccount.sol +++ b/contracts/abstraction/mocks/SimpleAccount.sol @@ -7,9 +7,9 @@ import {Ownable} from "../../access/Ownable.sol"; import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; import {Address} from "../../utils/Address.sol"; -import {Account} from "../account/Account.sol"; +import {AccountECDSA} from "../account/AccountECDSA.sol"; -contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder { +contract SimpleAccount is AccountECDSA, Ownable, ERC721Holder, ERC1155Holder { IEntryPoint private immutable _entryPoint; constructor(IEntryPoint entryPoint_, address initialOwner) Ownable(initialOwner) { @@ -26,7 +26,7 @@ contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder { return user == owner(); } - function execute(address target, uint256 value, bytes calldata data) external onlyAuthorizedOrSelf { + function execute(address target, uint256 value, bytes calldata data) public virtual onlyAuthorizedOrEntryPoint { _call(target, value, data); } @@ -34,7 +34,7 @@ contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder { address[] calldata targets, uint256[] calldata values, bytes[] calldata calldatas - ) external onlyAuthorizedOrSelf { + ) public virtual onlyAuthorizedOrEntryPoint { if (targets.length != calldatas.length || (values.length != 0 && values.length != targets.length)) { revert AccountInvalidBatchLength(); } diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/abstraction/utils/ERC4337Utils.sol index 429d7e84945..aad3bf28d98 100644 --- a/contracts/abstraction/utils/ERC4337Utils.sol +++ b/contracts/abstraction/utils/ERC4337Utils.sol @@ -40,9 +40,9 @@ library ERC4337Utils { return uint160(aggregator) | (uint256(validUntil) << 160) | (uint256(validAfter) << 208); } - function packValidationData(bool sigFailed, uint48 validUntil, uint48 validAfter) internal pure returns (uint256) { + function packValidationData(bool sigSuccess, uint48 validUntil, uint48 validAfter) internal pure returns (uint256) { return - (sigFailed ? SIG_VALIDATION_FAILED : SIG_VALIDATION_SUCCESS) | + Math.ternary(sigSuccess, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED) | (uint256(validUntil) << 160) | (uint256(validAfter) << 208); } @@ -109,7 +109,7 @@ library ERC4337Utils { unchecked { // Following values are "per gas" (uint256 maxPriorityFee, uint256 maxFee) = self.gasFees.asUint128x2().split(); - return maxFee == maxPriorityFee ? maxFee : Math.min(maxFee, maxPriorityFee + block.basefee); + return Math.ternary(maxFee == maxPriorityFee, maxFee, Math.min(maxFee, maxPriorityFee + block.basefee)); } } @@ -178,7 +178,7 @@ library ERC4337Utils { unchecked { uint256 maxFee = self.maxFeePerGas; uint256 maxPriorityFee = self.maxPriorityFeePerGas; - return maxFee == maxPriorityFee ? maxFee : Math.min(maxFee, maxPriorityFee + block.basefee); + return Math.ternary(maxFee == maxPriorityFee, maxFee, Math.min(maxFee, maxPriorityFee + block.basefee)); } } } diff --git a/contracts/mocks/CallReceiverMock.sol b/contracts/mocks/CallReceiverMock.sol index e371c7db800..a981241344c 100644 --- a/contracts/mocks/CallReceiverMock.sol +++ b/contracts/mocks/CallReceiverMock.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; contract CallReceiverMock { event MockFunctionCalled(); + event MockFunctionCalledExtra(address caller, uint256 value); event MockFunctionCalledWithArgs(uint256 a, uint256 b); uint256[] private _array; @@ -14,6 +15,10 @@ contract CallReceiverMock { return "0x1234"; } + function mockFunctionExtra() public payable { + emit MockFunctionCalledExtra(msg.sender, msg.value); + } + function mockFunctionEmptyReturn() public payable { emit MockFunctionCalled(); } diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js index 1b796d2e9b6..0ade24261b3 100644 --- a/test/abstraction/entrypoint.test.js +++ b/test/abstraction/entrypoint.test.js @@ -7,11 +7,13 @@ const { ERC4337Helper } = require('../helpers/erc4337'); async function fixture() { const accounts = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); const helper = new ERC4337Helper(); await helper.wait(); return { accounts, + target, helper, entrypoint: helper.entrypoint, factory: helper.factory, @@ -33,7 +35,11 @@ describe('EntryPoint', function () { expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); - const operation = await this.sender.createOp({}, true); + const operation = await this.sender + .createOp() + .then(op => op.addInitCode()) + .then(op => op.sign()); + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) .to.emit(this.sender, 'OwnershipTransferred') .withArgs(ethers.ZeroAddress, this.user) @@ -57,7 +63,10 @@ describe('EntryPoint', function () { expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); - // const operation = await this.sender.createOp({ paymaster: this.user }, true); + // const operation = await this.sender.createOp({ paymaster: this.user }) + // .then(op => op.addInitCode()) + // .then(op => op.sign()); + // // await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) // .to.emit(this.sender, 'OwnershipTransferred') // .withArgs(ethers.ZeroAddress, this.user) @@ -75,10 +84,29 @@ describe('EntryPoint', function () { expect(await ethers.provider.getCode(this.sender)).to.not.equal('0x'); }); + it('error: AA10 sender already constructed', async function () { + await this.sender.deploy(); + + expect(await ethers.provider.getCode(this.sender)).to.not.equal('0x'); + + const operation = await this.sender + .createOp() + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, 'AA10 sender already constructed'); + }); + it("error: AA21 didn't pay prefund", async function () { expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); - const operation = await this.sender.createOp({}, true); + const operation = await this.sender + .createOp() + .then(op => op.addInitCode()) + .then(op => op.sign()); + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') .withArgs(0, "AA21 didn't pay prefund"); @@ -91,7 +119,11 @@ describe('EntryPoint', function () { expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); - const operation = await this.sender.createOp({ nonce: 1n }, true); + const operation = await this.sender + .createOp({ nonce: 1n }) + .then(op => op.addInitCode()) + .then(op => op.sign()); + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') .withArgs(0, 'AA25 invalid account nonce'); @@ -99,4 +131,29 @@ describe('EntryPoint', function () { expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); }); }); + + describe('execute operation', function () { + describe('sender not reployed yet', function () { + it('success: deploy and call', async function () { + await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 42, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + }); + }); }); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 37e1c2c9ce5..3c8fa105701 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -28,7 +28,7 @@ class ERC4337Helper { .then(deployCode => ethers.concat([this.factory.target, deployCode])); const instance = await this.entrypoint.getSenderAddress .staticCall(initCode) - .then(address => this.account.attach(address)); + .then(address => this.account.attach(address).connect(user)); return new AbstractAccount(instance, initCode, this); } } @@ -41,8 +41,16 @@ class AbstractAccount extends ethers.BaseContract { this.context = context; } - async createOp(args = {}, withInit = false) { - const params = Object.assign({ sender: this, initCode: withInit ? this.initCode : '0x' }, args); + async deploy() { + this.deployTx = await this.runner.sendTransaction({ + to: '0x' + this.initCode.replace(/0x/, '').slice(0, 40), + data: '0x' + this.initCode.replace(/0x/, '').slice(40), + }); + return this; + } + + async createOp(args = {}) { + const params = Object.assign({ sender: this }, args); // fetch nonce if (!params.nonce) { params.nonce = await this.context.entrypointAsPromise.then(entrypoint => entrypoint.getNonce(this, 0)); @@ -67,7 +75,7 @@ class UserOperation { this.nonce = params.nonce; this.initCode = params.initCode ?? '0x'; this.callData = params.callData ?? '0x'; - this.verificationGas = params.verificationGas ?? 2_000_000n; + this.verificationGas = params.verificationGas ?? 10_000_000n; this.callGas = params.callGas ?? 100_000n; this.preVerificationGas = params.preVerificationGas ?? 100_000n; this.maxPriorityFee = params.maxPriorityFee ?? 100_000n; @@ -114,6 +122,16 @@ class UserOperation { ), ); } + + addInitCode() { + this.initCode = this.sender.initCode; + return this; + } + + async sign(signer = this.sender.runner) { + this.signature = await signer.signMessage(ethers.toBeArray(this.hash)); + return this; + } } module.exports = { From a129a45b7ccd0cf1eac47e88b2b9fad477d80b09 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 30 Apr 2024 15:36:00 +0200 Subject: [PATCH 34/66] improve tests --- test/abstraction/entrypoint.test.js | 32 +++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js index 0ade24261b3..25fbec83263 100644 --- a/test/abstraction/entrypoint.test.js +++ b/test/abstraction/entrypoint.test.js @@ -133,15 +133,17 @@ describe('EntryPoint', function () { }); describe('execute operation', function () { - describe('sender not reployed yet', function () { - it('success: deploy and call', async function () { - await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + beforeEach('fund account', async function () { + await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + }); + describe('account not deployed yet', function () { + it('success: deploy and call', async function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ this.target.target, - 42, + 17, this.target.interface.encodeFunctionData('mockFunctionExtra'), ]), }) @@ -151,6 +153,28 @@ describe('EntryPoint', function () { await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) .to.emit(this.entrypoint, 'AccountDeployed') .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 17); + }); + }); + + describe('account already deployed', function () { + beforeEach(async function () { + await this.sender.deploy(); + }); + + it('success: deploy and call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 42, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) .to.emit(this.target, 'MockFunctionCalledExtra') .withArgs(this.sender, 42); }); From 57b4fb2b9c3fba19e6cf454b2c5d2d9574c0d057 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 30 Apr 2024 17:32:27 +0200 Subject: [PATCH 35/66] use getBytes --- test/helpers/erc4337.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 3c8fa105701..78f47f93886 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -129,7 +129,7 @@ class UserOperation { } async sign(signer = this.sender.runner) { - this.signature = await signer.signMessage(ethers.toBeArray(this.hash)); + this.signature = await signer.signMessage(ethers.getBytes(this.hash)); return this; } } From c641f0aac1282f3ca4d5ff4ab55d7e65b4adf3ea Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 30 Apr 2024 21:26:04 +0200 Subject: [PATCH 36/66] update Account --- contracts/abstraction/account/Account.sol | 75 ++++++++++++++++++- .../abstraction/account/AccountECDSA.sol | 18 +++-- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol index 7cec9141124..f35d699f466 100644 --- a/contracts/abstraction/account/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -11,7 +11,10 @@ abstract contract Account is IAccount { error AccountUserRestricted(); error AccountInvalidBatchLength(); - // Modifiers + /**************************************************************************************************************** + * Modifiers * + ****************************************************************************************************************/ + modifier onlyEntryPoint() { if (msg.sender != address(entryPoint())) { revert AccountEntryPointRestricted(); @@ -26,25 +29,51 @@ abstract contract Account is IAccount { _; } - // Hooks + /**************************************************************************************************************** + * Hooks * + ****************************************************************************************************************/ + + /** + * @dev Return the entryPoint used by this account. + * Subclass should return the current entryPoint used by this account. + */ function entryPoint() public view virtual returns (IEntryPoint); + /** + * @dev Return weither an address (identity) is authorized to operate on this account. + * Subclass must implement this using their own access control mechanism. + */ function _isAuthorized(address) internal virtual returns (bool); + /** + * @dev Return the recovered signer, and signature validity window. + * Subclass must implement this following their choice of cryptography. + * If a signature is ill-formed, address(0) should be returned. + */ function _getSignerAndWindow( PackedUserOperation calldata userOp, bytes32 userOpHash ) internal virtual returns (address, uint48, uint48); - // Public interface + /**************************************************************************************************************** + * Public interface * + ****************************************************************************************************************/ + + /** + * @dev Return the account nonce for the canonical sequence. + */ function getNonce() public view virtual returns (uint256) { return entryPoint().getNonce(address(this), 0); } + /** + * @dev Return the account nonce for a given sequence (key). + */ function getNonce(uint192 key) public view virtual returns (uint256) { return entryPoint().getNonce(address(this), key); } + /// @inheritdoc IAccount function validateUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash, @@ -55,7 +84,24 @@ abstract contract Account is IAccount { _payPrefund(missingAccountFunds); } - // Internal mechanisms + /**************************************************************************************************************** + * Internal mechanisms * + ****************************************************************************************************************/ + + /** + * @dev Validate the signature is valid for this message. + * @param userOp - Validate the userOp.signature field. + * @param userOpHash - Convenient field: the hash of the request, to check the signature against. + * (also hashes the entrypoint and chain id) + * @return validationData - Signature and time-range of this operation. + * <20-byte> aggregatorOrSigFail - 0 for valid signature, 1 to mark signature failure, + * otherwise, an address of an aggregator contract. + * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" + * <6-byte> validAfter - first timestamp this operation is valid + * If the account doesn't use time-range, it is enough to return + * SIG_VALIDATION_FAILED value (1) for signature failure. + * Note that the validation code cannot use block.timestamp (or block.number) directly. + */ function _validateSignature( PackedUserOperation calldata userOp, bytes32 userOpHash @@ -64,8 +110,29 @@ abstract contract Account is IAccount { return ERC4337Utils.packValidationData(signer != address(0) && _isAuthorized(signer), validAfter, validUntil); } + /** + * @dev Validate the nonce of the UserOperation. + * This method may validate the nonce requirement of this account. + * e.g. + * To limit the nonce to use sequenced UserOps only (no "out of order" UserOps): + * `require(nonce < type(uint64).max)` + * + * The actual nonce uniqueness is managed by the EntryPoint, and thus no other + * action is needed by the account itself. + * + * @param nonce to validate + */ function _validateNonce(uint256 nonce) internal view virtual {} + /** + * @dev Sends to the entrypoint (msg.sender) the missing funds for this transaction. + * SubClass MAY override this method for better funds management + * (e.g. send to the entryPoint more than the minimum required, so that in future transactions + * it will not be required to send again). + * @param missingAccountFunds - The minimum value this method should send the entrypoint. + * This value MAY be zero, in case there is enough deposit, + * or the userOp has a paymaster. + */ function _payPrefund(uint256 missingAccountFunds) internal virtual { if (missingAccountFunds > 0) { (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}(""); diff --git a/contracts/abstraction/account/AccountECDSA.sol b/contracts/abstraction/account/AccountECDSA.sol index b9a746c3320..54e819dea58 100644 --- a/contracts/abstraction/account/AccountECDSA.sol +++ b/contracts/abstraction/account/AccountECDSA.sol @@ -13,14 +13,23 @@ abstract contract AccountECDSA is Account { bytes32 userOpHash ) internal virtual override returns (address, uint48, uint48) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); + // This implementation support both "normal" and short signature formats: // - If signature length is 65, process as "normal" signature (R,S,V) // - If signature length is 64, process as https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signature] (R,SV) ECDSA signature // This is safe because the UserOperations include a nonce (which is managed by the entrypoint) for replay protection. bytes calldata signature = userOp.signature; if (signature.length == 65) { - (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(msgHash, signature); - return (err == ECDSA.RecoverError.NoError ? recovered : address(0), 0, 0); + bytes32 r; + bytes32 s; + uint8 v; + /// @solidity memory-safe-assembly + assembly { + r := calldataload(add(signature.offset, 0x20)) + s := calldataload(add(signature.offset, 0x40)) + v := byte(0, calldataload(add(signature.offset, 0x60))) + } + return (ECDSA.recover(msgHash, signature), 0, 0); } else if (signature.length == 64) { bytes32 r; bytes32 vs; @@ -29,10 +38,9 @@ abstract contract AccountECDSA is Account { r := calldataload(add(signature.offset, 0x20)) vs := calldataload(add(signature.offset, 0x40)) } - (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(msgHash, r, vs); - return (err == ECDSA.RecoverError.NoError ? recovered : address(0), 0, 0); + return (ECDSA.recover(msgHash, r, vs), 0, 0); } else { - return (address(0), 0, 0); + revert ECDSA.ECDSAInvalidSignatureLength(signature.length); } } } From 618b5639fe5bd00cf1fa12e807e11c80b4641d95 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 30 Apr 2024 22:16:37 +0200 Subject: [PATCH 37/66] Add ECDSA and P256 variants of SimpleAccount --- contracts/abstraction/account/Account.sol | 4 +- .../abstraction/account/AccountECDSA.sol | 14 +-- contracts/abstraction/account/AccountP256.sol | 39 ++++++++ contracts/abstraction/mocks/SimpleAccount.sol | 12 ++- test/abstraction/accountECDSA.test.js | 97 +++++++++++++++++++ test/abstraction/accountP256.test.js | 80 +++++++++++++++ test/abstraction/entrypoint.test.js | 2 +- test/helpers/erc4337.js | 8 +- test/helpers/p256.js | 32 ++++++ 9 files changed, 273 insertions(+), 15 deletions(-) create mode 100644 contracts/abstraction/account/AccountP256.sol create mode 100644 test/abstraction/accountECDSA.test.js create mode 100644 test/abstraction/accountP256.test.js create mode 100644 test/helpers/p256.js diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol index f35d699f466..a10d0a1b09a 100644 --- a/contracts/abstraction/account/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -50,7 +50,7 @@ abstract contract Account is IAccount { * Subclass must implement this following their choice of cryptography. * If a signature is ill-formed, address(0) should be returned. */ - function _getSignerAndWindow( + function _processSignature( PackedUserOperation calldata userOp, bytes32 userOpHash ) internal virtual returns (address, uint48, uint48); @@ -106,7 +106,7 @@ abstract contract Account is IAccount { PackedUserOperation calldata userOp, bytes32 userOpHash ) internal virtual returns (uint256 validationData) { - (address signer, uint48 validAfter, uint48 validUntil) = _getSignerAndWindow(userOp, userOpHash); + (address signer, uint48 validAfter, uint48 validUntil) = _processSignature(userOp, userOpHash); return ERC4337Utils.packValidationData(signer != address(0) && _isAuthorized(signer), validAfter, validUntil); } diff --git a/contracts/abstraction/account/AccountECDSA.sol b/contracts/abstraction/account/AccountECDSA.sol index 54e819dea58..850a4045c2a 100644 --- a/contracts/abstraction/account/AccountECDSA.sol +++ b/contracts/abstraction/account/AccountECDSA.sol @@ -8,7 +8,7 @@ import {ECDSA} from "../../utils/cryptography/ECDSA.sol"; import {Account} from "./Account.sol"; abstract contract AccountECDSA is Account { - function _getSignerAndWindow( + function _processSignature( PackedUserOperation calldata userOp, bytes32 userOpHash ) internal virtual override returns (address, uint48, uint48) { @@ -25,18 +25,18 @@ abstract contract AccountECDSA is Account { uint8 v; /// @solidity memory-safe-assembly assembly { - r := calldataload(add(signature.offset, 0x20)) - s := calldataload(add(signature.offset, 0x40)) - v := byte(0, calldataload(add(signature.offset, 0x60))) + r := calldataload(add(signature.offset, 0x00)) + s := calldataload(add(signature.offset, 0x20)) + v := byte(0, calldataload(add(signature.offset, 0x40))) } - return (ECDSA.recover(msgHash, signature), 0, 0); + return (ECDSA.recover(msgHash, v, r, s), 0, 0); } else if (signature.length == 64) { bytes32 r; bytes32 vs; /// @solidity memory-safe-assembly assembly { - r := calldataload(add(signature.offset, 0x20)) - vs := calldataload(add(signature.offset, 0x40)) + r := calldataload(add(signature.offset, 0x00)) + vs := calldataload(add(signature.offset, 0x20)) } return (ECDSA.recover(msgHash, r, vs), 0, 0); } else { diff --git a/contracts/abstraction/account/AccountP256.sol b/contracts/abstraction/account/AccountP256.sol new file mode 100644 index 00000000000..7ac9477109e --- /dev/null +++ b/contracts/abstraction/account/AccountP256.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../interfaces/IERC4337.sol"; +import {MessageHashUtils} from "../../utils/cryptography/MessageHashUtils.sol"; +import {P256} from "../../utils/cryptography/P256.sol"; +import {Account} from "./Account.sol"; + +abstract contract AccountP256 is Account { + error P256InvalidSignatureLength(uint256 length); + + function _processSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (address, uint48, uint48) { + bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); + + // This implementation support both "normal" and short signature formats: + // - If signature length is 65, process as "normal" signature (R,S,V) + // - If signature length is 64, process as https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signature] (R,SV) ECDSA signature + // This is safe because the UserOperations include a nonce (which is managed by the entrypoint) for replay protection. + bytes calldata signature = userOp.signature; + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + /// @solidity memory-safe-assembly + assembly { + r := calldataload(add(signature.offset, 0x00)) + s := calldataload(add(signature.offset, 0x20)) + v := byte(0, calldataload(add(signature.offset, 0x40))) + } + return (P256.recoveryAddress(uint256(msgHash), v, uint256(r), uint256(s)), 0, 0); + } else { + revert P256InvalidSignatureLength(signature.length); + } + } +} diff --git a/contracts/abstraction/mocks/SimpleAccount.sol b/contracts/abstraction/mocks/SimpleAccount.sol index e2121769dec..b9778f6ca1e 100644 --- a/contracts/abstraction/mocks/SimpleAccount.sol +++ b/contracts/abstraction/mocks/SimpleAccount.sol @@ -7,9 +7,11 @@ import {Ownable} from "../../access/Ownable.sol"; import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; import {Address} from "../../utils/Address.sol"; +import {Account} from "../account/Account.sol"; import {AccountECDSA} from "../account/AccountECDSA.sol"; +import {AccountP256} from "../account/AccountP256.sol"; -contract SimpleAccount is AccountECDSA, Ownable, ERC721Holder, ERC1155Holder { +abstract contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder { IEntryPoint private immutable _entryPoint; constructor(IEntryPoint entryPoint_, address initialOwner) Ownable(initialOwner) { @@ -49,3 +51,11 @@ contract SimpleAccount is AccountECDSA, Ownable, ERC721Holder, ERC1155Holder { Address.verifyCallResult(success, returndata); } } + +contract SimpleAccountECDSA is SimpleAccount, AccountECDSA { + constructor(IEntryPoint entryPoint_, address initialOwner) SimpleAccount(entryPoint_, initialOwner) {} +} + +contract SimpleAccountP256 is SimpleAccount, AccountP256 { + constructor(IEntryPoint entryPoint_, address initialOwner) SimpleAccount(entryPoint_, initialOwner) {} +} diff --git a/test/abstraction/accountECDSA.test.js b/test/abstraction/accountECDSA.test.js new file mode 100644 index 00000000000..7f50bdc88de --- /dev/null +++ b/test/abstraction/accountECDSA.test.js @@ -0,0 +1,97 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { ERC4337Helper } = require('../helpers/erc4337'); + +async function fixture() { + const accounts = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); + const helper = new ERC4337Helper('SimpleAccountECDSA'); + await helper.wait(); + + return { + accounts, + target, + helper, + entrypoint: helper.entrypoint, + factory: helper.factory, + }; +} + +describe('EntryPoint', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + this.user = this.accounts.shift(); + this.beneficiary = this.accounts.shift(); + this.sender = await this.helper.newAccount(this.user); + }); + + describe('execute operation', function () { + beforeEach('fund account', async function () { + await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + }); + + describe('account not deployed yet', function () { + it('success: deploy and call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 17, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 17); + }); + }); + + describe('account already deployed', function () { + beforeEach(async function () { + await this.sender.deploy(); + }); + + it('success: call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 42, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + it('success: call with short signature', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 42, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.sign()); + + // compact signature + operation.signature = ethers.Signature.from(operation.signature).compactSerialized; + + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + }); + }); +}); diff --git a/test/abstraction/accountP256.test.js b/test/abstraction/accountP256.test.js new file mode 100644 index 00000000000..015f4faed6f --- /dev/null +++ b/test/abstraction/accountP256.test.js @@ -0,0 +1,80 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { ERC4337Helper } = require('../helpers/erc4337'); +const { P256Signer } = require('../helpers/p256'); + +async function fixture() { + const accounts = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMock'); + const helper = new ERC4337Helper('SimpleAccountP256'); + await helper.wait(); + + return { + accounts, + target, + helper, + entrypoint: helper.entrypoint, + factory: helper.factory, + }; +} + +describe('EntryPoint', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + this.user = P256Signer.random(); + this.beneficiary = this.accounts.shift(); + this.other = this.accounts.shift(); + this.sender = await this.helper.newAccount(this.user); + }); + + describe('execute operation', function () { + beforeEach('fund account', async function () { + await this.other.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + }); + + describe('account not deployed yet', function () { + it('success: deploy and call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 17, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.addInitCode()) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 17); + }); + }); + + describe('account already deployed', function () { + beforeEach(async function () { + await this.sender.deploy(this.other); + }); + + it('success: call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 42, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + }); + }); +}); diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js index 25fbec83263..b7ad9b69a6b 100644 --- a/test/abstraction/entrypoint.test.js +++ b/test/abstraction/entrypoint.test.js @@ -163,7 +163,7 @@ describe('EntryPoint', function () { await this.sender.deploy(); }); - it('success: deploy and call', async function () { + it('success: call', async function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 78f47f93886..a69944097ab 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -5,10 +5,10 @@ function pack(left, right) { } class ERC4337Helper { - constructor() { + constructor(account = 'SimpleAccountECDSA') { this.entrypointAsPromise = ethers.deployContract('EntryPoint'); this.factoryAsPromise = ethers.deployContract('$Create2'); - this.accountAsPromise = ethers.getContractFactory('SimpleAccount'); + this.accountAsPromise = ethers.getContractFactory(account); this.chainIdAsPromise = ethers.provider.getNetwork().then(({ chainId }) => chainId); } @@ -41,8 +41,8 @@ class AbstractAccount extends ethers.BaseContract { this.context = context; } - async deploy() { - this.deployTx = await this.runner.sendTransaction({ + async deploy(account = this.runner) { + this.deployTx = await account.sendTransaction({ to: '0x' + this.initCode.replace(/0x/, '').slice(0, 40), data: '0x' + this.initCode.replace(/0x/, '').slice(40), }); diff --git a/test/helpers/p256.js b/test/helpers/p256.js new file mode 100644 index 00000000000..12190349740 --- /dev/null +++ b/test/helpers/p256.js @@ -0,0 +1,32 @@ +const { ethers } = require('hardhat'); +const { secp256r1 } = require('@noble/curves/p256'); + +class P256Signer { + constructor(privateKey) { + this.privateKey = privateKey; + this.publicKey = ethers.concat( + [ + secp256r1.getPublicKey(this.privateKey, false).slice(0x01, 0x21), + secp256r1.getPublicKey(this.privateKey, false).slice(0x21, 0x41), + ].map(ethers.hexlify), + ); + this.address = ethers.getAddress(ethers.keccak256(this.publicKey).slice(-40)); + } + + static random() { + return new P256Signer(secp256r1.utils.randomPrivateKey()); + } + + getAddress() { + return this.address; + } + + signMessage(message) { + const { r, s, recovery } = secp256r1.sign(ethers.hashMessage(message).replace(/0x/, ''), this.privateKey); + return ethers.solidityPacked(['uint256', 'uint256', 'uint8'], [r, s, recovery]); + } +} + +module.exports = { + P256Signer, +}; From 14474fcf330c1010d89b5f486af3d207d73f5463 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 30 Apr 2024 22:19:49 +0200 Subject: [PATCH 38/66] move AccountECDSA and AccountP256 to a "modules" subfolder --- .../abstraction/account/{ => modules}/AccountECDSA.sol | 8 ++++---- .../abstraction/account/{ => modules}/AccountP256.sol | 8 ++++---- contracts/abstraction/mocks/SimpleAccount.sol | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) rename contracts/abstraction/account/{ => modules}/AccountECDSA.sol (87%) rename contracts/abstraction/account/{ => modules}/AccountP256.sol (85%) diff --git a/contracts/abstraction/account/AccountECDSA.sol b/contracts/abstraction/account/modules/AccountECDSA.sol similarity index 87% rename from contracts/abstraction/account/AccountECDSA.sol rename to contracts/abstraction/account/modules/AccountECDSA.sol index 850a4045c2a..5d9dd6837bd 100644 --- a/contracts/abstraction/account/AccountECDSA.sol +++ b/contracts/abstraction/account/modules/AccountECDSA.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.20; -import {PackedUserOperation} from "../../interfaces/IERC4337.sol"; -import {MessageHashUtils} from "../../utils/cryptography/MessageHashUtils.sol"; -import {ECDSA} from "../../utils/cryptography/ECDSA.sol"; -import {Account} from "./Account.sol"; +import {PackedUserOperation} from "../../../interfaces/IERC4337.sol"; +import {MessageHashUtils} from "../../../utils/cryptography/MessageHashUtils.sol"; +import {ECDSA} from "../../../utils/cryptography/ECDSA.sol"; +import {Account} from "../Account.sol"; abstract contract AccountECDSA is Account { function _processSignature( diff --git a/contracts/abstraction/account/AccountP256.sol b/contracts/abstraction/account/modules/AccountP256.sol similarity index 85% rename from contracts/abstraction/account/AccountP256.sol rename to contracts/abstraction/account/modules/AccountP256.sol index 7ac9477109e..ed2d51050ca 100644 --- a/contracts/abstraction/account/AccountP256.sol +++ b/contracts/abstraction/account/modules/AccountP256.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.20; -import {PackedUserOperation} from "../../interfaces/IERC4337.sol"; -import {MessageHashUtils} from "../../utils/cryptography/MessageHashUtils.sol"; -import {P256} from "../../utils/cryptography/P256.sol"; -import {Account} from "./Account.sol"; +import {PackedUserOperation} from "../../../interfaces/IERC4337.sol"; +import {MessageHashUtils} from "../../../utils/cryptography/MessageHashUtils.sol"; +import {P256} from "../../../utils/cryptography/P256.sol"; +import {Account} from "../Account.sol"; abstract contract AccountP256 is Account { error P256InvalidSignatureLength(uint256 length); diff --git a/contracts/abstraction/mocks/SimpleAccount.sol b/contracts/abstraction/mocks/SimpleAccount.sol index b9778f6ca1e..6b088406967 100644 --- a/contracts/abstraction/mocks/SimpleAccount.sol +++ b/contracts/abstraction/mocks/SimpleAccount.sol @@ -8,8 +8,8 @@ import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; import {Address} from "../../utils/Address.sol"; import {Account} from "../account/Account.sol"; -import {AccountECDSA} from "../account/AccountECDSA.sol"; -import {AccountP256} from "../account/AccountP256.sol"; +import {AccountECDSA} from "../account/modules/AccountECDSA.sol"; +import {AccountP256} from "../account/modules/AccountP256.sol"; abstract contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder { IEntryPoint private immutable _entryPoint; From 6a5c91b4a7c9937d52cd807d384758a3d3cee332 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 1 May 2024 13:03:11 +0200 Subject: [PATCH 39/66] fix comments --- contracts/abstraction/account/modules/AccountP256.sol | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/contracts/abstraction/account/modules/AccountP256.sol b/contracts/abstraction/account/modules/AccountP256.sol index ed2d51050ca..855bcac9531 100644 --- a/contracts/abstraction/account/modules/AccountP256.sol +++ b/contracts/abstraction/account/modules/AccountP256.sol @@ -16,14 +16,11 @@ abstract contract AccountP256 is Account { ) internal virtual override returns (address, uint48, uint48) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); - // This implementation support both "normal" and short signature formats: - // - If signature length is 65, process as "normal" signature (R,S,V) - // - If signature length is 64, process as https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signature] (R,SV) ECDSA signature - // This is safe because the UserOperations include a nonce (which is managed by the entrypoint) for replay protection. + // This implementation support signature that are 65 bytes long in the (R,S,V) format bytes calldata signature = userOp.signature; if (signature.length == 65) { - bytes32 r; - bytes32 s; + uint256 r; + uint256 s; uint8 v; /// @solidity memory-safe-assembly assembly { @@ -31,7 +28,7 @@ abstract contract AccountP256 is Account { s := calldataload(add(signature.offset, 0x20)) v := byte(0, calldataload(add(signature.offset, 0x40))) } - return (P256.recoveryAddress(uint256(msgHash), v, uint256(r), uint256(s)), 0, 0); + return (P256.recoveryAddress(uint256(msgHash), v, r, s), 0, 0); } else { revert P256InvalidSignatureLength(signature.length); } From 1487fdbd94812a498bdccc92734c7d2c5a1a679e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 2 May 2024 12:05:37 +0200 Subject: [PATCH 40/66] inline documentation --- .../abstraction/entrypoint/EntryPoint.sol | 223 ++++++++++-------- 1 file changed, 122 insertions(+), 101 deletions(-) diff --git a/contracts/abstraction/entrypoint/EntryPoint.sol b/contracts/abstraction/entrypoint/EntryPoint.sol index 48fdf8e9e59..b376730262a 100644 --- a/contracts/abstraction/entrypoint/EntryPoint.sol +++ b/contracts/abstraction/entrypoint/EntryPoint.sol @@ -17,13 +17,18 @@ import {SenderCreationHelper} from "./../utils/SenderCreationHelper.sol"; /* * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. * Only one instance required on each chain. + * + * WARNING: This contract is not properly tested. It is only present as an helper for the development of account + * contracts. The EntryPoint is a critical element or ERC-4337, and must be developped with exterm care to avoid any + * issue with gas payments/refunds in corner cases. A fully tested, production-ready, version may be available in the + * future, but for the moment this should NOT be used in production! */ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, NoncesWithKey, ReentrancyGuard { using ERC4337Utils for *; SenderCreationHelper private immutable _senderCreator = new SenderCreationHelper(); - // TODO: move to interface? + // TODO: move events to interface? event UserOperationEvent( bytes32 indexed userOpHash, address indexed sender, @@ -47,7 +52,7 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, error PostOpReverted(bytes returnData); error SignatureValidationFailed(address aggregator); - //compensate for innerHandleOps' emit message and deposit refund. + // compensate for innerHandleOps' emit message and deposit refund. // allow some slack for future gas price changes. uint256 private constant INNER_GAS_OVERHEAD = 10000; bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead"; @@ -75,37 +80,44 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, _mint(msg.sender, msg.value); } + /// @inheritdoc IEntryPointStake function balanceOf(address account) public view virtual override(ERC20, IEntryPointStake) returns (uint256) { return super.balanceOf(account); } + /// @inheritdoc IEntryPointStake function depositTo(address account) public payable virtual { _mint(account, msg.value); } + /// @inheritdoc IEntryPointStake function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) public virtual { _burn(msg.sender, withdrawAmount); Address.sendValue(withdrawAddress, withdrawAmount); } - // TODO: implement + /// @inheritdoc IEntryPointStake function addStake(uint32 /*unstakeDelaySec*/) public payable virtual { + // TODO: implement revert("Stake not Implemented yet"); } - // TODO: implement and remove pure + /// @inheritdoc IEntryPointStake function unlockStake() public pure virtual { + // TODO: implement and remove pure revert("Stake not Implemented yet"); } - // TODO: implement and remove pure + /// @inheritdoc IEntryPointStake function withdrawStake(address payable /*withdrawAddress*/) public pure virtual { + // TODO: implement and remove pure revert("Stake not Implemented yet"); } /**************************************************************************************************************** * IEntryPointNonces * ****************************************************************************************************************/ + /// @inheritdoc IEntryPointNonces function getNonce( address owner, uint192 key @@ -116,24 +128,26 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, /**************************************************************************************************************** * Handle user operations * ****************************************************************************************************************/ + /// @inheritdoc IEntryPoint function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) public nonReentrant { - ERC4337Utils.UserOpInfo[] memory opInfos = new ERC4337Utils.UserOpInfo[](ops.length); + ERC4337Utils.UserOpInfo[] memory userOpInfos = new ERC4337Utils.UserOpInfo[](ops.length); for (uint256 i = 0; i < ops.length; ++i) { - (uint256 validationData, uint256 pmValidationData) = _validatePrepayment(i, ops[i], opInfos[i]); - _validateAccountAndPaymasterValidationData(i, validationData, pmValidationData, address(0)); + (uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment(i, ops[i], userOpInfos[i]); + _validateAccountAndPaymasterValidationData(i, validationData, paymasterValidationData, address(0)); } emit BeforeExecution(); uint256 collected = 0; for (uint256 i = 0; i < ops.length; ++i) { - collected += _executeUserOp(i, ops[i], opInfos[i]); + collected += _executeUserOp(i, ops[i], userOpInfos[i]); } Address.sendValue(beneficiary, collected); } + /// @inheritdoc IEntryPoint function handleAggregatedOps( UserOpsPerAggregator[] calldata opsPerAggregator, address payable beneficiary @@ -144,9 +158,9 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, IAggregator aggregator = opsPerAggregator[i].aggregator; //address(1) is special marker of "signature error" - require(address(aggregator) != address(1), "AA96 invalid aggregator"); - if (address(aggregator) != address(0)) { - // solhint-disable-next-line no-empty-blocks + if (address(aggregator) == address(1)) { + revert("AA96 invalid aggregator"); + } else if (address(aggregator) != address(0)) { try aggregator.validateSignatures(ops, opsPerAggregator[i].signature) {} catch { revert SignatureValidationFailed(address(aggregator)); } @@ -154,7 +168,7 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, totalOps += ops.length; } - ERC4337Utils.UserOpInfo[] memory opInfos = new ERC4337Utils.UserOpInfo[](totalOps); + ERC4337Utils.UserOpInfo[] memory userOpInfos = new ERC4337Utils.UserOpInfo[](totalOps); uint256 opIndex = 0; for (uint256 a = 0; a < opsPerAggregator.length; ++a) { @@ -165,7 +179,7 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, (uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment( opIndex, ops[i], - opInfos[opIndex] + userOpInfos[opIndex] ); _validateAccountAndPaymasterValidationData( i, @@ -173,7 +187,7 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, paymasterValidationData, address(aggregator) ); - opIndex++; + ++opIndex; } } @@ -188,8 +202,8 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, emit SignatureAggregatorChanged(address(aggregator)); for (uint256 i = 0; i < ops.length; ++i) { - collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]); - opIndex++; + collected += _executeUserOp(opIndex, ops[i], userOpInfos[opIndex]); + ++opIndex; } } emit SignatureAggregatorChanged(address(0)); @@ -198,16 +212,16 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, } /** - * Execute a user operation. - * @param opIndex - Index into the opInfo array. + * @dev Execute a user operation. + * @param opIndex - Index into the userOpInfo array. * @param userOp - The userOp to execute. - * @param opInfo - The opInfo filled by validatePrepayment for this userOp. + * @param userOpInfo - The userOpInfo filled by validatePrepayment for this userOp. * @return collected - The total amount this userOp paid. */ function _executeUserOp( uint256 opIndex, PackedUserOperation calldata userOp, - ERC4337Utils.UserOpInfo memory opInfo + ERC4337Utils.UserOpInfo memory userOpInfo ) internal returns (uint256 collected) { uint256 preGas = gasleft(); @@ -217,9 +231,9 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, this.innerHandleOp, ( userOp.callData.length >= 0x04 && bytes4(userOp.callData[0:4]) == IAccountExecute.executeUserOp.selector - ? abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)) + ? abi.encodeCall(IAccountExecute.executeUserOp, (userOp, userOpInfo.userOpHash)) : userOp.callData, - opInfo + userOpInfo ) ); Memory.load(ptr); @@ -235,14 +249,14 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, revert FailedOp(opIndex, "AA95 out of gas"); } else if (result == INNER_REVERT_LOW_PREFUND) { // innerCall reverted on prefund too low. treat entire prefund as "gas cost" - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - uint256 actualGasCost = opInfo.prefund; - emit UserOperationPrefundTooLow(opInfo.userOpHash, opInfo.sender, opInfo.nonce); + uint256 actualGas = preGas - gasleft() + userOpInfo.preOpGas; + uint256 actualGasCost = userOpInfo.prefund; + emit UserOperationPrefundTooLow(userOpInfo.userOpHash, userOpInfo.sender, userOpInfo.nonce); emit UserOperationEvent( - opInfo.userOpHash, - opInfo.sender, - opInfo.paymaster, - opInfo.nonce, + userOpInfo.userOpHash, + userOpInfo.sender, + userOpInfo.paymaster, + userOpInfo.nonce, success, actualGasCost, actualGas @@ -250,87 +264,89 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, collected = actualGasCost; } else { emit PostOpRevertReason( - opInfo.userOpHash, - opInfo.sender, - opInfo.nonce, + userOpInfo.userOpHash, + userOpInfo.sender, + userOpInfo.nonce, Call.getReturnData(REVERT_REASON_MAX_LEN) ); - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - collected = _postExecution(IPaymaster.PostOpMode.postOpReverted, opInfo, actualGas); + uint256 actualGas = preGas - gasleft() + userOpInfo.preOpGas; + collected = _postExecution(IPaymaster.PostOpMode.postOpReverted, userOpInfo, actualGas); } } /** - * Inner function to handle a UserOperation. + * @dev Inner function to handle a UserOperation. * Must be declared "external" to open a call context, but it can only be called by handleOps. - * @param callData - The callData to execute. - * @param opInfo - The UserOpInfo struct. + * @param callData - The callData to execute. + * @param userOpInfo - The UserOpInfo struct. * @return actualGasCost - the actual cost in eth this UserOperation paid for gas */ function innerHandleOp( bytes memory callData, - ERC4337Utils.UserOpInfo memory opInfo + ERC4337Utils.UserOpInfo memory userOpInfo ) external returns (uint256 actualGasCost) { uint256 preGas = gasleft(); require(msg.sender == address(this), "AA92 internal call only"); unchecked { // handleOps was called with gas limit too low. abort entire bundle. - if ((gasleft() * 63) / 64 < opInfo.callGasLimit + opInfo.paymasterPostOpGasLimit + INNER_GAS_OVERHEAD) { + if ( + (gasleft() * 63) / 64 < + userOpInfo.callGasLimit + userOpInfo.paymasterPostOpGasLimit + INNER_GAS_OVERHEAD + ) { Call.revertWithCode(INNER_OUT_OF_GAS); } IPaymaster.PostOpMode mode; - if (callData.length == 0 || Call.call(opInfo.sender, 0, callData, opInfo.callGasLimit)) { + if (callData.length == 0 || Call.call(userOpInfo.sender, 0, callData, userOpInfo.callGasLimit)) { mode = IPaymaster.PostOpMode.opSucceeded; } else { mode = IPaymaster.PostOpMode.opReverted; // if we get here, that means callData.length > 0 and the Call failed if (Call.getReturnDataSize() > 0) { emit UserOperationRevertReason( - opInfo.userOpHash, - opInfo.sender, - opInfo.nonce, + userOpInfo.userOpHash, + userOpInfo.sender, + userOpInfo.nonce, Call.getReturnData(REVERT_REASON_MAX_LEN) ); } } - uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; - return _postExecution(mode, opInfo, actualGas); + uint256 actualGas = preGas - gasleft() + userOpInfo.preOpGas; + return _postExecution(mode, userOpInfo, actualGas); } } /** - * Create sender smart contract account if init code is provided. - * @param opIndex - The operation index. - * @param opInfo - The operation info. - * @param initCode - The init code for the smart contract account. + * @dev Create sender smart contract account if init code is provided. + * @param opIndex - The operation index. + * @param userOpInfo - The operation info. + * @param initCode - The init code for the smart contract account. */ function _createSenderIfNeeded( uint256 opIndex, - ERC4337Utils.UserOpInfo memory opInfo, + ERC4337Utils.UserOpInfo memory userOpInfo, bytes calldata initCode ) internal { if (initCode.length != 0) { - address sender = opInfo.sender; + address sender = userOpInfo.sender; if (sender.code.length != 0) revert FailedOp(opIndex, "AA10 sender already constructed"); - address deployed = _senderCreator.createSender{gas: opInfo.verificationGasLimit}(initCode); + address deployed = _senderCreator.createSender{gas: userOpInfo.verificationGasLimit}(initCode); if (deployed == address(0)) revert FailedOp(opIndex, "AA13 initCode failed or OOG"); else if (deployed != sender) revert FailedOp(opIndex, "AA14 initCode must return sender"); else if (deployed.code.length == 0) revert FailedOp(opIndex, "AA15 initCode must create sender"); - emit AccountDeployed(opInfo.userOpHash, sender, address(bytes20(initCode[0:20])), opInfo.paymaster); + emit AccountDeployed(userOpInfo.userOpHash, sender, address(bytes20(initCode[0:20])), userOpInfo.paymaster); } } /** - * Validate account and paymaster (if defined) and - * also make sure total validation doesn't exceed verificationGasLimit. - * This method is called off-chain (simulateValidation()) and on-chain (from handleOps) - * @param opIndex - The index of this userOp into the "opInfos" array. + * @dev Validate account and paymaster (if defined) and also make sure total validation doesn't exceed + * verificationGasLimit. + * @param opIndex - The index of this userOp into the "userOpInfos" array. * @param userOp - The userOp to validate. */ function _validatePrepayment( @@ -382,26 +398,27 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, } /** - * Call account.validateUserOp. - * Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. - * Decrement account's deposit if needed. + * @dev Validate prepayment (account part) + * - Call account.validateUserOp. + * - Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. + * - Decrement account's deposit if needed. * @param opIndex - The operation index. - * @param op - The user operation. - * @param opInfo - The operation info. + * @param userOp - The user operation. + * @param userOpInfo - The operation info. * @param requiredPrefund - The required prefund amount. */ function _validateAccountPrepayment( uint256 opIndex, - PackedUserOperation calldata op, - ERC4337Utils.UserOpInfo memory opInfo, + PackedUserOperation calldata userOp, + ERC4337Utils.UserOpInfo memory userOpInfo, uint256 requiredPrefund ) internal returns (uint256 validationData) { unchecked { - address sender = opInfo.sender; - address paymaster = opInfo.paymaster; - uint256 verificationGasLimit = opInfo.verificationGasLimit; + address sender = userOpInfo.sender; + address paymaster = userOpInfo.paymaster; + uint256 verificationGasLimit = userOpInfo.verificationGasLimit; - _createSenderIfNeeded(opIndex, opInfo, op.initCode); + _createSenderIfNeeded(opIndex, userOpInfo, userOp.initCode); uint256 missingAccountFunds = 0; if (paymaster == address(0)) { @@ -412,7 +429,11 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, } try - IAccount(sender).validateUserOp{gas: verificationGasLimit}(op, opInfo.userOpHash, missingAccountFunds) + IAccount(sender).validateUserOp{gas: verificationGasLimit}( + userOp, + userOpInfo.userOpHash, + missingAccountFunds + ) returns (uint256 _validationData) { validationData = _validationData; } catch { @@ -431,27 +452,27 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, } /** - * In case the request has a paymaster: + * @dev Validate prepayment (paymaster part) * - Validate paymaster has enough deposit. * - Call paymaster.validatePaymasterUserOp. * - Revert with proper FailedOp in case paymaster reverts. * - Decrement paymaster's deposit. - * @param opIndex - The operation index. - * @param op - The user operation. - * @param opInfo - The operation info. - * @param requiredPrefund - The required prefund amount. + * @param opIndex - The operation index. + * @param userOp - The user operation. + * @param userOpInfo - The operation info. + * @param requiredPrefund - The required prefund amount. */ function _validatePaymasterPrepayment( uint256 opIndex, - PackedUserOperation calldata op, - ERC4337Utils.UserOpInfo memory opInfo, + PackedUserOperation calldata userOp, + ERC4337Utils.UserOpInfo memory userOpInfo, uint256 requiredPrefund ) internal returns (bytes memory context, uint256 validationData) { unchecked { uint256 preGas = gasleft(); - address paymaster = opInfo.paymaster; - uint256 verificationGasLimit = opInfo.paymasterVerificationGasLimit; + address paymaster = userOpInfo.paymaster; + uint256 verificationGasLimit = userOpInfo.paymasterVerificationGasLimit; uint256 balance = balanceOf(paymaster); if (requiredPrefund > balance) { @@ -462,8 +483,8 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, try IPaymaster(paymaster).validatePaymasterUserOp{gas: verificationGasLimit}( - op, - opInfo.userOpHash, + userOp, + userOpInfo.userOpHash, requiredPrefund ) returns (bytes memory _context, uint256 _validationData) { @@ -480,7 +501,7 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, } /** - * Revert if either account validationData or paymaster validationData is expired. + * @dev Revert if either account validationData or paymaster validationData is expired. * @param opIndex - The operation index. * @param validationData - The account validationData. * @param paymasterValidationData - The paymaster validationData. @@ -509,30 +530,30 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, } /** - * Process post-operation, called just after the callData is executed. + * @dev Process post-operation, called just after the callData is executed. * If a paymaster is defined and its validation returned a non-empty context, its postOp is called. * The excess amount is refunded to the account (or paymaster - if it was used in the request). - * @param mode - Whether is called from innerHandleOp, or outside (postOpReverted). - * @param opInfo - UserOp fields and info collected during validation. - * @param actualGas - The gas used so far by this user operation. + * @param mode - Whether is called from innerHandleOp, or outside (postOpReverted). + * @param userOpInfo - UserOp fields and info collected during validation. + * @param actualGas - The gas used so far by this user operation. */ function _postExecution( IPaymaster.PostOpMode mode, - ERC4337Utils.UserOpInfo memory opInfo, + ERC4337Utils.UserOpInfo memory userOpInfo, uint256 actualGas ) private returns (uint256 actualGasCost) { uint256 preGas = gasleft(); unchecked { - address refundAddress = opInfo.paymaster; - uint256 gasPrice = opInfo.gasPrice(); + address refundAddress = userOpInfo.paymaster; + uint256 gasPrice = userOpInfo.gasPrice(); if (refundAddress == address(0)) { - refundAddress = opInfo.sender; - } else if (opInfo.context.length > 0 && mode != IPaymaster.PostOpMode.postOpReverted) { + refundAddress = userOpInfo.sender; + } else if (userOpInfo.context.length > 0 && mode != IPaymaster.PostOpMode.postOpReverted) { try - IPaymaster(refundAddress).postOp{gas: opInfo.paymasterPostOpGasLimit}( + IPaymaster(refundAddress).postOp{gas: userOpInfo.paymasterPostOpGasLimit}( mode, - opInfo.context, + userOpInfo.context, actualGas * gasPrice, gasPrice ) @@ -543,19 +564,19 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, actualGas += preGas - gasleft(); // Calculating a penalty for unused execution gas - uint256 executionGasLimit = opInfo.callGasLimit + opInfo.paymasterPostOpGasLimit; - uint256 executionGasUsed = actualGas - opInfo.preOpGas; + uint256 executionGasLimit = userOpInfo.callGasLimit + userOpInfo.paymasterPostOpGasLimit; + uint256 executionGasUsed = actualGas - userOpInfo.preOpGas; // this check is required for the gas used within EntryPoint and not covered by explicit gas limits if (executionGasLimit > executionGasUsed) { actualGas += ((executionGasLimit - executionGasUsed) * PENALTY_PERCENT) / 100; } actualGasCost = actualGas * gasPrice; - uint256 prefund = opInfo.prefund; + uint256 prefund = userOpInfo.prefund; if (prefund < actualGasCost) { if (mode == IPaymaster.PostOpMode.postOpReverted) { actualGasCost = prefund; - emit UserOperationPrefundTooLow(opInfo.userOpHash, opInfo.sender, opInfo.nonce); + emit UserOperationPrefundTooLow(userOpInfo.userOpHash, userOpInfo.sender, userOpInfo.nonce); } else { Call.revertWithCode(INNER_REVERT_LOW_PREFUND); } @@ -563,10 +584,10 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, _mint(refundAddress, prefund - actualGasCost); } emit UserOperationEvent( - opInfo.userOpHash, - opInfo.sender, - opInfo.paymaster, - opInfo.nonce, + userOpInfo.userOpHash, + userOpInfo.sender, + userOpInfo.paymaster, + userOpInfo.nonce, mode == IPaymaster.PostOpMode.opSucceeded, actualGasCost, actualGas From 6373a085b0b90168ded25da7d95659263c980563 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 2 May 2024 14:11:54 +0200 Subject: [PATCH 41/66] Inline documentation --- contracts/abstraction/entrypoint/EntryPoint.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/abstraction/entrypoint/EntryPoint.sol b/contracts/abstraction/entrypoint/EntryPoint.sol index b376730262a..45a85465e99 100644 --- a/contracts/abstraction/entrypoint/EntryPoint.sol +++ b/contracts/abstraction/entrypoint/EntryPoint.sol @@ -69,6 +69,11 @@ contract EntryPoint is IEntryPoint, ERC20("EntryPoint Deposit", "EPD"), ERC165, // || interfaceId == type(INonceManager).interfaceId; } + /** + * @dev Simulate the deployment of an account. + * @param initCode - The init code for the smart contract account, formated according to PackedUserOperation + * specifications. + */ function getSenderAddress(bytes calldata initCode) public returns (address) { return _senderCreator.getSenderAddress(initCode); } From 4512481e69e8902f2075d2b98ddfd8cd4c3f2ae7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 3 May 2024 15:21:15 +0200 Subject: [PATCH 42/66] add AccountMultisig module --- contracts/abstraction/account/Account.sol | 12 +- .../account/modules/AccountECDSA.sol | 13 +- .../account/modules/AccountMultisig.sol | 41 ++++++ .../account/modules/AccountP256.sol | 9 +- .../abstraction/mocks/AdvancedAccount.sol | 107 ++++++++++++++ contracts/abstraction/mocks/SimpleAccount.sol | 19 ++- test/abstraction/accountECDSA.test.js | 18 +-- test/abstraction/accountMultisig.test.js | 131 ++++++++++++++++++ test/abstraction/accountP256.test.js | 19 +-- test/abstraction/entrypoint.test.js | 35 ++--- test/helpers/erc4337.js | 12 +- 11 files changed, 352 insertions(+), 64 deletions(-) create mode 100644 contracts/abstraction/account/modules/AccountMultisig.sol create mode 100644 contracts/abstraction/mocks/AdvancedAccount.sol create mode 100644 test/abstraction/accountMultisig.test.js diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol index a10d0a1b09a..2d123accc23 100644 --- a/contracts/abstraction/account/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -8,7 +8,6 @@ import {ERC4337Utils} from "./../utils/ERC4337Utils.sol"; abstract contract Account is IAccount { error AccountEntryPointRestricted(); - error AccountUserRestricted(); error AccountInvalidBatchLength(); /**************************************************************************************************************** @@ -22,13 +21,6 @@ abstract contract Account is IAccount { _; } - modifier onlyAuthorizedOrEntryPoint() { - if (msg.sender != address(entryPoint()) && !_isAuthorized(msg.sender)) { - revert AccountUserRestricted(); - } - _; - } - /**************************************************************************************************************** * Hooks * ****************************************************************************************************************/ @@ -51,7 +43,7 @@ abstract contract Account is IAccount { * If a signature is ill-formed, address(0) should be returned. */ function _processSignature( - PackedUserOperation calldata userOp, + bytes memory signature, bytes32 userOpHash ) internal virtual returns (address, uint48, uint48); @@ -106,7 +98,7 @@ abstract contract Account is IAccount { PackedUserOperation calldata userOp, bytes32 userOpHash ) internal virtual returns (uint256 validationData) { - (address signer, uint48 validAfter, uint48 validUntil) = _processSignature(userOp, userOpHash); + (address signer, uint48 validAfter, uint48 validUntil) = _processSignature(userOp.signature, userOpHash); return ERC4337Utils.packValidationData(signer != address(0) && _isAuthorized(signer), validAfter, validUntil); } diff --git a/contracts/abstraction/account/modules/AccountECDSA.sol b/contracts/abstraction/account/modules/AccountECDSA.sol index 5d9dd6837bd..14b3a24d5d5 100644 --- a/contracts/abstraction/account/modules/AccountECDSA.sol +++ b/contracts/abstraction/account/modules/AccountECDSA.sol @@ -9,7 +9,7 @@ import {Account} from "../Account.sol"; abstract contract AccountECDSA is Account { function _processSignature( - PackedUserOperation calldata userOp, + bytes memory signature, bytes32 userOpHash ) internal virtual override returns (address, uint48, uint48) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); @@ -18,16 +18,15 @@ abstract contract AccountECDSA is Account { // - If signature length is 65, process as "normal" signature (R,S,V) // - If signature length is 64, process as https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signature] (R,SV) ECDSA signature // This is safe because the UserOperations include a nonce (which is managed by the entrypoint) for replay protection. - bytes calldata signature = userOp.signature; if (signature.length == 65) { bytes32 r; bytes32 s; uint8 v; /// @solidity memory-safe-assembly assembly { - r := calldataload(add(signature.offset, 0x00)) - s := calldataload(add(signature.offset, 0x20)) - v := byte(0, calldataload(add(signature.offset, 0x40))) + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) } return (ECDSA.recover(msgHash, v, r, s), 0, 0); } else if (signature.length == 64) { @@ -35,8 +34,8 @@ abstract contract AccountECDSA is Account { bytes32 vs; /// @solidity memory-safe-assembly assembly { - r := calldataload(add(signature.offset, 0x00)) - vs := calldataload(add(signature.offset, 0x20)) + r := mload(add(signature, 0x20)) + vs := mload(add(signature, 0x40)) } return (ECDSA.recover(msgHash, r, vs), 0, 0); } else { diff --git a/contracts/abstraction/account/modules/AccountMultisig.sol b/contracts/abstraction/account/modules/AccountMultisig.sol new file mode 100644 index 00000000000..bef8b9603e4 --- /dev/null +++ b/contracts/abstraction/account/modules/AccountMultisig.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../../interfaces/IERC4337.sol"; +import {Math} from "./../../../utils/math/Math.sol"; +import {ERC4337Utils} from "./../../utils/ERC4337Utils.sol"; +import {Account} from "../Account.sol"; + +abstract contract AccountMultisig is Account { + function requiredSignatures(PackedUserOperation calldata userOp) public view virtual returns (uint256); + + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256 validationData) { + bytes[] memory signatures = abi.decode(userOp.signature, (bytes[])); + + if (signatures.length < requiredSignatures(userOp)) { + return ERC4337Utils.SIG_VALIDATION_FAILED; + } + + address lastSigner = address(0); + uint48 globalValidAfter = 0; + uint48 globalValidUntil = 0; + + for (uint256 i = 0; i < signatures.length; ++i) { + (address signer, uint48 validAfter, uint48 validUntil) = _processSignature(signatures[i], userOpHash); + if (_isAuthorized(signer) && signer > lastSigner) { + lastSigner = signer; + globalValidAfter = uint48(Math.ternary(validUntil < globalValidUntil, globalValidUntil, validAfter)); + globalValidUntil = uint48( + Math.ternary(validUntil > globalValidUntil || validUntil == 0, globalValidUntil, validUntil) + ); + } else { + return ERC4337Utils.SIG_VALIDATION_FAILED; + } + } + return ERC4337Utils.packValidationData(true, globalValidAfter, globalValidUntil); + } +} diff --git a/contracts/abstraction/account/modules/AccountP256.sol b/contracts/abstraction/account/modules/AccountP256.sol index 855bcac9531..f3c10d638b4 100644 --- a/contracts/abstraction/account/modules/AccountP256.sol +++ b/contracts/abstraction/account/modules/AccountP256.sol @@ -11,22 +11,21 @@ abstract contract AccountP256 is Account { error P256InvalidSignatureLength(uint256 length); function _processSignature( - PackedUserOperation calldata userOp, + bytes memory signature, bytes32 userOpHash ) internal virtual override returns (address, uint48, uint48) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); // This implementation support signature that are 65 bytes long in the (R,S,V) format - bytes calldata signature = userOp.signature; if (signature.length == 65) { uint256 r; uint256 s; uint8 v; /// @solidity memory-safe-assembly assembly { - r := calldataload(add(signature.offset, 0x00)) - s := calldataload(add(signature.offset, 0x20)) - v := byte(0, calldataload(add(signature.offset, 0x40))) + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) } return (P256.recoveryAddress(uint256(msgHash), v, r, s), 0, 0); } else { diff --git a/contracts/abstraction/mocks/AdvancedAccount.sol b/contracts/abstraction/mocks/AdvancedAccount.sol new file mode 100644 index 00000000000..b6fdc90a28d --- /dev/null +++ b/contracts/abstraction/mocks/AdvancedAccount.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation, IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {AccessControl} from "../../access/AccessControl.sol"; +import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; +import {Address} from "../../utils/Address.sol"; +import {Account} from "../account/Account.sol"; +import {AccountMultisig} from "../account/modules/AccountMultisig.sol"; +import {AccountECDSA} from "../account/modules/AccountECDSA.sol"; +import {AccountP256} from "../account/modules/AccountP256.sol"; + +abstract contract AdvancedAccount is AccountMultisig, AccessControl, ERC721Holder, ERC1155Holder { + bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); + + IEntryPoint private immutable _entryPoint; + uint256 private _requiredSignatures; + + constructor(IEntryPoint entryPoint_, address admin_, address[] memory signers_, uint256 requiredSignatures_) { + _grantRole(DEFAULT_ADMIN_ROLE, admin_); + for (uint256 i = 0; i < signers_.length; ++i) { + _grantRole(SIGNER_ROLE, signers_[i]); + } + + _entryPoint = entryPoint_; + _requiredSignatures = requiredSignatures_; + } + + receive() external payable {} + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(AccessControl, ERC1155Holder) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + function requiredSignatures( + PackedUserOperation calldata /*userOp*/ + ) public view virtual override returns (uint256) { + return _requiredSignatures; + } + + function _isAuthorized(address user) internal view virtual override returns (bool) { + return hasRole(SIGNER_ROLE, user); + } + + function execute(address target, uint256 value, bytes calldata data) public virtual onlyEntryPoint { + _call(target, value, data); + } + + function executeBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata calldatas + ) public virtual onlyEntryPoint { + if (targets.length != calldatas.length || (values.length != 0 && values.length != targets.length)) { + revert AccountInvalidBatchLength(); + } + + for (uint256 i = 0; i < targets.length; ++i) { + _call(targets[i], (values.length == 0 ? 0 : values[i]), calldatas[i]); + } + } + + function _call(address target, uint256 value, bytes memory data) internal { + (bool success, bytes memory returndata) = target.call{value: value}(data); + Address.verifyCallResult(success, returndata); + } +} + +contract AdvancedAccountECDSA is AdvancedAccount, AccountECDSA { + constructor( + IEntryPoint entryPoint_, + address admin_, + address[] memory signers_, + uint256 requiredSignatures_ + ) AdvancedAccount(entryPoint_, admin_, signers_, requiredSignatures_) {} + + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override(Account, AccountMultisig) returns (uint256 validationData) { + return AccountMultisig._validateSignature(userOp, userOpHash); + } +} + +contract AdvancedAccountP256 is AdvancedAccount, AccountP256 { + constructor( + IEntryPoint entryPoint_, + address admin_, + address[] memory signers_, + uint256 requiredSignatures_ + ) AdvancedAccount(entryPoint_, admin_, signers_, requiredSignatures_) {} + + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override(Account, AccountMultisig) returns (uint256 validationData) { + return AccountMultisig._validateSignature(userOp, userOpHash); + } +} diff --git a/contracts/abstraction/mocks/SimpleAccount.sol b/contracts/abstraction/mocks/SimpleAccount.sol index 6b088406967..64456f6a1ee 100644 --- a/contracts/abstraction/mocks/SimpleAccount.sol +++ b/contracts/abstraction/mocks/SimpleAccount.sol @@ -14,7 +14,16 @@ import {AccountP256} from "../account/modules/AccountP256.sol"; abstract contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder { IEntryPoint private immutable _entryPoint; - constructor(IEntryPoint entryPoint_, address initialOwner) Ownable(initialOwner) { + error AccountUserRestricted(); + + modifier onlyOwnerOrEntryPoint() { + if (msg.sender != address(entryPoint()) && msg.sender != owner()) { + revert AccountUserRestricted(); + } + _; + } + + constructor(IEntryPoint entryPoint_, address owner_) Ownable(owner_) { _entryPoint = entryPoint_; } @@ -28,7 +37,7 @@ abstract contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder return user == owner(); } - function execute(address target, uint256 value, bytes calldata data) public virtual onlyAuthorizedOrEntryPoint { + function execute(address target, uint256 value, bytes calldata data) public virtual onlyOwnerOrEntryPoint { _call(target, value, data); } @@ -36,7 +45,7 @@ abstract contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder address[] calldata targets, uint256[] calldata values, bytes[] calldata calldatas - ) public virtual onlyAuthorizedOrEntryPoint { + ) public virtual onlyOwnerOrEntryPoint { if (targets.length != calldatas.length || (values.length != 0 && values.length != targets.length)) { revert AccountInvalidBatchLength(); } @@ -53,9 +62,9 @@ abstract contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder } contract SimpleAccountECDSA is SimpleAccount, AccountECDSA { - constructor(IEntryPoint entryPoint_, address initialOwner) SimpleAccount(entryPoint_, initialOwner) {} + constructor(IEntryPoint entryPoint_, address owner_) SimpleAccount(entryPoint_, owner_) {} } contract SimpleAccountP256 is SimpleAccount, AccountP256 { - constructor(IEntryPoint entryPoint_, address initialOwner) SimpleAccount(entryPoint_, initialOwner) {} + constructor(IEntryPoint entryPoint_, address owner_) SimpleAccount(entryPoint_, owner_) {} } diff --git a/test/abstraction/accountECDSA.test.js b/test/abstraction/accountECDSA.test.js index 7f50bdc88de..8c7fb875820 100644 --- a/test/abstraction/accountECDSA.test.js +++ b/test/abstraction/accountECDSA.test.js @@ -6,9 +6,13 @@ const { ERC4337Helper } = require('../helpers/erc4337'); async function fixture() { const accounts = await ethers.getSigners(); + accounts.user = accounts.shift(); + accounts.beneficiary = accounts.shift(); + const target = await ethers.deployContract('CallReceiverMock'); const helper = new ERC4337Helper('SimpleAccountECDSA'); await helper.wait(); + const sender = await helper.newAccount(accounts.user); return { accounts, @@ -16,20 +20,18 @@ async function fixture() { helper, entrypoint: helper.entrypoint, factory: helper.factory, + sender, }; } -describe('EntryPoint', function () { +describe('AccountECDSA', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); - this.user = this.accounts.shift(); - this.beneficiary = this.accounts.shift(); - this.sender = await this.helper.newAccount(this.user); }); describe('execute operation', function () { beforeEach('fund account', async function () { - await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); }); describe('account not deployed yet', function () { @@ -45,7 +47,7 @@ describe('EntryPoint', function () { .then(op => op.addInitCode()) .then(op => op.sign()); - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.entrypoint, 'AccountDeployed') .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) .to.emit(this.target, 'MockFunctionCalledExtra') @@ -69,7 +71,7 @@ describe('EntryPoint', function () { }) .then(op => op.sign()); - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.target, 'MockFunctionCalledExtra') .withArgs(this.sender, 42); }); @@ -88,7 +90,7 @@ describe('EntryPoint', function () { // compact signature operation.signature = ethers.Signature.from(operation.signature).compactSerialized; - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.target, 'MockFunctionCalledExtra') .withArgs(this.sender, 42); }); diff --git a/test/abstraction/accountMultisig.test.js b/test/abstraction/accountMultisig.test.js new file mode 100644 index 00000000000..1f0c33320a9 --- /dev/null +++ b/test/abstraction/accountMultisig.test.js @@ -0,0 +1,131 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { ERC4337Helper } = require('../helpers/erc4337'); + +async function fixture() { + const accounts = await ethers.getSigners(); + accounts.user = accounts.shift(); + accounts.beneficiary = accounts.shift(); + accounts.signers = Array(3) + .fill() + .map(() => accounts.shift()); + + const target = await ethers.deployContract('CallReceiverMock'); + const helper = new ERC4337Helper('AdvancedAccountECDSA'); + await helper.wait(); + const sender = await helper.newAccount(accounts.user, [accounts.signers, 2]); // 2-of-3 + + return { + accounts, + target, + helper, + entrypoint: helper.entrypoint, + factory: helper.factory, + sender, + }; +} + +describe('AccountMultisig', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('execute operation', function () { + beforeEach('fund account', async function () { + await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + }); + + describe('account not deployed yet', function () { + it('success: deploy and call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 17, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.addInitCode()) + .then(op => op.sign(this.accounts.signers)); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 17); + }); + }); + + describe('account already deployed', function () { + beforeEach(async function () { + await this.sender.deploy(); + }); + + it('success: 3 signers', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 42, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.sign(this.accounts.signers)); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + it('success: 2 signers', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 42, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.sign([this.accounts.signers[0], this.accounts.signers[2]])); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + it('revert: not enough signers', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 42, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.sign([this.accounts.signers[2]])); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, 'AA24 signature error'); + }); + + it('revert: unauthorized signer', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + this.target.target, + 42, + this.target.interface.encodeFunctionData('mockFunctionExtra'), + ]), + }) + .then(op => op.sign([this.accounts.user, this.accounts.signers[2]])); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, 'AA24 signature error'); + }); + }); + }); +}); diff --git a/test/abstraction/accountP256.test.js b/test/abstraction/accountP256.test.js index 015f4faed6f..07cd81982cf 100644 --- a/test/abstraction/accountP256.test.js +++ b/test/abstraction/accountP256.test.js @@ -7,9 +7,13 @@ const { P256Signer } = require('../helpers/p256'); async function fixture() { const accounts = await ethers.getSigners(); + accounts.user = accounts.shift(); + accounts.beneficiary = accounts.shift(); + const target = await ethers.deployContract('CallReceiverMock'); const helper = new ERC4337Helper('SimpleAccountP256'); await helper.wait(); + const sender = await helper.newAccount(P256Signer.random()); return { accounts, @@ -17,21 +21,18 @@ async function fixture() { helper, entrypoint: helper.entrypoint, factory: helper.factory, + sender, }; } -describe('EntryPoint', function () { +describe('AccountP256', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); - this.user = P256Signer.random(); - this.beneficiary = this.accounts.shift(); - this.other = this.accounts.shift(); - this.sender = await this.helper.newAccount(this.user); }); describe('execute operation', function () { beforeEach('fund account', async function () { - await this.other.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); }); describe('account not deployed yet', function () { @@ -47,7 +48,7 @@ describe('EntryPoint', function () { .then(op => op.addInitCode()) .then(op => op.sign()); - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.entrypoint, 'AccountDeployed') .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) .to.emit(this.target, 'MockFunctionCalledExtra') @@ -57,7 +58,7 @@ describe('EntryPoint', function () { describe('account already deployed', function () { beforeEach(async function () { - await this.sender.deploy(this.other); + await this.sender.deploy(this.accounts.user); }); it('success: call', async function () { @@ -71,7 +72,7 @@ describe('EntryPoint', function () { }) .then(op => op.sign()); - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.target, 'MockFunctionCalledExtra') .withArgs(this.sender, 42); }); diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js index b7ad9b69a6b..df73f560c8b 100644 --- a/test/abstraction/entrypoint.test.js +++ b/test/abstraction/entrypoint.test.js @@ -7,9 +7,13 @@ const { ERC4337Helper } = require('../helpers/erc4337'); async function fixture() { const accounts = await ethers.getSigners(); + accounts.user = accounts.shift(); + accounts.beneficiary = accounts.shift(); + const target = await ethers.deployContract('CallReceiverMock'); const helper = new ERC4337Helper(); await helper.wait(); + const sender = await helper.newAccount(accounts.user); return { accounts, @@ -17,21 +21,18 @@ async function fixture() { helper, entrypoint: helper.entrypoint, factory: helper.factory, + sender, }; } describe('EntryPoint', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); - - this.user = this.accounts.shift(); - this.beneficiary = this.accounts.shift(); - this.sender = await this.helper.newAccount(this.user); }); describe('deploy wallet contract', function () { it('success: counterfactual funding', async function () { - await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); @@ -40,9 +41,9 @@ describe('EntryPoint', function () { .then(op => op.addInitCode()) .then(op => op.sign()); - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.sender, 'OwnershipTransferred') - .withArgs(ethers.ZeroAddress, this.user) + .withArgs(ethers.ZeroAddress, this.accounts.user) .to.emit(this.factory, 'return$deploy') .withArgs(this.sender) .to.emit(this.entrypoint, 'AccountDeployed') @@ -63,13 +64,13 @@ describe('EntryPoint', function () { expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); - // const operation = await this.sender.createOp({ paymaster: this.user }) + // const operation = await this.sender.createOp({ paymaster: this.accounts.user }) // .then(op => op.addInitCode()) // .then(op => op.sign()); // - // await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + // await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) // .to.emit(this.sender, 'OwnershipTransferred') - // .withArgs(ethers.ZeroAddress, this.user) + // .withArgs(ethers.ZeroAddress, this.accounts.user) // .to.emit(this.factory, 'return$deploy') // .withArgs(this.sender) // .to.emit(this.entrypoint, 'AccountDeployed') @@ -94,7 +95,7 @@ describe('EntryPoint', function () { .then(op => op.addInitCode()) .then(op => op.sign()); - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') .withArgs(0, 'AA10 sender already constructed'); }); @@ -107,7 +108,7 @@ describe('EntryPoint', function () { .then(op => op.addInitCode()) .then(op => op.sign()); - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') .withArgs(0, "AA21 didn't pay prefund"); @@ -115,7 +116,7 @@ describe('EntryPoint', function () { }); it('error: AA25 invalid account nonce', async function () { - await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); @@ -124,7 +125,7 @@ describe('EntryPoint', function () { .then(op => op.addInitCode()) .then(op => op.sign()); - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') .withArgs(0, 'AA25 invalid account nonce'); @@ -134,7 +135,7 @@ describe('EntryPoint', function () { describe('execute operation', function () { beforeEach('fund account', async function () { - await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); }); describe('account not deployed yet', function () { @@ -150,7 +151,7 @@ describe('EntryPoint', function () { .then(op => op.addInitCode()) .then(op => op.sign()); - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.entrypoint, 'AccountDeployed') .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) .to.emit(this.target, 'MockFunctionCalledExtra') @@ -174,7 +175,7 @@ describe('EntryPoint', function () { }) .then(op => op.sign()); - await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.target, 'MockFunctionCalledExtra') .withArgs(this.sender, 42); }); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index a69944097ab..d225771ca0a 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -20,10 +20,10 @@ class ERC4337Helper { return this; } - async newAccount(user, salt = ethers.randomBytes(32)) { + async newAccount(user, extraArgs = [], salt = ethers.randomBytes(32)) { await this.wait(); const initCode = await this.account - .getDeployTransaction(this.entrypoint, user) + .getDeployTransaction(this.entrypoint, user, ...extraArgs) .then(tx => this.factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) .then(deployCode => ethers.concat([this.factory.target, deployCode])); const instance = await this.entrypoint.getSenderAddress @@ -129,7 +129,13 @@ class UserOperation { } async sign(signer = this.sender.runner) { - this.signature = await signer.signMessage(ethers.getBytes(this.hash)); + this.signature = await Promise.all( + (Array.isArray(signer) ? signer : [signer]) + .sort((signer1, signer2) => signer1.address - signer2.address) + .map(signer => signer.signMessage(ethers.getBytes(this.hash))), + ).then(signatures => + Array.isArray(signer) ? ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]'], [signatures]) : signatures[0], + ); return this; } } From ee47efc38d9e49a92af48436f7cec00d95f13f61 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 3 May 2024 15:49:01 +0200 Subject: [PATCH 43/66] update --- contracts/abstraction/mocks/AdvancedAccount.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/abstraction/mocks/AdvancedAccount.sol b/contracts/abstraction/mocks/AdvancedAccount.sol index b6fdc90a28d..011e579e2f4 100644 --- a/contracts/abstraction/mocks/AdvancedAccount.sol +++ b/contracts/abstraction/mocks/AdvancedAccount.sol @@ -86,6 +86,10 @@ contract AdvancedAccountECDSA is AdvancedAccount, AccountECDSA { PackedUserOperation calldata userOp, bytes32 userOpHash ) internal virtual override(Account, AccountMultisig) returns (uint256 validationData) { + // In this mock, calling super would work, but it may not depending on how the function is overriden by other + // modules. Using a more explicit override may bypass additional modules though. + // + // If possible, this should be improved. return AccountMultisig._validateSignature(userOp, userOpHash); } } @@ -102,6 +106,10 @@ contract AdvancedAccountP256 is AdvancedAccount, AccountP256 { PackedUserOperation calldata userOp, bytes32 userOpHash ) internal virtual override(Account, AccountMultisig) returns (uint256 validationData) { + // In this mock, calling super would work, but it may not depending on how the function is overriden by other + // modules. Using a more explicit override may bypass additional modules though. + // + // If possible, this should be improved. return AccountMultisig._validateSignature(userOp, userOpHash); } } From 003c232ab57fd310f13c38bc4d9c5b4dd5c8ffec Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sat, 4 May 2024 21:27:58 +0200 Subject: [PATCH 44/66] refactor signature processing --- contracts/abstraction/account/Account.sol | 50 +++++++++---------- .../account/modules/AccountECDSA.sol | 9 ++-- .../account/modules/AccountMultisig.sol | 25 ++++++---- .../account/modules/AccountP256.sol | 7 +-- .../abstraction/mocks/AdvancedAccount.sol | 28 ++++------- 5 files changed, 52 insertions(+), 67 deletions(-) diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol index 2d123accc23..286ae055ead 100644 --- a/contracts/abstraction/account/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -27,25 +27,27 @@ abstract contract Account is IAccount { /** * @dev Return the entryPoint used by this account. + * * Subclass should return the current entryPoint used by this account. */ function entryPoint() public view virtual returns (IEntryPoint); /** - * @dev Return weither an address (identity) is authorized to operate on this account. + * @dev Return weither an address (identity) is authorized to operate on this account. Depending on how the + * account is configured, this can be interpreted as either the owner of the account (if operating using a single + * owner -- default) or as an authorized signer if operating using as a multisig account. + * * Subclass must implement this using their own access control mechanism. */ function _isAuthorized(address) internal virtual returns (bool); /** - * @dev Return the recovered signer, and signature validity window. - * Subclass must implement this following their choice of cryptography. - * If a signature is ill-formed, address(0) should be returned. + * @dev Recover the signer for a given signature and user operation hash. This function does not need to verify + * that the recovered signer is authorized. + * + * Subclass must implement this using their own choice of cryptography. */ - function _processSignature( - bytes memory signature, - bytes32 userOpHash - ) internal virtual returns (address, uint48, uint48); + function _recoverSigner(bytes memory signature, bytes32 userOpHash) internal virtual returns (address); /**************************************************************************************************************** * Public interface * @@ -71,9 +73,10 @@ abstract contract Account is IAccount { bytes32 userOpHash, uint256 missingAccountFunds ) external virtual override onlyEntryPoint returns (uint256 validationData) { - validationData = _validateSignature(userOp, userOpHash); + (bool valid, , uint48 validAfter, uint48 validUntil) = _processSignature(userOp.signature, userOpHash); _validateNonce(userOp.nonce); _payPrefund(missingAccountFunds); + return ERC4337Utils.packValidationData(valid, validAfter, validUntil); } /**************************************************************************************************************** @@ -81,25 +84,20 @@ abstract contract Account is IAccount { ****************************************************************************************************************/ /** - * @dev Validate the signature is valid for this message. - * @param userOp - Validate the userOp.signature field. - * @param userOpHash - Convenient field: the hash of the request, to check the signature against. - * (also hashes the entrypoint and chain id) - * @return validationData - Signature and time-range of this operation. - * <20-byte> aggregatorOrSigFail - 0 for valid signature, 1 to mark signature failure, - * otherwise, an address of an aggregator contract. - * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" - * <6-byte> validAfter - first timestamp this operation is valid - * If the account doesn't use time-range, it is enough to return - * SIG_VALIDATION_FAILED value (1) for signature failure. - * Note that the validation code cannot use block.timestamp (or block.number) directly. + * @dev Process the signature is valid for this message. + * @param signature - The user's signature + * @param userOpHash - Hash of the request that must be signed (includes the entrypoint and chain id) + * @return valid - Signature is valid + * @return signer - Address of the signer that produced the signature + * @return validAfter - first timestamp this operation is valid + * @return validUntil - last timestamp this operation is valid. 0 for "indefinite" */ - function _validateSignature( - PackedUserOperation calldata userOp, + function _processSignature( + bytes memory signature, bytes32 userOpHash - ) internal virtual returns (uint256 validationData) { - (address signer, uint48 validAfter, uint48 validUntil) = _processSignature(userOp.signature, userOpHash); - return ERC4337Utils.packValidationData(signer != address(0) && _isAuthorized(signer), validAfter, validUntil); + ) internal virtual returns (bool valid, address signer, uint48 validAfter, uint48 validUntil) { + address recovered = _recoverSigner(signature, userOpHash); + return (_isAuthorized(recovered), recovered, 0, 0); } /** diff --git a/contracts/abstraction/account/modules/AccountECDSA.sol b/contracts/abstraction/account/modules/AccountECDSA.sol index 14b3a24d5d5..76d5a4e468a 100644 --- a/contracts/abstraction/account/modules/AccountECDSA.sol +++ b/contracts/abstraction/account/modules/AccountECDSA.sol @@ -8,10 +8,7 @@ import {ECDSA} from "../../../utils/cryptography/ECDSA.sol"; import {Account} from "../Account.sol"; abstract contract AccountECDSA is Account { - function _processSignature( - bytes memory signature, - bytes32 userOpHash - ) internal virtual override returns (address, uint48, uint48) { + function _recoverSigner(bytes memory signature, bytes32 userOpHash) internal virtual override returns (address) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); // This implementation support both "normal" and short signature formats: @@ -28,7 +25,7 @@ abstract contract AccountECDSA is Account { s := mload(add(signature, 0x40)) v := byte(0, mload(add(signature, 0x60))) } - return (ECDSA.recover(msgHash, v, r, s), 0, 0); + return ECDSA.recover(msgHash, v, r, s); } else if (signature.length == 64) { bytes32 r; bytes32 vs; @@ -37,7 +34,7 @@ abstract contract AccountECDSA is Account { r := mload(add(signature, 0x20)) vs := mload(add(signature, 0x40)) } - return (ECDSA.recover(msgHash, r, vs), 0, 0); + return ECDSA.recover(msgHash, r, vs); } else { revert ECDSA.ECDSAInvalidSignatureLength(signature.length); } diff --git a/contracts/abstraction/account/modules/AccountMultisig.sol b/contracts/abstraction/account/modules/AccountMultisig.sol index bef8b9603e4..3c84a0beed6 100644 --- a/contracts/abstraction/account/modules/AccountMultisig.sol +++ b/contracts/abstraction/account/modules/AccountMultisig.sol @@ -8,16 +8,16 @@ import {ERC4337Utils} from "./../../utils/ERC4337Utils.sol"; import {Account} from "../Account.sol"; abstract contract AccountMultisig is Account { - function requiredSignatures(PackedUserOperation calldata userOp) public view virtual returns (uint256); + function requiredSignatures() public view virtual returns (uint256); - function _validateSignature( - PackedUserOperation calldata userOp, + function _processSignature( + bytes memory signature, bytes32 userOpHash - ) internal virtual override returns (uint256 validationData) { - bytes[] memory signatures = abi.decode(userOp.signature, (bytes[])); + ) internal virtual override returns (bool, address, uint48, uint48) { + bytes[] memory signatures = abi.decode(signature, (bytes[])); - if (signatures.length < requiredSignatures(userOp)) { - return ERC4337Utils.SIG_VALIDATION_FAILED; + if (signatures.length < requiredSignatures()) { + return (false, address(0), 0, 0); } address lastSigner = address(0); @@ -25,17 +25,20 @@ abstract contract AccountMultisig is Account { uint48 globalValidUntil = 0; for (uint256 i = 0; i < signatures.length; ++i) { - (address signer, uint48 validAfter, uint48 validUntil) = _processSignature(signatures[i], userOpHash); - if (_isAuthorized(signer) && signer > lastSigner) { + (bool valid, address signer, uint48 validAfter, uint48 validUntil) = super._processSignature( + signatures[i], + userOpHash + ); + if (valid && signer > lastSigner) { lastSigner = signer; globalValidAfter = uint48(Math.ternary(validUntil < globalValidUntil, globalValidUntil, validAfter)); globalValidUntil = uint48( Math.ternary(validUntil > globalValidUntil || validUntil == 0, globalValidUntil, validUntil) ); } else { - return ERC4337Utils.SIG_VALIDATION_FAILED; + return (false, address(0), 0, 0); } } - return ERC4337Utils.packValidationData(true, globalValidAfter, globalValidUntil); + return (true, address(this), globalValidAfter, globalValidUntil); } } diff --git a/contracts/abstraction/account/modules/AccountP256.sol b/contracts/abstraction/account/modules/AccountP256.sol index f3c10d638b4..4726b36befd 100644 --- a/contracts/abstraction/account/modules/AccountP256.sol +++ b/contracts/abstraction/account/modules/AccountP256.sol @@ -10,10 +10,7 @@ import {Account} from "../Account.sol"; abstract contract AccountP256 is Account { error P256InvalidSignatureLength(uint256 length); - function _processSignature( - bytes memory signature, - bytes32 userOpHash - ) internal virtual override returns (address, uint48, uint48) { + function _recoverSigner(bytes memory signature, bytes32 userOpHash) internal virtual override returns (address) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); // This implementation support signature that are 65 bytes long in the (R,S,V) format @@ -27,7 +24,7 @@ abstract contract AccountP256 is Account { s := mload(add(signature, 0x40)) v := byte(0, mload(add(signature, 0x60))) } - return (P256.recoveryAddress(uint256(msgHash), v, r, s), 0, 0); + return P256.recoveryAddress(uint256(msgHash), v, r, s); } else { revert P256InvalidSignatureLength(signature.length); } diff --git a/contracts/abstraction/mocks/AdvancedAccount.sol b/contracts/abstraction/mocks/AdvancedAccount.sol index 011e579e2f4..5f60a41244b 100644 --- a/contracts/abstraction/mocks/AdvancedAccount.sol +++ b/contracts/abstraction/mocks/AdvancedAccount.sol @@ -40,9 +40,7 @@ abstract contract AdvancedAccount is AccountMultisig, AccessControl, ERC721Holde return _entryPoint; } - function requiredSignatures( - PackedUserOperation calldata /*userOp*/ - ) public view virtual override returns (uint256) { + function requiredSignatures() public view virtual override returns (uint256) { return _requiredSignatures; } @@ -82,15 +80,11 @@ contract AdvancedAccountECDSA is AdvancedAccount, AccountECDSA { uint256 requiredSignatures_ ) AdvancedAccount(entryPoint_, admin_, signers_, requiredSignatures_) {} - function _validateSignature( - PackedUserOperation calldata userOp, + function _processSignature( + bytes memory signature, bytes32 userOpHash - ) internal virtual override(Account, AccountMultisig) returns (uint256 validationData) { - // In this mock, calling super would work, but it may not depending on how the function is overriden by other - // modules. Using a more explicit override may bypass additional modules though. - // - // If possible, this should be improved. - return AccountMultisig._validateSignature(userOp, userOpHash); + ) internal virtual override(Account, AccountMultisig) returns (bool, address, uint48, uint48) { + return super._processSignature(signature, userOpHash); } } @@ -102,14 +96,10 @@ contract AdvancedAccountP256 is AdvancedAccount, AccountP256 { uint256 requiredSignatures_ ) AdvancedAccount(entryPoint_, admin_, signers_, requiredSignatures_) {} - function _validateSignature( - PackedUserOperation calldata userOp, + function _processSignature( + bytes memory signature, bytes32 userOpHash - ) internal virtual override(Account, AccountMultisig) returns (uint256 validationData) { - // In this mock, calling super would work, but it may not depending on how the function is overriden by other - // modules. Using a more explicit override may bypass additional modules though. - // - // If possible, this should be improved. - return AccountMultisig._validateSignature(userOp, userOpHash); + ) internal virtual override(Account, AccountMultisig) returns (bool, address, uint48, uint48) { + return super._processSignature(signature, userOpHash); } } From 318c372e6f2425f16be3b46ba3c25cdb32eb81e3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 13 May 2024 16:48:15 +0200 Subject: [PATCH 45/66] add AccountAllSignatures.sol that support ECDSA & P256 identities in the same account --- contracts/abstraction/account/Account.sol | 4 +-- .../account/modules/AccountAllSignatures.sol | 29 +++++++++++++++++++ .../account/modules/AccountECDSA.sol | 11 ++++--- .../account/modules/AccountP256.sol | 12 +++++++- 4 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 contracts/abstraction/account/modules/AccountAllSignatures.sol diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol index 286ae055ead..7582ce693a8 100644 --- a/contracts/abstraction/account/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -72,7 +72,7 @@ abstract contract Account is IAccount { PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds - ) external virtual override onlyEntryPoint returns (uint256 validationData) { + ) public virtual override onlyEntryPoint returns (uint256 validationData) { (bool valid, , uint48 validAfter, uint48 validUntil) = _processSignature(userOp.signature, userOpHash); _validateNonce(userOp.nonce); _payPrefund(missingAccountFunds); @@ -97,7 +97,7 @@ abstract contract Account is IAccount { bytes32 userOpHash ) internal virtual returns (bool valid, address signer, uint48 validAfter, uint48 validUntil) { address recovered = _recoverSigner(signature, userOpHash); - return (_isAuthorized(recovered), recovered, 0, 0); + return (recovered != address(0) && _isAuthorized(recovered), recovered, 0, 0); } /** diff --git a/contracts/abstraction/account/modules/AccountAllSignatures.sol b/contracts/abstraction/account/modules/AccountAllSignatures.sol new file mode 100644 index 00000000000..b05502cc736 --- /dev/null +++ b/contracts/abstraction/account/modules/AccountAllSignatures.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../../interfaces/IERC4337.sol"; +import {AccountECDSA} from "./AccountECDSA.sol"; +import {AccountP256} from "./AccountP256.sol"; + +abstract contract AccountAllSignatures is AccountECDSA, AccountP256 { + enum SignatureType { + ECDSA, // secp256k1 + P256 // secp256r1 + } + + function _recoverSigner( + bytes memory signature, + bytes32 userOpHash + ) internal virtual override(AccountECDSA, AccountP256) returns (address) { + (SignatureType sigType, bytes memory sigData) = abi.decode(signature, (SignatureType, bytes)); + + if (sigType == SignatureType.ECDSA) { + return AccountECDSA._recoverSigner(sigData, userOpHash); + } else if (sigType == SignatureType.P256) { + return AccountP256._recoverSigner(sigData, userOpHash); + } else { + return address(0); + } + } +} diff --git a/contracts/abstraction/account/modules/AccountECDSA.sol b/contracts/abstraction/account/modules/AccountECDSA.sol index 76d5a4e468a..81ab5b230f6 100644 --- a/contracts/abstraction/account/modules/AccountECDSA.sol +++ b/contracts/abstraction/account/modules/AccountECDSA.sol @@ -8,7 +8,10 @@ import {ECDSA} from "../../../utils/cryptography/ECDSA.sol"; import {Account} from "../Account.sol"; abstract contract AccountECDSA is Account { - function _recoverSigner(bytes memory signature, bytes32 userOpHash) internal virtual override returns (address) { + function _recoverSigner( + bytes memory signature, + bytes32 userOpHash + ) internal virtual override returns (address signer) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); // This implementation support both "normal" and short signature formats: @@ -25,7 +28,7 @@ abstract contract AccountECDSA is Account { s := mload(add(signature, 0x40)) v := byte(0, mload(add(signature, 0x60))) } - return ECDSA.recover(msgHash, v, r, s); + (signer, , ) = ECDSA.tryRecover(msgHash, v, r, s); // return address(0) on errors } else if (signature.length == 64) { bytes32 r; bytes32 vs; @@ -34,9 +37,9 @@ abstract contract AccountECDSA is Account { r := mload(add(signature, 0x20)) vs := mload(add(signature, 0x40)) } - return ECDSA.recover(msgHash, r, vs); + (signer, , ) = ECDSA.tryRecover(msgHash, r, vs); } else { - revert ECDSA.ECDSAInvalidSignatureLength(signature.length); + return address(0); } } } diff --git a/contracts/abstraction/account/modules/AccountP256.sol b/contracts/abstraction/account/modules/AccountP256.sol index 4726b36befd..43cf9e1c667 100644 --- a/contracts/abstraction/account/modules/AccountP256.sol +++ b/contracts/abstraction/account/modules/AccountP256.sol @@ -25,8 +25,18 @@ abstract contract AccountP256 is Account { v := byte(0, mload(add(signature, 0x60))) } return P256.recoveryAddress(uint256(msgHash), v, r, s); + } else if (signature.length == 96) { + uint256 qx; + uint256 r; + uint256 s; + /// @solidity memory-safe-assembly + assembly { + qx := mload(add(signature, 0x20)) + r := mload(add(signature, 0x40)) + s := mload(add(signature, 0x60)) + } } else { - revert P256InvalidSignatureLength(signature.length); + return address(0); } } } From 03dfa7131410a4015cb7b011d71b770f0478b87e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 May 2024 14:16:44 +0200 Subject: [PATCH 46/66] up --- .../abstraction/account/modules/AccountP256.sol | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/contracts/abstraction/account/modules/AccountP256.sol b/contracts/abstraction/account/modules/AccountP256.sol index 43cf9e1c667..ed1384d2117 100644 --- a/contracts/abstraction/account/modules/AccountP256.sol +++ b/contracts/abstraction/account/modules/AccountP256.sol @@ -25,16 +25,22 @@ abstract contract AccountP256 is Account { v := byte(0, mload(add(signature, 0x60))) } return P256.recoveryAddress(uint256(msgHash), v, r, s); - } else if (signature.length == 96) { + } else if (signature.length == 128) { uint256 qx; + uint256 qy; uint256 r; uint256 s; /// @solidity memory-safe-assembly assembly { qx := mload(add(signature, 0x20)) - r := mload(add(signature, 0x40)) - s := mload(add(signature, 0x60)) + qy := mload(add(signature, 0x40)) + r := mload(add(signature, 0x60)) + s := mload(add(signature, 0x80)) } + // can qx be reconstructed from qy to reduce size of signatures? + + // this can leverage EIP-7212 precompile if available + return P256.verify(uint256(msgHash), r, s, qx, qy) ? P256.getAddress(qx, qy) : address(0); } else { return address(0); } From 82d6bde6ac2954de5e4f37f2a6a2d9f2340783aa Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 20 May 2024 15:13:49 +0200 Subject: [PATCH 47/66] Add Account7702 --- contracts/abstraction/account/modules/Account7702.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 contracts/abstraction/account/modules/Account7702.sol diff --git a/contracts/abstraction/account/modules/Account7702.sol b/contracts/abstraction/account/modules/Account7702.sol new file mode 100644 index 00000000000..f13724fc049 --- /dev/null +++ b/contracts/abstraction/account/modules/Account7702.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Account} from "../Account.sol"; + +abstract contract Account7702 is Account { + function _isAuthorized(address user) internal view virtual override returns (bool) { + return user == address(this); + } +} From 612cadc1213c255f5147414b21b8dc68ce2ffdb1 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 21 May 2024 10:13:52 +0200 Subject: [PATCH 48/66] AccountCommon --- .../abstraction/account/AccountCommon.sol | 46 ++++++++++ .../abstraction/mocks/AdvancedAccount.sol | 83 ++++++++----------- contracts/abstraction/mocks/SimpleAccount.sol | 63 ++------------ 3 files changed, 89 insertions(+), 103 deletions(-) create mode 100644 contracts/abstraction/account/AccountCommon.sol diff --git a/contracts/abstraction/account/AccountCommon.sol b/contracts/abstraction/account/AccountCommon.sol new file mode 100644 index 00000000000..3c29ef4ef1b --- /dev/null +++ b/contracts/abstraction/account/AccountCommon.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; +import {Address} from "../../utils/Address.sol"; +import {Account} from "./Account.sol"; + +abstract contract AccountCommon is Account, ERC721Holder, ERC1155Holder { + IEntryPoint private immutable _entryPoint; + + constructor(IEntryPoint entryPoint_) { + _entryPoint = entryPoint_; + } + + receive() external payable {} + + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + function execute(address target, uint256 value, bytes calldata data) public virtual onlyEntryPoint { + _call(target, value, data); + } + + function executeBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata calldatas + ) public virtual onlyEntryPoint { + if (targets.length != calldatas.length || (values.length != 0 && values.length != targets.length)) { + revert AccountInvalidBatchLength(); + } + + for (uint256 i = 0; i < targets.length; ++i) { + _call(targets[i], (values.length == 0 ? 0 : values[i]), calldatas[i]); + } + } + + function _call(address target, uint256 value, bytes memory data) internal { + (bool success, bytes memory returndata) = target.call{value: value}(data); + Address.verifyCallResult(success, returndata); + } +} diff --git a/contracts/abstraction/mocks/AdvancedAccount.sol b/contracts/abstraction/mocks/AdvancedAccount.sol index 5f60a41244b..915304497d5 100644 --- a/contracts/abstraction/mocks/AdvancedAccount.sol +++ b/contracts/abstraction/mocks/AdvancedAccount.sol @@ -2,44 +2,38 @@ pragma solidity ^0.8.20; -import {PackedUserOperation, IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {IEntryPoint} from "../../interfaces/IERC4337.sol"; import {AccessControl} from "../../access/AccessControl.sol"; -import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; -import {Address} from "../../utils/Address.sol"; import {Account} from "../account/Account.sol"; +import {AccountCommon} from "../account/AccountCommon.sol"; import {AccountMultisig} from "../account/modules/AccountMultisig.sol"; import {AccountECDSA} from "../account/modules/AccountECDSA.sol"; import {AccountP256} from "../account/modules/AccountP256.sol"; -abstract contract AdvancedAccount is AccountMultisig, AccessControl, ERC721Holder, ERC1155Holder { +contract AdvancedAccountECDSA is AccessControl, AccountCommon, AccountECDSA, AccountMultisig { bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); - - IEntryPoint private immutable _entryPoint; uint256 private _requiredSignatures; - constructor(IEntryPoint entryPoint_, address admin_, address[] memory signers_, uint256 requiredSignatures_) { + constructor( + IEntryPoint entryPoint_, + address admin_, + address[] memory signers_, + uint256 requiredSignatures_ + ) AccountCommon(entryPoint_) { _grantRole(DEFAULT_ADMIN_ROLE, admin_); for (uint256 i = 0; i < signers_.length; ++i) { _grantRole(SIGNER_ROLE, signers_[i]); } - - _entryPoint = entryPoint_; _requiredSignatures = requiredSignatures_; } - receive() external payable {} - function supportsInterface( bytes4 interfaceId ) public view virtual override(AccessControl, ERC1155Holder) returns (bool) { return super.supportsInterface(interfaceId); } - function entryPoint() public view virtual override returns (IEntryPoint) { - return _entryPoint; - } - function requiredSignatures() public view virtual override returns (uint256) { return _requiredSignatures; } @@ -48,38 +42,6 @@ abstract contract AdvancedAccount is AccountMultisig, AccessControl, ERC721Holde return hasRole(SIGNER_ROLE, user); } - function execute(address target, uint256 value, bytes calldata data) public virtual onlyEntryPoint { - _call(target, value, data); - } - - function executeBatch( - address[] calldata targets, - uint256[] calldata values, - bytes[] calldata calldatas - ) public virtual onlyEntryPoint { - if (targets.length != calldatas.length || (values.length != 0 && values.length != targets.length)) { - revert AccountInvalidBatchLength(); - } - - for (uint256 i = 0; i < targets.length; ++i) { - _call(targets[i], (values.length == 0 ? 0 : values[i]), calldatas[i]); - } - } - - function _call(address target, uint256 value, bytes memory data) internal { - (bool success, bytes memory returndata) = target.call{value: value}(data); - Address.verifyCallResult(success, returndata); - } -} - -contract AdvancedAccountECDSA is AdvancedAccount, AccountECDSA { - constructor( - IEntryPoint entryPoint_, - address admin_, - address[] memory signers_, - uint256 requiredSignatures_ - ) AdvancedAccount(entryPoint_, admin_, signers_, requiredSignatures_) {} - function _processSignature( bytes memory signature, bytes32 userOpHash @@ -88,13 +50,36 @@ contract AdvancedAccountECDSA is AdvancedAccount, AccountECDSA { } } -contract AdvancedAccountP256 is AdvancedAccount, AccountP256 { +contract AdvancedAccountP256 is AccessControl, AccountCommon, AccountP256, AccountMultisig { + bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); + uint256 private _requiredSignatures; + constructor( IEntryPoint entryPoint_, address admin_, address[] memory signers_, uint256 requiredSignatures_ - ) AdvancedAccount(entryPoint_, admin_, signers_, requiredSignatures_) {} + ) AccountCommon(entryPoint_) { + _grantRole(DEFAULT_ADMIN_ROLE, admin_); + for (uint256 i = 0; i < signers_.length; ++i) { + _grantRole(SIGNER_ROLE, signers_[i]); + } + _requiredSignatures = requiredSignatures_; + } + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(AccessControl, ERC1155Holder) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function requiredSignatures() public view virtual override returns (uint256) { + return _requiredSignatures; + } + + function _isAuthorized(address user) internal view virtual override returns (bool) { + return hasRole(SIGNER_ROLE, user); + } function _processSignature( bytes memory signature, diff --git a/contracts/abstraction/mocks/SimpleAccount.sol b/contracts/abstraction/mocks/SimpleAccount.sol index 64456f6a1ee..76f5eb5986d 100644 --- a/contracts/abstraction/mocks/SimpleAccount.sol +++ b/contracts/abstraction/mocks/SimpleAccount.sol @@ -2,69 +2,24 @@ pragma solidity ^0.8.20; -import {PackedUserOperation, IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {IEntryPoint} from "../../interfaces/IERC4337.sol"; import {Ownable} from "../../access/Ownable.sol"; -import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; -import {Address} from "../../utils/Address.sol"; -import {Account} from "../account/Account.sol"; +import {AccountCommon} from "../account/AccountCommon.sol"; import {AccountECDSA} from "../account/modules/AccountECDSA.sol"; import {AccountP256} from "../account/modules/AccountP256.sol"; -abstract contract SimpleAccount is Account, Ownable, ERC721Holder, ERC1155Holder { - IEntryPoint private immutable _entryPoint; - - error AccountUserRestricted(); - - modifier onlyOwnerOrEntryPoint() { - if (msg.sender != address(entryPoint()) && msg.sender != owner()) { - revert AccountUserRestricted(); - } - _; - } - - constructor(IEntryPoint entryPoint_, address owner_) Ownable(owner_) { - _entryPoint = entryPoint_; - } - - receive() external payable {} - - function entryPoint() public view virtual override returns (IEntryPoint) { - return _entryPoint; - } +contract SimpleAccountECDSA is Ownable, AccountCommon, AccountECDSA { + constructor(IEntryPoint entryPoint_, address owner_) AccountCommon(entryPoint_) Ownable(owner_) {} function _isAuthorized(address user) internal view virtual override returns (bool) { return user == owner(); } - - function execute(address target, uint256 value, bytes calldata data) public virtual onlyOwnerOrEntryPoint { - _call(target, value, data); - } - - function executeBatch( - address[] calldata targets, - uint256[] calldata values, - bytes[] calldata calldatas - ) public virtual onlyOwnerOrEntryPoint { - if (targets.length != calldatas.length || (values.length != 0 && values.length != targets.length)) { - revert AccountInvalidBatchLength(); - } - - for (uint256 i = 0; i < targets.length; ++i) { - _call(targets[i], (values.length == 0 ? 0 : values[i]), calldatas[i]); - } - } - - function _call(address target, uint256 value, bytes memory data) internal { - (bool success, bytes memory returndata) = target.call{value: value}(data); - Address.verifyCallResult(success, returndata); - } } -contract SimpleAccountECDSA is SimpleAccount, AccountECDSA { - constructor(IEntryPoint entryPoint_, address owner_) SimpleAccount(entryPoint_, owner_) {} -} +contract SimpleAccountP256 is Ownable, AccountCommon, AccountP256 { + constructor(IEntryPoint entryPoint_, address owner_) AccountCommon(entryPoint_) Ownable(owner_) {} -contract SimpleAccountP256 is SimpleAccount, AccountP256 { - constructor(IEntryPoint entryPoint_, address owner_) SimpleAccount(entryPoint_, owner_) {} + function _isAuthorized(address user) internal view virtual override returns (bool) { + return user == owner(); + } } From b81817acfe6cd205e453fd8db025627440997ec9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 21 May 2024 13:06:31 +0200 Subject: [PATCH 49/66] rename --- .../account/modules/{Account7702.sol => AccountEIP7702.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/abstraction/account/modules/{Account7702.sol => AccountEIP7702.sol} (100%) diff --git a/contracts/abstraction/account/modules/Account7702.sol b/contracts/abstraction/account/modules/AccountEIP7702.sol similarity index 100% rename from contracts/abstraction/account/modules/Account7702.sol rename to contracts/abstraction/account/modules/AccountEIP7702.sol From 3696b7d598f592b233d3d3a86c9899c5ac66dd21 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 11 Jul 2024 00:40:26 +0200 Subject: [PATCH 50/66] Add clone variant with instance parameters stored in 'immutable storage' --- contracts/proxy/Clones.sol | 139 +++++++++++++++++++++++++ test/proxy/Clones.test.js | 202 ++++++++++++++++++++++++------------- 2 files changed, 269 insertions(+), 72 deletions(-) diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index d243d67f34b..c19618420a9 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -17,6 +17,8 @@ import {Errors} from "../utils/Errors.sol"; * deterministic method. */ library Clones { + error ImmutableArgsTooLarge(); + /** * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. * @@ -121,4 +123,141 @@ library Clones { ) internal view returns (address predicted) { return predictDeterministicAddress(implementation, salt, address(this)); } + + function cloneWithImmutableArgs(address implementation, bytes memory args) internal returns (address instance) { + return cloneWithImmutableArgs(implementation, args, 0); + } + + function cloneWithImmutableArgs( + address implementation, + bytes memory args, + uint256 value + ) internal returns (address instance) { + if (address(this).balance < value) { + revert Errors.InsufficientBalance(address(this).balance, value); + } + + uint256 extraLength = args.length; + uint256 codeLength = 0x2d + extraLength; + uint256 initLength = 0x38 + extraLength; + if (codeLength > 0xffff) revert ImmutableArgsTooLarge(); + + /// @solidity memory-safe-assembly + assembly { + // [ptr + 0x43] ...................................................................................................................................... // args + // [ptr + 0x23] ......................................................................00000000000000000000000000000000005af43d82803e903d91602b57fd5bf3...... // suffix + // [ptr + 0x14] ........................................000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.................................... // implementation + // [ptr + 0x00] 00000000000000000000003d61000080600a3d3981f3363d3d373d3d3d363d73............................................................................ // prefix + // [ptr + 0x0d] ..........................XX................................................................................................................ // length (part 1) + // [ptr + 0x0e] ............................XX.............................................................................................................. // length (part 2) + let ptr := mload(0x40) + mcopy(add(ptr, 0x43), add(args, 0x20), extraLength) + mstore(add(ptr, 0x23), 0x5af43d82803e903d91602b57fd5bf3) + mstore(add(ptr, 0x14), implementation) + mstore(add(ptr, 0x00), 0x3d61000080600b3d3981f3363d3d373d3d3d363d73) + mstore8(add(ptr, 0x0d), shr(8, codeLength)) + mstore8(add(ptr, 0x0e), shr(0, codeLength)) + instance := create(value, add(ptr, 0x0b), initLength) + } + + if (instance == address(0)) { + revert Errors.FailedDeployment(); + } + } + + function cloneWithImmutableArgsDeterministic( + address implementation, + bytes memory args, + bytes32 salt + ) internal returns (address instance) { + return cloneWithImmutableArgsDeterministic(implementation, args, salt, 0); + } + + function cloneWithImmutableArgsDeterministic( + address implementation, + bytes memory args, + bytes32 salt, + uint256 value + ) internal returns (address instance) { + if (address(this).balance < value) { + revert Errors.InsufficientBalance(address(this).balance, value); + } + + uint256 extraLength = args.length; + uint256 codeLength = 0x2d + extraLength; + uint256 initLength = 0x38 + extraLength; + if (codeLength > 0xffff) revert ImmutableArgsTooLarge(); + + /// @solidity memory-safe-assembly + assembly { + // [ptr + 0x43] ...................................................................................................................................... // args + // [ptr + 0x23] ......................................................................00000000000000000000000000000000005af43d82803e903d91602b57fd5bf3...... // suffix + // [ptr + 0x14] ........................................000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.................................... // implementation + // [ptr + 0x00] 00000000000000000000003d61000080600a3d3981f3363d3d373d3d3d363d73............................................................................ // prefix + // [ptr + 0x0d] ..........................XX................................................................................................................ // length (part 1) + // [ptr + 0x0e] ............................XX.............................................................................................................. // length (part 2) + let ptr := mload(0x40) + mcopy(add(ptr, 0x43), add(args, 0x20), extraLength) + mstore(add(ptr, 0x23), 0x5af43d82803e903d91602b57fd5bf3) + mstore(add(ptr, 0x14), implementation) + mstore(add(ptr, 0x00), 0x3d61000080600b3d3981f3363d3d373d3d3d363d73) + mstore8(add(ptr, 0x0d), shr(8, codeLength)) + mstore8(add(ptr, 0x0e), shr(0, codeLength)) + instance := create2(value, add(ptr, 0x0b), initLength, salt) + } + if (instance == address(0)) { + revert Errors.FailedDeployment(); + } + } + + function predictWithImmutableArgsDeterministicAddress( + address implementation, + bytes memory args, + bytes32 salt, + address deployer + ) internal pure returns (address predicted) { + uint256 extraLength = args.length; + uint256 codeLength = 0x2d + extraLength; + uint256 initLength = 0x38 + extraLength; + if (codeLength > 0xffff) revert ImmutableArgsTooLarge(); + + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(add(ptr, add(0x58, extraLength)), salt) + mstore(add(ptr, add(0x38, extraLength)), deployer) + mstore8(add(ptr, add(0x43, extraLength)), 0xff) + mcopy(add(ptr, 0x43), add(args, 0x20), extraLength) + mstore(add(ptr, 0x23), 0x5af43d82803e903d91602b57fd5bf3) + mstore(add(ptr, 0x14), implementation) + mstore(add(ptr, 0x00), 0x3d61000080600b3d3981f3363d3d373d3d3d363d73) + mstore8(add(ptr, 0x0d), shr(8, codeLength)) + mstore8(add(ptr, 0x0e), shr(0, codeLength)) + mstore(add(ptr, add(0x78, extraLength)), keccak256(add(ptr, 0x0b), initLength)) + predicted := and( + keccak256(add(ptr, add(0x43, extraLength)), 0x55), + 0xffffffffffffffffffffffffffffffffffffffff + ) + } + } + + function predictWithImmutableArgsDeterministicAddress( + address implementation, + bytes memory args, + bytes32 salt + ) internal view returns (address predicted) { + return predictWithImmutableArgsDeterministicAddress(implementation, args, salt, address(this)); + } + + function fetchCloneArgs(address instance) internal view returns (bytes memory result) { + uint256 argsLength = instance.code.length - 0x2d; // revert if length is too short + assembly { + // reserve space + result := mload(0x40) + mstore(0x40, add(result, add(0x20, argsLength))) + // load + mstore(result, argsLength) + extcodecopy(instance, add(result, 0x20), 0x2d, argsLength) + } + } } diff --git a/test/proxy/Clones.test.js b/test/proxy/Clones.test.js index 70220fbf7a0..6ad7d55ccae 100644 --- a/test/proxy/Clones.test.js +++ b/test/proxy/Clones.test.js @@ -10,30 +10,47 @@ async function fixture() { const factory = await ethers.deployContract('$Clones'); const implementation = await ethers.deployContract('DummyImplementation'); - const newClone = async (opts = {}) => { - const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address)); - const tx = await (opts.deployValue - ? factory.$clone(implementation, ethers.Typed.uint256(opts.deployValue)) - : factory.$clone(implementation)); - if (opts.initData || opts.initValue) { - await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' }); - } - return Object.assign(clone, { deploymentTransaction: () => tx }); - }; - - const newCloneDeterministic = async (opts = {}) => { - const salt = opts.salt ?? ethers.randomBytes(32); - const clone = await factory.$cloneDeterministic - .staticCall(implementation, salt) - .then(address => implementation.attach(address)); - const tx = await (opts.deployValue - ? factory.$cloneDeterministic(implementation, salt, ethers.Typed.uint256(opts.deployValue)) - : factory.$cloneDeterministic(implementation, salt)); - if (opts.initData || opts.initValue) { - await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' }); - } - return Object.assign(clone, { deploymentTransaction: () => tx }); - }; + const newClone = + args => + async (opts = {}) => { + const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address)); + const tx = await (opts.deployValue + ? args + ? factory.$cloneWithImmutableArgs(implementation, args, ethers.Typed.uint256(opts.deployValue)) + : factory.$clone(implementation, ethers.Typed.uint256(opts.deployValue)) + : args + ? factory.$cloneWithImmutableArgs(implementation, args) + : factory.$clone(implementation)); + if (opts.initData || opts.initValue) { + await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' }); + } + return Object.assign(clone, { deploymentTransaction: () => tx }); + }; + + const newCloneDeterministic = + args => + async (opts = {}) => { + const salt = opts.salt ?? ethers.randomBytes(32); + const clone = await factory.$cloneDeterministic + .staticCall(implementation, salt) + .then(address => implementation.attach(address)); + const tx = await (opts.deployValue + ? args + ? factory.$cloneWithImmutableArgsDeterministic( + implementation, + args, + salt, + ethers.Typed.uint256(opts.deployValue), + ) + : factory.$cloneDeterministic(implementation, salt, ethers.Typed.uint256(opts.deployValue)) + : args + ? factory.$cloneWithImmutableArgsDeterministic(implementation, args, salt) + : factory.$cloneDeterministic(implementation, salt)); + if (opts.initData || opts.initValue) { + await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' }); + } + return Object.assign(clone, { deploymentTransaction: () => tx }); + }; return { deployer, factory, implementation, newClone, newCloneDeterministic }; } @@ -43,53 +60,94 @@ describe('Clones', function () { Object.assign(this, await loadFixture(fixture)); }); - describe('clone', function () { - beforeEach(async function () { - this.createClone = this.newClone; + for (const args of [undefined, '0x', '0x11223344']) { + describe(args ? `with immutable args: ${args}` : 'without immutable args', function () { + describe('clone', function () { + beforeEach(async function () { + this.createClone = this.newClone(args); + }); + + shouldBehaveLikeClone(); + + it('get immutable arguments', async function () { + const instance = await this.createClone(); + expect(await this.factory.$fetchCloneArgs(instance)).to.equal(args ?? '0x'); + }); + }); + + describe('cloneDeterministic', function () { + beforeEach(async function () { + this.createClone = this.newCloneDeterministic(undefined); + }); + + shouldBehaveLikeClone(); + + it('revert if address already used', async function () { + const salt = ethers.randomBytes(32); + + const deployClone = () => + args + ? this.factory.$cloneWithImmutableArgsDeterministic(this.implementation, args, salt) + : this.factory.$cloneDeterministic(this.implementation, salt); + + // deploy once + await expect(deployClone()).to.not.be.reverted; + + // deploy twice + await expect(deployClone()).to.be.revertedWithCustomError(this.factory, 'FailedDeployment'); + }); + + it('address prediction', async function () { + const salt = ethers.randomBytes(32); + + if (args) { + const expected = ethers.getCreate2Address( + this.factory.target, + salt, + ethers.keccak256( + ethers.concat([ + '0x3d61', + ethers.toBeHex(0x2d + ethers.getBytes(args).length, 2), + '0x80600b3d3981f3363d3d373d3d3d363d73', + this.implementation.target, + '0x5af43d82803e903d91602b57fd5bf3', + args, + ]), + ), + ); + + const predicted = await this.factory.$predictWithImmutableArgsDeterministicAddress( + this.implementation, + args, + salt, + ); + expect(predicted).to.equal(expected); + + await expect(this.factory.$cloneWithImmutableArgsDeterministic(this.implementation, args, salt)) + .to.emit(this.factory, 'return$cloneWithImmutableArgsDeterministic_address_bytes_bytes32') + .withArgs(predicted); + } else { + const expected = ethers.getCreate2Address( + this.factory.target, + salt, + ethers.keccak256( + ethers.concat([ + '0x3d602d80600a3d3981f3363d3d373d3d3d363d73', + this.implementation.target, + '0x5af43d82803e903d91602b57fd5bf3', + ]), + ), + ); + + const predicted = await this.factory.$predictDeterministicAddress(this.implementation, salt); + expect(predicted).to.equal(expected); + + await expect(this.factory.$cloneDeterministic(this.implementation, salt)) + .to.emit(this.factory, 'return$cloneDeterministic_address_bytes32') + .withArgs(predicted); + } + }); + }); }); - - shouldBehaveLikeClone(); - }); - - describe('cloneDeterministic', function () { - beforeEach(async function () { - this.createClone = this.newCloneDeterministic; - }); - - shouldBehaveLikeClone(); - - it('revert if address already used', async function () { - const salt = ethers.randomBytes(32); - - // deploy once - await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.emit( - this.factory, - 'return$cloneDeterministic_address_bytes32', - ); - - // deploy twice - await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.be.revertedWithCustomError( - this.factory, - 'FailedDeployment', - ); - }); - - it('address prediction', async function () { - const salt = ethers.randomBytes(32); - - const creationCode = ethers.concat([ - '0x3d602d80600a3d3981f3363d3d373d3d3d363d73', - this.implementation.target, - '0x5af43d82803e903d91602b57fd5bf3', - ]); - - const predicted = await this.factory.$predictDeterministicAddress(this.implementation, salt); - const expected = ethers.getCreate2Address(this.factory.target, salt, ethers.keccak256(creationCode)); - expect(predicted).to.equal(expected); - - await expect(this.factory.$cloneDeterministic(this.implementation, salt)) - .to.emit(this.factory, 'return$cloneDeterministic_address_bytes32') - .withArgs(predicted); - }); - }); + } }); From 6121a839103e977f7a977c0263914c16476454ca Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 11 Jul 2024 10:21:21 +0200 Subject: [PATCH 51/66] add RSA and P256 immutable identity contracts --- contracts/identity/IdentityP256.sol | 46 +++++++++++++++++++++++++++++ contracts/identity/IdentityRSA.sol | 42 ++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 contracts/identity/IdentityP256.sol create mode 100644 contracts/identity/IdentityRSA.sol diff --git a/contracts/identity/IdentityP256.sol b/contracts/identity/IdentityP256.sol new file mode 100644 index 00000000000..311fb091a72 --- /dev/null +++ b/contracts/identity/IdentityP256.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC1271} from "../interfaces/IERC1271.sol"; +import {Clones} from "../proxy/Clones.sol"; +import {P256} from "../utils/cryptography/P256.sol"; + +contract IdentityP256Implementation is IERC1271 { + function publicKey() public view returns (bytes32 qx, bytes32 qy) { + return abi.decode(Clones.fetchCloneArgs(address(this)), (bytes32, bytes32)); + } + + function isValidSignature(bytes32 h, bytes memory signature) external view returns (bytes4 magicValue) { + // fetch immutable public key for the clone + (bytes32 qx, bytes32 qy) = publicKey(); + + bytes32 r; + bytes32 s; + assembly ("memory-safe") { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + } + + return P256.verify(h, r, s, qx, qy) ? IERC1271.isValidSignature.selector : bytes4(0); + } +} + +contract IdentityP256Factory { + address public immutable implementation = address(new IdentityP256Implementation()); + + function create(bytes32 qx, bytes32 qy) public returns (address instance) { + // predict the address of the instance for that key + address predicted = predict(qx, qy); + // if instance does not exist ... + if (predicted.code.length == 0) { + // ... deploy it + Clones.cloneWithImmutableArgsDeterministic(implementation, abi.encode(qx, qy), bytes32(0)); + } + return predicted; + } + + function predict(bytes32 qx, bytes32 qy) public view returns (address instance) { + return Clones.predictWithImmutableArgsDeterministicAddress(implementation, abi.encode(qx, qy), bytes32(0)); + } +} diff --git a/contracts/identity/IdentityRSA.sol b/contracts/identity/IdentityRSA.sol new file mode 100644 index 00000000000..4ab81acfce6 --- /dev/null +++ b/contracts/identity/IdentityRSA.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC1271} from "../interfaces/IERC1271.sol"; +import {Clones} from "../proxy/Clones.sol"; +import {RSA} from "../utils/cryptography/RSA.sol"; + +contract IdentityRSAImplementation is IERC1271 { + function publicKey() public view returns (bytes memory e, bytes memory n) { + return abi.decode(Clones.fetchCloneArgs(address(this)), (bytes, bytes)); + } + + function isValidSignature(bytes32 h, bytes memory signature) external view returns (bytes4 magicValue) { + // fetch immutable public key for the clone + (bytes memory e, bytes memory n) = publicKey(); + + // here we don't use pkcs1 directly, because `h` is likely not the result of a sha256 hash, but rather of a + // keccak256 one. This means RSA signers should compute the "ethereum" keccak256 hash of the data, and re-hash + // it using sha256 + return RSA.pkcs1Sha256(abi.encode(h), signature, e, n) ? IERC1271.isValidSignature.selector : bytes4(0); + } +} + +contract IdentityRSAFactory { + address public immutable implementation = address(new IdentityRSAImplementation()); + + function create(bytes memory e, bytes memory n) public returns (address instance) { + // predict the address of the instance for that key + address predicted = predict(e, n); + // if instance does not exist ... + if (predicted.code.length == 0) { + // ... deploy it + Clones.cloneWithImmutableArgsDeterministic(implementation, abi.encode(e, n), bytes32(0)); + } + return predicted; + } + + function predict(bytes memory e, bytes memory n) public view returns (address instance) { + return Clones.predictWithImmutableArgsDeterministicAddress(implementation, abi.encode(e, n), bytes32(0)); + } +} From 5745f600918395b1618603e7bb210ccd36489237 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 11 Jul 2024 14:29:41 +0200 Subject: [PATCH 52/66] erc1271 recovery --- .../account/modules/AccountEIP7702.sol | 4 +- .../account/modules/AccountP256.sol | 48 ------------------- .../{ => recovery}/AccountAllSignatures.sol | 14 +++--- .../modules/{ => recovery}/AccountECDSA.sol | 8 ++-- .../modules/recovery/AccountERC1271.sol | 21 ++++++++ .../identity/IdentityP256.sol | 24 +++++----- .../identity/IdentityRSA.sol | 6 +-- .../abstraction/mocks/AdvancedAccount.sol | 6 +-- contracts/abstraction/mocks/SimpleAccount.sol | 6 +-- ...untP256.test.js => accountERC1271.test.js} | 18 +++++-- test/helpers/p256.js | 20 +++++++- 11 files changed, 87 insertions(+), 88 deletions(-) delete mode 100644 contracts/abstraction/account/modules/AccountP256.sol rename contracts/abstraction/account/modules/{ => recovery}/AccountAllSignatures.sol (52%) rename contracts/abstraction/account/modules/{ => recovery}/AccountECDSA.sol (85%) create mode 100644 contracts/abstraction/account/modules/recovery/AccountERC1271.sol rename contracts/{ => abstraction}/identity/IdentityP256.sol (60%) rename contracts/{ => abstraction}/identity/IdentityRSA.sol (91%) rename test/abstraction/{accountP256.test.js => accountERC1271.test.js} (82%) diff --git a/contracts/abstraction/account/modules/AccountEIP7702.sol b/contracts/abstraction/account/modules/AccountEIP7702.sol index f13724fc049..5bf5423fb5f 100644 --- a/contracts/abstraction/account/modules/AccountEIP7702.sol +++ b/contracts/abstraction/account/modules/AccountEIP7702.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.20; -import {Account} from "../Account.sol"; +import {AccountECDSA} from "./recovery/AccountECDSA.sol"; -abstract contract Account7702 is Account { +abstract contract Account7702 is AccountECDSA { function _isAuthorized(address user) internal view virtual override returns (bool) { return user == address(this); } diff --git a/contracts/abstraction/account/modules/AccountP256.sol b/contracts/abstraction/account/modules/AccountP256.sol deleted file mode 100644 index ed1384d2117..00000000000 --- a/contracts/abstraction/account/modules/AccountP256.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {PackedUserOperation} from "../../../interfaces/IERC4337.sol"; -import {MessageHashUtils} from "../../../utils/cryptography/MessageHashUtils.sol"; -import {P256} from "../../../utils/cryptography/P256.sol"; -import {Account} from "../Account.sol"; - -abstract contract AccountP256 is Account { - error P256InvalidSignatureLength(uint256 length); - - function _recoverSigner(bytes memory signature, bytes32 userOpHash) internal virtual override returns (address) { - bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); - - // This implementation support signature that are 65 bytes long in the (R,S,V) format - if (signature.length == 65) { - uint256 r; - uint256 s; - uint8 v; - /// @solidity memory-safe-assembly - assembly { - r := mload(add(signature, 0x20)) - s := mload(add(signature, 0x40)) - v := byte(0, mload(add(signature, 0x60))) - } - return P256.recoveryAddress(uint256(msgHash), v, r, s); - } else if (signature.length == 128) { - uint256 qx; - uint256 qy; - uint256 r; - uint256 s; - /// @solidity memory-safe-assembly - assembly { - qx := mload(add(signature, 0x20)) - qy := mload(add(signature, 0x40)) - r := mload(add(signature, 0x60)) - s := mload(add(signature, 0x80)) - } - // can qx be reconstructed from qy to reduce size of signatures? - - // this can leverage EIP-7212 precompile if available - return P256.verify(uint256(msgHash), r, s, qx, qy) ? P256.getAddress(qx, qy) : address(0); - } else { - return address(0); - } - } -} diff --git a/contracts/abstraction/account/modules/AccountAllSignatures.sol b/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol similarity index 52% rename from contracts/abstraction/account/modules/AccountAllSignatures.sol rename to contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol index b05502cc736..5bfd7e78c6d 100644 --- a/contracts/abstraction/account/modules/AccountAllSignatures.sol +++ b/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol @@ -2,26 +2,26 @@ pragma solidity ^0.8.20; -import {PackedUserOperation} from "../../../interfaces/IERC4337.sol"; +import {PackedUserOperation} from "../../../../interfaces/IERC4337.sol"; import {AccountECDSA} from "./AccountECDSA.sol"; -import {AccountP256} from "./AccountP256.sol"; +import {AccountERC1271} from "./AccountERC1271.sol"; -abstract contract AccountAllSignatures is AccountECDSA, AccountP256 { +abstract contract AccountAllSignatures is AccountECDSA, AccountERC1271 { enum SignatureType { ECDSA, // secp256k1 - P256 // secp256r1 + ERC1271 // others through erc1271 identity (support P256, RSA, ...) } function _recoverSigner( bytes memory signature, bytes32 userOpHash - ) internal virtual override(AccountECDSA, AccountP256) returns (address) { + ) internal virtual override(AccountECDSA, AccountERC1271) returns (address) { (SignatureType sigType, bytes memory sigData) = abi.decode(signature, (SignatureType, bytes)); if (sigType == SignatureType.ECDSA) { return AccountECDSA._recoverSigner(sigData, userOpHash); - } else if (sigType == SignatureType.P256) { - return AccountP256._recoverSigner(sigData, userOpHash); + } else if (sigType == SignatureType.ERC1271) { + return AccountERC1271._recoverSigner(sigData, userOpHash); } else { return address(0); } diff --git a/contracts/abstraction/account/modules/AccountECDSA.sol b/contracts/abstraction/account/modules/recovery/AccountECDSA.sol similarity index 85% rename from contracts/abstraction/account/modules/AccountECDSA.sol rename to contracts/abstraction/account/modules/recovery/AccountECDSA.sol index 81ab5b230f6..0ece7006267 100644 --- a/contracts/abstraction/account/modules/AccountECDSA.sol +++ b/contracts/abstraction/account/modules/recovery/AccountECDSA.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.20; -import {PackedUserOperation} from "../../../interfaces/IERC4337.sol"; -import {MessageHashUtils} from "../../../utils/cryptography/MessageHashUtils.sol"; -import {ECDSA} from "../../../utils/cryptography/ECDSA.sol"; -import {Account} from "../Account.sol"; +import {PackedUserOperation} from "../../../../interfaces/IERC4337.sol"; +import {MessageHashUtils} from "../../../../utils/cryptography/MessageHashUtils.sol"; +import {ECDSA} from "../../../../utils/cryptography/ECDSA.sol"; +import {Account} from "../../Account.sol"; abstract contract AccountECDSA is Account { function _recoverSigner( diff --git a/contracts/abstraction/account/modules/recovery/AccountERC1271.sol b/contracts/abstraction/account/modules/recovery/AccountERC1271.sol new file mode 100644 index 00000000000..91df58ed346 --- /dev/null +++ b/contracts/abstraction/account/modules/recovery/AccountERC1271.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../../../interfaces/IERC4337.sol"; +import {MessageHashUtils} from "../../../../utils/cryptography/MessageHashUtils.sol"; +import {SignatureChecker} from "../../../../utils/cryptography/SignatureChecker.sol"; +import {Account} from "../../Account.sol"; + +abstract contract AccountERC1271 is Account { + error P256InvalidSignatureLength(uint256 length); + + function _recoverSigner(bytes memory signature, bytes32 userOpHash) internal virtual override returns (address) { + bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); + (address signer, bytes memory sig) = abi.decode(signature, (address, bytes)); + + return SignatureChecker.isValidERC1271SignatureNow(signer, msgHash, sig) + ? signer + : address(0); + } +} diff --git a/contracts/identity/IdentityP256.sol b/contracts/abstraction/identity/IdentityP256.sol similarity index 60% rename from contracts/identity/IdentityP256.sol rename to contracts/abstraction/identity/IdentityP256.sol index 311fb091a72..d520a011a54 100644 --- a/contracts/identity/IdentityP256.sol +++ b/contracts/abstraction/identity/IdentityP256.sol @@ -2,18 +2,18 @@ pragma solidity ^0.8.20; -import {IERC1271} from "../interfaces/IERC1271.sol"; -import {Clones} from "../proxy/Clones.sol"; -import {P256} from "../utils/cryptography/P256.sol"; +import {IERC1271} from "../../interfaces/IERC1271.sol"; +import {Clones} from "../../proxy/Clones.sol"; +import {P256} from "../../utils/cryptography/P256.sol"; contract IdentityP256Implementation is IERC1271 { - function publicKey() public view returns (bytes32 qx, bytes32 qy) { - return abi.decode(Clones.fetchCloneArgs(address(this)), (bytes32, bytes32)); + function publicKey() public view returns (bytes memory) { + return Clones.fetchCloneArgs(address(this)); } function isValidSignature(bytes32 h, bytes memory signature) external view returns (bytes4 magicValue) { - // fetch immutable public key for the clone - (bytes32 qx, bytes32 qy) = publicKey(); + // fetch and decode immutable public key for the clone + (bytes32 qx, bytes32 qy) = abi.decode(publicKey(), (bytes32, bytes32)); bytes32 r; bytes32 s; @@ -29,18 +29,18 @@ contract IdentityP256Implementation is IERC1271 { contract IdentityP256Factory { address public immutable implementation = address(new IdentityP256Implementation()); - function create(bytes32 qx, bytes32 qy) public returns (address instance) { + function create(bytes memory publicKey) public returns (address instance) { // predict the address of the instance for that key - address predicted = predict(qx, qy); + address predicted = predict(publicKey); // if instance does not exist ... if (predicted.code.length == 0) { // ... deploy it - Clones.cloneWithImmutableArgsDeterministic(implementation, abi.encode(qx, qy), bytes32(0)); + Clones.cloneWithImmutableArgsDeterministic(implementation, publicKey, bytes32(0)); } return predicted; } - function predict(bytes32 qx, bytes32 qy) public view returns (address instance) { - return Clones.predictWithImmutableArgsDeterministicAddress(implementation, abi.encode(qx, qy), bytes32(0)); + function predict(bytes memory publicKey) public view returns (address instance) { + return Clones.predictWithImmutableArgsDeterministicAddress(implementation, publicKey, bytes32(0)); } } diff --git a/contracts/identity/IdentityRSA.sol b/contracts/abstraction/identity/IdentityRSA.sol similarity index 91% rename from contracts/identity/IdentityRSA.sol rename to contracts/abstraction/identity/IdentityRSA.sol index 4ab81acfce6..b9d721c5a5f 100644 --- a/contracts/identity/IdentityRSA.sol +++ b/contracts/abstraction/identity/IdentityRSA.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.20; -import {IERC1271} from "../interfaces/IERC1271.sol"; -import {Clones} from "../proxy/Clones.sol"; -import {RSA} from "../utils/cryptography/RSA.sol"; +import {IERC1271} from "../../interfaces/IERC1271.sol"; +import {Clones} from "../../proxy/Clones.sol"; +import {RSA} from "../../utils/cryptography/RSA.sol"; contract IdentityRSAImplementation is IERC1271 { function publicKey() public view returns (bytes memory e, bytes memory n) { diff --git a/contracts/abstraction/mocks/AdvancedAccount.sol b/contracts/abstraction/mocks/AdvancedAccount.sol index 915304497d5..642ebdf12bf 100644 --- a/contracts/abstraction/mocks/AdvancedAccount.sol +++ b/contracts/abstraction/mocks/AdvancedAccount.sol @@ -8,8 +8,8 @@ import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; import {Account} from "../account/Account.sol"; import {AccountCommon} from "../account/AccountCommon.sol"; import {AccountMultisig} from "../account/modules/AccountMultisig.sol"; -import {AccountECDSA} from "../account/modules/AccountECDSA.sol"; -import {AccountP256} from "../account/modules/AccountP256.sol"; +import {AccountECDSA} from "../account/modules/recovery/AccountECDSA.sol"; +import {AccountERC1271} from "../account/modules/recovery/AccountERC1271.sol"; contract AdvancedAccountECDSA is AccessControl, AccountCommon, AccountECDSA, AccountMultisig { bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); @@ -50,7 +50,7 @@ contract AdvancedAccountECDSA is AccessControl, AccountCommon, AccountECDSA, Acc } } -contract AdvancedAccountP256 is AccessControl, AccountCommon, AccountP256, AccountMultisig { +contract AdvancedAccountERC1271 is AccessControl, AccountCommon, AccountERC1271, AccountMultisig { bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); uint256 private _requiredSignatures; diff --git a/contracts/abstraction/mocks/SimpleAccount.sol b/contracts/abstraction/mocks/SimpleAccount.sol index 76f5eb5986d..34bd27fdf96 100644 --- a/contracts/abstraction/mocks/SimpleAccount.sol +++ b/contracts/abstraction/mocks/SimpleAccount.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.20; import {IEntryPoint} from "../../interfaces/IERC4337.sol"; import {Ownable} from "../../access/Ownable.sol"; import {AccountCommon} from "../account/AccountCommon.sol"; -import {AccountECDSA} from "../account/modules/AccountECDSA.sol"; -import {AccountP256} from "../account/modules/AccountP256.sol"; +import {AccountECDSA} from "../account/modules/recovery/AccountECDSA.sol"; +import {AccountERC1271} from "../account/modules/recovery/AccountERC1271.sol"; contract SimpleAccountECDSA is Ownable, AccountCommon, AccountECDSA { constructor(IEntryPoint entryPoint_, address owner_) AccountCommon(entryPoint_) Ownable(owner_) {} @@ -16,7 +16,7 @@ contract SimpleAccountECDSA is Ownable, AccountCommon, AccountECDSA { } } -contract SimpleAccountP256 is Ownable, AccountCommon, AccountP256 { +contract SimpleAccountERC1271 is Ownable, AccountCommon, AccountERC1271 { constructor(IEntryPoint entryPoint_, address owner_) AccountCommon(entryPoint_) Ownable(owner_) {} function _isAuthorized(address user) internal view virtual override returns (bool) { diff --git a/test/abstraction/accountP256.test.js b/test/abstraction/accountERC1271.test.js similarity index 82% rename from test/abstraction/accountP256.test.js rename to test/abstraction/accountERC1271.test.js index 07cd81982cf..a16dfb6f752 100644 --- a/test/abstraction/accountP256.test.js +++ b/test/abstraction/accountERC1271.test.js @@ -10,10 +10,20 @@ async function fixture() { accounts.user = accounts.shift(); accounts.beneficiary = accounts.shift(); - const target = await ethers.deployContract('CallReceiverMock'); - const helper = new ERC4337Helper('SimpleAccountP256'); + // 4337 helper + const helper = new ERC4337Helper('SimpleAccountERC1271'); await helper.wait(); - const sender = await helper.newAccount(P256Signer.random()); + + // environment + const target = await ethers.deployContract('CallReceiverMock'); + const identifyFactory = await ethers.deployContract('IdentityP256Factory'); + + // create P256 key and identity contract + const signer = P256Signer.random(); + signer.address = await identifyFactory.predict(signer.publicKey); // override address of the signer + signer.sigParams.prefixAddress = true; + await identifyFactory.create(signer.publicKey); + const sender = await helper.newAccount(signer); return { accounts, @@ -25,7 +35,7 @@ async function fixture() { }; } -describe('AccountP256', function () { +describe('AccountERC1271', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); diff --git a/test/helpers/p256.js b/test/helpers/p256.js index 12190349740..8ee2436c74e 100644 --- a/test/helpers/p256.js +++ b/test/helpers/p256.js @@ -1,6 +1,8 @@ const { ethers } = require('hardhat'); const { secp256r1 } = require('@noble/curves/p256'); +const N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n; + class P256Signer { constructor(privateKey) { this.privateKey = privateKey; @@ -11,6 +13,7 @@ class P256Signer { ].map(ethers.hexlify), ); this.address = ethers.getAddress(ethers.keccak256(this.publicKey).slice(-40)); + this.sigParams = { prefixAddress: false, includeRecovery: true }; } static random() { @@ -22,8 +25,21 @@ class P256Signer { } signMessage(message) { - const { r, s, recovery } = secp256r1.sign(ethers.hashMessage(message).replace(/0x/, ''), this.privateKey); - return ethers.solidityPacked(['uint256', 'uint256', 'uint8'], [r, s, recovery]); + let { r, s, recovery } = secp256r1.sign(ethers.hashMessage(message).replace(/0x/, ''), this.privateKey); + + // ensureLowerOrderS + if (s > N / 2n) { + s = N - s; + recovery = 1 - recovery; + } + + // pack signature + const signature = this.sigParams.includeRecovery + ? ethers.solidityPacked(['uint256', 'uint256', 'uint8'], [r, s, recovery]) + : ethers.solidityPacked(['uint256', 'uint256'], [r, s]); + return this.sigParams.prefixAddress + ? ethers.AbiCoder.defaultAbiCoder().encode([ 'address', 'bytes' ], [ this.address, signature ]) + : signature; } } From add996164f876f6673fc0ddd44005542b566812c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 12 Jul 2024 01:56:09 +0200 Subject: [PATCH 53/66] identify / multisig with ERC1271 --- .../modules/recovery/AccountAllSignatures.sol | 1 - .../modules/recovery/AccountERC1271.sol | 4 +- .../abstraction/mocks/AdvancedAccount.sol | 44 +------------------ test/abstraction/accountECDSA.test.js | 19 +++++--- test/abstraction/accountERC1271.test.js | 19 ++++---- test/abstraction/accountMultisig.test.js | 36 +++++++++------ test/helpers/erc4337.js | 38 +++++++++++----- test/helpers/identity.js | 43 ++++++++++++++++++ test/helpers/p256.js | 2 +- 9 files changed, 119 insertions(+), 87 deletions(-) create mode 100644 test/helpers/identity.js diff --git a/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol b/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol index 5bfd7e78c6d..471a9b680e4 100644 --- a/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol +++ b/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol @@ -17,7 +17,6 @@ abstract contract AccountAllSignatures is AccountECDSA, AccountERC1271 { bytes32 userOpHash ) internal virtual override(AccountECDSA, AccountERC1271) returns (address) { (SignatureType sigType, bytes memory sigData) = abi.decode(signature, (SignatureType, bytes)); - if (sigType == SignatureType.ECDSA) { return AccountECDSA._recoverSigner(sigData, userOpHash); } else if (sigType == SignatureType.ERC1271) { diff --git a/contracts/abstraction/account/modules/recovery/AccountERC1271.sol b/contracts/abstraction/account/modules/recovery/AccountERC1271.sol index 91df58ed346..bc2eb152fd7 100644 --- a/contracts/abstraction/account/modules/recovery/AccountERC1271.sol +++ b/contracts/abstraction/account/modules/recovery/AccountERC1271.sol @@ -14,8 +14,6 @@ abstract contract AccountERC1271 is Account { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); (address signer, bytes memory sig) = abi.decode(signature, (address, bytes)); - return SignatureChecker.isValidERC1271SignatureNow(signer, msgHash, sig) - ? signer - : address(0); + return SignatureChecker.isValidERC1271SignatureNow(signer, msgHash, sig) ? signer : address(0); } } diff --git a/contracts/abstraction/mocks/AdvancedAccount.sol b/contracts/abstraction/mocks/AdvancedAccount.sol index 642ebdf12bf..dc4dd237a16 100644 --- a/contracts/abstraction/mocks/AdvancedAccount.sol +++ b/contracts/abstraction/mocks/AdvancedAccount.sol @@ -8,49 +8,9 @@ import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; import {Account} from "../account/Account.sol"; import {AccountCommon} from "../account/AccountCommon.sol"; import {AccountMultisig} from "../account/modules/AccountMultisig.sol"; -import {AccountECDSA} from "../account/modules/recovery/AccountECDSA.sol"; -import {AccountERC1271} from "../account/modules/recovery/AccountERC1271.sol"; +import {AccountAllSignatures} from "../account/modules/recovery/AccountAllSignatures.sol"; -contract AdvancedAccountECDSA is AccessControl, AccountCommon, AccountECDSA, AccountMultisig { - bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); - uint256 private _requiredSignatures; - - constructor( - IEntryPoint entryPoint_, - address admin_, - address[] memory signers_, - uint256 requiredSignatures_ - ) AccountCommon(entryPoint_) { - _grantRole(DEFAULT_ADMIN_ROLE, admin_); - for (uint256 i = 0; i < signers_.length; ++i) { - _grantRole(SIGNER_ROLE, signers_[i]); - } - _requiredSignatures = requiredSignatures_; - } - - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(AccessControl, ERC1155Holder) returns (bool) { - return super.supportsInterface(interfaceId); - } - - function requiredSignatures() public view virtual override returns (uint256) { - return _requiredSignatures; - } - - function _isAuthorized(address user) internal view virtual override returns (bool) { - return hasRole(SIGNER_ROLE, user); - } - - function _processSignature( - bytes memory signature, - bytes32 userOpHash - ) internal virtual override(Account, AccountMultisig) returns (bool, address, uint48, uint48) { - return super._processSignature(signature, userOpHash); - } -} - -contract AdvancedAccountERC1271 is AccessControl, AccountCommon, AccountERC1271, AccountMultisig { +contract AdvancedAccount is AccessControl, AccountCommon, AccountAllSignatures, AccountMultisig { bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); uint256 private _requiredSignatures; diff --git a/test/abstraction/accountECDSA.test.js b/test/abstraction/accountECDSA.test.js index 8c7fb875820..190bf70763c 100644 --- a/test/abstraction/accountECDSA.test.js +++ b/test/abstraction/accountECDSA.test.js @@ -3,16 +3,23 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); +const { IdentityHelper } = require('../helpers/identity'); async function fixture() { const accounts = await ethers.getSigners(); - accounts.user = accounts.shift(); + accounts.relayer = accounts.shift(); accounts.beneficiary = accounts.shift(); - const target = await ethers.deployContract('CallReceiverMock'); + // 4337 helper const helper = new ERC4337Helper('SimpleAccountECDSA'); - await helper.wait(); - const sender = await helper.newAccount(accounts.user); + const identity = new IdentityHelper(); + + // environment + const target = await ethers.deployContract('CallReceiverMock'); + + // create 4337 account controlled by ECDSA + const signer = await identity.newECDSASigner(); + const sender = await helper.newAccount(signer); return { accounts, @@ -31,7 +38,7 @@ describe('AccountECDSA', function () { describe('execute operation', function () { beforeEach('fund account', async function () { - await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + await this.accounts.relayer.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); }); describe('account not deployed yet', function () { @@ -57,7 +64,7 @@ describe('AccountECDSA', function () { describe('account already deployed', function () { beforeEach(async function () { - await this.sender.deploy(); + await this.sender.deploy(this.accounts.relayer); }); it('success: call', async function () { diff --git a/test/abstraction/accountERC1271.test.js b/test/abstraction/accountERC1271.test.js index a16dfb6f752..14dfbea0845 100644 --- a/test/abstraction/accountERC1271.test.js +++ b/test/abstraction/accountERC1271.test.js @@ -3,26 +3,22 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); -const { P256Signer } = require('../helpers/p256'); +const { IdentityHelper } = require('../helpers/identity'); async function fixture() { const accounts = await ethers.getSigners(); - accounts.user = accounts.shift(); + accounts.relayer = accounts.shift(); accounts.beneficiary = accounts.shift(); // 4337 helper const helper = new ERC4337Helper('SimpleAccountERC1271'); - await helper.wait(); + const identity = new IdentityHelper(); // environment const target = await ethers.deployContract('CallReceiverMock'); - const identifyFactory = await ethers.deployContract('IdentityP256Factory'); - // create P256 key and identity contract - const signer = P256Signer.random(); - signer.address = await identifyFactory.predict(signer.publicKey); // override address of the signer - signer.sigParams.prefixAddress = true; - await identifyFactory.create(signer.publicKey); + // create 4337 account controlled by P256 + const signer = await identity.newP256Signer(); const sender = await helper.newAccount(signer); return { @@ -31,6 +27,7 @@ async function fixture() { helper, entrypoint: helper.entrypoint, factory: helper.factory, + signer, sender, }; } @@ -42,7 +39,7 @@ describe('AccountERC1271', function () { describe('execute operation', function () { beforeEach('fund account', async function () { - await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + await this.accounts.relayer.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); }); describe('account not deployed yet', function () { @@ -68,7 +65,7 @@ describe('AccountERC1271', function () { describe('account already deployed', function () { beforeEach(async function () { - await this.sender.deploy(this.accounts.user); + await this.sender.deploy(this.accounts.relayer); }); it('success: call', async function () { diff --git a/test/abstraction/accountMultisig.test.js b/test/abstraction/accountMultisig.test.js index 1f0c33320a9..44562f9771b 100644 --- a/test/abstraction/accountMultisig.test.js +++ b/test/abstraction/accountMultisig.test.js @@ -3,19 +3,28 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../helpers/erc4337'); +const { IdentityHelper } = require('../helpers/identity'); async function fixture() { const accounts = await ethers.getSigners(); - accounts.user = accounts.shift(); + accounts.relayer = accounts.shift(); accounts.beneficiary = accounts.shift(); - accounts.signers = Array(3) - .fill() - .map(() => accounts.shift()); + // 4337 helper + const helper = new ERC4337Helper('AdvancedAccount'); + const identity = new IdentityHelper(); + + // environment const target = await ethers.deployContract('CallReceiverMock'); - const helper = new ERC4337Helper('AdvancedAccountECDSA'); - await helper.wait(); - const sender = await helper.newAccount(accounts.user, [accounts.signers, 2]); // 2-of-3 + + // create 4337 account controlled by multiple signers + const signers = await Promise.all([ + identity.newECDSASigner(), // secp256k1 + identity.newP256Signer(), // secp256r1 + identity.newP256Signer(), // secp256r1 + identity.newECDSASigner(), // secp256k1 + ]); + const sender = await helper.newAccount(accounts.relayer, [signers, 2]); // 2-of-4 return { accounts, @@ -23,6 +32,7 @@ async function fixture() { helper, entrypoint: helper.entrypoint, factory: helper.factory, + signers, sender, }; } @@ -34,7 +44,7 @@ describe('AccountMultisig', function () { describe('execute operation', function () { beforeEach('fund account', async function () { - await this.accounts.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + await this.accounts.relayer.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); }); describe('account not deployed yet', function () { @@ -48,7 +58,7 @@ describe('AccountMultisig', function () { ]), }) .then(op => op.addInitCode()) - .then(op => op.sign(this.accounts.signers)); + .then(op => op.sign(this.signers, true)); await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.entrypoint, 'AccountDeployed') @@ -72,7 +82,7 @@ describe('AccountMultisig', function () { this.target.interface.encodeFunctionData('mockFunctionExtra'), ]), }) - .then(op => op.sign(this.accounts.signers)); + .then(op => op.sign(this.signers, true)); await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.target, 'MockFunctionCalledExtra') @@ -88,7 +98,7 @@ describe('AccountMultisig', function () { this.target.interface.encodeFunctionData('mockFunctionExtra'), ]), }) - .then(op => op.sign([this.accounts.signers[0], this.accounts.signers[2]])); + .then(op => op.sign([this.signers[0], this.signers[2]], true)); await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.target, 'MockFunctionCalledExtra') @@ -104,7 +114,7 @@ describe('AccountMultisig', function () { this.target.interface.encodeFunctionData('mockFunctionExtra'), ]), }) - .then(op => op.sign([this.accounts.signers[2]])); + .then(op => op.sign([this.signers[2]], true)); await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') @@ -120,7 +130,7 @@ describe('AccountMultisig', function () { this.target.interface.encodeFunctionData('mockFunctionExtra'), ]), }) - .then(op => op.sign([this.accounts.user, this.accounts.signers[2]])); + .then(op => op.sign([this.accounts.relayer, this.signers[2]], true)); await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index d225771ca0a..419f42129ac 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -1,9 +1,12 @@ const { ethers } = require('hardhat'); +const { SignatureType } = require('./identity'); + function pack(left, right) { return ethers.solidityPacked(['uint128', 'uint128'], [left, right]); } +/// Global ERC-4337 environment helper. class ERC4337Helper { constructor(account = 'SimpleAccountECDSA') { this.entrypointAsPromise = ethers.deployContract('EntryPoint'); @@ -20,19 +23,20 @@ class ERC4337Helper { return this; } - async newAccount(user, extraArgs = [], salt = ethers.randomBytes(32)) { + async newAccount(signer, extraArgs = [], salt = ethers.randomBytes(32)) { await this.wait(); const initCode = await this.account - .getDeployTransaction(this.entrypoint, user, ...extraArgs) + .getDeployTransaction(this.entrypoint, signer, ...extraArgs) .then(tx => this.factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) .then(deployCode => ethers.concat([this.factory.target, deployCode])); const instance = await this.entrypoint.getSenderAddress .staticCall(initCode) - .then(address => this.account.attach(address).connect(user)); + .then(address => this.account.attach(address).connect(signer)); return new AbstractAccount(instance, initCode, this); } } +/// Represent one ERC-4337 account contract. class AbstractAccount extends ethers.BaseContract { constructor(instance, initCode, context) { super(instance.target, instance.interface, instance.runner, instance.deployTx); @@ -69,6 +73,7 @@ class AbstractAccount extends ethers.BaseContract { } } +/// Represent one user operation class UserOperation { constructor(params) { this.sender = params.sender; @@ -128,14 +133,27 @@ class UserOperation { return this; } - async sign(signer = this.sender.runner) { - this.signature = await Promise.all( - (Array.isArray(signer) ? signer : [signer]) - .sort((signer1, signer2) => signer1.address - signer2.address) - .map(signer => signer.signMessage(ethers.getBytes(this.hash))), - ).then(signatures => - Array.isArray(signer) ? ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]'], [signatures]) : signatures[0], + async sign(signer = this.sender.runner, withTypePrefix = false) { + const signers = (Array.isArray(signer) ? signer : [signer]).sort( + (signer1, signer2) => signer1.address - signer2.address, + ); + const signatures = await Promise.all( + signers.map(signer => + Promise.resolve(signer.signMessage(ethers.getBytes(this.hash))).then(signature => + withTypePrefix + ? ethers.AbiCoder.defaultAbiCoder().encode( + ['uint8', 'bytes'], + [signer.type ?? SignatureType.ECDSA, signature], + ) + : signature, + ), + ), ); + + this.signature = Array.isArray(signer) + ? ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]'], [signatures]) + : signatures[0]; + return this; } } diff --git a/test/helpers/identity.js b/test/helpers/identity.js new file mode 100644 index 00000000000..7076e402028 --- /dev/null +++ b/test/helpers/identity.js @@ -0,0 +1,43 @@ +const { ethers } = require('hardhat'); + +const { Enum } = require('./enums'); +const { P256Signer } = require('./p256'); + +const SignatureType = Enum('ECDSA', 'ERC1271'); + +class IdentityHelper { + constructor() { + this.p256FactoryAsPromise = ethers.deployContract('IdentityP256Factory'); + this.rsaFactoryAsPromise = ethers.deployContract('IdentityRSAFactory'); + } + + async wait() { + this.p256Factory = await this.p256FactoryAsPromise; + this.rsaFactory = await this.rsaFactoryAsPromise; + return this; + } + + async newECDSASigner() { + return Object.assign(ethers.Wallet.createRandom(), { type: SignatureType.ECDSA }); + } + + async newP256Signer(sigParams = { prefixAddress: true }) { + await this.wait(); + + const signer = P256Signer.random(); + return Promise.all([this.p256Factory.predict(signer.publicKey), this.p256Factory.create(signer.publicKey)]).then( + ([address]) => Object.assign(signer, { address, sigParams, type: SignatureType.ERC1271 }), + ); + } + + async newRSASigner() { + await this.wait(); + + return Promise.reject('Not implemented yet'); + } +} + +module.exports = { + SignatureType, + IdentityHelper, +}; diff --git a/test/helpers/p256.js b/test/helpers/p256.js index 8ee2436c74e..12e5efbf0ba 100644 --- a/test/helpers/p256.js +++ b/test/helpers/p256.js @@ -38,7 +38,7 @@ class P256Signer { ? ethers.solidityPacked(['uint256', 'uint256', 'uint8'], [r, s, recovery]) : ethers.solidityPacked(['uint256', 'uint256'], [r, s]); return this.sigParams.prefixAddress - ? ethers.AbiCoder.defaultAbiCoder().encode([ 'address', 'bytes' ], [ this.address, signature ]) + ? ethers.AbiCoder.defaultAbiCoder().encode(['address', 'bytes'], [this.address, signature]) : signature; } } From 2004e2a0e0a8bc5f60e46c12b4e976c000af5cb3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 12 Jul 2024 15:17:18 +0200 Subject: [PATCH 54/66] move signature to calldata --- contracts/abstraction/account/Account.sol | 12 +++---- .../account/modules/AccountMultisig.sol | 30 ++++++++++++---- .../modules/recovery/AccountAllSignatures.sol | 11 +++--- .../account/modules/recovery/AccountECDSA.sol | 14 ++++---- .../modules/recovery/AccountERC1271.sol | 6 ++-- .../abstraction/mocks/AdvancedAccount.sol | 6 ++-- test/abstraction/accountMultisig.test.js | 14 ++++---- test/helpers/enums.js | 1 + test/helpers/erc4337.js | 14 ++++---- test/helpers/identity.js | 14 ++++---- test/helpers/p256.js | 35 ++++++++++++------- 11 files changed, 92 insertions(+), 65 deletions(-) diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol index 7582ce693a8..38e8f9a6274 100644 --- a/contracts/abstraction/account/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -47,7 +47,7 @@ abstract contract Account is IAccount { * * Subclass must implement this using their own choice of cryptography. */ - function _recoverSigner(bytes memory signature, bytes32 userOpHash) internal virtual returns (address); + function _recoverSigner(bytes32 userOpHash, bytes calldata signature) internal virtual returns (address); /**************************************************************************************************************** * Public interface * @@ -73,7 +73,7 @@ abstract contract Account is IAccount { bytes32 userOpHash, uint256 missingAccountFunds ) public virtual override onlyEntryPoint returns (uint256 validationData) { - (bool valid, , uint48 validAfter, uint48 validUntil) = _processSignature(userOp.signature, userOpHash); + (bool valid, , uint48 validAfter, uint48 validUntil) = _processSignature(userOpHash, userOp.signature); _validateNonce(userOp.nonce); _payPrefund(missingAccountFunds); return ERC4337Utils.packValidationData(valid, validAfter, validUntil); @@ -85,18 +85,18 @@ abstract contract Account is IAccount { /** * @dev Process the signature is valid for this message. - * @param signature - The user's signature * @param userOpHash - Hash of the request that must be signed (includes the entrypoint and chain id) + * @param signature - The user's signature * @return valid - Signature is valid * @return signer - Address of the signer that produced the signature * @return validAfter - first timestamp this operation is valid * @return validUntil - last timestamp this operation is valid. 0 for "indefinite" */ function _processSignature( - bytes memory signature, - bytes32 userOpHash + bytes32 userOpHash, + bytes calldata signature ) internal virtual returns (bool valid, address signer, uint48 validAfter, uint48 validUntil) { - address recovered = _recoverSigner(signature, userOpHash); + address recovered = _recoverSigner(userOpHash, signature); return (recovered != address(0) && _isAuthorized(recovered), recovered, 0, 0); } diff --git a/contracts/abstraction/account/modules/AccountMultisig.sol b/contracts/abstraction/account/modules/AccountMultisig.sol index 3c84a0beed6..50f296d1a6f 100644 --- a/contracts/abstraction/account/modules/AccountMultisig.sol +++ b/contracts/abstraction/account/modules/AccountMultisig.sol @@ -11,12 +11,12 @@ abstract contract AccountMultisig is Account { function requiredSignatures() public view virtual returns (uint256); function _processSignature( - bytes memory signature, - bytes32 userOpHash + bytes32 userOpHash, + bytes calldata signatures ) internal virtual override returns (bool, address, uint48, uint48) { - bytes[] memory signatures = abi.decode(signature, (bytes[])); + uint256 arrayLength = _getUint256(signatures, _getUint256(signatures, 0)); - if (signatures.length < requiredSignatures()) { + if (arrayLength < requiredSignatures()) { return (false, address(0), 0, 0); } @@ -24,10 +24,11 @@ abstract contract AccountMultisig is Account { uint48 globalValidAfter = 0; uint48 globalValidUntil = 0; - for (uint256 i = 0; i < signatures.length; ++i) { + for (uint256 i = 0; i < arrayLength; ++i) { + bytes calldata signature = _getBytesArrayElement(signatures, i); (bool valid, address signer, uint48 validAfter, uint48 validUntil) = super._processSignature( - signatures[i], - userOpHash + userOpHash, + signature ); if (valid && signer > lastSigner) { lastSigner = signer; @@ -41,4 +42,19 @@ abstract contract AccountMultisig is Account { } return (true, address(this), globalValidAfter, globalValidUntil); } + + function _getUint256(bytes calldata data, uint256 pos) private pure returns (uint256 result) { + assembly ("memory-safe") { + result := calldataload(add(data.offset, pos)) + } + } + + function _getBytesArrayElement(bytes calldata data, uint256 i) private pure returns (bytes calldata result) { + assembly ("memory-safe") { + let begin := add(calldataload(data.offset), 0x20) + let offset := add(calldataload(add(add(data.offset, begin), mul(i, 0x20))), begin) + result.length := calldataload(add(data.offset, offset)) + result.offset := add(add(data.offset, offset), 0x20) + } + } } diff --git a/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol b/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol index 471a9b680e4..97f87448dd7 100644 --- a/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol +++ b/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol @@ -13,14 +13,15 @@ abstract contract AccountAllSignatures is AccountECDSA, AccountERC1271 { } function _recoverSigner( - bytes memory signature, - bytes32 userOpHash + bytes32 userOpHash, + bytes calldata signature ) internal virtual override(AccountECDSA, AccountERC1271) returns (address) { - (SignatureType sigType, bytes memory sigData) = abi.decode(signature, (SignatureType, bytes)); + SignatureType sigType = SignatureType(uint8(bytes1(signature))); + if (sigType == SignatureType.ECDSA) { - return AccountECDSA._recoverSigner(sigData, userOpHash); + return AccountECDSA._recoverSigner(userOpHash, signature[0x01:]); } else if (sigType == SignatureType.ERC1271) { - return AccountERC1271._recoverSigner(sigData, userOpHash); + return AccountERC1271._recoverSigner(userOpHash, signature[0x01:]); } else { return address(0); } diff --git a/contracts/abstraction/account/modules/recovery/AccountECDSA.sol b/contracts/abstraction/account/modules/recovery/AccountECDSA.sol index 0ece7006267..206d65f0f9d 100644 --- a/contracts/abstraction/account/modules/recovery/AccountECDSA.sol +++ b/contracts/abstraction/account/modules/recovery/AccountECDSA.sol @@ -9,8 +9,8 @@ import {Account} from "../../Account.sol"; abstract contract AccountECDSA is Account { function _recoverSigner( - bytes memory signature, - bytes32 userOpHash + bytes32 userOpHash, + bytes calldata signature ) internal virtual override returns (address signer) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); @@ -24,9 +24,9 @@ abstract contract AccountECDSA is Account { uint8 v; /// @solidity memory-safe-assembly assembly { - r := mload(add(signature, 0x20)) - s := mload(add(signature, 0x40)) - v := byte(0, mload(add(signature, 0x60))) + r := calldataload(add(signature.offset, 0x00)) + s := calldataload(add(signature.offset, 0x20)) + v := byte(0, calldataload(add(signature.offset, 0x40))) } (signer, , ) = ECDSA.tryRecover(msgHash, v, r, s); // return address(0) on errors } else if (signature.length == 64) { @@ -34,8 +34,8 @@ abstract contract AccountECDSA is Account { bytes32 vs; /// @solidity memory-safe-assembly assembly { - r := mload(add(signature, 0x20)) - vs := mload(add(signature, 0x40)) + r := calldataload(add(signature.offset, 0x00)) + vs := calldataload(add(signature.offset, 0x20)) } (signer, , ) = ECDSA.tryRecover(msgHash, r, vs); } else { diff --git a/contracts/abstraction/account/modules/recovery/AccountERC1271.sol b/contracts/abstraction/account/modules/recovery/AccountERC1271.sol index bc2eb152fd7..43423c3a7a5 100644 --- a/contracts/abstraction/account/modules/recovery/AccountERC1271.sol +++ b/contracts/abstraction/account/modules/recovery/AccountERC1271.sol @@ -10,10 +10,10 @@ import {Account} from "../../Account.sol"; abstract contract AccountERC1271 is Account { error P256InvalidSignatureLength(uint256 length); - function _recoverSigner(bytes memory signature, bytes32 userOpHash) internal virtual override returns (address) { + function _recoverSigner(bytes32 userOpHash, bytes calldata signature) internal virtual override returns (address) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); - (address signer, bytes memory sig) = abi.decode(signature, (address, bytes)); + address signer = address(bytes20(signature[0x00:0x14])); - return SignatureChecker.isValidERC1271SignatureNow(signer, msgHash, sig) ? signer : address(0); + return SignatureChecker.isValidERC1271SignatureNow(signer, msgHash, signature[0x14:]) ? signer : address(0); } } diff --git a/contracts/abstraction/mocks/AdvancedAccount.sol b/contracts/abstraction/mocks/AdvancedAccount.sol index dc4dd237a16..f60d9f7bd98 100644 --- a/contracts/abstraction/mocks/AdvancedAccount.sol +++ b/contracts/abstraction/mocks/AdvancedAccount.sol @@ -42,9 +42,9 @@ contract AdvancedAccount is AccessControl, AccountCommon, AccountAllSignatures, } function _processSignature( - bytes memory signature, - bytes32 userOpHash + bytes32 userOpHash, + bytes calldata signature ) internal virtual override(Account, AccountMultisig) returns (bool, address, uint48, uint48) { - return super._processSignature(signature, userOpHash); + return super._processSignature(userOpHash, signature); } } diff --git a/test/abstraction/accountMultisig.test.js b/test/abstraction/accountMultisig.test.js index 44562f9771b..37593fbb183 100644 --- a/test/abstraction/accountMultisig.test.js +++ b/test/abstraction/accountMultisig.test.js @@ -11,7 +11,7 @@ async function fixture() { accounts.beneficiary = accounts.shift(); // 4337 helper - const helper = new ERC4337Helper('AdvancedAccount'); + const helper = new ERC4337Helper('AdvancedAccount', { withTypePrefix: true }); const identity = new IdentityHelper(); // environment @@ -48,7 +48,7 @@ describe('AccountMultisig', function () { }); describe('account not deployed yet', function () { - it('success: deploy and call', async function () { + it.only('success: deploy and call', async function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ @@ -58,7 +58,7 @@ describe('AccountMultisig', function () { ]), }) .then(op => op.addInitCode()) - .then(op => op.sign(this.signers, true)); + .then(op => op.sign(this.signers)); await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.entrypoint, 'AccountDeployed') @@ -82,7 +82,7 @@ describe('AccountMultisig', function () { this.target.interface.encodeFunctionData('mockFunctionExtra'), ]), }) - .then(op => op.sign(this.signers, true)); + .then(op => op.sign(this.signers)); await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.target, 'MockFunctionCalledExtra') @@ -98,7 +98,7 @@ describe('AccountMultisig', function () { this.target.interface.encodeFunctionData('mockFunctionExtra'), ]), }) - .then(op => op.sign([this.signers[0], this.signers[2]], true)); + .then(op => op.sign([this.signers[0], this.signers[2]])); await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.emit(this.target, 'MockFunctionCalledExtra') @@ -114,7 +114,7 @@ describe('AccountMultisig', function () { this.target.interface.encodeFunctionData('mockFunctionExtra'), ]), }) - .then(op => op.sign([this.signers[2]], true)); + .then(op => op.sign([this.signers[2]])); await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') @@ -130,7 +130,7 @@ describe('AccountMultisig', function () { this.target.interface.encodeFunctionData('mockFunctionExtra'), ]), }) - .then(op => op.sign([this.accounts.relayer, this.signers[2]], true)); + .then(op => op.sign([this.accounts.relayer, this.signers[2]])); await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') diff --git a/test/helpers/enums.js b/test/helpers/enums.js index f95767ab7e7..a6024eda22d 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -9,4 +9,5 @@ module.exports = { Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'), OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'), RevertType: Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'), + SignatureType: Enum('ECDSA', 'ERC1271'), }; diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 419f42129ac..4cc3b926964 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -1,6 +1,6 @@ const { ethers } = require('hardhat'); -const { SignatureType } = require('./identity'); +const { SignatureType } = require('./enums'); function pack(left, right) { return ethers.solidityPacked(['uint128', 'uint128'], [left, right]); @@ -8,11 +8,12 @@ function pack(left, right) { /// Global ERC-4337 environment helper. class ERC4337Helper { - constructor(account = 'SimpleAccountECDSA') { + constructor(account = 'SimpleAccountECDSA', params = {}) { this.entrypointAsPromise = ethers.deployContract('EntryPoint'); this.factoryAsPromise = ethers.deployContract('$Create2'); this.accountAsPromise = ethers.getContractFactory(account); this.chainIdAsPromise = ethers.provider.getNetwork().then(({ chainId }) => chainId); + this.params = params; } async wait() { @@ -133,7 +134,9 @@ class UserOperation { return this; } - async sign(signer = this.sender.runner, withTypePrefix = false) { + async sign(signer = this.sender.runner, args = {}) { + const withTypePrefix = args.withTypePrefix ?? this.sender.context.params.withTypePrefix; + const signers = (Array.isArray(signer) ? signer : [signer]).sort( (signer1, signer2) => signer1.address - signer2.address, ); @@ -141,10 +144,7 @@ class UserOperation { signers.map(signer => Promise.resolve(signer.signMessage(ethers.getBytes(this.hash))).then(signature => withTypePrefix - ? ethers.AbiCoder.defaultAbiCoder().encode( - ['uint8', 'bytes'], - [signer.type ?? SignatureType.ECDSA, signature], - ) + ? ethers.solidityPacked(['uint8', 'bytes'], [signer.type ?? SignatureType.ECDSA, signature]) : signature, ), ), diff --git a/test/helpers/identity.js b/test/helpers/identity.js index 7076e402028..8debe8a5283 100644 --- a/test/helpers/identity.js +++ b/test/helpers/identity.js @@ -1,9 +1,7 @@ const { ethers } = require('hardhat'); -const { Enum } = require('./enums'); const { P256Signer } = require('./p256'); - -const SignatureType = Enum('ECDSA', 'ERC1271'); +const { SignatureType } = require('./enums'); class IdentityHelper { constructor() { @@ -21,13 +19,15 @@ class IdentityHelper { return Object.assign(ethers.Wallet.createRandom(), { type: SignatureType.ECDSA }); } - async newP256Signer(sigParams = { prefixAddress: true }) { + async newP256Signer(params = { withPrefixAddress: true }) { await this.wait(); - const signer = P256Signer.random(); - return Promise.all([this.p256Factory.predict(signer.publicKey), this.p256Factory.create(signer.publicKey)]).then( - ([address]) => Object.assign(signer, { address, sigParams, type: SignatureType.ERC1271 }), + const signer = P256Signer.random(params); + await Promise.all([this.p256Factory.predict(signer.publicKey), this.p256Factory.create(signer.publicKey)]).then( + ([address]) => Object.assign(signer, { address }), ); + + return signer; } async newRSASigner() { diff --git a/test/helpers/p256.js b/test/helpers/p256.js index 12e5efbf0ba..eec5f47bede 100644 --- a/test/helpers/p256.js +++ b/test/helpers/p256.js @@ -1,10 +1,10 @@ const { ethers } = require('hardhat'); const { secp256r1 } = require('@noble/curves/p256'); -const N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n; +const { SignatureType } = require('./enums'); class P256Signer { - constructor(privateKey) { + constructor(privateKey, params = {}) { this.privateKey = privateKey; this.publicKey = ethers.concat( [ @@ -13,11 +13,15 @@ class P256Signer { ].map(ethers.hexlify), ); this.address = ethers.getAddress(ethers.keccak256(this.publicKey).slice(-40)); - this.sigParams = { prefixAddress: false, includeRecovery: true }; + this.params = Object.assign({ withPrefixAddress: false, withRecovery: true }, params); } - static random() { - return new P256Signer(secp256r1.utils.randomPrivateKey()); + get type() { + return SignatureType.ERC1271; + } + + static random(params = {}) { + return new P256Signer(secp256r1.utils.randomPrivateKey(), params); } getAddress() { @@ -28,18 +32,23 @@ class P256Signer { let { r, s, recovery } = secp256r1.sign(ethers.hashMessage(message).replace(/0x/, ''), this.privateKey); // ensureLowerOrderS - if (s > N / 2n) { - s = N - s; + if (s > secp256r1.CURVE.n / 2n) { + s = secp256r1.CURVE.n - s; recovery = 1 - recovery; } // pack signature - const signature = this.sigParams.includeRecovery - ? ethers.solidityPacked(['uint256', 'uint256', 'uint8'], [r, s, recovery]) - : ethers.solidityPacked(['uint256', 'uint256'], [r, s]); - return this.sigParams.prefixAddress - ? ethers.AbiCoder.defaultAbiCoder().encode(['address', 'bytes'], [this.address, signature]) - : signature; + const elements = [ + this.params.withPrefixAddress && { type: 'address', value: this.address }, + { type: 'uint256', value: ethers.toBeHex(r) }, + { type: 'uint256', value: ethers.toBeHex(s) }, + this.params.withRecovery && { type: 'uint8', value: recovery }, + ].filter(Boolean); + + return ethers.solidityPacked( + elements.map(({ type }) => type), + elements.map(({ value }) => value), + ); } } From fbaa7a19ef9ee7ccac80c524f6c1d87e4614029c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 12 Jul 2024 22:41:57 +0200 Subject: [PATCH 55/66] fix stack too deep --- .../account/modules/AccountMultisig.sol | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/contracts/abstraction/account/modules/AccountMultisig.sol b/contracts/abstraction/account/modules/AccountMultisig.sol index 50f296d1a6f..df7d893b473 100644 --- a/contracts/abstraction/account/modules/AccountMultisig.sol +++ b/contracts/abstraction/account/modules/AccountMultisig.sol @@ -13,7 +13,7 @@ abstract contract AccountMultisig is Account { function _processSignature( bytes32 userOpHash, bytes calldata signatures - ) internal virtual override returns (bool, address, uint48, uint48) { + ) internal virtual override returns (bool, address, uint48 validAfter, uint48 validUntil) { uint256 arrayLength = _getUint256(signatures, _getUint256(signatures, 0)); if (arrayLength < requiredSignatures()) { @@ -21,26 +21,23 @@ abstract contract AccountMultisig is Account { } address lastSigner = address(0); - uint48 globalValidAfter = 0; - uint48 globalValidUntil = 0; for (uint256 i = 0; i < arrayLength; ++i) { - bytes calldata signature = _getBytesArrayElement(signatures, i); - (bool valid, address signer, uint48 validAfter, uint48 validUntil) = super._processSignature( + (bool sigValid, address sigSigner, uint48 sigValidAfter, uint48 sigValidUntil) = super._processSignature( userOpHash, - signature + _getBytesArrayElement(signatures, i) ); - if (valid && signer > lastSigner) { - lastSigner = signer; - globalValidAfter = uint48(Math.ternary(validUntil < globalValidUntil, globalValidUntil, validAfter)); - globalValidUntil = uint48( - Math.ternary(validUntil > globalValidUntil || validUntil == 0, globalValidUntil, validUntil) + if (sigValid && sigSigner > lastSigner) { + lastSigner = sigSigner; + validAfter = uint48(Math.ternary(validAfter > sigValidAfter, validAfter, sigValidAfter)); + validUntil = uint48( + Math.ternary(validUntil < sigValidUntil || sigValidUntil == 0, validUntil, sigValidUntil) ); } else { return (false, address(0), 0, 0); } } - return (true, address(this), globalValidAfter, globalValidUntil); + return (true, address(this), validAfter, validUntil); } function _getUint256(bytes calldata data, uint256 pos) private pure returns (uint256 result) { @@ -51,10 +48,10 @@ abstract contract AccountMultisig is Account { function _getBytesArrayElement(bytes calldata data, uint256 i) private pure returns (bytes calldata result) { assembly ("memory-safe") { - let begin := add(calldataload(data.offset), 0x20) - let offset := add(calldataload(add(add(data.offset, begin), mul(i, 0x20))), begin) - result.length := calldataload(add(data.offset, offset)) - result.offset := add(add(data.offset, offset), 0x20) + let begin := add(add(data.offset, calldataload(data.offset)), 0x20) // data.offset + internal offset + skip length + let offset := add(begin, calldataload(add(begin, mul(i, 0x20)))) // begin + element offset (stored at begin + i * 20) + result.length := calldataload(offset) // length + result.offset := add(offset, 0x20) // location } } } From e170ceb669a1b1e4d16627cd9cc6b8e830f520d9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 14 Jul 2024 23:37:45 +0200 Subject: [PATCH 56/66] use abi.encodePacked instead of mcopy --- contracts/proxy/Clones.sol | 152 ++++++++++++----------- test/abstraction/accountMultisig.test.js | 2 +- 2 files changed, 78 insertions(+), 76 deletions(-) diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index c19618420a9..661811d3fc0 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; +import {Create2} from "../utils/Create2.sol"; import {Errors} from "../utils/Errors.sol"; /** @@ -124,10 +125,23 @@ library Clones { return predictDeterministicAddress(implementation, salt, address(this)); } + /** + * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`, with `args` + * attached to it as immutable arguments (that can be fetched using {fetchCloneArgs}). + * + * This function uses the create opcode, which should never revert. + */ function cloneWithImmutableArgs(address implementation, bytes memory args) internal returns (address instance) { return cloneWithImmutableArgs(implementation, args, 0); } + /** + * @dev Same as {xref-Clones-cloneWithImmutableArgs-address-bytes-}[cloneWithImmutableArgs], but with a `value` + * parameter to send native currency to the new contract. + * + * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory) + * to always have enough balance for new deployments. Consider exposing this function under a payable method. + */ function cloneWithImmutableArgs( address implementation, bytes memory args, @@ -136,35 +150,23 @@ library Clones { if (address(this).balance < value) { revert Errors.InsufficientBalance(address(this).balance, value); } - - uint256 extraLength = args.length; - uint256 codeLength = 0x2d + extraLength; - uint256 initLength = 0x38 + extraLength; - if (codeLength > 0xffff) revert ImmutableArgsTooLarge(); - - /// @solidity memory-safe-assembly - assembly { - // [ptr + 0x43] ...................................................................................................................................... // args - // [ptr + 0x23] ......................................................................00000000000000000000000000000000005af43d82803e903d91602b57fd5bf3...... // suffix - // [ptr + 0x14] ........................................000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.................................... // implementation - // [ptr + 0x00] 00000000000000000000003d61000080600a3d3981f3363d3d373d3d3d363d73............................................................................ // prefix - // [ptr + 0x0d] ..........................XX................................................................................................................ // length (part 1) - // [ptr + 0x0e] ............................XX.............................................................................................................. // length (part 2) - let ptr := mload(0x40) - mcopy(add(ptr, 0x43), add(args, 0x20), extraLength) - mstore(add(ptr, 0x23), 0x5af43d82803e903d91602b57fd5bf3) - mstore(add(ptr, 0x14), implementation) - mstore(add(ptr, 0x00), 0x3d61000080600b3d3981f3363d3d373d3d3d363d73) - mstore8(add(ptr, 0x0d), shr(8, codeLength)) - mstore8(add(ptr, 0x0e), shr(0, codeLength)) - instance := create(value, add(ptr, 0x0b), initLength) + bytes memory bytecode = _cloneWithImmutableArgsCode(implementation, args); + assembly ("memory-safe") { + instance := create(value, add(bytecode, 0x20), mload(bytecode)) } - if (instance == address(0)) { revert Errors.FailedDeployment(); } } + /** + * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`, with `args` + * attached to it as immutable arguments (that can be fetched using {fetchCloneArgs}). + * + * This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same + * `implementation` and `salt` multiple time will revert, since the clones cannot be deployed twice at the same + * address. + */ function cloneWithImmutableArgsDeterministic( address implementation, bytes memory args, @@ -173,74 +175,39 @@ library Clones { return cloneWithImmutableArgsDeterministic(implementation, args, salt, 0); } + /** + * @dev Same as {xref-Clones-cloneWithImmutableArgsDeterministic-address-bytes-bytes32-}[cloneWithImmutableArgsDeterministic], + * but with a `value` parameter to send native currency to the new contract. + * + * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory) + * to always have enough balance for new deployments. Consider exposing this function under a payable method. + */ function cloneWithImmutableArgsDeterministic( address implementation, bytes memory args, bytes32 salt, uint256 value ) internal returns (address instance) { - if (address(this).balance < value) { - revert Errors.InsufficientBalance(address(this).balance, value); - } - - uint256 extraLength = args.length; - uint256 codeLength = 0x2d + extraLength; - uint256 initLength = 0x38 + extraLength; - if (codeLength > 0xffff) revert ImmutableArgsTooLarge(); - - /// @solidity memory-safe-assembly - assembly { - // [ptr + 0x43] ...................................................................................................................................... // args - // [ptr + 0x23] ......................................................................00000000000000000000000000000000005af43d82803e903d91602b57fd5bf3...... // suffix - // [ptr + 0x14] ........................................000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.................................... // implementation - // [ptr + 0x00] 00000000000000000000003d61000080600a3d3981f3363d3d373d3d3d363d73............................................................................ // prefix - // [ptr + 0x0d] ..........................XX................................................................................................................ // length (part 1) - // [ptr + 0x0e] ............................XX.............................................................................................................. // length (part 2) - let ptr := mload(0x40) - mcopy(add(ptr, 0x43), add(args, 0x20), extraLength) - mstore(add(ptr, 0x23), 0x5af43d82803e903d91602b57fd5bf3) - mstore(add(ptr, 0x14), implementation) - mstore(add(ptr, 0x00), 0x3d61000080600b3d3981f3363d3d373d3d3d363d73) - mstore8(add(ptr, 0x0d), shr(8, codeLength)) - mstore8(add(ptr, 0x0e), shr(0, codeLength)) - instance := create2(value, add(ptr, 0x0b), initLength, salt) - } - if (instance == address(0)) { - revert Errors.FailedDeployment(); - } + bytes memory bytecode = _cloneWithImmutableArgsCode(implementation, args); + return Create2.deploy(value, salt, bytecode); } + /** + * @dev Computes the address of a clone deployed using {Clones-cloneWithImmutableArgsDeterministic}. + */ function predictWithImmutableArgsDeterministicAddress( address implementation, bytes memory args, bytes32 salt, address deployer ) internal pure returns (address predicted) { - uint256 extraLength = args.length; - uint256 codeLength = 0x2d + extraLength; - uint256 initLength = 0x38 + extraLength; - if (codeLength > 0xffff) revert ImmutableArgsTooLarge(); - - /// @solidity memory-safe-assembly - assembly { - let ptr := mload(0x40) - mstore(add(ptr, add(0x58, extraLength)), salt) - mstore(add(ptr, add(0x38, extraLength)), deployer) - mstore8(add(ptr, add(0x43, extraLength)), 0xff) - mcopy(add(ptr, 0x43), add(args, 0x20), extraLength) - mstore(add(ptr, 0x23), 0x5af43d82803e903d91602b57fd5bf3) - mstore(add(ptr, 0x14), implementation) - mstore(add(ptr, 0x00), 0x3d61000080600b3d3981f3363d3d373d3d3d363d73) - mstore8(add(ptr, 0x0d), shr(8, codeLength)) - mstore8(add(ptr, 0x0e), shr(0, codeLength)) - mstore(add(ptr, add(0x78, extraLength)), keccak256(add(ptr, 0x0b), initLength)) - predicted := and( - keccak256(add(ptr, add(0x43, extraLength)), 0x55), - 0xffffffffffffffffffffffffffffffffffffffff - ) - } + bytes memory bytecode = _cloneWithImmutableArgsCode(implementation, args); + return Create2.computeAddress(salt, keccak256(bytecode), deployer); } + /** + * @dev Computes the address of a clone deployed using {Clones-cloneWithImmutableArgsDeterministic}. + */ function predictWithImmutableArgsDeterministicAddress( address implementation, bytes memory args, @@ -249,6 +216,17 @@ library Clones { return predictWithImmutableArgsDeterministicAddress(implementation, args, salt, address(this)); } + /** + * @dev Get the immutable args attached to a clone. + * + * - If `instance` is a clone that was deployed using `clone` or `cloneDeterministic`, this + * function will return an empty array. + * - If `instance` is a clone that was deployed using `cloneWithImmutableArgs` or + * `cloneWithImmutableArgsDeterministic`, this function will return the args array used at + * creation. + * - If `instance` is NOT a clone deployed using this library, the behavior is undefined. This + * function should only be used to check addresses that are known to be clones. + */ function fetchCloneArgs(address instance) internal view returns (bytes memory result) { uint256 argsLength = instance.code.length - 0x2d; // revert if length is too short assembly { @@ -260,4 +238,28 @@ library Clones { extcodecopy(instance, add(result, 0x20), 0x2d, argsLength) } } + + /** + * @dev Helper that prepares the initcode of the proxy with immutable args. + * + * An assembly variant of this function requires copying the `args` array, which can be efficiently done using + * `mcopy`. Unfortunatelly, that opcode is not available before cancun. A pure solidity implemenation using + * abi.encodePacked is more expensive but also more portable and easier to review. + */ + function _cloneWithImmutableArgsCode( + address implementation, + bytes memory args + ) private pure returns (bytes memory) { + uint256 initCodeLength = args.length + 0x2d; + if (initCodeLength > type(uint16).max) revert ImmutableArgsTooLarge(); + return + abi.encodePacked( + hex"3d61", + uint16(initCodeLength), + hex"80600b3d3981f3363d3d373d3d3d363d73", + implementation, + hex"5af43d82803e903d91602b57fd5bf3", + args + ); + } } diff --git a/test/abstraction/accountMultisig.test.js b/test/abstraction/accountMultisig.test.js index 37593fbb183..b8d65061ec6 100644 --- a/test/abstraction/accountMultisig.test.js +++ b/test/abstraction/accountMultisig.test.js @@ -48,7 +48,7 @@ describe('AccountMultisig', function () { }); describe('account not deployed yet', function () { - it.only('success: deploy and call', async function () { + it('success: deploy and call', async function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ From 5a4b30096c3106a91393c84eb006557c18e19ff1 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Jul 2024 09:47:06 +0200 Subject: [PATCH 57/66] simplify --- .../account/modules/AccountMultisig.sol | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/contracts/abstraction/account/modules/AccountMultisig.sol b/contracts/abstraction/account/modules/AccountMultisig.sol index df7d893b473..67498695148 100644 --- a/contracts/abstraction/account/modules/AccountMultisig.sol +++ b/contracts/abstraction/account/modules/AccountMultisig.sol @@ -14,18 +14,18 @@ abstract contract AccountMultisig is Account { bytes32 userOpHash, bytes calldata signatures ) internal virtual override returns (bool, address, uint48 validAfter, uint48 validUntil) { - uint256 arrayLength = _getUint256(signatures, _getUint256(signatures, 0)); + bytes[] calldata signatureArray = _decodeBytesArray(signatures); - if (arrayLength < requiredSignatures()) { + if (signatureArray.length < requiredSignatures()) { return (false, address(0), 0, 0); } address lastSigner = address(0); - for (uint256 i = 0; i < arrayLength; ++i) { + for (uint256 i = 0; i < signatureArray.length; ++i) { (bool sigValid, address sigSigner, uint48 sigValidAfter, uint48 sigValidUntil) = super._processSignature( userOpHash, - _getBytesArrayElement(signatures, i) + signatureArray[i] ); if (sigValid && sigSigner > lastSigner) { lastSigner = sigSigner; @@ -40,18 +40,11 @@ abstract contract AccountMultisig is Account { return (true, address(this), validAfter, validUntil); } - function _getUint256(bytes calldata data, uint256 pos) private pure returns (uint256 result) { + function _decodeBytesArray(bytes calldata input) private pure returns (bytes[] calldata output) { assembly ("memory-safe") { - result := calldataload(add(data.offset, pos)) - } - } - - function _getBytesArrayElement(bytes calldata data, uint256 i) private pure returns (bytes calldata result) { - assembly ("memory-safe") { - let begin := add(add(data.offset, calldataload(data.offset)), 0x20) // data.offset + internal offset + skip length - let offset := add(begin, calldataload(add(begin, mul(i, 0x20)))) // begin + element offset (stored at begin + i * 20) - result.length := calldataload(offset) // length - result.offset := add(offset, 0x20) // location + let ptr := add(input.offset, calldataload(input.offset)) + output.offset := add(ptr, 32) + output.length := calldataload(ptr) } } } From de032e3b72b46a547284fc5a39739e8f56cdc711 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Jul 2024 11:34:34 +0200 Subject: [PATCH 58/66] starting rewrite account common as ERC7579 --- contracts/abstraction/account/Account.sol | 21 +- .../abstraction/account/AccountCommon.sol | 46 -- .../abstraction/account/ERC7579Account.sol | 200 ++++++++ .../account/modules/AccountMultisig.sol | 2 +- .../modules/recovery/AccountAllSignatures.sol | 2 +- .../account/modules/recovery/AccountECDSA.sol | 2 +- .../modules/recovery/AccountERC1271.sol | 5 +- .../abstraction/mocks/AdvancedAccount.sol | 11 +- contracts/abstraction/mocks/SimpleAccount.sol | 10 +- contracts/abstraction/utils/ERC7579Utils.sol | 110 +++++ contracts/interfaces/IERC7579Account.sol | 114 +++++ contracts/interfaces/IERC7579Module.sol | 81 ++++ contracts/utils/Packing.sol | 448 ++++++++++++++++++ scripts/generate/templates/Packing.opts.js | 2 +- test/abstraction/TODO.md | 5 + test/abstraction/accountECDSA.test.js | 18 +- test/abstraction/accountERC1271.test.js | 13 +- test/abstraction/accountMultisig.test.js | 28 +- test/abstraction/entrypoint.test.js | 11 +- test/helpers/erc7579.js | 23 + test/utils/Packing.t.sol | 312 ++++++++++++ 21 files changed, 1355 insertions(+), 109 deletions(-) delete mode 100644 contracts/abstraction/account/AccountCommon.sol create mode 100644 contracts/abstraction/account/ERC7579Account.sol create mode 100644 contracts/abstraction/utils/ERC7579Utils.sol create mode 100644 contracts/interfaces/IERC7579Account.sol create mode 100644 contracts/interfaces/IERC7579Module.sol create mode 100644 test/abstraction/TODO.md create mode 100644 test/helpers/erc7579.js diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol index 38e8f9a6274..42bd0bba599 100644 --- a/contracts/abstraction/account/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.20; -import {PackedUserOperation, IAccount, IEntryPoint} from "../../interfaces/IERC4337.sol"; -import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol"; +import {PackedUserOperation, IAccount, IAccountExecute, IEntryPoint} from "../../interfaces/IERC4337.sol"; import {ERC4337Utils} from "./../utils/ERC4337Utils.sol"; +import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol"; +import {Address} from "../../utils/Address.sol"; -abstract contract Account is IAccount { +abstract contract Account is IAccount, IAccountExecute { error AccountEntryPointRestricted(); - error AccountInvalidBatchLength(); /**************************************************************************************************************** * Modifiers * @@ -39,7 +39,7 @@ abstract contract Account is IAccount { * * Subclass must implement this using their own access control mechanism. */ - function _isAuthorized(address) internal virtual returns (bool); + function _isAuthorized(address) internal view virtual returns (bool); /** * @dev Recover the signer for a given signature and user operation hash. This function does not need to verify @@ -47,7 +47,7 @@ abstract contract Account is IAccount { * * Subclass must implement this using their own choice of cryptography. */ - function _recoverSigner(bytes32 userOpHash, bytes calldata signature) internal virtual returns (address); + function _recoverSigner(bytes32 userOpHash, bytes calldata signature) internal view virtual returns (address); /**************************************************************************************************************** * Public interface * @@ -72,13 +72,18 @@ abstract contract Account is IAccount { PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds - ) public virtual override onlyEntryPoint returns (uint256 validationData) { + ) public virtual onlyEntryPoint returns (uint256 validationData) { (bool valid, , uint48 validAfter, uint48 validUntil) = _processSignature(userOpHash, userOp.signature); _validateNonce(userOp.nonce); _payPrefund(missingAccountFunds); return ERC4337Utils.packValidationData(valid, validAfter, validUntil); } + /// @inheritdoc IAccountExecute + function executeUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/) public virtual onlyEntryPoint { + Address.functionDelegateCall(address(this), userOp.callData[4:]); + } + /**************************************************************************************************************** * Internal mechanisms * ****************************************************************************************************************/ @@ -95,7 +100,7 @@ abstract contract Account is IAccount { function _processSignature( bytes32 userOpHash, bytes calldata signature - ) internal virtual returns (bool valid, address signer, uint48 validAfter, uint48 validUntil) { + ) internal view virtual returns (bool valid, address signer, uint48 validAfter, uint48 validUntil) { address recovered = _recoverSigner(userOpHash, signature); return (recovered != address(0) && _isAuthorized(recovered), recovered, 0, 0); } diff --git a/contracts/abstraction/account/AccountCommon.sol b/contracts/abstraction/account/AccountCommon.sol deleted file mode 100644 index 3c29ef4ef1b..00000000000 --- a/contracts/abstraction/account/AccountCommon.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {IEntryPoint} from "../../interfaces/IERC4337.sol"; -import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; -import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; -import {Address} from "../../utils/Address.sol"; -import {Account} from "./Account.sol"; - -abstract contract AccountCommon is Account, ERC721Holder, ERC1155Holder { - IEntryPoint private immutable _entryPoint; - - constructor(IEntryPoint entryPoint_) { - _entryPoint = entryPoint_; - } - - receive() external payable {} - - function entryPoint() public view virtual override returns (IEntryPoint) { - return _entryPoint; - } - - function execute(address target, uint256 value, bytes calldata data) public virtual onlyEntryPoint { - _call(target, value, data); - } - - function executeBatch( - address[] calldata targets, - uint256[] calldata values, - bytes[] calldata calldatas - ) public virtual onlyEntryPoint { - if (targets.length != calldatas.length || (values.length != 0 && values.length != targets.length)) { - revert AccountInvalidBatchLength(); - } - - for (uint256 i = 0; i < targets.length; ++i) { - _call(targets[i], (values.length == 0 ? 0 : values[i]), calldatas[i]); - } - } - - function _call(address target, uint256 value, bytes memory data) internal { - (bool success, bytes memory returndata) = target.call{value: value}(data); - Address.verifyCallResult(success, returndata); - } -} diff --git a/contracts/abstraction/account/ERC7579Account.sol b/contracts/abstraction/account/ERC7579Account.sol new file mode 100644 index 00000000000..7ae35bc8906 --- /dev/null +++ b/contracts/abstraction/account/ERC7579Account.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Account} from "./Account.sol"; +import {Address} from "../../utils/Address.sol"; +import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; +import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; +import {IEntryPoint} from "../../interfaces/IERC4337.sol"; +import {IERC1271} from "../../interfaces/IERC1271.sol"; +import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; +import {IERC7579Execution, IERC7579AccountConfig, IERC7579ModuleConfig, Execution} from "../../interfaces/IERC7579Account.sol"; +import {ERC7579Utils, Mode, CallType, ExecType} from "../utils/ERC7579Utils.sol"; + +abstract contract ERC7579Account is + IERC165, // required by erc-7579 + IERC1271, // required by erc-7579 + IERC7579Execution, // required by erc-7579 + IERC7579AccountConfig, // required by erc-7579 + IERC7579ModuleConfig, // required by erc-7579 + Account, + ERC165, + ERC721Holder, + ERC1155Holder +{ + using ERC7579Utils for *; + + IEntryPoint private immutable _entryPoint; + + event ERC7579TryExecuteUnsuccessful(uint256 batchExecutionindex, bytes result); + error ERC7579UnsupportedCallType(CallType); + error ERC7579UnsupportedExecType(ExecType); + + modifier onlyExecutorModule() { + // TODO + _; + } + + constructor(IEntryPoint entryPoint_) { + _entryPoint = entryPoint_; + } + + receive() external payable {} + + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + /// @inheritdoc IERC165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC165, ERC1155Holder) returns (bool) { + // TODO: more? + return super.supportsInterface(interfaceId); + } + + /// @inheritdoc IERC1271 + function isValidSignature(bytes32 hash, bytes calldata signature) public view returns (bytes4 magicValue) { + (bool valid, , uint48 validAfter, uint48 validUntil) = _processSignature(hash, signature); + return + (valid && validAfter < block.timestamp && (validUntil == 0 || validUntil > block.timestamp)) + ? IERC1271.isValidSignature.selector + : bytes4(0); + } + + /// @inheritdoc IERC7579Execution + function execute(bytes32 mode, bytes calldata executionCalldata) public virtual onlyEntryPoint { + _execute(Mode.wrap(mode), executionCalldata); + } + + /// @inheritdoc IERC7579Execution + function executeFromExecutor( + bytes32 mode, + bytes calldata executionCalldata + ) public virtual onlyExecutorModule returns (bytes[] memory) { + return _execute(Mode.wrap(mode), executionCalldata); + } + + function _execute( + Mode mode, + bytes calldata executionCalldata + ) internal virtual returns (bytes[] memory returnData) { + // TODO: ModeSelector? ModePayload? + (CallType callType, ExecType execType, , ) = mode.decodeMode(); + + if (callType == ERC7579Utils.CALLTYPE_SINGLE) { + (address target, uint256 value, bytes calldata callData) = executionCalldata.decodeSingle(); + returnData = new bytes[](1); + returnData[0] = _execute(0, execType, target, value, callData); + } else if (callType == ERC7579Utils.CALLTYPE_BATCH) { + Execution[] calldata executionBatch = executionCalldata.decodeBatch(); + returnData = new bytes[](executionBatch.length); + for (uint256 i = 0; i < executionBatch.length; ++i) { + returnData[i] = _execute( + i, + execType, + executionBatch[i].target, + executionBatch[i].value, + executionBatch[i].callData + ); + } + } else if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) { + (address target, bytes calldata callData) = executionCalldata.decodeDelegate(); + returnData = new bytes[](1); + returnData[0] = _executeDelegate(0, execType, target, callData); + } else { + revert ERC7579UnsupportedCallType(callType); + } + } + + function _execute( + uint256 index, + ExecType execType, + address target, + uint256 value, + bytes memory data + ) private returns (bytes memory) { + if (execType == ERC7579Utils.EXECTYPE_DEFAULT) { + (bool success, bytes memory returndata) = target.call{value: value}(data); + Address.verifyCallResult(success, returndata); + return returndata; + } else if (execType == ERC7579Utils.EXECTYPE_TRY) { + (bool success, bytes memory returndata) = target.call{value: value}(data); + if (!success) emit ERC7579TryExecuteUnsuccessful(index, returndata); + return returndata; + } else { + revert ERC7579UnsupportedExecType(execType); + } + } + + function _executeDelegate( + uint256 index, + ExecType execType, + address target, + bytes memory data + ) private returns (bytes memory) { + if (execType == ERC7579Utils.EXECTYPE_DEFAULT) { + (bool success, bytes memory returndata) = target.delegatecall(data); + Address.verifyCallResult(success, returndata); + return returndata; + } else if (execType == ERC7579Utils.EXECTYPE_TRY) { + (bool success, bytes memory returndata) = target.delegatecall(data); + if (!success) emit ERC7579TryExecuteUnsuccessful(index, returndata); + return returndata; + } else { + revert ERC7579UnsupportedExecType(execType); + } + } + + /// @inheritdoc IERC7579AccountConfig + function accountId() public view virtual returns (string memory) { + //vendorname.accountname.semver + return "@openzeppelin/contracts.erc7579account.v0-beta"; + } + + /// @inheritdoc IERC7579AccountConfig + function supportsExecutionMode(bytes32 encodedMode) public view virtual returns (bool) { + (CallType callType, , , ) = Mode.wrap(encodedMode).decodeMode(); + return + callType == ERC7579Utils.CALLTYPE_SINGLE || + callType == ERC7579Utils.CALLTYPE_BATCH || + callType == ERC7579Utils.CALLTYPE_DELEGATECALL; + } + + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 moduleTypeId) public view virtual returns (bool) { + // TODO: update when module support is added + moduleTypeId; + return false; + } + + /// @inheritdoc IERC7579ModuleConfig + function installModule(uint256 moduleTypeId, address module, bytes calldata initData) public pure { + moduleTypeId; + module; + initData; + revert("not-implemented-yet"); + } + + /// @inheritdoc IERC7579ModuleConfig + function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) public pure { + moduleTypeId; + module; + deInitData; + revert("not-implemented-yet"); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) public view returns (bool) { + moduleTypeId; + module; + additionalContext; + address(this); + revert("not-implemented-yet"); + } +} diff --git a/contracts/abstraction/account/modules/AccountMultisig.sol b/contracts/abstraction/account/modules/AccountMultisig.sol index 67498695148..c6c6f8f6caa 100644 --- a/contracts/abstraction/account/modules/AccountMultisig.sol +++ b/contracts/abstraction/account/modules/AccountMultisig.sol @@ -13,7 +13,7 @@ abstract contract AccountMultisig is Account { function _processSignature( bytes32 userOpHash, bytes calldata signatures - ) internal virtual override returns (bool, address, uint48 validAfter, uint48 validUntil) { + ) internal view virtual override returns (bool, address, uint48 validAfter, uint48 validUntil) { bytes[] calldata signatureArray = _decodeBytesArray(signatures); if (signatureArray.length < requiredSignatures()) { diff --git a/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol b/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol index 97f87448dd7..a10aa0cdff4 100644 --- a/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol +++ b/contracts/abstraction/account/modules/recovery/AccountAllSignatures.sol @@ -15,7 +15,7 @@ abstract contract AccountAllSignatures is AccountECDSA, AccountERC1271 { function _recoverSigner( bytes32 userOpHash, bytes calldata signature - ) internal virtual override(AccountECDSA, AccountERC1271) returns (address) { + ) internal view virtual override(AccountECDSA, AccountERC1271) returns (address) { SignatureType sigType = SignatureType(uint8(bytes1(signature))); if (sigType == SignatureType.ECDSA) { diff --git a/contracts/abstraction/account/modules/recovery/AccountECDSA.sol b/contracts/abstraction/account/modules/recovery/AccountECDSA.sol index 206d65f0f9d..51cc214f079 100644 --- a/contracts/abstraction/account/modules/recovery/AccountECDSA.sol +++ b/contracts/abstraction/account/modules/recovery/AccountECDSA.sol @@ -11,7 +11,7 @@ abstract contract AccountECDSA is Account { function _recoverSigner( bytes32 userOpHash, bytes calldata signature - ) internal virtual override returns (address signer) { + ) internal view virtual override returns (address signer) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); // This implementation support both "normal" and short signature formats: diff --git a/contracts/abstraction/account/modules/recovery/AccountERC1271.sol b/contracts/abstraction/account/modules/recovery/AccountERC1271.sol index 43423c3a7a5..c92f323f4aa 100644 --- a/contracts/abstraction/account/modules/recovery/AccountERC1271.sol +++ b/contracts/abstraction/account/modules/recovery/AccountERC1271.sol @@ -10,7 +10,10 @@ import {Account} from "../../Account.sol"; abstract contract AccountERC1271 is Account { error P256InvalidSignatureLength(uint256 length); - function _recoverSigner(bytes32 userOpHash, bytes calldata signature) internal virtual override returns (address) { + function _recoverSigner( + bytes32 userOpHash, + bytes calldata signature + ) internal view virtual override returns (address) { bytes32 msgHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); address signer = address(bytes20(signature[0x00:0x14])); diff --git a/contracts/abstraction/mocks/AdvancedAccount.sol b/contracts/abstraction/mocks/AdvancedAccount.sol index f60d9f7bd98..9475a768104 100644 --- a/contracts/abstraction/mocks/AdvancedAccount.sol +++ b/contracts/abstraction/mocks/AdvancedAccount.sol @@ -4,13 +4,12 @@ pragma solidity ^0.8.20; import {IEntryPoint} from "../../interfaces/IERC4337.sol"; import {AccessControl} from "../../access/AccessControl.sol"; -import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; import {Account} from "../account/Account.sol"; -import {AccountCommon} from "../account/AccountCommon.sol"; +import {ERC7579Account} from "../account/ERC7579Account.sol"; import {AccountMultisig} from "../account/modules/AccountMultisig.sol"; import {AccountAllSignatures} from "../account/modules/recovery/AccountAllSignatures.sol"; -contract AdvancedAccount is AccessControl, AccountCommon, AccountAllSignatures, AccountMultisig { +contract AdvancedAccount is AccessControl, ERC7579Account, AccountAllSignatures, AccountMultisig { bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); uint256 private _requiredSignatures; @@ -19,7 +18,7 @@ contract AdvancedAccount is AccessControl, AccountCommon, AccountAllSignatures, address admin_, address[] memory signers_, uint256 requiredSignatures_ - ) AccountCommon(entryPoint_) { + ) ERC7579Account(entryPoint_) { _grantRole(DEFAULT_ADMIN_ROLE, admin_); for (uint256 i = 0; i < signers_.length; ++i) { _grantRole(SIGNER_ROLE, signers_[i]); @@ -29,7 +28,7 @@ contract AdvancedAccount is AccessControl, AccountCommon, AccountAllSignatures, function supportsInterface( bytes4 interfaceId - ) public view virtual override(AccessControl, ERC1155Holder) returns (bool) { + ) public view virtual override(ERC7579Account, AccessControl) returns (bool) { return super.supportsInterface(interfaceId); } @@ -44,7 +43,7 @@ contract AdvancedAccount is AccessControl, AccountCommon, AccountAllSignatures, function _processSignature( bytes32 userOpHash, bytes calldata signature - ) internal virtual override(Account, AccountMultisig) returns (bool, address, uint48, uint48) { + ) internal view virtual override(Account, AccountMultisig) returns (bool, address, uint48, uint48) { return super._processSignature(userOpHash, signature); } } diff --git a/contracts/abstraction/mocks/SimpleAccount.sol b/contracts/abstraction/mocks/SimpleAccount.sol index 34bd27fdf96..9c23cd07ea3 100644 --- a/contracts/abstraction/mocks/SimpleAccount.sol +++ b/contracts/abstraction/mocks/SimpleAccount.sol @@ -4,20 +4,20 @@ pragma solidity ^0.8.20; import {IEntryPoint} from "../../interfaces/IERC4337.sol"; import {Ownable} from "../../access/Ownable.sol"; -import {AccountCommon} from "../account/AccountCommon.sol"; +import {ERC7579Account} from "../account/ERC7579Account.sol"; import {AccountECDSA} from "../account/modules/recovery/AccountECDSA.sol"; import {AccountERC1271} from "../account/modules/recovery/AccountERC1271.sol"; -contract SimpleAccountECDSA is Ownable, AccountCommon, AccountECDSA { - constructor(IEntryPoint entryPoint_, address owner_) AccountCommon(entryPoint_) Ownable(owner_) {} +contract SimpleAccountECDSA is Ownable, ERC7579Account, AccountECDSA { + constructor(IEntryPoint entryPoint_, address owner_) ERC7579Account(entryPoint_) Ownable(owner_) {} function _isAuthorized(address user) internal view virtual override returns (bool) { return user == owner(); } } -contract SimpleAccountERC1271 is Ownable, AccountCommon, AccountERC1271 { - constructor(IEntryPoint entryPoint_, address owner_) AccountCommon(entryPoint_) Ownable(owner_) {} +contract SimpleAccountERC1271 is Ownable, ERC7579Account, AccountERC1271 { + constructor(IEntryPoint entryPoint_, address owner_) ERC7579Account(entryPoint_) Ownable(owner_) {} function _isAuthorized(address user) internal view virtual override returns (bool) { return user == owner(); diff --git a/contracts/abstraction/utils/ERC7579Utils.sol b/contracts/abstraction/utils/ERC7579Utils.sol new file mode 100644 index 00000000000..8c68084e778 --- /dev/null +++ b/contracts/abstraction/utils/ERC7579Utils.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Execution} from "../../interfaces/IERC7579Account.sol"; +import {Packing} from "../../utils/Packing.sol"; + +type Mode is bytes32; +type CallType is bytes1; +type ExecType is bytes1; +type ModeSelector is bytes4; +type ModePayload is bytes22; + +library ERC7579Utils { + using Packing for *; + + CallType constant CALLTYPE_SINGLE = CallType.wrap(0x00); + CallType constant CALLTYPE_BATCH = CallType.wrap(0x01); + CallType constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); + ExecType constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); + ExecType constant EXECTYPE_TRY = ExecType.wrap(0x01); + + function encodeMode( + CallType callType, + ExecType execType, + ModeSelector selector, + ModePayload payload + ) internal pure returns (Mode mode) { + return + Mode.wrap( + CallType + .unwrap(callType) + .pack_1_1(ExecType.unwrap(execType)) + .pack_2_4(bytes4(0)) + .pack_6_4(ModeSelector.unwrap(selector)) + .pack_10_22(ModePayload.unwrap(payload)) + ); + } + + function decodeMode( + Mode mode + ) internal pure returns (CallType callType, ExecType execType, ModeSelector selector, ModePayload payload) { + return ( + CallType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 0)), + ExecType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 1)), + ModeSelector.wrap(Packing.extract_32_4(Mode.unwrap(mode), 6)), + ModePayload.wrap(Packing.extract_32_22(Mode.unwrap(mode), 10)) + ); + } + + function encodeSingle( + address target, + uint256 value, + bytes memory callData + ) internal pure returns (bytes memory executionCalldata) { + return abi.encodePacked(target, value, callData); + } + + function decodeSingle( + bytes calldata executionCalldata + ) internal pure returns (address target, uint256 value, bytes calldata callData) { + target = address(bytes20(executionCalldata[0:20])); + value = uint256(bytes32(executionCalldata[20:52])); + callData = executionCalldata[52:]; + } + + function encodeDelegate( + address target, + bytes memory callData + ) internal pure returns (bytes memory executionCalldata) { + return abi.encodePacked(target, callData); + } + + function decodeDelegate( + bytes calldata executionCalldata + ) internal pure returns (address target, bytes calldata callData) { + target = address(bytes20(executionCalldata[0:20])); + callData = executionCalldata[20:]; + } + + function encodeBatch(Execution[] memory executionBatch) internal pure returns (bytes memory executionCalldata) { + return abi.encode(executionBatch); + } + + function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) { + assembly ("memory-safe") { + let ptr := add(executionCalldata.offset, calldataload(executionCalldata.offset)) + // Extract the ERC7579 Executions + executionBatch.offset := add(ptr, 32) + executionBatch.length := calldataload(ptr) + } + } +} + +// Operators +using {eqCallType as ==} for CallType global; +using {eqExecType as ==} for ExecType global; +using {eqModeSelector as ==} for ModeSelector global; + +function eqCallType(CallType a, CallType b) pure returns (bool) { + return CallType.unwrap(a) == CallType.unwrap(b); +} + +function eqExecType(ExecType a, ExecType b) pure returns (bool) { + return ExecType.unwrap(a) == ExecType.unwrap(b); +} + +function eqModeSelector(ModeSelector a, ModeSelector b) pure returns (bool) { + return ModeSelector.unwrap(a) == ModeSelector.unwrap(b); +} diff --git a/contracts/interfaces/IERC7579Account.sol b/contracts/interfaces/IERC7579Account.sol new file mode 100644 index 00000000000..0be805f5b1f --- /dev/null +++ b/contracts/interfaces/IERC7579Account.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +// import { CallType, ExecType, ModeCode } from "../lib/ModeLib.sol"; +import {IERC165} from "./IERC165.sol"; +import {IERC1271} from "./IERC1271.sol"; + +struct Execution { + address target; + uint256 value; + bytes callData; +} + +interface IERC7579Execution { + /** + * @dev Executes a transaction on behalf of the account. + * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details + * @param executionCalldata The encoded execution call data + * + * MUST ensure adequate authorization control: e.g. onlyEntryPointOrSelf if used with ERC-4337 + * If a mode is requested that is not supported by the Account, it MUST revert + */ + function execute(bytes32 mode, bytes calldata executionCalldata) external; + + /** + * @dev Executes a transaction on behalf of the account. + * This function is intended to be called by Executor Modules + * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details + * @param executionCalldata The encoded execution call data + * + * MUST ensure adequate authorization control: i.e. onlyExecutorModule + * If a mode is requested that is not supported by the Account, it MUST revert + */ + function executeFromExecutor( + bytes32 mode, + bytes calldata executionCalldata + ) external returns (bytes[] memory returnData); +} + +interface IERC7579AccountConfig { + /** + * @dev Returns the account id of the smart account + * @return accountImplementationId the account id of the smart account + * + * MUST return a non-empty string + * The accountId SHOULD be structured like so: + * "vendorname.accountname.semver" + * The id SHOULD be unique across all smart accounts + */ + function accountId() external view returns (string memory accountImplementationId); + + /** + * @dev Function to check if the account supports a certain execution mode (see above) + * @param encodedMode the encoded mode + * + * MUST return true if the account supports the mode and false otherwise + */ + function supportsExecutionMode(bytes32 encodedMode) external view returns (bool); + + /** + * @dev Function to check if the account supports a certain module typeId + * @param moduleTypeId the module type ID according to the ERC-7579 spec + * + * MUST return true if the account supports the module type and false otherwise + */ + function supportsModule(uint256 moduleTypeId) external view returns (bool); +} + +interface IERC7579ModuleConfig { + event ModuleInstalled(uint256 moduleTypeId, address module); + event ModuleUninstalled(uint256 moduleTypeId, address module); + + /** + * @dev Installs a Module of a certain type on the smart account + * @param moduleTypeId the module type ID according to the ERC-7579 spec + * @param module the module address + * @param initData arbitrary data that may be required on the module during `onInstall` + * initialization. + * + * MUST implement authorization control + * MUST call `onInstall` on the module with the `initData` parameter if provided + * MUST emit ModuleInstalled event + * MUST revert if the module is already installed or the initialization on the module failed + */ + function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external; + + /** + * @dev Uninstalls a Module of a certain type on the smart account + * @param moduleTypeId the module type ID according the ERC-7579 spec + * @param module the module address + * @param deInitData arbitrary data that may be required on the module during `onInstall` + * initialization. + * + * MUST implement authorization control + * MUST call `onUninstall` on the module with the `deInitData` parameter if provided + * MUST emit ModuleUninstalled event + * MUST revert if the module is not installed or the deInitialization on the module failed + */ + function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external; + + /** + * @dev Returns whether a module is installed on the smart account + * @param moduleTypeId the module type ID according the ERC-7579 spec + * @param module the module address + * @param additionalContext arbitrary data that may be required to determine if the module is installed + * + * MUST return true if the module is installed and false otherwise + */ + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) external view returns (bool); +} diff --git a/contracts/interfaces/IERC7579Module.sol b/contracts/interfaces/IERC7579Module.sol new file mode 100644 index 00000000000..0350e7e8853 --- /dev/null +++ b/contracts/interfaces/IERC7579Module.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "./IERC4337.sol"; + +interface IERC7579Module { + /** + * @dev This function is called by the smart account during installation of the module + * @param data arbitrary data that may be required on the module during `onInstall` initialization + * + * MUST revert on error (e.g. if module is already enabled) + */ + function onInstall(bytes calldata data) external; + + /** + * @dev This function is called by the smart account during uninstallation of the module + * @param data arbitrary data that may be required on the module during `onUninstall` de-initialization + * + * MUST revert on error + */ + function onUninstall(bytes calldata data) external; + + /** + * @dev Returns boolean value if module is a certain type + * @param moduleTypeId the module type ID according the ERC-7579 spec + * + * MUST return true if the module is of the given type and false otherwise + */ + function isModuleType(uint256 moduleTypeId) external view returns (bool); +} + +interface IERC7579Validator is IERC7579Module { + /** + * @dev Validates a UserOperation + * @param userOp the ERC-4337 PackedUserOperation + * @param userOpHash the hash of the ERC-4337 PackedUserOperation + * + * MUST validate that the signature is a valid signature of the userOpHash + * SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch + */ + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); + + /** + * @dev Validates a signature using ERC-1271 + * @param sender the address that sent the ERC-1271 request to the smart account + * @param hash the hash of the ERC-1271 request + * @param signature the signature of the ERC-1271 request + * + * MUST return the ERC-1271 `MAGIC_VALUE` if the signature is valid + * MUST NOT modify state + */ + function isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata signature + ) external view returns (bytes4); +} + +interface IERC7579Hook is IERC7579Module { + /** + * @dev Called by the smart account before execution + * @param msgSender the address that called the smart account + * @param value the value that was sent to the smart account + * @param msgData the data that was sent to the smart account + * + * MAY return arbitrary data in the `hookData` return value + */ + function preCheck( + address msgSender, + uint256 value, + bytes calldata msgData + ) external returns (bytes memory hookData); + + /** + * @dev Called by the smart account after execution + * @param hookData the data that was returned by the `preCheck` function + * + * MAY validate the `hookData` to validate transaction context of the `preCheck` function + */ + function postCheck(bytes calldata hookData) external; +} diff --git a/contracts/utils/Packing.sol b/contracts/utils/Packing.sol index a5a38b50397..61cfa42c757 100644 --- a/contracts/utils/Packing.sol +++ b/contracts/utils/Packing.sol @@ -57,6 +57,30 @@ library Packing { } } + function pack_2_8(bytes2 left, bytes8 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + result := or(left, shr(16, right)) + } + } + + function pack_2_10(bytes2 left, bytes10 right) internal pure returns (bytes12 result) { + assembly ("memory-safe") { + result := or(left, shr(16, right)) + } + } + + function pack_2_20(bytes2 left, bytes20 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(16, right)) + } + } + + function pack_2_22(bytes2 left, bytes22 right) internal pure returns (bytes24 result) { + assembly ("memory-safe") { + result := or(left, shr(16, right)) + } + } + function pack_4_2(bytes4 left, bytes2 right) internal pure returns (bytes6 result) { assembly ("memory-safe") { result := or(left, shr(32, right)) @@ -69,6 +93,12 @@ library Packing { } } + function pack_4_6(bytes4 left, bytes6 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + result := or(left, shr(32, right)) + } + } + function pack_4_8(bytes4 left, bytes8 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { result := or(left, shr(32, right)) @@ -111,12 +141,42 @@ library Packing { } } + function pack_6_4(bytes6 left, bytes4 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + result := or(left, shr(48, right)) + } + } + function pack_6_6(bytes6 left, bytes6 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { result := or(left, shr(48, right)) } } + function pack_6_10(bytes6 left, bytes10 right) internal pure returns (bytes16 result) { + assembly ("memory-safe") { + result := or(left, shr(48, right)) + } + } + + function pack_6_16(bytes6 left, bytes16 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(48, right)) + } + } + + function pack_6_22(bytes6 left, bytes22 right) internal pure returns (bytes28 result) { + assembly ("memory-safe") { + result := or(left, shr(48, right)) + } + } + + function pack_8_2(bytes8 left, bytes2 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + result := or(left, shr(64, right)) + } + } + function pack_8_4(bytes8 left, bytes4 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { result := or(left, shr(64, right)) @@ -153,6 +213,36 @@ library Packing { } } + function pack_10_2(bytes10 left, bytes2 right) internal pure returns (bytes12 result) { + assembly ("memory-safe") { + result := or(left, shr(80, right)) + } + } + + function pack_10_6(bytes10 left, bytes6 right) internal pure returns (bytes16 result) { + assembly ("memory-safe") { + result := or(left, shr(80, right)) + } + } + + function pack_10_10(bytes10 left, bytes10 right) internal pure returns (bytes20 result) { + assembly ("memory-safe") { + result := or(left, shr(80, right)) + } + } + + function pack_10_12(bytes10 left, bytes12 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(80, right)) + } + } + + function pack_10_22(bytes10 left, bytes22 right) internal pure returns (bytes32 result) { + assembly ("memory-safe") { + result := or(left, shr(80, right)) + } + } + function pack_12_4(bytes12 left, bytes4 right) internal pure returns (bytes16 result) { assembly ("memory-safe") { result := or(left, shr(96, right)) @@ -165,6 +255,12 @@ library Packing { } } + function pack_12_10(bytes12 left, bytes10 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(96, right)) + } + } + function pack_12_12(bytes12 left, bytes12 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { result := or(left, shr(96, right)) @@ -189,6 +285,12 @@ library Packing { } } + function pack_16_6(bytes16 left, bytes6 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(128, right)) + } + } + function pack_16_8(bytes16 left, bytes8 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { result := or(left, shr(128, right)) @@ -207,6 +309,12 @@ library Packing { } } + function pack_20_2(bytes20 left, bytes2 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + result := or(left, shr(160, right)) + } + } + function pack_20_4(bytes20 left, bytes4 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { result := or(left, shr(160, right)) @@ -225,6 +333,24 @@ library Packing { } } + function pack_22_2(bytes22 left, bytes2 right) internal pure returns (bytes24 result) { + assembly ("memory-safe") { + result := or(left, shr(176, right)) + } + } + + function pack_22_6(bytes22 left, bytes6 right) internal pure returns (bytes28 result) { + assembly ("memory-safe") { + result := or(left, shr(176, right)) + } + } + + function pack_22_10(bytes22 left, bytes10 right) internal pure returns (bytes32 result) { + assembly ("memory-safe") { + result := or(left, shr(176, right)) + } + } + function pack_24_4(bytes24 left, bytes4 right) internal pure returns (bytes28 result) { assembly ("memory-safe") { result := or(left, shr(192, right)) @@ -383,6 +509,76 @@ library Packing { } } + function extract_10_1(bytes10 self, uint8 offset) internal pure returns (bytes1 result) { + if (offset > 9) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(248, not(0))) + } + } + + function replace_10_1(bytes10 self, bytes1 value, uint8 offset) internal pure returns (bytes10 result) { + bytes1 oldValue = extract_10_1(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_2(bytes10 self, uint8 offset) internal pure returns (bytes2 result) { + if (offset > 8) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(240, not(0))) + } + } + + function replace_10_2(bytes10 self, bytes2 value, uint8 offset) internal pure returns (bytes10 result) { + bytes2 oldValue = extract_10_2(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_4(bytes10 self, uint8 offset) internal pure returns (bytes4 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(224, not(0))) + } + } + + function replace_10_4(bytes10 self, bytes4 value, uint8 offset) internal pure returns (bytes10 result) { + bytes4 oldValue = extract_10_4(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_6(bytes10 self, uint8 offset) internal pure returns (bytes6 result) { + if (offset > 4) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(208, not(0))) + } + } + + function replace_10_6(bytes10 self, bytes6 value, uint8 offset) internal pure returns (bytes10 result) { + bytes6 oldValue = extract_10_6(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_8(bytes10 self, uint8 offset) internal pure returns (bytes8 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(192, not(0))) + } + } + + function replace_10_8(bytes10 self, bytes8 value, uint8 offset) internal pure returns (bytes10 result) { + bytes8 oldValue = extract_10_8(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_12_1(bytes12 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 11) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -453,6 +649,20 @@ library Packing { } } + function extract_12_10(bytes12 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_12_10(bytes12 self, bytes10 value, uint8 offset) internal pure returns (bytes12 result) { + bytes10 oldValue = extract_12_10(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_16_1(bytes16 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 15) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -523,6 +733,20 @@ library Packing { } } + function extract_16_10(bytes16 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_16_10(bytes16 self, bytes10 value, uint8 offset) internal pure returns (bytes16 result) { + bytes10 oldValue = extract_16_10(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_16_12(bytes16 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 4) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -607,6 +831,20 @@ library Packing { } } + function extract_20_10(bytes20 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 10) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_20_10(bytes20 self, bytes10 value, uint8 offset) internal pure returns (bytes20 result) { + bytes10 oldValue = extract_20_10(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_20_12(bytes20 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 8) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -635,6 +873,132 @@ library Packing { } } + function extract_22_1(bytes22 self, uint8 offset) internal pure returns (bytes1 result) { + if (offset > 21) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(248, not(0))) + } + } + + function replace_22_1(bytes22 self, bytes1 value, uint8 offset) internal pure returns (bytes22 result) { + bytes1 oldValue = extract_22_1(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_2(bytes22 self, uint8 offset) internal pure returns (bytes2 result) { + if (offset > 20) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(240, not(0))) + } + } + + function replace_22_2(bytes22 self, bytes2 value, uint8 offset) internal pure returns (bytes22 result) { + bytes2 oldValue = extract_22_2(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_4(bytes22 self, uint8 offset) internal pure returns (bytes4 result) { + if (offset > 18) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(224, not(0))) + } + } + + function replace_22_4(bytes22 self, bytes4 value, uint8 offset) internal pure returns (bytes22 result) { + bytes4 oldValue = extract_22_4(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_6(bytes22 self, uint8 offset) internal pure returns (bytes6 result) { + if (offset > 16) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(208, not(0))) + } + } + + function replace_22_6(bytes22 self, bytes6 value, uint8 offset) internal pure returns (bytes22 result) { + bytes6 oldValue = extract_22_6(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_8(bytes22 self, uint8 offset) internal pure returns (bytes8 result) { + if (offset > 14) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(192, not(0))) + } + } + + function replace_22_8(bytes22 self, bytes8 value, uint8 offset) internal pure returns (bytes22 result) { + bytes8 oldValue = extract_22_8(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_10(bytes22 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 12) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_22_10(bytes22 self, bytes10 value, uint8 offset) internal pure returns (bytes22 result) { + bytes10 oldValue = extract_22_10(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_12(bytes22 self, uint8 offset) internal pure returns (bytes12 result) { + if (offset > 10) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(160, not(0))) + } + } + + function replace_22_12(bytes22 self, bytes12 value, uint8 offset) internal pure returns (bytes22 result) { + bytes12 oldValue = extract_22_12(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_16(bytes22 self, uint8 offset) internal pure returns (bytes16 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(128, not(0))) + } + } + + function replace_22_16(bytes22 self, bytes16 value, uint8 offset) internal pure returns (bytes22 result) { + bytes16 oldValue = extract_22_16(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_20(bytes22 self, uint8 offset) internal pure returns (bytes20 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(96, not(0))) + } + } + + function replace_22_20(bytes22 self, bytes20 value, uint8 offset) internal pure returns (bytes22 result) { + bytes20 oldValue = extract_22_20(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_24_1(bytes24 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 23) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -705,6 +1069,20 @@ library Packing { } } + function extract_24_10(bytes24 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 14) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_24_10(bytes24 self, bytes10 value, uint8 offset) internal pure returns (bytes24 result) { + bytes10 oldValue = extract_24_10(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_24_12(bytes24 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 12) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -747,6 +1125,20 @@ library Packing { } } + function extract_24_22(bytes24 self, uint8 offset) internal pure returns (bytes22 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(80, not(0))) + } + } + + function replace_24_22(bytes24 self, bytes22 value, uint8 offset) internal pure returns (bytes24 result) { + bytes22 oldValue = extract_24_22(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_28_1(bytes28 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 27) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -817,6 +1209,20 @@ library Packing { } } + function extract_28_10(bytes28 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 18) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_28_10(bytes28 self, bytes10 value, uint8 offset) internal pure returns (bytes28 result) { + bytes10 oldValue = extract_28_10(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_28_12(bytes28 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 16) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -859,6 +1265,20 @@ library Packing { } } + function extract_28_22(bytes28 self, uint8 offset) internal pure returns (bytes22 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(80, not(0))) + } + } + + function replace_28_22(bytes28 self, bytes22 value, uint8 offset) internal pure returns (bytes28 result) { + bytes22 oldValue = extract_28_22(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_28_24(bytes28 self, uint8 offset) internal pure returns (bytes24 result) { if (offset > 4) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -943,6 +1363,20 @@ library Packing { } } + function extract_32_10(bytes32 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 22) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_32_10(bytes32 self, bytes10 value, uint8 offset) internal pure returns (bytes32 result) { + bytes10 oldValue = extract_32_10(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_32_12(bytes32 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 20) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -985,6 +1419,20 @@ library Packing { } } + function extract_32_22(bytes32 self, uint8 offset) internal pure returns (bytes22 result) { + if (offset > 10) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(80, not(0))) + } + } + + function replace_32_22(bytes32 self, bytes22 value, uint8 offset) internal pure returns (bytes32 result) { + bytes22 oldValue = extract_32_22(self, offset); + assembly ("memory-safe") { + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_32_24(bytes32 self, uint8 offset) internal pure returns (bytes24 result) { if (offset > 8) revert OutOfRangeAccess(); assembly ("memory-safe") { diff --git a/scripts/generate/templates/Packing.opts.js b/scripts/generate/templates/Packing.opts.js index de9ab77ff53..893ad6297cf 100644 --- a/scripts/generate/templates/Packing.opts.js +++ b/scripts/generate/templates/Packing.opts.js @@ -1,3 +1,3 @@ module.exports = { - SIZES: [1, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32], + SIZES: [1, 2, 4, 6, 8, 10, 12, 16, 20, 22, 24, 28, 32], }; diff --git a/test/abstraction/TODO.md b/test/abstraction/TODO.md new file mode 100644 index 00000000000..1cb0be092ae --- /dev/null +++ b/test/abstraction/TODO.md @@ -0,0 +1,5 @@ +- [ ] test ERC1271 +- [ ] test batch exec mode +- [ ] implement and test delegate exec mode ? +- [ ] implement module support +- [ ] implement ECDSA, 1271, multisig as modules ? \ No newline at end of file diff --git a/test/abstraction/accountECDSA.test.js b/test/abstraction/accountECDSA.test.js index 190bf70763c..5bb9727ec01 100644 --- a/test/abstraction/accountECDSA.test.js +++ b/test/abstraction/accountECDSA.test.js @@ -2,8 +2,9 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { ERC4337Helper } = require('../helpers/erc4337'); const { IdentityHelper } = require('../helpers/identity'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { encodeMode, encodeSingle } = require('../helpers/erc7579'); async function fixture() { const accounts = await ethers.getSigners(); @@ -46,9 +47,8 @@ describe('AccountECDSA', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 17, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.addInitCode()) @@ -71,9 +71,8 @@ describe('AccountECDSA', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 42, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.sign()); @@ -87,9 +86,8 @@ describe('AccountECDSA', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 42, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.sign()); diff --git a/test/abstraction/accountERC1271.test.js b/test/abstraction/accountERC1271.test.js index 14dfbea0845..a12788d6312 100644 --- a/test/abstraction/accountERC1271.test.js +++ b/test/abstraction/accountERC1271.test.js @@ -2,8 +2,9 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { ERC4337Helper } = require('../helpers/erc4337'); const { IdentityHelper } = require('../helpers/identity'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { encodeMode, encodeSingle } = require('../helpers/erc7579'); async function fixture() { const accounts = await ethers.getSigners(); @@ -47,9 +48,8 @@ describe('AccountERC1271', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 17, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.addInitCode()) @@ -72,9 +72,8 @@ describe('AccountERC1271', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 42, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.sign()); diff --git a/test/abstraction/accountMultisig.test.js b/test/abstraction/accountMultisig.test.js index b8d65061ec6..bbe5924b366 100644 --- a/test/abstraction/accountMultisig.test.js +++ b/test/abstraction/accountMultisig.test.js @@ -2,8 +2,9 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { ERC4337Helper } = require('../helpers/erc4337'); const { IdentityHelper } = require('../helpers/identity'); +const { ERC4337Helper } = require('../helpers/erc4337'); +const { encodeMode, encodeSingle } = require('../helpers/erc7579'); async function fixture() { const accounts = await ethers.getSigners(); @@ -52,9 +53,8 @@ describe('AccountMultisig', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 17, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.addInitCode()) @@ -77,9 +77,8 @@ describe('AccountMultisig', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 42, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.sign(this.signers)); @@ -93,9 +92,8 @@ describe('AccountMultisig', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 42, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.sign([this.signers[0], this.signers[2]])); @@ -109,9 +107,8 @@ describe('AccountMultisig', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 42, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.sign([this.signers[2]])); @@ -125,9 +122,8 @@ describe('AccountMultisig', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 42, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.sign([this.accounts.relayer, this.signers[2]])); diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js index df73f560c8b..fab14678411 100644 --- a/test/abstraction/entrypoint.test.js +++ b/test/abstraction/entrypoint.test.js @@ -4,6 +4,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); const { ERC4337Helper } = require('../helpers/erc4337'); +const { encodeMode, encodeSingle } = require('../helpers/erc7579'); async function fixture() { const accounts = await ethers.getSigners(); @@ -143,9 +144,8 @@ describe('EntryPoint', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 17, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 17, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.addInitCode()) @@ -168,9 +168,8 @@ describe('EntryPoint', function () { const operation = await this.sender .createOp({ callData: this.sender.interface.encodeFunctionData('execute', [ - this.target.target, - 42, - this.target.interface.encodeFunctionData('mockFunctionExtra'), + encodeMode(), + encodeSingle(this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')), ]), }) .then(op => op.sign()); diff --git a/test/helpers/erc7579.js b/test/helpers/erc7579.js new file mode 100644 index 00000000000..ac0e7127a25 --- /dev/null +++ b/test/helpers/erc7579.js @@ -0,0 +1,23 @@ +const { ethers } = require('hardhat'); + +const encodeMode = ({ + callType = '0x00', + execType = '0x00', + selector = '0x00000000', + payload = '0x00000000000000000000000000000000000000000000', +} = {}) => + ethers.solidityPacked( + ['bytes1', 'bytes1', 'bytes4', 'bytes4', 'bytes22'], + [callType, execType, '0x00000000', selector, payload], + ); + +const encodeSingle = (target, value = 0n, data = '0x') => + ethers.solidityPacked(['address', 'uint256', 'bytes'], [target.target ?? target.address ?? target, value, data]); + +/// TODO +// const encodeBatch = + +module.exports = { + encodeMode, + encodeSingle, +}; diff --git a/test/utils/Packing.t.sol b/test/utils/Packing.t.sol index 9531f1bffbb..e8adda48920 100644 --- a/test/utils/Packing.t.sol +++ b/test/utils/Packing.t.sol @@ -29,6 +29,26 @@ contract PackingTest is Test { assertEq(right, Packing.pack_2_6(left, right).extract_8_6(2)); } + function testPack(bytes2 left, bytes8 right) external { + assertEq(left, Packing.pack_2_8(left, right).extract_10_2(0)); + assertEq(right, Packing.pack_2_8(left, right).extract_10_8(2)); + } + + function testPack(bytes2 left, bytes10 right) external { + assertEq(left, Packing.pack_2_10(left, right).extract_12_2(0)); + assertEq(right, Packing.pack_2_10(left, right).extract_12_10(2)); + } + + function testPack(bytes2 left, bytes20 right) external { + assertEq(left, Packing.pack_2_20(left, right).extract_22_2(0)); + assertEq(right, Packing.pack_2_20(left, right).extract_22_20(2)); + } + + function testPack(bytes2 left, bytes22 right) external { + assertEq(left, Packing.pack_2_22(left, right).extract_24_2(0)); + assertEq(right, Packing.pack_2_22(left, right).extract_24_22(2)); + } + function testPack(bytes4 left, bytes2 right) external { assertEq(left, Packing.pack_4_2(left, right).extract_6_4(0)); assertEq(right, Packing.pack_4_2(left, right).extract_6_2(4)); @@ -39,6 +59,11 @@ contract PackingTest is Test { assertEq(right, Packing.pack_4_4(left, right).extract_8_4(4)); } + function testPack(bytes4 left, bytes6 right) external { + assertEq(left, Packing.pack_4_6(left, right).extract_10_4(0)); + assertEq(right, Packing.pack_4_6(left, right).extract_10_6(4)); + } + function testPack(bytes4 left, bytes8 right) external { assertEq(left, Packing.pack_4_8(left, right).extract_12_4(0)); assertEq(right, Packing.pack_4_8(left, right).extract_12_8(4)); @@ -74,11 +99,36 @@ contract PackingTest is Test { assertEq(right, Packing.pack_6_2(left, right).extract_8_2(6)); } + function testPack(bytes6 left, bytes4 right) external { + assertEq(left, Packing.pack_6_4(left, right).extract_10_6(0)); + assertEq(right, Packing.pack_6_4(left, right).extract_10_4(6)); + } + function testPack(bytes6 left, bytes6 right) external { assertEq(left, Packing.pack_6_6(left, right).extract_12_6(0)); assertEq(right, Packing.pack_6_6(left, right).extract_12_6(6)); } + function testPack(bytes6 left, bytes10 right) external { + assertEq(left, Packing.pack_6_10(left, right).extract_16_6(0)); + assertEq(right, Packing.pack_6_10(left, right).extract_16_10(6)); + } + + function testPack(bytes6 left, bytes16 right) external { + assertEq(left, Packing.pack_6_16(left, right).extract_22_6(0)); + assertEq(right, Packing.pack_6_16(left, right).extract_22_16(6)); + } + + function testPack(bytes6 left, bytes22 right) external { + assertEq(left, Packing.pack_6_22(left, right).extract_28_6(0)); + assertEq(right, Packing.pack_6_22(left, right).extract_28_22(6)); + } + + function testPack(bytes8 left, bytes2 right) external { + assertEq(left, Packing.pack_8_2(left, right).extract_10_8(0)); + assertEq(right, Packing.pack_8_2(left, right).extract_10_2(8)); + } + function testPack(bytes8 left, bytes4 right) external { assertEq(left, Packing.pack_8_4(left, right).extract_12_8(0)); assertEq(right, Packing.pack_8_4(left, right).extract_12_4(8)); @@ -109,6 +159,31 @@ contract PackingTest is Test { assertEq(right, Packing.pack_8_24(left, right).extract_32_24(8)); } + function testPack(bytes10 left, bytes2 right) external { + assertEq(left, Packing.pack_10_2(left, right).extract_12_10(0)); + assertEq(right, Packing.pack_10_2(left, right).extract_12_2(10)); + } + + function testPack(bytes10 left, bytes6 right) external { + assertEq(left, Packing.pack_10_6(left, right).extract_16_10(0)); + assertEq(right, Packing.pack_10_6(left, right).extract_16_6(10)); + } + + function testPack(bytes10 left, bytes10 right) external { + assertEq(left, Packing.pack_10_10(left, right).extract_20_10(0)); + assertEq(right, Packing.pack_10_10(left, right).extract_20_10(10)); + } + + function testPack(bytes10 left, bytes12 right) external { + assertEq(left, Packing.pack_10_12(left, right).extract_22_10(0)); + assertEq(right, Packing.pack_10_12(left, right).extract_22_12(10)); + } + + function testPack(bytes10 left, bytes22 right) external { + assertEq(left, Packing.pack_10_22(left, right).extract_32_10(0)); + assertEq(right, Packing.pack_10_22(left, right).extract_32_22(10)); + } + function testPack(bytes12 left, bytes4 right) external { assertEq(left, Packing.pack_12_4(left, right).extract_16_12(0)); assertEq(right, Packing.pack_12_4(left, right).extract_16_4(12)); @@ -119,6 +194,11 @@ contract PackingTest is Test { assertEq(right, Packing.pack_12_8(left, right).extract_20_8(12)); } + function testPack(bytes12 left, bytes10 right) external { + assertEq(left, Packing.pack_12_10(left, right).extract_22_12(0)); + assertEq(right, Packing.pack_12_10(left, right).extract_22_10(12)); + } + function testPack(bytes12 left, bytes12 right) external { assertEq(left, Packing.pack_12_12(left, right).extract_24_12(0)); assertEq(right, Packing.pack_12_12(left, right).extract_24_12(12)); @@ -139,6 +219,11 @@ contract PackingTest is Test { assertEq(right, Packing.pack_16_4(left, right).extract_20_4(16)); } + function testPack(bytes16 left, bytes6 right) external { + assertEq(left, Packing.pack_16_6(left, right).extract_22_16(0)); + assertEq(right, Packing.pack_16_6(left, right).extract_22_6(16)); + } + function testPack(bytes16 left, bytes8 right) external { assertEq(left, Packing.pack_16_8(left, right).extract_24_16(0)); assertEq(right, Packing.pack_16_8(left, right).extract_24_8(16)); @@ -154,6 +239,11 @@ contract PackingTest is Test { assertEq(right, Packing.pack_16_16(left, right).extract_32_16(16)); } + function testPack(bytes20 left, bytes2 right) external { + assertEq(left, Packing.pack_20_2(left, right).extract_22_20(0)); + assertEq(right, Packing.pack_20_2(left, right).extract_22_2(20)); + } + function testPack(bytes20 left, bytes4 right) external { assertEq(left, Packing.pack_20_4(left, right).extract_24_20(0)); assertEq(right, Packing.pack_20_4(left, right).extract_24_4(20)); @@ -169,6 +259,21 @@ contract PackingTest is Test { assertEq(right, Packing.pack_20_12(left, right).extract_32_12(20)); } + function testPack(bytes22 left, bytes2 right) external { + assertEq(left, Packing.pack_22_2(left, right).extract_24_22(0)); + assertEq(right, Packing.pack_22_2(left, right).extract_24_2(22)); + } + + function testPack(bytes22 left, bytes6 right) external { + assertEq(left, Packing.pack_22_6(left, right).extract_28_22(0)); + assertEq(right, Packing.pack_22_6(left, right).extract_28_6(22)); + } + + function testPack(bytes22 left, bytes10 right) external { + assertEq(left, Packing.pack_22_10(left, right).extract_32_22(0)); + assertEq(right, Packing.pack_22_10(left, right).extract_32_10(22)); + } + function testPack(bytes24 left, bytes4 right) external { assertEq(left, Packing.pack_24_4(left, right).extract_28_24(0)); assertEq(right, Packing.pack_24_4(left, right).extract_28_4(24)); @@ -274,6 +379,51 @@ contract PackingTest is Test { assertEq(container, container.replace_8_6(newValue, offset).replace_8_6(oldValue, offset)); } + function testReplace(bytes10 container, bytes1 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 9)); + + bytes1 oldValue = container.extract_10_1(offset); + + assertEq(newValue, container.replace_10_1(newValue, offset).extract_10_1(offset)); + assertEq(container, container.replace_10_1(newValue, offset).replace_10_1(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes2 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 8)); + + bytes2 oldValue = container.extract_10_2(offset); + + assertEq(newValue, container.replace_10_2(newValue, offset).extract_10_2(offset)); + assertEq(container, container.replace_10_2(newValue, offset).replace_10_2(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes4 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 6)); + + bytes4 oldValue = container.extract_10_4(offset); + + assertEq(newValue, container.replace_10_4(newValue, offset).extract_10_4(offset)); + assertEq(container, container.replace_10_4(newValue, offset).replace_10_4(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes6 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 4)); + + bytes6 oldValue = container.extract_10_6(offset); + + assertEq(newValue, container.replace_10_6(newValue, offset).extract_10_6(offset)); + assertEq(container, container.replace_10_6(newValue, offset).replace_10_6(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes8 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 2)); + + bytes8 oldValue = container.extract_10_8(offset); + + assertEq(newValue, container.replace_10_8(newValue, offset).extract_10_8(offset)); + assertEq(container, container.replace_10_8(newValue, offset).replace_10_8(oldValue, offset)); + } + function testReplace(bytes12 container, bytes1 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 11)); @@ -319,6 +469,15 @@ contract PackingTest is Test { assertEq(container, container.replace_12_8(newValue, offset).replace_12_8(oldValue, offset)); } + function testReplace(bytes12 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 2)); + + bytes10 oldValue = container.extract_12_10(offset); + + assertEq(newValue, container.replace_12_10(newValue, offset).extract_12_10(offset)); + assertEq(container, container.replace_12_10(newValue, offset).replace_12_10(oldValue, offset)); + } + function testReplace(bytes16 container, bytes1 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 15)); @@ -364,6 +523,15 @@ contract PackingTest is Test { assertEq(container, container.replace_16_8(newValue, offset).replace_16_8(oldValue, offset)); } + function testReplace(bytes16 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 6)); + + bytes10 oldValue = container.extract_16_10(offset); + + assertEq(newValue, container.replace_16_10(newValue, offset).extract_16_10(offset)); + assertEq(container, container.replace_16_10(newValue, offset).replace_16_10(oldValue, offset)); + } + function testReplace(bytes16 container, bytes12 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 4)); @@ -418,6 +586,15 @@ contract PackingTest is Test { assertEq(container, container.replace_20_8(newValue, offset).replace_20_8(oldValue, offset)); } + function testReplace(bytes20 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 10)); + + bytes10 oldValue = container.extract_20_10(offset); + + assertEq(newValue, container.replace_20_10(newValue, offset).extract_20_10(offset)); + assertEq(container, container.replace_20_10(newValue, offset).replace_20_10(oldValue, offset)); + } + function testReplace(bytes20 container, bytes12 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 8)); @@ -436,6 +613,87 @@ contract PackingTest is Test { assertEq(container, container.replace_20_16(newValue, offset).replace_20_16(oldValue, offset)); } + function testReplace(bytes22 container, bytes1 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 21)); + + bytes1 oldValue = container.extract_22_1(offset); + + assertEq(newValue, container.replace_22_1(newValue, offset).extract_22_1(offset)); + assertEq(container, container.replace_22_1(newValue, offset).replace_22_1(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes2 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 20)); + + bytes2 oldValue = container.extract_22_2(offset); + + assertEq(newValue, container.replace_22_2(newValue, offset).extract_22_2(offset)); + assertEq(container, container.replace_22_2(newValue, offset).replace_22_2(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes4 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 18)); + + bytes4 oldValue = container.extract_22_4(offset); + + assertEq(newValue, container.replace_22_4(newValue, offset).extract_22_4(offset)); + assertEq(container, container.replace_22_4(newValue, offset).replace_22_4(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes6 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 16)); + + bytes6 oldValue = container.extract_22_6(offset); + + assertEq(newValue, container.replace_22_6(newValue, offset).extract_22_6(offset)); + assertEq(container, container.replace_22_6(newValue, offset).replace_22_6(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes8 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 14)); + + bytes8 oldValue = container.extract_22_8(offset); + + assertEq(newValue, container.replace_22_8(newValue, offset).extract_22_8(offset)); + assertEq(container, container.replace_22_8(newValue, offset).replace_22_8(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 12)); + + bytes10 oldValue = container.extract_22_10(offset); + + assertEq(newValue, container.replace_22_10(newValue, offset).extract_22_10(offset)); + assertEq(container, container.replace_22_10(newValue, offset).replace_22_10(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes12 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 10)); + + bytes12 oldValue = container.extract_22_12(offset); + + assertEq(newValue, container.replace_22_12(newValue, offset).extract_22_12(offset)); + assertEq(container, container.replace_22_12(newValue, offset).replace_22_12(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes16 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 6)); + + bytes16 oldValue = container.extract_22_16(offset); + + assertEq(newValue, container.replace_22_16(newValue, offset).extract_22_16(offset)); + assertEq(container, container.replace_22_16(newValue, offset).replace_22_16(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes20 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 2)); + + bytes20 oldValue = container.extract_22_20(offset); + + assertEq(newValue, container.replace_22_20(newValue, offset).extract_22_20(offset)); + assertEq(container, container.replace_22_20(newValue, offset).replace_22_20(oldValue, offset)); + } + function testReplace(bytes24 container, bytes1 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 23)); @@ -481,6 +739,15 @@ contract PackingTest is Test { assertEq(container, container.replace_24_8(newValue, offset).replace_24_8(oldValue, offset)); } + function testReplace(bytes24 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 14)); + + bytes10 oldValue = container.extract_24_10(offset); + + assertEq(newValue, container.replace_24_10(newValue, offset).extract_24_10(offset)); + assertEq(container, container.replace_24_10(newValue, offset).replace_24_10(oldValue, offset)); + } + function testReplace(bytes24 container, bytes12 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 12)); @@ -508,6 +775,15 @@ contract PackingTest is Test { assertEq(container, container.replace_24_20(newValue, offset).replace_24_20(oldValue, offset)); } + function testReplace(bytes24 container, bytes22 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 2)); + + bytes22 oldValue = container.extract_24_22(offset); + + assertEq(newValue, container.replace_24_22(newValue, offset).extract_24_22(offset)); + assertEq(container, container.replace_24_22(newValue, offset).replace_24_22(oldValue, offset)); + } + function testReplace(bytes28 container, bytes1 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 27)); @@ -553,6 +829,15 @@ contract PackingTest is Test { assertEq(container, container.replace_28_8(newValue, offset).replace_28_8(oldValue, offset)); } + function testReplace(bytes28 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 18)); + + bytes10 oldValue = container.extract_28_10(offset); + + assertEq(newValue, container.replace_28_10(newValue, offset).extract_28_10(offset)); + assertEq(container, container.replace_28_10(newValue, offset).replace_28_10(oldValue, offset)); + } + function testReplace(bytes28 container, bytes12 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 16)); @@ -580,6 +865,15 @@ contract PackingTest is Test { assertEq(container, container.replace_28_20(newValue, offset).replace_28_20(oldValue, offset)); } + function testReplace(bytes28 container, bytes22 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 6)); + + bytes22 oldValue = container.extract_28_22(offset); + + assertEq(newValue, container.replace_28_22(newValue, offset).extract_28_22(offset)); + assertEq(container, container.replace_28_22(newValue, offset).replace_28_22(oldValue, offset)); + } + function testReplace(bytes28 container, bytes24 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 4)); @@ -634,6 +928,15 @@ contract PackingTest is Test { assertEq(container, container.replace_32_8(newValue, offset).replace_32_8(oldValue, offset)); } + function testReplace(bytes32 container, bytes10 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 22)); + + bytes10 oldValue = container.extract_32_10(offset); + + assertEq(newValue, container.replace_32_10(newValue, offset).extract_32_10(offset)); + assertEq(container, container.replace_32_10(newValue, offset).replace_32_10(oldValue, offset)); + } + function testReplace(bytes32 container, bytes12 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 20)); @@ -661,6 +964,15 @@ contract PackingTest is Test { assertEq(container, container.replace_32_20(newValue, offset).replace_32_20(oldValue, offset)); } + function testReplace(bytes32 container, bytes22 newValue, uint8 offset) external { + offset = uint8(bound(offset, 0, 10)); + + bytes22 oldValue = container.extract_32_22(offset); + + assertEq(newValue, container.replace_32_22(newValue, offset).extract_32_22(offset)); + assertEq(container, container.replace_32_22(newValue, offset).replace_32_22(oldValue, offset)); + } + function testReplace(bytes32 container, bytes24 newValue, uint8 offset) external { offset = uint8(bound(offset, 0, 8)); From 45abf842775090dbf0bc3290f2514ad959dc5960 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Jul 2024 15:31:08 +0200 Subject: [PATCH 59/66] fuzzing test of ERC7579Utils --- contracts/abstraction/utils/ERC7579Utils.sol | 5 ++ test/abstraction/ERC7579Utils.t.sol | 68 ++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 test/abstraction/ERC7579Utils.t.sol diff --git a/contracts/abstraction/utils/ERC7579Utils.sol b/contracts/abstraction/utils/ERC7579Utils.sol index 8c68084e778..b35e157101b 100644 --- a/contracts/abstraction/utils/ERC7579Utils.sol +++ b/contracts/abstraction/utils/ERC7579Utils.sol @@ -96,6 +96,7 @@ library ERC7579Utils { using {eqCallType as ==} for CallType global; using {eqExecType as ==} for ExecType global; using {eqModeSelector as ==} for ModeSelector global; +using {eqModePayload as ==} for ModePayload global; function eqCallType(CallType a, CallType b) pure returns (bool) { return CallType.unwrap(a) == CallType.unwrap(b); @@ -108,3 +109,7 @@ function eqExecType(ExecType a, ExecType b) pure returns (bool) { function eqModeSelector(ModeSelector a, ModeSelector b) pure returns (bool) { return ModeSelector.unwrap(a) == ModeSelector.unwrap(b); } + +function eqModePayload(ModePayload a, ModePayload b) pure returns (bool) { + return ModePayload.unwrap(a) == ModePayload.unwrap(b); +} diff --git a/test/abstraction/ERC7579Utils.t.sol b/test/abstraction/ERC7579Utils.t.sol new file mode 100644 index 00000000000..b5a53a5c17c --- /dev/null +++ b/test/abstraction/ERC7579Utils.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {ERC7579Utils, Execution, Mode, CallType, ExecType, ModeSelector, ModePayload} from "@openzeppelin/contracts/abstraction/utils/ERC7579Utils.sol"; + +contract ERC7579UtilsTest is Test { + using ERC7579Utils for *; + + function testEncodeDecodeMode( + CallType callType, + ExecType execType, + ModeSelector modeSelector, + ModePayload modePayload + ) public { + (CallType callType2, ExecType execType2, ModeSelector modeSelector2, ModePayload modePayload2) = ERC7579Utils + .encodeMode(callType, execType, modeSelector, modePayload) + .decodeMode(); + + assertTrue(callType == callType2); + assertTrue(execType == execType2); + assertTrue(modeSelector == modeSelector2); + assertTrue(modePayload == modePayload2); + } + + function testEncodeDecodeSingle(address target, uint256 value, bytes memory callData) public { + (address target2, uint256 value2, bytes memory callData2) = this._decodeSingle( + ERC7579Utils.encodeSingle(target, value, callData) + ); + + assertEq(target, target2); + assertEq(value, value2); + assertEq(callData, callData2); + } + + function testEncodeDecodeDelegate(address target, bytes memory callData) public { + (address target2, bytes memory callData2) = this._decodeDelegate(ERC7579Utils.encodeDelegate(target, callData)); + + assertEq(target, target2); + assertEq(callData, callData2); + } + + function testEncodeDecodeBatch(Execution[] memory executionBatch) public { + Execution[] memory executionBatch2 = this._decodeBatch(ERC7579Utils.encodeBatch(executionBatch)); + + assertEq(abi.encode(executionBatch), abi.encode(executionBatch2)); + } + + function _decodeSingle( + bytes calldata executionCalldata + ) external pure returns (address target, uint256 value, bytes calldata callData) { + return ERC7579Utils.decodeSingle(executionCalldata); + } + + function _decodeDelegate( + bytes calldata executionCalldata + ) external pure returns (address target, bytes calldata callData) { + return ERC7579Utils.decodeDelegate(executionCalldata); + } + + function _decodeBatch( + bytes calldata executionCalldata + ) external pure returns (Execution[] calldata executionBatch) { + return ERC7579Utils.decodeBatch(executionCalldata); + } +} From e7fec8403eb8ce75ba25ab4e5f958a9a8d152416 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Jul 2024 17:26:19 +0200 Subject: [PATCH 60/66] move module support (by types) to extensions --- contracts/abstraction/account/Account.sol | 24 ++++ .../abstraction/account/ERC7579Account.sol | 87 +++++++++----- .../modules/ERC7579AccountModuleExecutor.sol | 63 ++++++++++ .../modules/ERC7579AccountModuleFallback.sol | 112 ++++++++++++++++++ .../modules/ERC7579AccountModuleHook.sol | 49 ++++++++ .../modules/ERC7579AccountModuleValidator.sol | 58 +++++++++ contracts/abstraction/utils/ERC7579Utils.sol | 1 + contracts/interfaces/IERC7579Module.sol | 7 ++ 8 files changed, 369 insertions(+), 32 deletions(-) create mode 100644 contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol create mode 100644 contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol create mode 100644 contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol create mode 100644 contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol index 42bd0bba599..5b132212714 100644 --- a/contracts/abstraction/account/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -9,11 +9,19 @@ import {Address} from "../../utils/Address.sol"; abstract contract Account is IAccount, IAccountExecute { error AccountEntryPointRestricted(); + error AccountExecutorModuleRestricted(address); /**************************************************************************************************************** * Modifiers * ****************************************************************************************************************/ + modifier onlyEntryPointOrSelf() { + if (msg.sender != address(this) && msg.sender != address(entryPoint())) { + revert AccountEntryPointRestricted(); + } + _; + } + modifier onlyEntryPoint() { if (msg.sender != address(entryPoint())) { revert AccountEntryPointRestricted(); @@ -21,6 +29,13 @@ abstract contract Account is IAccount, IAccountExecute { _; } + modifier onlyExecutor() { + if (_isExecutor(msg.sender)) { + revert AccountExecutorModuleRestricted(msg.sender); + } + _; + } + /**************************************************************************************************************** * Hooks * ****************************************************************************************************************/ @@ -41,6 +56,15 @@ abstract contract Account is IAccount, IAccountExecute { */ function _isAuthorized(address) internal view virtual returns (bool); + /** + * @dev Return weither an address (module) is authorized to perform execution from this account. + * + * By default, no module are supported. Subclass may implement this using their own module management mechanism. + */ + function _isExecutor(address) internal view virtual returns (bool) { + return false; + } + /** * @dev Recover the signer for a given signature and user operation hash. This function does not need to verify * that the recovered signer is authorized. diff --git a/contracts/abstraction/account/ERC7579Account.sol b/contracts/abstraction/account/ERC7579Account.sol index 7ae35bc8906..96f65d9fbc9 100644 --- a/contracts/abstraction/account/ERC7579Account.sol +++ b/contracts/abstraction/account/ERC7579Account.sol @@ -7,10 +7,11 @@ import {Address} from "../../utils/Address.sol"; import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol"; import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol"; import {IEntryPoint} from "../../interfaces/IERC4337.sol"; -import {IERC1271} from "../../interfaces/IERC1271.sol"; import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; -import {IERC7579Execution, IERC7579AccountConfig, IERC7579ModuleConfig, Execution} from "../../interfaces/IERC7579Account.sol"; -import {ERC7579Utils, Mode, CallType, ExecType} from "../utils/ERC7579Utils.sol"; +import {IERC1271} from "../../interfaces/IERC1271.sol"; +import {IERC7579Execution, IERC7579AccountConfig, IERC7579ModuleConfig} from "../../interfaces/IERC7579Account.sol"; +import {IERC7579Module} from "../../interfaces/IERC7579Module.sol"; +import {ERC7579Utils, Execution, Mode, CallType, ExecType} from "../utils/ERC7579Utils.sol"; abstract contract ERC7579Account is IERC165, // required by erc-7579 @@ -28,13 +29,10 @@ abstract contract ERC7579Account is IEntryPoint private immutable _entryPoint; event ERC7579TryExecuteUnsuccessful(uint256 batchExecutionindex, bytes result); - error ERC7579UnsupportedCallType(CallType); - error ERC7579UnsupportedExecType(ExecType); - - modifier onlyExecutorModule() { - // TODO - _; - } + error ERC7579UnsupportedCallType(CallType callType); + error ERC7579UnsupportedExecType(ExecType execType); + error MismatchModuleTypeId(uint256 moduleTypeId, address module); + error UnsupportedModuleType(uint256 moduleTypeId); constructor(IEntryPoint entryPoint_) { _entryPoint = entryPoint_; @@ -63,6 +61,10 @@ abstract contract ERC7579Account is : bytes4(0); } + /**************************************************************************************************************** + * ERC-7579 Execution * + ****************************************************************************************************************/ + /// @inheritdoc IERC7579Execution function execute(bytes32 mode, bytes calldata executionCalldata) public virtual onlyEntryPoint { _execute(Mode.wrap(mode), executionCalldata); @@ -72,7 +74,7 @@ abstract contract ERC7579Account is function executeFromExecutor( bytes32 mode, bytes calldata executionCalldata - ) public virtual onlyExecutorModule returns (bytes[] memory) { + ) public virtual onlyExecutor returns (bytes[] memory) { return _execute(Mode.wrap(mode), executionCalldata); } @@ -147,6 +149,10 @@ abstract contract ERC7579Account is } } + /**************************************************************************************************************** + * ERC-7579 Account and Modules * + ****************************************************************************************************************/ + /// @inheritdoc IERC7579AccountConfig function accountId() public view virtual returns (string memory) { //vendorname.accountname.semver @@ -163,38 +169,55 @@ abstract contract ERC7579Account is } /// @inheritdoc IERC7579AccountConfig - function supportsModule(uint256 moduleTypeId) public view virtual returns (bool) { - // TODO: update when module support is added - moduleTypeId; + function supportsModule(uint256 /*moduleTypeId*/) public view virtual returns (bool) { return false; } /// @inheritdoc IERC7579ModuleConfig - function installModule(uint256 moduleTypeId, address module, bytes calldata initData) public pure { - moduleTypeId; - module; - initData; - revert("not-implemented-yet"); + function installModule( + uint256 moduleTypeId, + address module, + bytes calldata initData + ) public virtual onlyEntryPointOrSelf { + if (!IERC7579Module(module).isModuleType(moduleTypeId)) revert MismatchModuleTypeId(moduleTypeId, module); + _installModule(moduleTypeId, module, initData); + /// TODO: silent unreachable and re-enable this event + // emit ModuleInstalled(moduleTypeId, module); } /// @inheritdoc IERC7579ModuleConfig - function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) public pure { - moduleTypeId; - module; - deInitData; - revert("not-implemented-yet"); + function uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) public virtual onlyEntryPointOrSelf { + _uninstallModule(moduleTypeId, module, deInitData); + /// TODO: silent unreachable and re-enable this event + // emit ModuleUninstalled(moduleTypeId, module); } /// @inheritdoc IERC7579ModuleConfig function isModuleInstalled( + uint256 /*moduleTypeId*/, + address /*module*/, + bytes calldata /*additionalContext*/ + ) public view virtual returns (bool) { + return false; + } + + /**************************************************************************************************************** + * Hooks * + ****************************************************************************************************************/ + + function _installModule(uint256 moduleTypeId, address /*module*/, bytes calldata /*initData*/) internal virtual { + revert UnsupportedModuleType(moduleTypeId); + } + + function _uninstallModule( uint256 moduleTypeId, - address module, - bytes calldata additionalContext - ) public view returns (bool) { - moduleTypeId; - module; - additionalContext; - address(this); - revert("not-implemented-yet"); + address /*module*/, + bytes calldata /*deInitData*/ + ) internal virtual { + revert UnsupportedModuleType(moduleTypeId); } } diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol new file mode 100644 index 00000000000..0341b933c86 --- /dev/null +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC7579AccountConfig, IERC7579ModuleConfig} from "../../../interfaces/IERC7579Account.sol"; +import {IERC7579Module, MODULE_TYPE_EXECUTOR} from "../../../interfaces/IERC7579Module.sol"; +import {Account} from "../Account.sol"; +import {ERC7579Account} from "../ERC7579Account.sol"; +import {EnumerableSet} from "../../../utils/structs/EnumerableSet.sol"; + +abstract contract ERC7579AccountModuleExecutor is ERC7579Account { + using EnumerableSet for *; + + EnumerableSet.AddressSet private _executors; + + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) { + return moduleTypeId == MODULE_TYPE_EXECUTOR || super.supportsModule(moduleTypeId); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) public view virtual override returns (bool) { + return + moduleTypeId == MODULE_TYPE_EXECUTOR + ? _executors.contains(module) + : super.isModuleInstalled(moduleTypeId, module, additionalContext); + } + + /// @inheritdoc ERC7579Account + function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { + if (moduleTypeId == MODULE_TYPE_EXECUTOR) { + require(_executors.add(module)); + IERC7579Module(module).onInstall(initData); + } else { + super._installModule(moduleTypeId, module, initData); + } + } + + /// @inheritdoc ERC7579Account + function _uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) internal virtual override { + if (moduleTypeId == MODULE_TYPE_EXECUTOR) { + require(_executors.remove(module)); + IERC7579Module(module).onUninstall(deInitData); + } else { + super._uninstallModule(moduleTypeId, module, deInitData); + } + } + + /// @inheritdoc Account + function _isExecutor(address module) internal view virtual override returns (bool) { + return _executors.contains(module); + } + + // TODO: enumerability? +} diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol new file mode 100644 index 00000000000..fc300d5ac7e --- /dev/null +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC7579AccountConfig, IERC7579ModuleConfig} from "../../../interfaces/IERC7579Account.sol"; +import {IERC7579Module, MODULE_TYPE_FALLBACK} from "../../../interfaces/IERC7579Module.sol"; +import {ERC7579Utils, CallType} from "../../utils/ERC7579Utils.sol"; +import {ERC7579Account} from "../ERC7579Account.sol"; + +abstract contract ERC7579AccountModuleFallback is ERC7579Account { + struct FallbackHandler { + address handler; + CallType calltype; + } + + mapping(bytes4 => FallbackHandler) private _fallbacks; + + error NoFallbackHandler(bytes4 selector); + + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) { + return moduleTypeId == MODULE_TYPE_FALLBACK || super.supportsModule(moduleTypeId); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) public view virtual override returns (bool) { + /// TODO + return super.isModuleInstalled(moduleTypeId, module, additionalContext); + } + + /// @inheritdoc ERC7579Account + function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { + if (moduleTypeId == MODULE_TYPE_FALLBACK) { + bytes4 selector = bytes4(initData[0:4]); + CallType calltype = CallType.wrap(bytes1(initData[4])); + + require(_fallbacks[selector].handler == address(0), "Function selector already used"); + require( + calltype == ERC7579Utils.CALLTYPE_SINGLE || calltype == ERC7579Utils.CALLTYPE_STATIC, + "Invalid fallback handler CallType" + ); + _fallbacks[selector] = FallbackHandler(module, calltype); + + IERC7579Module(module).onInstall(initData[5:]); + } else { + super._installModule(moduleTypeId, module, initData); + } + } + + /// @inheritdoc ERC7579Account + function _uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) internal virtual override { + if (moduleTypeId == MODULE_TYPE_FALLBACK) { + bytes4 selector = bytes4(deInitData[0:4]); + address handler = _fallbacks[selector].handler; + + require(handler != address(0), "Function selector not used"); + require(handler != module, "Function selector not used by this handler"); + delete _fallbacks[selector]; + + IERC7579Module(module).onUninstall(deInitData[4:]); + } else { + super._uninstallModule(moduleTypeId, module, deInitData); + } + } + + fallback() external payable { + uint256 value = msg.value; + address handler = _fallbacks[msg.sig].handler; + CallType calltype = _fallbacks[msg.sig].calltype; + + if (handler != address(0) && calltype == ERC7579Utils.CALLTYPE_SINGLE) { + assembly ("memory-safe") { + calldatacopy(0, 0, calldatasize()) + let result := call(gas(), handler, value, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } else if (handler != address(0) && calltype == ERC7579Utils.CALLTYPE_STATIC) { + require(value == 0, "Static fallback handler should not receive value"); + assembly ("memory-safe") { + calldatacopy(0, 0, calldatasize()) + let result := staticcall(gas(), handler, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } else { + revert NoFallbackHandler(msg.sig); + } + } + + // TODO: getters? +} diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol new file mode 100644 index 00000000000..e26997b672e --- /dev/null +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC7579AccountConfig, IERC7579ModuleConfig} from "../../../interfaces/IERC7579Account.sol"; +import {IERC7579Module, MODULE_TYPE_HOOK} from "../../../interfaces/IERC7579Module.sol"; +import {ERC7579Account} from "../ERC7579Account.sol"; + +abstract contract ERC7579AccountModuleHook is ERC7579Account { + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) { + return moduleTypeId == MODULE_TYPE_HOOK || super.supportsModule(moduleTypeId); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) public view virtual override returns (bool) { + return + moduleTypeId == MODULE_TYPE_HOOK ? false : super.isModuleInstalled(moduleTypeId, module, additionalContext); + } + + /// @inheritdoc ERC7579Account + function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { + if (moduleTypeId == MODULE_TYPE_HOOK) { + // TODO + } else { + super._installModule(moduleTypeId, module, initData); + } + } + + /// @inheritdoc ERC7579Account + function _uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) internal virtual override { + if (moduleTypeId == MODULE_TYPE_HOOK) { + // TODO + } else { + super._uninstallModule(moduleTypeId, module, deInitData); + } + } + + // TODO: do something with the hooks? + // TODO: getters? +} diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol new file mode 100644 index 00000000000..350e71cdca4 --- /dev/null +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC7579AccountConfig, IERC7579ModuleConfig} from "../../../interfaces/IERC7579Account.sol"; +import {IERC7579Module, MODULE_TYPE_VALIDATOR} from "../../../interfaces/IERC7579Module.sol"; +import {ERC7579Account} from "../ERC7579Account.sol"; +import {EnumerableSet} from "../../../utils/structs/EnumerableSet.sol"; + +abstract contract ERC7579AccountModuleValidator is ERC7579Account { + using EnumerableSet for *; + + EnumerableSet.AddressSet private _validators; + + /// @inheritdoc IERC7579AccountConfig + function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) { + return moduleTypeId == MODULE_TYPE_VALIDATOR || super.supportsModule(moduleTypeId); + } + + /// @inheritdoc IERC7579ModuleConfig + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) public view virtual override returns (bool) { + return + moduleTypeId == MODULE_TYPE_VALIDATOR + ? _validators.contains(module) + : super.isModuleInstalled(moduleTypeId, module, additionalContext); + } + + /// @inheritdoc ERC7579Account + function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { + if (moduleTypeId == MODULE_TYPE_VALIDATOR) { + require(_validators.add(module)); + IERC7579Module(module).onInstall(initData); + } else { + super._installModule(moduleTypeId, module, initData); + } + } + + /// @inheritdoc ERC7579Account + function _uninstallModule( + uint256 moduleTypeId, + address module, + bytes calldata deInitData + ) internal virtual override { + if (moduleTypeId == MODULE_TYPE_VALIDATOR) { + require(_validators.remove(module)); + IERC7579Module(module).onUninstall(deInitData); + } else { + super._uninstallModule(moduleTypeId, module, deInitData); + } + } + + // TODO: do something with the validators? + // TODO: enumerability? +} diff --git a/contracts/abstraction/utils/ERC7579Utils.sol b/contracts/abstraction/utils/ERC7579Utils.sol index b35e157101b..6071a4aa931 100644 --- a/contracts/abstraction/utils/ERC7579Utils.sol +++ b/contracts/abstraction/utils/ERC7579Utils.sol @@ -16,6 +16,7 @@ library ERC7579Utils { CallType constant CALLTYPE_SINGLE = CallType.wrap(0x00); CallType constant CALLTYPE_BATCH = CallType.wrap(0x01); + CallType constant CALLTYPE_STATIC = CallType.wrap(0xFE); CallType constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); ExecType constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); ExecType constant EXECTYPE_TRY = ExecType.wrap(0x01); diff --git a/contracts/interfaces/IERC7579Module.sol b/contracts/interfaces/IERC7579Module.sol index 0350e7e8853..8be9445530f 100644 --- a/contracts/interfaces/IERC7579Module.sol +++ b/contracts/interfaces/IERC7579Module.sol @@ -3,6 +3,13 @@ pragma solidity ^0.8.20; import {PackedUserOperation} from "./IERC4337.sol"; +uint256 constant VALIDATION_SUCCESS = 0; +uint256 constant VALIDATION_FAILED = 1; +uint256 constant MODULE_TYPE_VALIDATOR = 1; +uint256 constant MODULE_TYPE_EXECUTOR = 2; +uint256 constant MODULE_TYPE_FALLBACK = 3; +uint256 constant MODULE_TYPE_HOOK = 4; + interface IERC7579Module { /** * @dev This function is called by the smart account during installation of the module From 625db1b2a9bd8cfd349fdce9b521b5b1f25f9e1f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Jul 2024 17:56:04 +0200 Subject: [PATCH 61/66] up --- contracts/abstraction/account/Account.sol | 17 ----- .../abstraction/account/ERC7579Account.sol | 13 +++- .../modules/ERC7579AccountModuleExecutor.sol | 5 -- .../modules/ERC7579AccountModuleFallback.sol | 72 +++++++------------ .../modules/ERC7579AccountModuleHook.sol | 29 ++++++-- contracts/abstraction/utils/ERC7579Utils.sol | 1 - 6 files changed, 59 insertions(+), 78 deletions(-) diff --git a/contracts/abstraction/account/Account.sol b/contracts/abstraction/account/Account.sol index 5b132212714..6dc112a1fd1 100644 --- a/contracts/abstraction/account/Account.sol +++ b/contracts/abstraction/account/Account.sol @@ -9,7 +9,6 @@ import {Address} from "../../utils/Address.sol"; abstract contract Account is IAccount, IAccountExecute { error AccountEntryPointRestricted(); - error AccountExecutorModuleRestricted(address); /**************************************************************************************************************** * Modifiers * @@ -29,13 +28,6 @@ abstract contract Account is IAccount, IAccountExecute { _; } - modifier onlyExecutor() { - if (_isExecutor(msg.sender)) { - revert AccountExecutorModuleRestricted(msg.sender); - } - _; - } - /**************************************************************************************************************** * Hooks * ****************************************************************************************************************/ @@ -56,15 +48,6 @@ abstract contract Account is IAccount, IAccountExecute { */ function _isAuthorized(address) internal view virtual returns (bool); - /** - * @dev Return weither an address (module) is authorized to perform execution from this account. - * - * By default, no module are supported. Subclass may implement this using their own module management mechanism. - */ - function _isExecutor(address) internal view virtual returns (bool) { - return false; - } - /** * @dev Recover the signer for a given signature and user operation hash. This function does not need to verify * that the recovered signer is authorized. diff --git a/contracts/abstraction/account/ERC7579Account.sol b/contracts/abstraction/account/ERC7579Account.sol index 96f65d9fbc9..2db3931b16b 100644 --- a/contracts/abstraction/account/ERC7579Account.sol +++ b/contracts/abstraction/account/ERC7579Account.sol @@ -10,7 +10,7 @@ import {IEntryPoint} from "../../interfaces/IERC4337.sol"; import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; import {IERC1271} from "../../interfaces/IERC1271.sol"; import {IERC7579Execution, IERC7579AccountConfig, IERC7579ModuleConfig} from "../../interfaces/IERC7579Account.sol"; -import {IERC7579Module} from "../../interfaces/IERC7579Module.sol"; +import {IERC7579Module, MODULE_TYPE_EXECUTOR} from "../../interfaces/IERC7579Module.sol"; import {ERC7579Utils, Execution, Mode, CallType, ExecType} from "../utils/ERC7579Utils.sol"; abstract contract ERC7579Account is @@ -33,6 +33,15 @@ abstract contract ERC7579Account is error ERC7579UnsupportedExecType(ExecType execType); error MismatchModuleTypeId(uint256 moduleTypeId, address module); error UnsupportedModuleType(uint256 moduleTypeId); + error ModuleRestricted(uint256 moduleTypeId, address caller); + + modifier onlyModule(uint256 moduleTypeId) { + /// TODO: msg.data? + if (!isModuleInstalled(moduleTypeId, msg.sender, msg.data[0:0])) { + revert ModuleRestricted(moduleTypeId, msg.sender); + } + _; + } constructor(IEntryPoint entryPoint_) { _entryPoint = entryPoint_; @@ -74,7 +83,7 @@ abstract contract ERC7579Account is function executeFromExecutor( bytes32 mode, bytes calldata executionCalldata - ) public virtual onlyExecutor returns (bytes[] memory) { + ) public virtual onlyModule(MODULE_TYPE_EXECUTOR) returns (bytes[] memory) { return _execute(Mode.wrap(mode), executionCalldata); } diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol index 0341b933c86..3f442a18ed8 100644 --- a/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol @@ -54,10 +54,5 @@ abstract contract ERC7579AccountModuleExecutor is ERC7579Account { } } - /// @inheritdoc Account - function _isExecutor(address module) internal view virtual override returns (bool) { - return _executors.contains(module); - } - // TODO: enumerability? } diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol index fc300d5ac7e..f5683fa24ef 100644 --- a/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol @@ -8,12 +8,7 @@ import {ERC7579Utils, CallType} from "../../utils/ERC7579Utils.sol"; import {ERC7579Account} from "../ERC7579Account.sol"; abstract contract ERC7579AccountModuleFallback is ERC7579Account { - struct FallbackHandler { - address handler; - CallType calltype; - } - - mapping(bytes4 => FallbackHandler) private _fallbacks; + mapping(bytes4 => address) private _fallbacks; error NoFallbackHandler(bytes4 selector); @@ -28,24 +23,21 @@ abstract contract ERC7579AccountModuleFallback is ERC7579Account { address module, bytes calldata additionalContext ) public view virtual override returns (bool) { - /// TODO - return super.isModuleInstalled(moduleTypeId, module, additionalContext); + return + moduleTypeId == MODULE_TYPE_FALLBACK + ? _fallbacks[bytes4(additionalContext[0:4])] == module + : super.isModuleInstalled(moduleTypeId, module, additionalContext); } /// @inheritdoc ERC7579Account function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { if (moduleTypeId == MODULE_TYPE_FALLBACK) { bytes4 selector = bytes4(initData[0:4]); - CallType calltype = CallType.wrap(bytes1(initData[4])); - require(_fallbacks[selector].handler == address(0), "Function selector already used"); - require( - calltype == ERC7579Utils.CALLTYPE_SINGLE || calltype == ERC7579Utils.CALLTYPE_STATIC, - "Invalid fallback handler CallType" - ); - _fallbacks[selector] = FallbackHandler(module, calltype); + require(_fallbacks[selector] == address(0), "Function selector already used"); + _fallbacks[selector] = module; - IERC7579Module(module).onInstall(initData[5:]); + IERC7579Module(module).onInstall(initData[4:]); } else { super._installModule(moduleTypeId, module, initData); } @@ -59,7 +51,7 @@ abstract contract ERC7579AccountModuleFallback is ERC7579Account { ) internal virtual override { if (moduleTypeId == MODULE_TYPE_FALLBACK) { bytes4 selector = bytes4(deInitData[0:4]); - address handler = _fallbacks[selector].handler; + address handler = _fallbacks[selector]; require(handler != address(0), "Function selector not used"); require(handler != module, "Function selector not used by this handler"); @@ -72,39 +64,23 @@ abstract contract ERC7579AccountModuleFallback is ERC7579Account { } fallback() external payable { - uint256 value = msg.value; - address handler = _fallbacks[msg.sig].handler; - CallType calltype = _fallbacks[msg.sig].calltype; - - if (handler != address(0) && calltype == ERC7579Utils.CALLTYPE_SINGLE) { - assembly ("memory-safe") { - calldatacopy(0, 0, calldatasize()) - let result := call(gas(), handler, value, 0, calldatasize(), 0, 0) - returndatacopy(0, 0, returndatasize()) - switch result - case 0 { - revert(0, returndatasize()) - } - default { - return(0, returndatasize()) - } + address handler = _fallbacks[msg.sig]; + if (handler == address(0)) revert NoFallbackHandler(msg.sig); + + // From https://eips.ethereum.org/EIPS/eip-7579#fallback[ERC-7579 specifications]: + // - MUST utilize ERC-2771 to add the original msg.sender to the calldata sent to the fallback handler + // - MUST use call to invoke the fallback handler + (bool success, bytes memory returndata) = handler.call{value: msg.value}( + abi.encodePacked(msg.data, msg.sender) + ); + assembly ("memory-safe") { + switch success + case 0 { + revert(add(returndata, 0x20), mload(returndata)) } - } else if (handler != address(0) && calltype == ERC7579Utils.CALLTYPE_STATIC) { - require(value == 0, "Static fallback handler should not receive value"); - assembly ("memory-safe") { - calldatacopy(0, 0, calldatasize()) - let result := staticcall(gas(), handler, 0, calldatasize(), 0, 0) - returndatacopy(0, 0, returndatasize()) - switch result - case 0 { - revert(0, returndatasize()) - } - default { - return(0, returndatasize()) - } + default { + return(add(returndata, 0x20), mload(returndata)) } - } else { - revert NoFallbackHandler(msg.sig); } } diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol index e26997b672e..5f06b6cf339 100644 --- a/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol @@ -3,10 +3,23 @@ pragma solidity ^0.8.20; import {IERC7579AccountConfig, IERC7579ModuleConfig} from "../../../interfaces/IERC7579Account.sol"; -import {IERC7579Module, MODULE_TYPE_HOOK} from "../../../interfaces/IERC7579Module.sol"; +import {IERC7579Module, IERC7579Hook, MODULE_TYPE_HOOK} from "../../../interfaces/IERC7579Module.sol"; import {ERC7579Account} from "../ERC7579Account.sol"; abstract contract ERC7579AccountModuleHook is ERC7579Account { + address private _hook; + + modifier withHook() { + address hook = _hook; + if (hook == address(0)) { + _; + } else { + bytes memory hookData = IERC7579Hook(hook).preCheck(msg.sender, msg.value, msg.data); + _; + IERC7579Hook(hook).postCheck(hookData); + } + } + /// @inheritdoc IERC7579AccountConfig function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) { return moduleTypeId == MODULE_TYPE_HOOK || super.supportsModule(moduleTypeId); @@ -19,13 +32,17 @@ abstract contract ERC7579AccountModuleHook is ERC7579Account { bytes calldata additionalContext ) public view virtual override returns (bool) { return - moduleTypeId == MODULE_TYPE_HOOK ? false : super.isModuleInstalled(moduleTypeId, module, additionalContext); + moduleTypeId == MODULE_TYPE_HOOK + ? module == _hook + : super.isModuleInstalled(moduleTypeId, module, additionalContext); } /// @inheritdoc ERC7579Account function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { if (moduleTypeId == MODULE_TYPE_HOOK) { - // TODO + require(_hook == address(0), "HookAlreadyInstalled"); + _hook = module; + IERC7579Module(module).onInstall(initData); } else { super._installModule(moduleTypeId, module, initData); } @@ -38,12 +55,14 @@ abstract contract ERC7579AccountModuleHook is ERC7579Account { bytes calldata deInitData ) internal virtual override { if (moduleTypeId == MODULE_TYPE_HOOK) { - // TODO + require(_hook == module, "HookNotInstalled"); + delete _hook; + IERC7579Module(module).onUninstall(deInitData); } else { super._uninstallModule(moduleTypeId, module, deInitData); } } - // TODO: do something with the hooks? + // TODO: do something with the modifier? // TODO: getters? } diff --git a/contracts/abstraction/utils/ERC7579Utils.sol b/contracts/abstraction/utils/ERC7579Utils.sol index 6071a4aa931..b35e157101b 100644 --- a/contracts/abstraction/utils/ERC7579Utils.sol +++ b/contracts/abstraction/utils/ERC7579Utils.sol @@ -16,7 +16,6 @@ library ERC7579Utils { CallType constant CALLTYPE_SINGLE = CallType.wrap(0x00); CallType constant CALLTYPE_BATCH = CallType.wrap(0x01); - CallType constant CALLTYPE_STATIC = CallType.wrap(0xFE); CallType constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); ExecType constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); ExecType constant EXECTYPE_TRY = ExecType.wrap(0x01); From 6fb68a1c74070704fe71270952cc9ff9f17ccea6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Jul 2024 18:02:44 +0200 Subject: [PATCH 62/66] up --- contracts/abstraction/account/ERC7579Account.sol | 2 ++ .../account/modules/ERC7579AccountModuleExecutor.sol | 4 ++-- .../account/modules/ERC7579AccountModuleFallback.sol | 9 +++++---- .../account/modules/ERC7579AccountModuleHook.sol | 4 ++-- .../account/modules/ERC7579AccountModuleValidator.sol | 4 ++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/contracts/abstraction/account/ERC7579Account.sol b/contracts/abstraction/account/ERC7579Account.sol index 2db3931b16b..ccdbef8175d 100644 --- a/contracts/abstraction/account/ERC7579Account.sol +++ b/contracts/abstraction/account/ERC7579Account.sol @@ -34,6 +34,8 @@ abstract contract ERC7579Account is error MismatchModuleTypeId(uint256 moduleTypeId, address module); error UnsupportedModuleType(uint256 moduleTypeId); error ModuleRestricted(uint256 moduleTypeId, address caller); + error ModuleAlreadyInstalled(uint256 moduleTypeId, address module); + error ModuleNotInstalled(uint256 moduleTypeId, address module); modifier onlyModule(uint256 moduleTypeId) { /// TODO: msg.data? diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol index 3f442a18ed8..ee9cfbfdc30 100644 --- a/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleExecutor.sol @@ -33,7 +33,7 @@ abstract contract ERC7579AccountModuleExecutor is ERC7579Account { /// @inheritdoc ERC7579Account function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { if (moduleTypeId == MODULE_TYPE_EXECUTOR) { - require(_executors.add(module)); + if (!_executors.add(module)) revert ModuleAlreadyInstalled(moduleTypeId, module); IERC7579Module(module).onInstall(initData); } else { super._installModule(moduleTypeId, module, initData); @@ -47,7 +47,7 @@ abstract contract ERC7579AccountModuleExecutor is ERC7579Account { bytes calldata deInitData ) internal virtual override { if (moduleTypeId == MODULE_TYPE_EXECUTOR) { - require(_executors.remove(module)); + if (!_executors.remove(module)) revert ModuleNotInstalled(moduleTypeId, module); IERC7579Module(module).onUninstall(deInitData); } else { super._uninstallModule(moduleTypeId, module, deInitData); diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol index f5683fa24ef..85cadfbf30f 100644 --- a/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleFallback.sol @@ -10,6 +10,8 @@ import {ERC7579Account} from "../ERC7579Account.sol"; abstract contract ERC7579AccountModuleFallback is ERC7579Account { mapping(bytes4 => address) private _fallbacks; + error FallbackHandlerAlreadySet(bytes4 selector); + error FallbackHandlerNotSet(bytes4 selector); error NoFallbackHandler(bytes4 selector); /// @inheritdoc IERC7579AccountConfig @@ -34,7 +36,7 @@ abstract contract ERC7579AccountModuleFallback is ERC7579Account { if (moduleTypeId == MODULE_TYPE_FALLBACK) { bytes4 selector = bytes4(initData[0:4]); - require(_fallbacks[selector] == address(0), "Function selector already used"); + if (_fallbacks[selector] != address(0)) revert FallbackHandlerAlreadySet(selector); _fallbacks[selector] = module; IERC7579Module(module).onInstall(initData[4:]); @@ -51,10 +53,9 @@ abstract contract ERC7579AccountModuleFallback is ERC7579Account { ) internal virtual override { if (moduleTypeId == MODULE_TYPE_FALLBACK) { bytes4 selector = bytes4(deInitData[0:4]); - address handler = _fallbacks[selector]; - require(handler != address(0), "Function selector not used"); - require(handler != module, "Function selector not used by this handler"); + address handler = _fallbacks[selector]; + if (handler == address(0) || handler != module) revert FallbackHandlerNotSet(selector); delete _fallbacks[selector]; IERC7579Module(module).onUninstall(deInitData[4:]); diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol index 5f06b6cf339..681623da977 100644 --- a/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleHook.sol @@ -40,7 +40,7 @@ abstract contract ERC7579AccountModuleHook is ERC7579Account { /// @inheritdoc ERC7579Account function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { if (moduleTypeId == MODULE_TYPE_HOOK) { - require(_hook == address(0), "HookAlreadyInstalled"); + if (_hook != address(0)) revert ModuleNotInstalled(moduleTypeId, _hook); _hook = module; IERC7579Module(module).onInstall(initData); } else { @@ -55,7 +55,7 @@ abstract contract ERC7579AccountModuleHook is ERC7579Account { bytes calldata deInitData ) internal virtual override { if (moduleTypeId == MODULE_TYPE_HOOK) { - require(_hook == module, "HookNotInstalled"); + if (_hook != module) revert ModuleAlreadyInstalled(moduleTypeId, module); delete _hook; IERC7579Module(module).onUninstall(deInitData); } else { diff --git a/contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol b/contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol index 350e71cdca4..0333b0c5f0a 100644 --- a/contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol +++ b/contracts/abstraction/account/modules/ERC7579AccountModuleValidator.sol @@ -32,7 +32,7 @@ abstract contract ERC7579AccountModuleValidator is ERC7579Account { /// @inheritdoc ERC7579Account function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal virtual override { if (moduleTypeId == MODULE_TYPE_VALIDATOR) { - require(_validators.add(module)); + if (!_validators.add(module)) revert ModuleAlreadyInstalled(moduleTypeId, module); IERC7579Module(module).onInstall(initData); } else { super._installModule(moduleTypeId, module, initData); @@ -46,7 +46,7 @@ abstract contract ERC7579AccountModuleValidator is ERC7579Account { bytes calldata deInitData ) internal virtual override { if (moduleTypeId == MODULE_TYPE_VALIDATOR) { - require(_validators.remove(module)); + if (!_validators.remove(module)) revert ModuleNotInstalled(moduleTypeId, module); IERC7579Module(module).onUninstall(deInitData); } else { super._uninstallModule(moduleTypeId, module, deInitData); From fb90b926c58df054af05d1f5cb82b88d29a58c1e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Jul 2024 22:18:40 +0200 Subject: [PATCH 63/66] up --- contracts/abstraction/account/ERC7579Account.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/abstraction/account/ERC7579Account.sol b/contracts/abstraction/account/ERC7579Account.sol index ccdbef8175d..24239abcd6c 100644 --- a/contracts/abstraction/account/ERC7579Account.sol +++ b/contracts/abstraction/account/ERC7579Account.sol @@ -38,8 +38,7 @@ abstract contract ERC7579Account is error ModuleNotInstalled(uint256 moduleTypeId, address module); modifier onlyModule(uint256 moduleTypeId) { - /// TODO: msg.data? - if (!isModuleInstalled(moduleTypeId, msg.sender, msg.data[0:0])) { + if (!isModuleInstalled(moduleTypeId, msg.sender, msg.data)) { revert ModuleRestricted(moduleTypeId, msg.sender); } _; From 22ddfb7ae12773455176d28a7864d023cd2b934a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Jul 2024 23:19:39 +0200 Subject: [PATCH 64/66] test batch --- test/abstraction/accountECDSA.test.js | 68 ++++++++++++++++++++++++- test/abstraction/accountERC1271.test.js | 68 ++++++++++++++++++++++++- test/helpers/erc7579.js | 14 ++++- test/helpers/error.js | 7 +++ test/helpers/identity.js | 11 ++-- 5 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 test/helpers/error.js diff --git a/test/abstraction/accountECDSA.test.js b/test/abstraction/accountECDSA.test.js index 5bb9727ec01..a252ad4d7c8 100644 --- a/test/abstraction/accountECDSA.test.js +++ b/test/abstraction/accountECDSA.test.js @@ -4,7 +4,8 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { IdentityHelper } = require('../helpers/identity'); const { ERC4337Helper } = require('../helpers/erc4337'); -const { encodeMode, encodeSingle } = require('../helpers/erc7579'); +const { encodeMode, encodeSingle, encodeBatch } = require('../helpers/erc7579'); +const { encodeError } = require('../helpers/error'); async function fixture() { const accounts = await ethers.getSigners(); @@ -19,7 +20,7 @@ async function fixture() { const target = await ethers.deployContract('CallReceiverMock'); // create 4337 account controlled by ECDSA - const signer = await identity.newECDSASigner(); + const signer = await identity.newECDSASigner({ provider: ethers.provider }); const sender = await helper.newAccount(signer); return { @@ -99,6 +100,69 @@ describe('AccountECDSA', function () { .to.emit(this.target, 'MockFunctionCalledExtra') .withArgs(this.sender, 42); }); + + describe('batch', function () { + it('success: batch call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + it('revert: batch call with one revert', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 69, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)).to.emit( + this.helper.entrypoint, + 'UserOperationRevertReason', + ); + }); + + it('success: try batch call with one revert', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01', execType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 69, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.sender, 'ERC7579TryExecuteUnsuccessful') + .withArgs(1, encodeError('CallReceiverMock: reverting')) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + }); }); }); }); diff --git a/test/abstraction/accountERC1271.test.js b/test/abstraction/accountERC1271.test.js index a12788d6312..e637d569a98 100644 --- a/test/abstraction/accountERC1271.test.js +++ b/test/abstraction/accountERC1271.test.js @@ -4,7 +4,8 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { IdentityHelper } = require('../helpers/identity'); const { ERC4337Helper } = require('../helpers/erc4337'); -const { encodeMode, encodeSingle } = require('../helpers/erc7579'); +const { encodeMode, encodeSingle, encodeBatch } = require('../helpers/erc7579'); +const { encodeError } = require('../helpers/error'); async function fixture() { const accounts = await ethers.getSigners(); @@ -19,7 +20,7 @@ async function fixture() { const target = await ethers.deployContract('CallReceiverMock'); // create 4337 account controlled by P256 - const signer = await identity.newP256Signer(); + const signer = await identity.newP256Signer({ provider: ethers.provider }); const sender = await helper.newAccount(signer); return { @@ -82,6 +83,69 @@ describe('AccountERC1271', function () { .to.emit(this.target, 'MockFunctionCalledExtra') .withArgs(this.sender, 42); }); + + describe('batch', function () { + it('success: batch call', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + + it('revert: batch call with one revert', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 69, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)).to.emit( + this.helper.entrypoint, + 'UserOperationRevertReason', + ); + }); + + it('success: try batch call with one revert', async function () { + const operation = await this.sender + .createOp({ + callData: this.sender.interface.encodeFunctionData('execute', [ + encodeMode({ callType: '0x01', execType: '0x01' }), + encodeBatch( + [this.target, 17, this.target.interface.encodeFunctionData('mockFunction')], + [this.target, 69, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')], + [this.target, 42, this.target.interface.encodeFunctionData('mockFunctionExtra')], + ), + ]), + }) + .then(op => op.sign()); + + await expect(this.entrypoint.handleOps([operation.packed], this.accounts.beneficiary)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.sender, 'ERC7579TryExecuteUnsuccessful') + .withArgs(1, encodeError('CallReceiverMock: reverting')) + .to.emit(this.target, 'MockFunctionCalledExtra') + .withArgs(this.sender, 42); + }); + }); }); }); }); diff --git a/test/helpers/erc7579.js b/test/helpers/erc7579.js index ac0e7127a25..e2ffdfa08e9 100644 --- a/test/helpers/erc7579.js +++ b/test/helpers/erc7579.js @@ -14,10 +14,20 @@ const encodeMode = ({ const encodeSingle = (target, value = 0n, data = '0x') => ethers.solidityPacked(['address', 'uint256', 'bytes'], [target.target ?? target.address ?? target, value, data]); -/// TODO -// const encodeBatch = +const encodeBatch = (...entries) => + ethers.AbiCoder.defaultAbiCoder().encode( + ['(address,uint256,bytes)[]'], + [ + entries.map(entry => + Array.isArray(entry) + ? [entry[0].target ?? entry[0].address ?? entry[0], entry[1] ?? 0n, entry[2] ?? '0x'] + : [entry.target.target ?? entry.target.address ?? entry.target, entry.value ?? 0n, entry.data ?? '0x'], + ), + ], + ); module.exports = { encodeMode, encodeSingle, + encodeBatch, }; diff --git a/test/helpers/error.js b/test/helpers/error.js new file mode 100644 index 00000000000..19af0235e73 --- /dev/null +++ b/test/helpers/error.js @@ -0,0 +1,7 @@ +const { ethers } = require('ethers'); + +const interface = ethers.Interface.from(['error Error(string)']); + +module.exports = { + encodeError: str => interface.encodeErrorResult('Error', [str]), +}; diff --git a/test/helpers/identity.js b/test/helpers/identity.js index 8debe8a5283..cb0064675b9 100644 --- a/test/helpers/identity.js +++ b/test/helpers/identity.js @@ -15,16 +15,17 @@ class IdentityHelper { return this; } - async newECDSASigner() { - return Object.assign(ethers.Wallet.createRandom(), { type: SignatureType.ECDSA }); + async newECDSASigner(params = {}) { + return Object.assign(ethers.Wallet.createRandom(params.provider), { type: SignatureType.ECDSA }); } - async newP256Signer(params = { withPrefixAddress: true }) { - await this.wait(); + async newP256Signer(params = {}) { + params.withPrefixAddress ??= true; + await this.wait(); const signer = P256Signer.random(params); await Promise.all([this.p256Factory.predict(signer.publicKey), this.p256Factory.create(signer.publicKey)]).then( - ([address]) => Object.assign(signer, { address }), + ([address]) => Object.assign(signer, { address, provider: params.provider }), ); return signer; From d0fdca7825c46fa7b9087f240b0f3828030924a5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 22 Jul 2024 14:58:39 +0200 Subject: [PATCH 65/66] use calldata --- contracts/abstraction/identity/IdentityP256.sol | 14 ++++++++------ contracts/abstraction/identity/IdentityRSA.sol | 6 +++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/contracts/abstraction/identity/IdentityP256.sol b/contracts/abstraction/identity/IdentityP256.sol index d520a011a54..d0fb085104d 100644 --- a/contracts/abstraction/identity/IdentityP256.sol +++ b/contracts/abstraction/identity/IdentityP256.sol @@ -11,17 +11,19 @@ contract IdentityP256Implementation is IERC1271 { return Clones.fetchCloneArgs(address(this)); } - function isValidSignature(bytes32 h, bytes memory signature) external view returns (bytes4 magicValue) { - // fetch and decode immutable public key for the clone - (bytes32 qx, bytes32 qy) = abi.decode(publicKey(), (bytes32, bytes32)); - + function isValidSignature(bytes32 h, bytes calldata signature) external view returns (bytes4 magicValue) { + // parse signature + if (signature.length < 0x40) return bytes4(0); bytes32 r; bytes32 s; assembly ("memory-safe") { - r := mload(add(signature, 0x20)) - s := mload(add(signature, 0x40)) + r := calldataload(add(signature.offset, 0x00)) + s := calldataload(add(signature.offset, 0x20)) } + // fetch and decode immutable public key for the clone + (bytes32 qx, bytes32 qy) = abi.decode(publicKey(), (bytes32, bytes32)); + return P256.verify(h, r, s, qx, qy) ? IERC1271.isValidSignature.selector : bytes4(0); } } diff --git a/contracts/abstraction/identity/IdentityRSA.sol b/contracts/abstraction/identity/IdentityRSA.sol index b9d721c5a5f..e8af41721f3 100644 --- a/contracts/abstraction/identity/IdentityRSA.sol +++ b/contracts/abstraction/identity/IdentityRSA.sol @@ -11,7 +11,7 @@ contract IdentityRSAImplementation is IERC1271 { return abi.decode(Clones.fetchCloneArgs(address(this)), (bytes, bytes)); } - function isValidSignature(bytes32 h, bytes memory signature) external view returns (bytes4 magicValue) { + function isValidSignature(bytes32 h, bytes calldata signature) external view returns (bytes4 magicValue) { // fetch immutable public key for the clone (bytes memory e, bytes memory n) = publicKey(); @@ -25,7 +25,7 @@ contract IdentityRSAImplementation is IERC1271 { contract IdentityRSAFactory { address public immutable implementation = address(new IdentityRSAImplementation()); - function create(bytes memory e, bytes memory n) public returns (address instance) { + function create(bytes calldata e, bytes calldata n) public returns (address instance) { // predict the address of the instance for that key address predicted = predict(e, n); // if instance does not exist ... @@ -36,7 +36,7 @@ contract IdentityRSAFactory { return predicted; } - function predict(bytes memory e, bytes memory n) public view returns (address instance) { + function predict(bytes calldata e, bytes calldata n) public view returns (address instance) { return Clones.predictWithImmutableArgsDeterministicAddress(implementation, abi.encode(e, n), bytes32(0)); } } From 89e9504ca0c21b6b49cd4e1591bb7db565d85173 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 22 Jul 2024 15:04:51 +0200 Subject: [PATCH 66/66] avoid assembly --- contracts/abstraction/identity/IdentityP256.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/contracts/abstraction/identity/IdentityP256.sol b/contracts/abstraction/identity/IdentityP256.sol index d0fb085104d..dd684786924 100644 --- a/contracts/abstraction/identity/IdentityP256.sol +++ b/contracts/abstraction/identity/IdentityP256.sol @@ -14,12 +14,8 @@ contract IdentityP256Implementation is IERC1271 { function isValidSignature(bytes32 h, bytes calldata signature) external view returns (bytes4 magicValue) { // parse signature if (signature.length < 0x40) return bytes4(0); - bytes32 r; - bytes32 s; - assembly ("memory-safe") { - r := calldataload(add(signature.offset, 0x00)) - s := calldataload(add(signature.offset, 0x20)) - } + bytes32 r = bytes32(signature[0x00:0x20]); + bytes32 s = bytes32(signature[0x20:0x40]); // fetch and decode immutable public key for the clone (bytes32 qx, bytes32 qy) = abi.decode(publicKey(), (bytes32, bytes32));