mirror of
https://github.com/getwax/zk-account-abstraction.git
synced 2026-01-08 04:03:59 -05:00
@@ -6,4 +6,5 @@ module.exports = {
|
||||
"samples/gnosis",
|
||||
"utils/Exec.sol"
|
||||
],
|
||||
configureYulOptimizer: true,
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
/* solhint-disable reason-string */
|
||||
/* solhint-disable no-inline-assembly */
|
||||
|
||||
import "../core/BasePaymaster.sol";
|
||||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
||||
|
||||
/**
|
||||
* A sample paymaster that uses external service to decide whether to pay for the UserOp.
|
||||
* The paymaster trusts an external signer to sign the transaction.
|
||||
@@ -22,10 +22,32 @@ contract VerifyingPaymaster is BasePaymaster {
|
||||
|
||||
address public immutable verifyingSigner;
|
||||
|
||||
uint256 private constant VALID_TIMESTAMP_OFFSET = 20;
|
||||
|
||||
uint256 private constant SIGNATURE_OFFSET = 84;
|
||||
|
||||
constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) {
|
||||
verifyingSigner = _verifyingSigner;
|
||||
}
|
||||
|
||||
mapping(address => uint256) public senderNonce;
|
||||
|
||||
function pack(UserOperation calldata userOp) internal pure returns (bytes memory ret) {
|
||||
// lighter signature scheme. must match UserOp.ts#packUserOp
|
||||
bytes calldata pnd = userOp.paymasterAndData;
|
||||
// copy directly the userOp from calldata up to (but not including) the paymasterAndData.
|
||||
// this encoding depends on the ABI encoding of calldata, but is much lighter to copy
|
||||
// than referencing each field separately.
|
||||
assembly {
|
||||
let ofs := userOp
|
||||
let len := sub(sub(pnd.offset, ofs), 32)
|
||||
ret := mload(0x40)
|
||||
mstore(0x40, add(ret, add(len, 32)))
|
||||
mstore(ret, len)
|
||||
calldatacopy(add(ret, 32), ofs, len)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* return the hash we're going to sign off-chain (and validate on-chain)
|
||||
* this method is called by the off-chain service, to sign the request.
|
||||
@@ -33,45 +55,50 @@ contract VerifyingPaymaster is BasePaymaster {
|
||||
* note that this signature covers all fields of the UserOperation, except the "paymasterAndData",
|
||||
* which will carry the signature itself.
|
||||
*/
|
||||
function getHash(UserOperation calldata userOp)
|
||||
public pure returns (bytes32) {
|
||||
function getHash(UserOperation calldata userOp, uint48 validUntil, uint48 validAfter)
|
||||
public view returns (bytes32) {
|
||||
//can't use userOp.hash(), since it contains also the paymasterAndData itself.
|
||||
|
||||
return keccak256(abi.encode(
|
||||
userOp.getSender(),
|
||||
userOp.nonce,
|
||||
keccak256(userOp.initCode),
|
||||
keccak256(userOp.callData),
|
||||
userOp.callGasLimit,
|
||||
userOp.verificationGasLimit,
|
||||
userOp.preVerificationGas,
|
||||
userOp.maxFeePerGas,
|
||||
userOp.maxPriorityFeePerGas
|
||||
pack(userOp),
|
||||
block.chainid,
|
||||
address(this),
|
||||
senderNonce[userOp.getSender()],
|
||||
validUntil,
|
||||
validAfter
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* verify our external signer signed this request.
|
||||
* the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params
|
||||
* paymasterAndData[:20] : address(this)
|
||||
* paymasterAndData[20:84] : abi.encode(validUntil, validAfter)
|
||||
* paymasterAndData[84:] : signature
|
||||
*/
|
||||
function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund)
|
||||
internal view override returns (bytes memory context, uint256 validationData) {
|
||||
internal override returns (bytes memory context, uint256 validationData) {
|
||||
(requiredPreFund);
|
||||
|
||||
bytes32 hash = getHash(userOp);
|
||||
bytes calldata paymasterAndData = userOp.paymasterAndData;
|
||||
uint256 sigLength = paymasterAndData.length - 20;
|
||||
(uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData);
|
||||
//ECDSA library supports both 64 and 65-byte long signatures.
|
||||
// we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA"
|
||||
require(sigLength == 64 || sigLength == 65, "VerifyingPaymaster: invalid signature length in paymasterAndData");
|
||||
require(signature.length == 64 || signature.length == 65, "VerifyingPaymaster: invalid signature length in paymasterAndData");
|
||||
bytes32 hash = ECDSA.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter));
|
||||
senderNonce[userOp.getSender()]++;
|
||||
|
||||
//don't revert on signature failure: return SIG_VALIDATION_FAILED
|
||||
if (verifyingSigner != hash.toEthSignedMessageHash().recover(paymasterAndData[20 :])) {
|
||||
return ("",1);
|
||||
if (verifyingSigner != ECDSA.recover(hash, signature)) {
|
||||
return ("",_packValidationData(true,validUntil,validAfter));
|
||||
}
|
||||
|
||||
//no need for other on-chain validation: entire UserOp should have been checked
|
||||
// by the external service prior to signing it.
|
||||
return ("", 0);
|
||||
return ("",_packValidationData(false,validUntil,validAfter));
|
||||
}
|
||||
|
||||
function parsePaymasterAndData(bytes calldata paymasterAndData) public pure returns(uint48 validUntil, uint48 validAfter, bytes calldata signature) {
|
||||
(validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET:SIGNATURE_OFFSET],(uint48, uint48));
|
||||
signature = paymasterAndData[SIGNATURE_OFFSET:];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"ethereumjs-wallet": "^1.0.1",
|
||||
"hardhat-deploy": "^0.9.3",
|
||||
"hardhat-deploy-ethers": "^0.3.0-beta.11",
|
||||
"solidity-coverage": "^0.7.18",
|
||||
"solidity-coverage": "^0.8.2",
|
||||
"source-map-support": "^0.5.19",
|
||||
"table": "^6.8.0",
|
||||
"typescript": "^4.3.5"
|
||||
|
||||
@@ -260,7 +260,7 @@ describe('Gnosis Proxy', function () {
|
||||
expect(oldManager.toLowerCase()).to.eq(manager.address.toLowerCase())
|
||||
await ethersSigner.sendTransaction({
|
||||
to: ownerAddress,
|
||||
value: parseEther('0.1')
|
||||
value: parseEther('33')
|
||||
})
|
||||
|
||||
const replaceManagerCallData = manager.interface.encodeFunctionData('replaceEIP4337Manager',
|
||||
|
||||
@@ -13,9 +13,13 @@ import {
|
||||
deployEntryPoint, simulationResultCatch
|
||||
} from './testutils'
|
||||
import { fillAndSign } from './UserOp'
|
||||
import { arrayify, hexConcat, parseEther } from 'ethers/lib/utils'
|
||||
import { arrayify, defaultAbiCoder, hexConcat, parseEther } from 'ethers/lib/utils'
|
||||
import { UserOperation } from './UserOperation'
|
||||
|
||||
const MOCK_VALID_UNTIL = '0x00000000deadbeef'
|
||||
const MOCK_VALID_AFTER = '0x0000000000001234'
|
||||
const MOCK_SIG = '0x1234'
|
||||
|
||||
describe('EntryPoint with VerifyingPaymaster', function () {
|
||||
let entryPoint: EntryPoint
|
||||
let accountOwner: Wallet
|
||||
@@ -37,11 +41,22 @@ describe('EntryPoint with VerifyingPaymaster', function () {
|
||||
({ proxy: account } = await createAccount(ethersSigner, accountOwner.address, entryPoint.address))
|
||||
})
|
||||
|
||||
describe('#parsePaymasterAndData', () => {
|
||||
it('should parse data properly', async () => {
|
||||
const paymasterAndData = hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), MOCK_SIG])
|
||||
console.log(paymasterAndData)
|
||||
const res = await paymaster.parsePaymasterAndData(paymasterAndData)
|
||||
expect(res.validUntil).to.be.equal(ethers.BigNumber.from(MOCK_VALID_UNTIL))
|
||||
expect(res.validAfter).to.be.equal(ethers.BigNumber.from(MOCK_VALID_AFTER))
|
||||
expect(res.signature).equal(MOCK_SIG)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#validatePaymasterUserOp', () => {
|
||||
it('should reject on no signature', async () => {
|
||||
const userOp = await fillAndSign({
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, '0x1234'])
|
||||
paymasterAndData: hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x1234'])
|
||||
}, accountOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(userOp)).to.be.revertedWith('invalid signature length in paymasterAndData')
|
||||
})
|
||||
@@ -49,7 +64,7 @@ describe('EntryPoint with VerifyingPaymaster', function () {
|
||||
it('should reject on invalid signature', async () => {
|
||||
const userOp = await fillAndSign({
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, '0x' + '00'.repeat(65)])
|
||||
paymasterAndData: hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x' + '00'.repeat(65)])
|
||||
}, accountOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(userOp)).to.be.revertedWith('ECDSA: invalid signature')
|
||||
})
|
||||
@@ -61,7 +76,7 @@ describe('EntryPoint with VerifyingPaymaster', function () {
|
||||
const sig = await offchainSigner.signMessage(arrayify('0xdead'))
|
||||
wrongSigUserOp = await fillAndSign({
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, sig])
|
||||
paymasterAndData: hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), sig])
|
||||
}, accountOwner, entryPoint)
|
||||
})
|
||||
|
||||
@@ -77,15 +92,19 @@ describe('EntryPoint with VerifyingPaymaster', function () {
|
||||
|
||||
it('succeed with valid signature', async () => {
|
||||
const userOp1 = await fillAndSign({
|
||||
sender: account.address
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x' + '00'.repeat(65)])
|
||||
}, accountOwner, entryPoint)
|
||||
const hash = await paymaster.getHash(userOp1)
|
||||
const hash = await paymaster.getHash(userOp1, MOCK_VALID_UNTIL, MOCK_VALID_AFTER)
|
||||
const sig = await offchainSigner.signMessage(arrayify(hash))
|
||||
const userOp = await fillAndSign({
|
||||
...userOp1,
|
||||
paymasterAndData: hexConcat([paymaster.address, sig])
|
||||
paymasterAndData: hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), sig])
|
||||
}, accountOwner, entryPoint)
|
||||
await entryPoint.callStatic.simulateValidation(userOp).catch(simulationResultCatch)
|
||||
const res = await entryPoint.callStatic.simulateValidation(userOp).catch(simulationResultCatch)
|
||||
expect(res.returnInfo.sigFailed).to.be.false
|
||||
expect(res.returnInfo.validAfter).to.be.equal(ethers.BigNumber.from(MOCK_VALID_AFTER))
|
||||
expect(res.returnInfo.validUntil).to.be.equal(ethers.BigNumber.from(MOCK_VALID_UNTIL))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user