diff --git a/script/deployChainAbstractionSetup.s.sol b/script/deployChainAbstractionSetup.s.sol index 8b9050c..c159268 100644 --- a/script/deployChainAbstractionSetup.s.sol +++ b/script/deployChainAbstractionSetup.s.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.0; import {Script, console} from "forge-std/Script.sol"; -import {IEntryPoint} from "account-abstraction/core/EntryPoint.sol"; import {IInvoiceManager} from "../src/interfaces/IInvoiceManager.sol"; import {IVaultManager} from "../src/interfaces/IVaultManager.sol"; + +import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; import {CheckOrDeployEntryPoint} from "./auxiliary/checkOrDeployEntrypoint.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {UpgradeableOpenfortProxy} from "../src/proxy/UpgradeableOpenfortProxy.sol"; @@ -84,9 +85,9 @@ contract DeployChainAbstractionSetup is Script, CheckOrDeployEntryPoint { IEntryPoint entryPoint = checkOrDeployEntryPoint(); CABPaymaster paymaster = new CABPaymaster{salt: versionSalt}( - entryPoint, IInvoiceManager(address(invoiceManager)), ICrossL2Prover(crossL2Prover), verifyingSigner, owner + ICrossL2Prover(crossL2Prover), IInvoiceManager(address(invoiceManager)), verifyingSigner, owner ); - + paymaster.initialize(tokens); console.log("Paymaster Address", address(paymaster)); vm.stopBroadcast(); } diff --git a/src/interfaces/IPaymasterVerifier.sol b/src/interfaces/IPaymasterVerifier.sol index a4dbf84..b3b8326 100644 --- a/src/interfaces/IPaymasterVerifier.sol +++ b/src/interfaces/IPaymasterVerifier.sol @@ -31,4 +31,14 @@ interface IPaymasterVerifier { * @notice Withdraw the token. */ function withdraw(address token, uint256 amount) external; + + /** + * @notice Emitted when a CABPaymaster is initialized. + * @param cabPaymaster The address of the CABPaymaster. + * @param supportedTokensLength The length of the supported tokens. + * @param supportedTokensHash The hash of the supported tokens. + */ + event CABPaymasterInitialized( + address indexed cabPaymaster, uint256 indexed supportedTokensLength, bytes32 indexed supportedTokensHash + ); } diff --git a/src/paymasters/CABPaymaster.sol b/src/paymasters/CABPaymaster.sol index 663d9e9..1737232 100644 --- a/src/paymasters/CABPaymaster.sol +++ b/src/paymasters/CABPaymaster.sol @@ -9,18 +9,23 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {IInvoiceManager} from "../interfaces/IInvoiceManager.sol"; +import {LibTokens} from "./LibTokens.sol"; import {IVault} from "../interfaces/IVault.sol"; import {IPaymasterVerifier} from "../interfaces/IPaymasterVerifier.sol"; import {ICrossL2Prover} from "@vibc-core-smart-contracts/contracts/interfaces/ICrossL2Prover.sol"; import {LibBytes} from "@solady/utils/LibBytes.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; /** * @title CABPaymaster * @dev A paymaster used in chain abstracted balance to sponsor the gas fee and tokens cross-chain. */ -contract CABPaymaster is IPaymasterVerifier, BasePaymaster { +contract CABPaymaster is IPaymasterVerifier, BasePaymaster, Initializable { using SafeERC20 for IERC20; using UserOperationLib for PackedUserOperation; + using LibTokens for LibTokens.TokensStore; + + LibTokens.TokensStore private tokensStore; IInvoiceManager public immutable invoiceManager; ICrossL2Prover public immutable crossL2Prover; @@ -30,20 +35,25 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster { uint256 private constant VALID_TIMESTAMP_OFFSET = PAYMASTER_DATA_OFFSET; uint256 private constant SIGNATURE_OFFSET = VALID_TIMESTAMP_OFFSET + 12; - address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address public constant ENTRY_POINT_V7 = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; constructor( - IEntryPoint _entryPoint, - IInvoiceManager _invoiceManager, ICrossL2Prover _crossL2Prover, + IInvoiceManager _invoiceManager, address _verifyingSigner, address _owner - ) BasePaymaster(_entryPoint, _owner) { - invoiceManager = _invoiceManager; + ) BasePaymaster(IEntryPoint(ENTRY_POINT_V7), _owner) { crossL2Prover = _crossL2Prover; + invoiceManager = _invoiceManager; verifyingSigner = _verifyingSigner; } + function initialize(address[] memory _supportedTokens) public initializer { + for (uint256 i = 0; i < _supportedTokens.length; i++) { + tokensStore.addSupportedToken(_supportedTokens[i]); + } + } + /// @inheritdoc IPaymasterVerifier function verifyInvoice( bytes32 _invoiceId, @@ -67,7 +77,7 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster { } function withdraw(address token, uint256 amount) external override onlyOwner { - if (token == NATIVE_TOKEN) { + if (token == LibTokens.NATIVE_TOKEN) { (bool success,) = payable(owner()).call{value: amount}(""); require(success, "Native token transfer failed"); } else { @@ -104,18 +114,6 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster { ); } - function getInvoiceHash(IInvoiceManager.InvoiceWithRepayTokens calldata invoice) public pure returns (bytes32) { - return keccak256( - abi.encode( - invoice.account, - invoice.nonce, - invoice.paymaster, - invoice.sponsorChainId, - keccak256(abi.encode(invoice.repayTokenInfos)) - ) - ); - } - function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 requiredPreFund) internal override @@ -134,10 +132,12 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster { // revoke the approval at the end of userOp for (uint256 i = 0; i < sponsorTokenLength;) { address token = sponsorTokens[i].token; + require(tokensStore.supported[token], "Unsupported token"); + address spender = sponsorTokens[i].spender; uint256 amount = sponsorTokens[i].amount; - if (token == NATIVE_TOKEN) { + if (token == LibTokens.NATIVE_TOKEN) { (bool success,) = payable(spender).call{value: amount}(""); require(success, "Native token transfer failed"); } else { @@ -161,7 +161,6 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster { _packValidationData(true, validUntil, validAfter) ); } - return ( abi.encodePacked(invoiceId, sender, userOp.nonce, sponsorTokenData[0:1 + sponsorTokenLength * 72]), _packValidationData(false, validUntil, validAfter) @@ -179,8 +178,8 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster { for (uint8 i = 0; i < sponsorTokenLength;) { address token = sponsorTokens[i].token; address spender = sponsorTokens[i].spender; - if (token != NATIVE_TOKEN) { - require(IERC20(token).approve(spender, 0), "Reset approval failed"); + if (token != LibTokens.NATIVE_TOKEN) { + require(IERC20(token).approve(spender, 0), "CABPaymaster: Reset approval failed"); } unchecked { i++; @@ -189,7 +188,6 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster { // TODO: Batch Proving Optimistation -> write in settlement contract on `opSucceeded` if (mode == PostOpMode.opSucceeded) { //emit IInvoiceManager.InvoiceCreated(bytes32(context[:32]), address(bytes20(context[32:52])), address(this)); - // This add ~= 100k gas compared to only emitting the InvoiceCreated event // Question: is storing the invoices onchain truly necessary? bytes32 invoiceId = bytes32(context[:32]); @@ -281,5 +279,19 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster { return abi.encodePacked(uint8(repayTokens.length), encodedRepayToken); } + function addSupportedToken(address token) public onlyOwner { + tokensStore.addSupportedToken(token); + emit LibTokens.SupportedTokenAdded(token); + } + + function removeSupportedToken(address token) public onlyOwner { + tokensStore.removeSupportedToken(token); + emit LibTokens.SupportedTokenRemoved(token); + } + + function getSupportedTokens() public view returns (address[] memory) { + return tokensStore.getSupportedTokens(); + } + receive() external payable {} } diff --git a/src/paymasters/CABPaymasterFactory.sol b/src/paymasters/CABPaymasterFactory.sol new file mode 100644 index 0000000..0a1b04d --- /dev/null +++ b/src/paymasters/CABPaymasterFactory.sol @@ -0,0 +1,95 @@ +import "account-abstraction/core/BasePaymaster.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {CABPaymaster} from "./CABPaymaster.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IInvoiceManager} from "../interfaces/IInvoiceManager.sol"; +import {ICrossL2Prover} from "@vibc-core-smart-contracts/contracts/interfaces/ICrossL2Prover.sol"; + +contract CABPaymasterFactory is Ownable { + address public invoiceManager; + address public crossL2Prover; + address public verifyingSigner; + + event CABPaymasterCreated(address indexed owner, address indexed cabPaymaster); + + event InvoiceManagerUpdated(address indexed newInvoiceManager); + event CrossL2ProverUpdated(address indexed newCrossL2Prover); + event VerifyingSignerUpdated(address indexed newVerifyingSigner); + + constructor(address _owner, address _crossL2Prover, address _invoiceManager, address _verifyingSigner) + Ownable(_owner) + { + crossL2Prover = _crossL2Prover; + invoiceManager = _invoiceManager; + verifyingSigner = _verifyingSigner; + } + + /* + * @notice Create a CABPaymaster with the given _owner and _salt. + * @param _owner The owner of the CABPaymaster. + * @param _nonce The nonce for the CABPaymaster. + * @return cabPaymaster The address of the CABPaymaster. + */ + function createCABPaymaster(address _owner, bytes32 _nonce, address[] memory _supportedTokens) + external + returns (address cabPaymaster) + { + bytes32 salt = keccak256(abi.encode(_owner, _nonce)); + require(_owner != owner(), "CABPaymasterFactory: Wrong owner"); + cabPaymaster = getAddressWithNonce(_owner, _nonce); + if (cabPaymaster.code.length > 0) return cabPaymaster; + cabPaymaster = address( + new CABPaymaster{salt: salt}( + ICrossL2Prover(crossL2Prover), IInvoiceManager(invoiceManager), verifyingSigner, _owner + ) + ); + + CABPaymaster(payable(cabPaymaster)).initialize(_supportedTokens); + emit CABPaymasterCreated(_owner, cabPaymaster); + } + + /* + * @notice Return the address of a CABPaymaster that would be deployed with the given _salt. + */ + function getAddressWithNonce(address _owner, bytes32 _nonce) public view returns (address) { + bytes32 salt = keccak256(abi.encode(_owner, _nonce)); + return Create2.computeAddress( + salt, + keccak256( + abi.encodePacked( + type(CABPaymaster).creationCode, abi.encode(crossL2Prover, invoiceManager, verifyingSigner, _owner) + ) + ) + ); + } + + /* + * @notice Update the invoice manager. + * @param _invoiceManager The new invoice manager. + */ + function updateInvoiceManager(address _invoiceManager) public onlyOwner { + require(_invoiceManager != address(0), "Invoice manager cannot be the zero address"); + invoiceManager = _invoiceManager; + emit InvoiceManagerUpdated(_invoiceManager); + } + + /* + * @notice Update the cross-chain prover. + * @param _crossL2Prover The new cross-chain prover. + */ + function updateCrossL2Prover(address _crossL2Prover) public onlyOwner { + require(_crossL2Prover != address(0), "Cross-chain prover cannot be the zero address"); + crossL2Prover = _crossL2Prover; + emit CrossL2ProverUpdated(_crossL2Prover); + } + + /* + * @notice Update the verifying signer. + * @param _verifyingSigner The new verifying signer. + */ + function updateVerifyingSigner(address _verifyingSigner) public onlyOwner { + require(_verifyingSigner != address(0), "Verifying signer cannot be the zero address"); + verifyingSigner = _verifyingSigner; + emit VerifyingSignerUpdated(_verifyingSigner); + } +} diff --git a/src/paymasters/LibTokens.sol b/src/paymasters/LibTokens.sol new file mode 100644 index 0000000..716f559 --- /dev/null +++ b/src/paymasters/LibTokens.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +library LibTokens { + address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + event SupportedTokenAdded(address token); + event SupportedTokenRemoved(address token); + + struct TokensStore { + address[] tokens; + mapping(address => bool) supported; + } + + function addSupportedToken(TokensStore storage store, address token) public { + require(!store.supported[token], "TokenManager: token already supported"); + store.supported[token] = true; + store.tokens.push(token); + } + + function removeSupportedToken(TokensStore storage store, address token) public { + if (token == NATIVE_TOKEN) { + require(address(this).balance == 0, "TokenManager: native token has balance"); + } else { + uint256 balance = IERC20(token).balanceOf(address(this)); + require(balance == 0, "TokenManager: token has balance"); + } + + uint256 length = store.tokens.length; + for (uint256 i = 0; i < length;) { + if (store.tokens[i] == token) { + store.supported[token] = false; + store.tokens[i] = store.tokens[length - 1]; + store.tokens.pop(); + break; + } + unchecked { + i++; + } + } + } + + function getSupportedTokens(TokensStore storage store) public view returns (address[] memory) { + return store.tokens; + } +} diff --git a/test/CABPaymater.t.sol b/test/CABPaymater.t.sol index 36f73ec..7939d84 100644 --- a/test/CABPaymater.t.sol +++ b/test/CABPaymater.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; import {Test, console} from "forge-std/Test.sol"; -import {EntryPoint} from "account-abstraction/core/EntryPoint.sol"; import {PackedUserOperation} from "account-abstraction/core/UserOperationLib.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {CABPaymaster} from "../src/paymasters/CABPaymaster.sol"; @@ -17,7 +16,6 @@ import {UpgradeableOpenfortProxy} from "../src/proxy/UpgradeableOpenfortProxy.so import {MockERC20} from "../src/mocks/MockERC20.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {IPaymasterVerifier} from "../src/interfaces/IPaymasterVerifier.sol"; -import {UserOpSettlement} from "../src/settlement/UserOpSettlement.sol"; import {IPaymaster} from "account-abstraction/interfaces/IPaymaster.sol"; import {ICrossL2Prover} from "@vibc-core-smart-contracts/contracts/interfaces/ICrossL2Prover.sol"; import {MockCrossL2Prover} from "../src/mocks/MockCrossL2Prover.sol"; @@ -34,17 +32,15 @@ contract CABPaymasterTest is Test { BaseVault public openfortVault; MockERC20 public mockERC20; - EntryPoint public entryPoint; - - UserOpSettlement public settlement; - address public verifyingSignerAddress; uint256 public verifyingSignerPrivateKey; address public owner; address public rekt; + address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address public constant ENTRY_POINT_V7 = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + function setUp() public { - entryPoint = new EntryPoint(); owner = address(1); rekt = address(0x9590Ed0C18190a310f4e93CAccc4CC17270bED40); @@ -77,10 +73,16 @@ contract CABPaymasterTest is Test { ) ) ); + invoiceManager.initialize(owner, IVaultManager(address(vaultManager))); - settlement = UserOpSettlement(payable(new UpgradeableOpenfortProxy(address(new UserOpSettlement()), ""))); - paymaster = new CABPaymaster(entryPoint, invoiceManager, crossL2Prover, verifyingSignerAddress, owner); - settlement.initialize(owner, address(paymaster)); + + // Initialize the supportedTokens array + address[] memory supportedTokens = new address[](2); + supportedTokens[0] = address(mockERC20); + supportedTokens[1] = NATIVE_TOKEN; + + paymaster = new CABPaymaster(crossL2Prover, invoiceManager, verifyingSignerAddress, owner); + paymaster.initialize(supportedTokens); mockERC20.mint(address(paymaster), PAYMSTER_BASE_MOCK_ERC20_BALANCE); @@ -93,10 +95,10 @@ contract CABPaymasterTest is Test { ); } - function getEncodedSponsorTokens(uint8 len) internal returns (bytes memory encodedSponsorToken) { + function getEncodedSponsorTokens(uint8 len, address token) internal returns (bytes memory encodedSponsorToken) { IPaymasterVerifier.SponsorToken[] memory sponsorTokens = new IPaymasterVerifier.SponsorToken[](len); for (uint8 i = 0; i < len; i++) { - sponsorTokens[i] = IPaymasterVerifier.SponsorToken({token: address(mockERC20), spender: rekt, amount: 500}); + sponsorTokens[i] = IPaymasterVerifier.SponsorToken({token: token, spender: rekt, amount: 500}); encodedSponsorToken = bytes.concat( encodedSponsorToken, bytes20(sponsorTokens[i].token), @@ -152,9 +154,9 @@ contract CABPaymasterTest is Test { ); } - function testValidateUserOp() public { + function testValidateUserOpWithERC20SponsorToken() public { vm.chainId(BASE_SEPOLIA_CHAIN_ID); - bytes memory sponsorTokensBytes = getEncodedSponsorTokens(1); + bytes memory sponsorTokensBytes = getEncodedSponsorTokens(1, address(mockERC20)); bytes memory repayTokensBytes = getEncodedRepayTokens(1); uint48 validUntil = 1732810044 + 1000; @@ -209,7 +211,7 @@ contract CABPaymasterTest is Test { userOp.paymasterAndData = bytes.concat(userOp.paymasterAndData, signature); - vm.startPrank(address(entryPoint)); + vm.startPrank(ENTRY_POINT_V7); (bytes memory context, uint256 validationData) = paymaster.validatePaymasterUserOp(userOp, userOpHash, type(uint256).max); @@ -233,6 +235,84 @@ contract CABPaymasterTest is Test { assertEq(allowanceAfterExecution, 0); } + function testValidateUserOpWithNativeSponsorToken() public { + vm.chainId(BASE_SEPOLIA_CHAIN_ID); + bytes memory sponsorTokensBytes = getEncodedSponsorTokens(1, NATIVE_TOKEN); + bytes memory repayTokensBytes = getEncodedRepayTokens(1); + + uint48 validUntil = 1732810044 + 1000; + uint48 validAfter = 1732810044; + uint128 preVerificationGas = 1e5; + uint128 postVerificationGas = 1e5; + + bytes memory paymasterAndData = bytes.concat( + bytes20(address(paymaster)), + bytes16(preVerificationGas), + bytes16(postVerificationGas), + bytes6(validUntil), + bytes6(validAfter), + repayTokensBytes, + sponsorTokensBytes + ); + + PackedUserOperation memory userOp = PackedUserOperation({ + sender: rekt, + nonce: 31994562304018791559173496635392, + initCode: "", + callData: "", + accountGasLimits: bytes32(uint256(1e18)), + preVerificationGas: preVerificationGas, + gasFees: bytes32(uint256(1e4)), + paymasterAndData: paymasterAndData, + signature: "" + }); + + bytes32 userOpHash = keccak256( + abi.encode( + userOp.sender, + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.accountGasLimits, + keccak256(abi.encode(repayTokensBytes, sponsorTokensBytes)), + bytes32(abi.encodePacked(preVerificationGas, postVerificationGas)), + userOp.preVerificationGas, + userOp.gasFees, + block.chainid, + address(paymaster), + validUntil, + validAfter + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(verifyingSignerPrivateKey, MessageHashUtils.toEthSignedMessageHash(userOpHash)); + // Append signature to paymasterAndData + bytes memory signature = abi.encodePacked(r, s, v); + + userOp.paymasterAndData = bytes.concat(userOp.paymasterAndData, signature); + + vm.startPrank(ENTRY_POINT_V7); + vm.deal(address(paymaster), 1 ether); + (bytes memory context, uint256 validationData) = + paymaster.validatePaymasterUserOp(userOp, userOpHash, type(uint256).max); + + assertEq(address(userOp.sender).balance, 500); + + // validate postOp + // This is the event that we must track on dest chain and prove on source chain with Polymer proof system + + // Calculate the expected invoiceId + bytes32 expectedInvoiceId = + invoiceManager.getInvoiceId(rekt, address(paymaster), userOp.nonce, BASE_SEPOLIA_CHAIN_ID, repayTokensBytes); + + // don't know why comparison of paymaster address fails + // even though it's the same address + vm.expectEmit(true, true, true, false); + emit IInvoiceManager.InvoiceCreated(expectedInvoiceId, rekt, address(paymaster)); + paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 1222, 42); + } + function testGetInvoiceId() public { address account = 0x5E3Ae8798eAdE56c3B4fe8F085DAd16D4912Ba83; address paymaster = 0xF6e64504ed56ec2725CDd0b3C1b23626D66008A2;