├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── audits └── report-base-paymaster.pdf ├── foundry.toml ├── go.mod ├── go.sum ├── script └── DeployPaymaster.s.sol ├── signer ├── signer.go └── user_operation.go ├── src ├── LimitingPaymaster.sol └── Paymaster.sol └── test └── Paymaster.t.sol /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | pull_request: 7 | branches: 8 | - "main" 9 | jobs: 10 | check: 11 | name: Foundry project 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | submodules: recursive 17 | - name: Install Foundry 18 | uses: foundry-rs/foundry-toolchain@v1 19 | - name: Run tests 20 | run: forge test -vvv 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | # Intellij 17 | .idea/ 18 | 19 | /records/ 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/account-abstraction"] 5 | path = lib/account-abstraction 6 | url = https://github.com/eth-infinitism/account-abstraction 7 | [submodule "lib/openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 10 | [submodule "lib/openzeppelin-contracts-upgradeable"] 11 | path = lib/openzeppelin-contracts-upgradeable 12 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 13 | [submodule "lib/solady"] 14 | path = lib/solady 15 | url = https://github.com/Vectorized/solady 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Base 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Base paymaster 2 | 3 | This repo contains a verifying paymaster contract that can be used for gas subsidies for [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) transactions. 4 | It contains a clone of the [eth-infinitism VerifyingPaymaster](https://github.com/eth-infinitism/account-abstraction/blob/73a676999999843f5086ee546e192cbef25c0c4a/contracts/samples/VerifyingPaymaster.sol) with an additional `receive()` function for simple deposits, as well as some additional changes made in response to an audit. 5 | 6 | ### Deployments 7 | 8 | _(More coming soon)_ 9 | 10 | | Network | Address | 11 | |--------------|------------------------------------------------------------------------------------------------------------------------------------| 12 | | Base Sepolia | [0xf5d253B62543C6Ef526309D497f619CeF95aD430](https://sepolia-explorer.base.org/address/0xf5d253B62543C6Ef526309D497f619CeF95aD430) | 13 | 14 | ### Obtaining a signature for use with the paymaster contract 15 | 16 | If you'd like to use the paymaster to sponsor your 4337 user operations, follow these steps: 17 | 18 | 1. Construct your user operation, without a paymaster set, and left unsigned. 19 | 2. Call `eth_paymasterAndDataForEstimateGas` JSON-RPC method on https://paymaster.base.org. Parameters: 20 | 1. `Object` - the unsigned user operation 21 | 2. `string` - the address of the entrypoint contract 22 | 3. `string` - the chain ID, in hexadecimal 23 | ```shell 24 | curl "https://paymaster.base.org" \ 25 | -H 'content-type: application/json' \ 26 | -d ' 27 | { 28 | "id": 1, 29 | "jsonrpc": "2.0", 30 | "method": "eth_paymasterAndDataForEstimateGas", 31 | "params": [ 32 | { 33 | "sender": "0x0000000000000000000000000000000000000000", 34 | "nonce": "0x2a", 35 | "initCode": "0x", 36 | "callData": "0x", 37 | "callGasLimit": "0x1", 38 | "verificationGasLimit": "0x1", 39 | "preVerificationGas": "0x1", 40 | "maxFeePerGas": "0x1", 41 | "maxPriorityFeePerGas": "0x1" 42 | }, 43 | "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", 44 | "0x14A34" 45 | ] 46 | } 47 | ' 48 | ``` 49 | 3. If the request is successful and the response contains a hex-encoded byte array, use that as the `paymasterAndData` field in the userOp for gas estimation in step 4. 50 | Note that this is a dummy signature that won't be accepted by the paymaster, except for gas estimation. 51 | If an error is returned or the result is empty, the paymaster is not available for the given operation or chain. You can stop here and choose to proceed with another paymaster or self-funding the user operation. 52 | 4. Call estimate gas on your bundler of choice. 53 | 5. Add some headroom to make room for additional paymaster verification gas. In our testing we've found the following values work, but it would depend on your bundler: 54 | 1. `op.PreVerificationGas = estimate.PreVerificationGas + 2000` 55 | 2. `op.VerificationGasLimit = estimate.VerificationGasLimit + 4000` 56 | 6. Call `eth_paymasterAndDataForUserOperation` JSON-RPC method on https://paymaster.base.org. Parameters: 57 | 1. `Object` - the unsigned user operation 58 | 2. `string` - the address of the entrypoint contract 59 | 3. `string` - the chain ID, in hexadecimal 60 | ```shell 61 | curl "https://paymaster.base.org" \ 62 | -H 'content-type: application/json' \ 63 | -d ' 64 | { 65 | "id": 1, 66 | "jsonrpc": "2.0", 67 | "method": "eth_paymasterAndDataForUserOperation", 68 | "params": [ 69 | { 70 | "sender": "0x0000000000000000000000000000000000000000", 71 | "nonce": "0x2a", 72 | "initCode": "0x", 73 | "callData": "0x", 74 | "callGasLimit": "0x1", 75 | "verificationGasLimit": "0x1", 76 | "preVerificationGas": "0x1", 77 | "maxFeePerGas": "0x1", 78 | "maxPriorityFeePerGas": "0x1" 79 | }, 80 | "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", 81 | "0x14A34" 82 | ] 83 | } 84 | ' 85 | ``` 86 | 7. If the request is successful and the response contains a hex-encoded byte array, use that as the `paymasterAndData` field in the userOp. 87 | If an error is returned or the result is empty, the paymaster is not available for the given operation or chain. You can choose to proceed with another paymaster or self-funding the user operation. 88 | 8. Sign the user operation, and submit to your bundler of choice. 89 | 90 | Note that the `paymasterAndData` returned in step 6 contains a signature of the provided userOp, so any modification of the userOp post step 6 (except for the `sig` field) will result in the paymaster rejecting the operation. 91 | 92 | ## Deploying and using your own paymaster 93 | 94 | If you want to deploy your own paymaster, you can deploy [Paymaster.sol](./src/Paymaster.sol) to your chain of choice. 95 | In order to generate signatures for your paymaster, there's a Golang package in the [signer](./signer) directory that allows you to sign userOperations that will be accepted by the paymaster (call `UserOperation.PaymasterSign`). 96 | Of course you can also rewrite this in your language of choice; the Golang package will provide a good example. 97 | -------------------------------------------------------------------------------- /audits/report-base-paymaster.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/base/paymaster/70c7740430c55ca0604bb2ce00fc5e71ef4fb666/audits/report-base-paymaster.pdf -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | broadcast = 'records' 6 | optimizer = true 7 | optimizer_runs = 999999 8 | solc_version = "0.8.20" 9 | remappings = [ 10 | '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts', 11 | '@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts', 12 | '@account-abstraction/=lib/account-abstraction/contracts', 13 | '@solady/=lib/solady/src', 14 | ] 15 | via_ir = true 16 | 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/base-org/paymaster 2 | 3 | go 1.21.1 4 | 5 | require github.com/ethereum/go-ethereum v1.13.5 6 | 7 | require ( 8 | github.com/bits-and-blooms/bitset v1.7.0 // indirect 9 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect 10 | github.com/consensys/bavard v0.1.13 // indirect 11 | github.com/consensys/gnark-crypto v0.12.1 // indirect 12 | github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect 13 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 14 | github.com/ethereum/c-kzg-4844 v0.4.0 // indirect 15 | github.com/go-stack/stack v1.8.1 // indirect 16 | github.com/holiman/uint256 v1.2.3 // indirect 17 | github.com/mmcloughlin/addchain v0.4.0 // indirect 18 | github.com/supranational/blst v0.3.11 // indirect 19 | golang.org/x/crypto v0.14.0 // indirect 20 | golang.org/x/sync v0.3.0 // indirect 21 | golang.org/x/sys v0.13.0 // indirect 22 | rsc.io/tmplfunc v0.0.3 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= 2 | github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 3 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= 4 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 5 | github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= 6 | github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= 7 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 8 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 9 | github.com/bits-and-blooms/bitset v1.7.0 h1:YjAGVd3XmtK9ktAbX8Zg2g2PwLIMjGREZJHlV4j7NEo= 10 | github.com/bits-and-blooms/bitset v1.7.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= 11 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= 12 | github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= 13 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= 14 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 15 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 16 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y= 18 | github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= 19 | github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= 20 | github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= 21 | github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 h1:aPEJyR4rPBvDmeyi+l/FS/VtA00IWvjeFvjen1m1l1A= 22 | github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593/go.mod h1:6hk1eMY/u5t+Cf18q5lFMUA1Rc+Sm5I6Ra1QuPyxXCo= 23 | github.com/cockroachdb/redact v1.0.8 h1:8QG/764wK+vmEYoOlfobpe12EQcS81ukx/a4hdVMxNw= 24 | github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 25 | github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 h1:IKgmqgMQlVJIZj19CdocBeSfSaiCbEBZGKODaixqtHM= 26 | github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= 27 | github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= 28 | github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= 29 | github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= 30 | github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= 31 | github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= 32 | github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= 33 | github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA= 34 | github.com/crate-crypto/go-kzg-4844 v0.7.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= 35 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 36 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= 38 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 39 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= 40 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 41 | github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY= 42 | github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= 43 | github.com/ethereum/go-ethereum v1.13.5 h1:U6TCRciCqZRe4FPXmy1sMGxTfuk8P7u2UoinF3VbaFk= 44 | github.com/ethereum/go-ethereum v1.13.5/go.mod h1:yMTu38GSuyxaYzQMViqNmQ1s3cE84abZexQmTgenWk0= 45 | github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= 46 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 47 | github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= 48 | github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= 49 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 50 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 51 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 52 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 53 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 54 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 55 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= 56 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 57 | github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 58 | github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= 59 | github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= 60 | github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= 61 | github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= 62 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 63 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 64 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 65 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 66 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 67 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 68 | github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= 69 | github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= 70 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 71 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 72 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= 73 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 74 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= 75 | github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= 76 | github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= 77 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 78 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 79 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 80 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 81 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 82 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 83 | github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg= 84 | github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 85 | github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y= 86 | github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 87 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= 88 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 89 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 90 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 91 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 92 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 93 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 94 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 95 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= 96 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 97 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 98 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 99 | github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= 100 | github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 101 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= 102 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 103 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 104 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 105 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 106 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 107 | golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= 108 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 109 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 110 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 111 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 112 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 113 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 114 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 116 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 117 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 118 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 119 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 120 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 121 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= 122 | rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= 123 | -------------------------------------------------------------------------------- /script/DeployPaymaster.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "../src/Paymaster.sol"; 6 | import "@account-abstraction/interfaces/IEntryPoint.sol"; 7 | 8 | // This script deploys a Paymaster 9 | contract DeployPaymaster is Script { 10 | address entryPoint = 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789; 11 | 12 | function run() public { 13 | address signer = vm.envAddress("VERIFYING_SIGNER"); 14 | vm.broadcast(); 15 | Paymaster paymaster = new Paymaster(IEntryPoint(entryPoint), signer); 16 | require(paymaster.verifyingSigner() == signer, "Deploy: verifyingSigner is incorrect"); 17 | require(paymaster.owner() == tx.origin, "Deploy: owner is incorrect"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /signer/signer.go: -------------------------------------------------------------------------------- 1 | package signer 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/crypto" 8 | ) 9 | 10 | type Signer interface { 11 | SignHash(h common.Hash) ([]byte, error) 12 | } 13 | 14 | type PrivateKeySigner struct { 15 | *ecdsa.PrivateKey 16 | } 17 | 18 | func (s *PrivateKeySigner) SignHash(h common.Hash) ([]byte, error) { 19 | signature, err := crypto.Sign(h[:], s.PrivateKey) 20 | if err == nil { 21 | signature[64] += 27 22 | } 23 | return signature, err 24 | } 25 | -------------------------------------------------------------------------------- /signer/user_operation.go: -------------------------------------------------------------------------------- 1 | package signer 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/ethereum/go-ethereum/accounts" 7 | "github.com/ethereum/go-ethereum/accounts/abi" 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/ethereum/go-ethereum/common/hexutil" 10 | "github.com/ethereum/go-ethereum/crypto" 11 | ) 12 | 13 | type UserOperation struct { 14 | Sender common.Address `json:"sender" mapstructure:"sender" validate:"required"` 15 | Nonce *hexutil.Big `json:"nonce" mapstructure:"nonce" validate:"required"` 16 | InitCode hexutil.Bytes `json:"initCode" mapstructure:"initCode" validate:"required"` 17 | CallData hexutil.Bytes `json:"callData" mapstructure:"callData" validate:"required"` 18 | CallGasLimit *hexutil.Big `json:"callGasLimit" mapstructure:"callGasLimit" validate:"required"` 19 | VerificationGasLimit *hexutil.Big `json:"verificationGasLimit" mapstructure:"verificationGasLimit" validate:"required"` 20 | PreVerificationGas *hexutil.Big `json:"preVerificationGas" mapstructure:"preVerificationGas" validate:"required"` 21 | MaxFeePerGas *hexutil.Big `json:"maxFeePerGas" mapstructure:"maxFeePerGas" validate:"required"` 22 | MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas" mapstructure:"maxPriorityFeePerGas" validate:"required"` 23 | PaymasterAndData hexutil.Bytes `json:"paymasterAndData" mapstructure:"paymasterAndData" validate:"required"` 24 | Signature hexutil.Bytes `json:"signature" mapstructure:"signature" validate:"required"` 25 | } 26 | 27 | func (op *UserOperation) PaymasterSign(paymaster common.Address, chainID, validUntil, validAfter *big.Int, signer Signer) ([]byte, error) { 28 | hash, err := op.PaymasterHash(paymaster, chainID, validUntil, validAfter) 29 | if err != nil { 30 | return nil, err 31 | } 32 | h := common.BytesToHash(accounts.TextHash(hash[:])) 33 | return signer.SignHash(h) 34 | } 35 | 36 | func (op *UserOperation) PaymasterHash(paymaster common.Address, chainID, validUntil, validAfter *big.Int) (common.Hash, error) { 37 | address, _ := abi.NewType("address", "", nil) 38 | uint256, _ := abi.NewType("uint256", "", nil) 39 | uint48, _ := abi.NewType("uint48", "", nil) 40 | bytes32, _ := abi.NewType("bytes32", "", nil) 41 | args := abi.Arguments{ 42 | {Name: "sender", Type: address}, 43 | {Name: "nonce", Type: uint256}, 44 | {Name: "hashInitCode", Type: bytes32}, 45 | {Name: "hashCallData", Type: bytes32}, 46 | {Name: "callGasLimit", Type: uint256}, 47 | {Name: "verificationGasLimit", Type: uint256}, 48 | {Name: "preVerificationGas", Type: uint256}, 49 | {Name: "maxFeePerGas", Type: uint256}, 50 | {Name: "maxPriorityFeePerGas", Type: uint256}, 51 | {Name: "chainId", Type: uint256}, 52 | {Name: "paymaster", Type: address}, 53 | {Name: "validUntil", Type: uint48}, 54 | {Name: "validAfter", Type: uint48}, 55 | } 56 | packed, err := args.Pack( 57 | op.Sender, 58 | op.Nonce.ToInt(), 59 | crypto.Keccak256Hash(op.InitCode), 60 | crypto.Keccak256Hash(op.CallData), 61 | op.CallGasLimit.ToInt(), 62 | op.VerificationGasLimit.ToInt(), 63 | op.PreVerificationGas.ToInt(), 64 | op.MaxFeePerGas.ToInt(), 65 | op.MaxPriorityFeePerGas.ToInt(), 66 | chainID, 67 | paymaster, 68 | validUntil, 69 | validAfter, 70 | ) 71 | if err != nil { 72 | return common.Hash{}, err 73 | } 74 | return crypto.Keccak256Hash(packed), nil 75 | } 76 | -------------------------------------------------------------------------------- /src/LimitingPaymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.20; 3 | 4 | /* solhint-disable reason-string */ 5 | 6 | import "@account-abstraction/core/BasePaymaster.sol"; 7 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 8 | 9 | /** 10 | * A paymaster that uses external service to decide whether to pay for the UserOp. 11 | * Also limits spending to "spendMax" per "spentKey", passed in via paymaster data. 12 | * The paymaster trusts an external signer to sign the transaction. 13 | * The calling user must pass the UserOp to that external signer first, which performs 14 | * whatever off-chain verification before signing the UserOp. 15 | * Note that this signature is NOT a replacement for the account-specific signature: 16 | * - the paymaster checks a signature to agree to PAY for GAS. 17 | * - the account checks a signature to prove identity and account ownership. 18 | */ 19 | contract LimitingPaymaster is BasePaymaster { 20 | using UserOperationLib for UserOperation; 21 | 22 | uint256 public constant newSenderPostOpOverhead = 23231; 23 | uint256 public constant oldSenderPostOpOverhead = 6143; 24 | 25 | address public immutable verifyingSigner; 26 | 27 | mapping (uint32 => uint96) public spent; 28 | mapping (address => bool) public bundlerAllowed; 29 | 30 | constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) Ownable() { 31 | require(address(_entryPoint).code.length > 0, "Paymaster: passed _entryPoint is not currently a contract"); 32 | require(_verifyingSigner != address(0), "Paymaster: verifyingSigner cannot be address(0)"); 33 | require(_verifyingSigner != msg.sender, "Paymaster: verifyingSigner cannot be the owner"); 34 | verifyingSigner = _verifyingSigner; 35 | } 36 | 37 | /** 38 | * return the hash we're going to sign off-chain (and validate on-chain) 39 | * this method is called by the off-chain service, to sign the request. 40 | * it is called on-chain from the validatePaymasterUserOp, to validate the signature. 41 | * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", 42 | * which will carry the signature itself. 43 | */ 44 | function getHash(UserOperation calldata userOp, uint48 validUntil, uint48 validAfter, uint32 spentKey, uint96 spentMax, bool allowAnyBundler) 45 | public view returns (bytes32) { 46 | // can't use userOp.hash(), since it contains also the paymasterAndData itself. 47 | return keccak256( 48 | abi.encode( 49 | userOp.getSender(), 50 | userOp.nonce, 51 | calldataKeccak(userOp.initCode), 52 | calldataKeccak(userOp.callData), 53 | userOp.callGasLimit, 54 | userOp.verificationGasLimit, 55 | userOp.preVerificationGas, 56 | userOp.maxFeePerGas, 57 | userOp.maxPriorityFeePerGas, 58 | block.chainid, 59 | address(this), 60 | validUntil, 61 | validAfter, 62 | spentKey, 63 | spentMax, 64 | allowAnyBundler 65 | ) 66 | ); 67 | } 68 | 69 | /** 70 | * verify our external signer signed this request. 71 | * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params 72 | * paymasterAndData[:20] : address(this) 73 | * paymasterAndData[20:26] : validUntil 74 | * paymasterAndData[26:32] : validAfter 75 | * paymasterAndData[32:36] : spendKey 76 | * paymasterAndData[36:48] : spendMax 77 | * paymasterAndData[48] : allowAnyBundler 78 | * paymasterAndData[49:114] : signature 79 | */ 80 | function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) 81 | internal view override returns (bytes memory context, uint256 validationData) { 82 | (uint48 validUntil, uint48 validAfter, uint32 spentKey, uint96 spentMax, bool allowAnyBundler, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData); 83 | require(spent[spentKey] + requiredPreFund <= spentMax, "Paymaster: spender funds are depleted"); 84 | // Only support 65-byte signatures, to avoid potential replay attacks. 85 | require(signature.length == 65, "Paymaster: invalid signature length in paymasterAndData"); 86 | bytes32 hash = ECDSA.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter, spentKey, spentMax, allowAnyBundler)); 87 | 88 | // don't revert on signature failure: return SIG_VALIDATION_FAILED 89 | if (verifyingSigner != ECDSA.recover(hash, signature)) { 90 | return ("", _packValidationData(true, validUntil, validAfter)); 91 | } 92 | 93 | // no need for other on-chain validation: entire UserOp should have been checked 94 | // by the external service prior to signing it. 95 | return (_packContextData(userOp, spentKey, allowAnyBundler), _packValidationData(false, validUntil, validAfter)); 96 | } 97 | 98 | function _packContextData(UserOperation calldata userOp, uint32 spentKey, bool allowAnyBundler) internal pure returns (bytes memory) { 99 | return abi.encode(userOp.maxFeePerGas, userOp.maxPriorityFeePerGas, spentKey, allowAnyBundler); 100 | } 101 | 102 | function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) internal override { 103 | (uint256 maxFeePerGas, uint256 maxPriorityFeePerGas, uint32 spentKey, bool allowAnyBundler) = abi.decode(context, (uint256, uint256, uint32, bool)); 104 | // unfortunately tx.origin is not allowed in validation, so we check here 105 | require(allowAnyBundler || bundlerAllowed[tx.origin], "Paymaster: bundler not allowed"); 106 | 107 | if (mode != PostOpMode.postOpReverted) { 108 | uint256 overhead = spent[spentKey] == 0 ? newSenderPostOpOverhead : oldSenderPostOpOverhead; 109 | uint256 gasPrice = min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); 110 | spent[spentKey] += uint96(actualGasCost + overhead*gasPrice); 111 | } 112 | } 113 | 114 | function parsePaymasterAndData(bytes calldata paymasterAndData) 115 | internal pure returns(uint48 validUntil, uint48 validAfter, uint32 spentKey, uint96 spentMax, bool allowAnyBundler, bytes calldata signature) { 116 | validUntil = uint48(bytes6(paymasterAndData[20:26])); 117 | validAfter = uint48(bytes6(paymasterAndData[26:32])); 118 | spentKey = uint32(bytes4(paymasterAndData[32:36])); 119 | spentMax = uint96(bytes12(paymasterAndData[36:48])); 120 | allowAnyBundler = paymasterAndData[48] > 0; 121 | signature = paymasterAndData[49:]; 122 | } 123 | 124 | function renounceOwnership() public override view onlyOwner { 125 | revert("Paymaster: renouncing ownership is not allowed"); 126 | } 127 | 128 | function transferOwnership(address newOwner) public override onlyOwner { 129 | require(newOwner != address(0), "Paymaster: owner cannot be address(0)"); 130 | require(newOwner != verifyingSigner, "Paymaster: owner cannot be the verifyingSigner"); 131 | _transferOwnership(newOwner); 132 | } 133 | 134 | function addBundler(address bundler) public onlyOwner { 135 | bundlerAllowed[bundler] = true; 136 | } 137 | 138 | function removeBundler(address bundler) public onlyOwner { 139 | bundlerAllowed[bundler] = false; 140 | } 141 | 142 | function min(uint256 a, uint256 b) internal pure returns (uint256) { 143 | return a < b ? a : b; 144 | } 145 | 146 | receive() external payable { 147 | // use address(this).balance rather than msg.value in case of force-send 148 | (bool callSuccess, ) = payable(address(entryPoint)).call{value: address(this).balance}(""); 149 | require(callSuccess, "Deposit failed"); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Paymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.20; 3 | 4 | /* solhint-disable reason-string */ 5 | 6 | import "@account-abstraction/core/BasePaymaster.sol"; 7 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 8 | 9 | /** 10 | * A paymaster that uses external service to decide whether to pay for the UserOp. 11 | * The paymaster trusts an external signer to sign the transaction. 12 | * The calling user must pass the UserOp to that external signer first, which performs 13 | * whatever off-chain verification before signing the UserOp. 14 | * Note that this signature is NOT a replacement for the account-specific signature: 15 | * - the paymaster checks a signature to agree to PAY for GAS. 16 | * - the account checks a signature to prove identity and account ownership. 17 | */ 18 | contract Paymaster is BasePaymaster { 19 | using UserOperationLib for UserOperation; 20 | 21 | address public immutable verifyingSigner; 22 | 23 | uint256 private constant VALID_TIMESTAMP_OFFSET = 20; 24 | uint256 private constant SIGNATURE_OFFSET = VALID_TIMESTAMP_OFFSET + 64; 25 | 26 | constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) Ownable() { 27 | require(address(_entryPoint).code.length > 0, "Paymaster: passed _entryPoint is not currently a contract"); 28 | require(_verifyingSigner != address(0), "Paymaster: verifyingSigner cannot be address(0)"); 29 | require(_verifyingSigner != msg.sender, "Paymaster: verifyingSigner cannot be the owner"); 30 | verifyingSigner = _verifyingSigner; 31 | } 32 | 33 | /** 34 | * return the hash we're going to sign off-chain (and validate on-chain) 35 | * this method is called by the off-chain service, to sign the request. 36 | * it is called on-chain from the validatePaymasterUserOp, to validate the signature. 37 | * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", 38 | * which will carry the signature itself. 39 | */ 40 | function getHash(UserOperation calldata userOp, uint48 validUntil, uint48 validAfter) 41 | public view returns (bytes32) { 42 | // can't use userOp.hash(), since it contains also the paymasterAndData itself. 43 | return keccak256( 44 | abi.encode( 45 | userOp.getSender(), 46 | userOp.nonce, 47 | calldataKeccak(userOp.initCode), 48 | calldataKeccak(userOp.callData), 49 | userOp.callGasLimit, 50 | userOp.verificationGasLimit, 51 | userOp.preVerificationGas, 52 | userOp.maxFeePerGas, 53 | userOp.maxPriorityFeePerGas, 54 | block.chainid, 55 | address(this), 56 | validUntil, 57 | validAfter 58 | ) 59 | ); 60 | } 61 | 62 | /** 63 | * verify our external signer signed this request. 64 | * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params 65 | * paymasterAndData[:20] : address(this) 66 | * paymasterAndData[20:84] : abi.encode(validUntil, validAfter) 67 | * paymasterAndData[84:] : signature 68 | */ 69 | function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 /*requiredPreFund*/) 70 | internal view override returns (bytes memory context, uint256 validationData) { 71 | (uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData); 72 | // Only support 65-byte signatures, to avoid potential replay attacks. 73 | require(signature.length == 65, "Paymaster: invalid signature length in paymasterAndData"); 74 | bytes32 hash = ECDSA.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter)); 75 | 76 | // don't revert on signature failure: return SIG_VALIDATION_FAILED 77 | if (verifyingSigner != ECDSA.recover(hash, signature)) { 78 | return ("", _packValidationData(true, validUntil, validAfter)); 79 | } 80 | 81 | // no need for other on-chain validation: entire UserOp should have been checked 82 | // by the external service prior to signing it. 83 | return ("", _packValidationData(false, validUntil, validAfter)); 84 | } 85 | 86 | function parsePaymasterAndData(bytes calldata paymasterAndData) 87 | internal pure returns(uint48 validUntil, uint48 validAfter, bytes calldata signature) { 88 | (validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET:SIGNATURE_OFFSET],(uint48, uint48)); 89 | signature = paymasterAndData[SIGNATURE_OFFSET:]; 90 | } 91 | 92 | function renounceOwnership() public override view onlyOwner { 93 | revert("Paymaster: renouncing ownership is not allowed"); 94 | } 95 | 96 | function transferOwnership(address newOwner) public override onlyOwner { 97 | require(newOwner != address(0), "Paymaster: owner cannot be address(0)"); 98 | require(newOwner != verifyingSigner, "Paymaster: owner cannot be the verifyingSigner"); 99 | _transferOwnership(newOwner); 100 | } 101 | 102 | receive() external payable { 103 | // use address(this).balance rather than msg.value in case of force-send 104 | (bool callSuccess, ) = payable(address(entryPoint)).call{value: address(this).balance}(""); 105 | require(callSuccess, "Deposit failed"); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/Paymaster.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {Paymaster} from "../src/Paymaster.sol"; 6 | import {IEntryPoint} from "@account-abstraction/interfaces/IEntryPoint.sol"; 7 | import {IStakeManager} from "@account-abstraction/interfaces/IStakeManager.sol"; 8 | import {EntryPoint} from "@account-abstraction/core/EntryPoint.sol"; 9 | import {UserOperation} from "@account-abstraction/interfaces/UserOperation.sol"; 10 | import {SimpleAccountFactory} from "@account-abstraction/samples/SimpleAccountFactory.sol"; 11 | import {SimpleAccount} from "@account-abstraction/samples/SimpleAccount.sol"; 12 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 13 | 14 | contract PaymasterTest is Test { 15 | EntryPoint public entrypoint; 16 | Paymaster public paymaster; 17 | SimpleAccount public account; 18 | 19 | uint48 constant MOCK_VALID_UNTIL = 0x00000000deadbeef; 20 | uint48 constant MOCK_VALID_AFTER = 0x0000000000001234; 21 | bytes constant MOCK_SIG = "0x1234"; 22 | address constant PAYMASTER_SIGNER = 0xC3Bf2750F0d47098f487D45b2FB26b32eCbAf9a2; 23 | uint256 constant PAYMASTER_SIGNER_KEY = 0x6a6c11c6f4703865cc4a88c6ebf0a605fdeeccd8052d66101d1d02730740a3c0; 24 | address constant ACCOUNT_OWNER = 0x39c0Bb04Bf6B779ac994f6A5211204e3Dbe16741; 25 | uint256 constant ACCOUNT_OWNER_KEY = 0x4034df11fcc455209edcb8948449a4dff732376dab6d03dc2d099d0084b0f023; 26 | 27 | function setUp() public { 28 | entrypoint = new EntryPoint(); 29 | paymaster = new Paymaster(entrypoint, PAYMASTER_SIGNER); 30 | SimpleAccountFactory factory = new SimpleAccountFactory(entrypoint); 31 | account = factory.createAccount(ACCOUNT_OWNER, 0); 32 | } 33 | 34 | function test_zeroAddressVerifyingSigner() public { 35 | vm.expectRevert("Paymaster: verifyingSigner cannot be address(0)"); 36 | new Paymaster(entrypoint, address(0)); 37 | } 38 | 39 | function test_ownerVerifyingSigner() public { 40 | vm.expectRevert("Paymaster: verifyingSigner cannot be the owner"); 41 | new Paymaster(entrypoint, address(this)); 42 | } 43 | 44 | function test_entryPointNotAContract() public { 45 | vm.expectRevert("Paymaster: passed _entryPoint is not currently a contract"); 46 | new Paymaster(IEntryPoint(address(0x1234)), PAYMASTER_SIGNER); 47 | } 48 | 49 | function test_noRenounceOwnership() public { 50 | vm.expectRevert("Paymaster: renouncing ownership is not allowed"); 51 | paymaster.renounceOwnership(); 52 | } 53 | 54 | function test_zeroAddressTransferOwnership() public { 55 | vm.expectRevert("Paymaster: owner cannot be address(0)"); 56 | paymaster.transferOwnership(address(0)); 57 | } 58 | 59 | function test_verifyingSignerTransferOwnership() public { 60 | vm.expectRevert("Paymaster: owner cannot be the verifyingSigner"); 61 | paymaster.transferOwnership(PAYMASTER_SIGNER); 62 | } 63 | 64 | function test_getHash() public { 65 | UserOperation memory userOp = createUserOp(); 66 | userOp.sender = ACCOUNT_OWNER; 67 | userOp.initCode = "initCode"; 68 | userOp.callData = "callData"; 69 | bytes32 hash = paymaster.getHash(userOp, MOCK_VALID_UNTIL, MOCK_VALID_AFTER); 70 | assertEq(hash, 0x1f6a1f43ed14fbbfa7bc28cdb6847b602be224a706e76955a5066e06a1823d72); 71 | } 72 | 73 | function test_validatePaymasterUserOpValidSignature() public { 74 | UserOperation memory userOp = createUserOp(); 75 | signUserOp(userOp); 76 | 77 | vm.expectRevert(createEncodedValidationResult(false, 53025)); 78 | entrypoint.simulateValidation(userOp); 79 | } 80 | 81 | function test_validatePaymasterUserOpWrongSigner() public { 82 | UserOperation memory userOp = createUserOp(); 83 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(ACCOUNT_OWNER_KEY, ECDSA.toEthSignedMessageHash(paymaster.getHash(userOp, MOCK_VALID_UNTIL, MOCK_VALID_AFTER))); 84 | userOp.paymasterAndData = abi.encodePacked(address(paymaster), abi.encode(MOCK_VALID_UNTIL, MOCK_VALID_AFTER), r, s, v); 85 | signUserOp(userOp); 86 | 87 | vm.expectRevert(createEncodedValidationResult(true, 53035)); 88 | entrypoint.simulateValidation(userOp); 89 | } 90 | 91 | function test_validatePaymasterUserOpNoSignature() public { 92 | UserOperation memory userOp = createUserOp(); 93 | userOp.paymasterAndData = abi.encodePacked(address(paymaster), abi.encode(MOCK_VALID_UNTIL, MOCK_VALID_AFTER)); 94 | signUserOp(userOp); 95 | 96 | vm.expectRevert( 97 | abi.encodeWithSelector( 98 | IEntryPoint.FailedOp.selector, 99 | 0, "AA33 reverted: Paymaster: invalid signature length in paymasterAndData" 100 | ) 101 | ); 102 | entrypoint.simulateValidation(userOp); 103 | } 104 | 105 | function test_validatePaymasterUserOpInvalidSignature() public { 106 | UserOperation memory userOp = createUserOp(); 107 | userOp.paymasterAndData = abi.encodePacked(address(paymaster), abi.encode(MOCK_VALID_UNTIL, MOCK_VALID_AFTER), bytes32(0), bytes32(0), uint8(0)); 108 | signUserOp(userOp); 109 | 110 | vm.expectRevert( 111 | abi.encodeWithSelector( 112 | IEntryPoint.FailedOp.selector, 113 | 0, "AA33 reverted: ECDSA: invalid signature" 114 | ) 115 | ); 116 | entrypoint.simulateValidation(userOp); 117 | } 118 | 119 | function test_receive() public { 120 | assertEq(0, entrypoint.balanceOf(address(paymaster))); 121 | (bool callSuccess, ) = address(paymaster).call{value: 1 ether}(""); 122 | require(callSuccess, "Receive failed"); 123 | assertEq(1 ether, entrypoint.balanceOf(address(paymaster))); 124 | } 125 | 126 | function createUserOp() public view returns (UserOperation memory) { 127 | UserOperation memory userOp; 128 | userOp.sender = address(account); 129 | userOp.verificationGasLimit = 100000; 130 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(PAYMASTER_SIGNER_KEY, ECDSA.toEthSignedMessageHash(paymaster.getHash(userOp, MOCK_VALID_UNTIL, MOCK_VALID_AFTER))); 131 | userOp.paymasterAndData = abi.encodePacked(address(paymaster), abi.encode(MOCK_VALID_UNTIL, MOCK_VALID_AFTER), r, s, v); 132 | return userOp; 133 | } 134 | 135 | function signUserOp(UserOperation memory userOp) public view { 136 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(ACCOUNT_OWNER_KEY, ECDSA.toEthSignedMessageHash(entrypoint.getUserOpHash(userOp))); 137 | userOp.signature = abi.encodePacked(r, s, v); 138 | } 139 | 140 | function createEncodedValidationResult(bool sigFailed, uint256 preOpGas) public pure returns (bytes memory) { 141 | uint256 prefund = 0; 142 | bytes memory paymasterContext = ""; 143 | return abi.encodeWithSelector( 144 | IEntryPoint.ValidationResult.selector, 145 | IEntryPoint.ReturnInfo(preOpGas, prefund, sigFailed, MOCK_VALID_AFTER, MOCK_VALID_UNTIL, paymasterContext), 146 | IStakeManager.StakeInfo(0, 0), 147 | IStakeManager.StakeInfo(0, 0), 148 | IStakeManager.StakeInfo(0, 0) 149 | ); 150 | } 151 | } 152 | --------------------------------------------------------------------------------