feat: add VerifyingPaymaster
contract
diff --git a/.env.example b/.env.example
index c7eab5b..f74fd4f 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,3 @@
THIRDWEB_API_KEY=
-PAYMASTER_SIGNER_KEY=
\ No newline at end of file
+PAYMASTER_SIGNER_KEY=
+CELOSCAN_KEY=
\ No newline at end of file
diff --git a/contracts/VerifyingPaymaster.sol b/contracts/VerifyingPaymaster.sol
new file mode 100644
index 0000000..5c945e7
--- /dev/null
+++ b/contracts/VerifyingPaymaster.sol
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-3.0
+pragma solidity ^0.8.12;
+
+/* solhint-disable reason-string */
+/* solhint-disable no-inline-assembly */
+
+import "@account-abstraction/contracts/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.
+ * The calling user must pass the UserOp to that external signer first, which performs
+ * whatever off-chain verification before signing the UserOp.
+ * Note that this signature is NOT a replacement for the account-specific signature:
+ * - the paymaster checks a signature to agree to PAY for GAS.
+ * - the account checks a signature to prove identity and account ownership.
+ */
+contract VerifyingPaymaster is BasePaymaster {
+ using ECDSA for bytes32;
+ using UserOperationLib for UserOperation;
+
+ 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
+ ) public 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.
+ * it is called on-chain from the validatePaymasterUserOp, to validate the signature.
+ * note that this signature covers all fields of the UserOperation, except the "paymasterAndData",
+ * which will carry the signature itself.
+ */
+ 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(
+ 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 override returns (bytes memory context, uint256 validationData) {
+ (requiredPreFund);
+
+ (
+ 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(
+ 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 != 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 ("", _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:];
+ }
+}
diff --git a/hardhat.config.js b/hardhat.config.js
new file mode 100644
index 0000000..09713f3
--- /dev/null
+++ b/hardhat.config.js
@@ -0,0 +1,19 @@
+require("@nomicfoundation/hardhat-toolbox");
+require("dotenv").config();
+require("hardhat-celo");
+
+/** @type import('hardhat/config').HardhatUserConfig */
+module.exports = {
+ solidity: "0.8.17",
+ networks: {
+ alfajores: {
+ accounts: [process.env.PAYMASTER_SIGNER_KEY],
+ url: "https://alfajores-forno.celo-testnet.org",
+ },
+ },
+ etherscan: {
+ apiKey: {
+ alfajores: process.env.CELOSCAN_KEY,
+ },
+ },
+};
diff --git a/package.json b/package.json
index b38b15b..341e691 100644
--- a/package.json
+++ b/package.json
@@ -15,11 +15,28 @@
"@thirdweb-dev/sdk": "0.0.0-dev-2067f33-20230609035419",
"@thirdweb-dev/wallets": "0.0.0-dev-2067f33-20230609035419",
"dotenv": "^16.0.3",
- "ethers": "^5"
+ "ethers": "^5",
+ "@account-abstraction/contracts": "^0.6.0",
+ "@openzeppelin/contracts": "^4.9.2"
},
"devDependencies": {
"@types/node": "^18.11.17",
"ts-node": "^10.9.1",
- "typescript": "^4.9.4"
+ "typescript": "^4.9.4",
+ "typechain": "^8.1.0",
+ "@nomicfoundation/hardhat-chai-matchers": "^1.0.0",
+ "@nomicfoundation/hardhat-network-helpers": "^1.0.0",
+ "@nomicfoundation/hardhat-toolbox": "^2.0.2",
+ "@nomiclabs/hardhat-ethers": "^2.0.0",
+ "@nomiclabs/hardhat-etherscan": "^3.0.0",
+ "@typechain/ethers-v5": "^10.1.0",
+ "@typechain/hardhat": "^6.1.2",
+ "@types/chai": "^4.2.0",
+ "@types/mocha": ">=9.1.0",
+ "chai": "^4.2.0",
+ "hardhat": "^2.16.1",
+ "hardhat-celo": "^0.0.4",
+ "hardhat-gas-reporter": "^1.0.8",
+ "solidity-coverage": "^0.8.1"
}
}
diff --git a/scripts/deploy.js b/scripts/deploy.js
new file mode 100644
index 0000000..ccb1ffe
--- /dev/null
+++ b/scripts/deploy.js
@@ -0,0 +1,27 @@
+// We require the Hardhat Runtime Environment explicitly here. This is optional
[... diff too long, it was truncated ...]
GitHub
sha: a550cbd46f197911ae8a210e080aa0b4af11b451