diff --git a/contracts/AccountFactory.sol b/contracts/AccountFactory.sol index 52f9895..d75be0b 100644 --- a/contracts/AccountFactory.sol +++ b/contracts/AccountFactory.sol @@ -7,6 +7,7 @@ import {Ownable, Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step import {EfficientCall} from '@matterlabs/zksync-contracts/contracts/system-contracts/libraries/EfficientCall.sol'; import {Errors} from './libraries/Errors.sol'; import {IAGWRegistry} from './interfaces/IAGWRegistry.sol'; +import {IAccountFactory} from './interfaces/IAccountFactory.sol'; /** * @title Factory contract to create AGW accounts @@ -14,7 +15,7 @@ import {IAGWRegistry} from './interfaces/IAGWRegistry.sol'; * @author https://abs.xyz * @author https://getclave.io */ -contract AccountFactory is Ownable2Step { +contract AccountFactory is Ownable2Step, IAccountFactory { /** * @notice Address of the account implementation diff --git a/contracts/interfaces/IAccountFactory.sol b/contracts/interfaces/IAccountFactory.sol new file mode 100644 index 0000000..a66303c --- /dev/null +++ b/contracts/interfaces/IAccountFactory.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IAccountFactory { + function deployAccount(bytes32 salt, bytes calldata initializer) + external + payable + returns (address accountAddress); + + function authorizedDeployers(address) external view returns (bool); +} diff --git a/contracts/paymasters/ChainOpsPaymaster.sol b/contracts/paymasters/ChainOpsPaymaster.sol new file mode 100644 index 0000000..a51c611 --- /dev/null +++ b/contracts/paymasters/ChainOpsPaymaster.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Transaction} from "@matterlabs/zksync-contracts/contracts/system-contracts/libraries/TransactionHelper.sol"; +import { + IPaymaster, + ExecutionResult, + PAYMASTER_VALIDATION_SUCCESS_MAGIC +} from "@matterlabs/zksync-contracts/contracts/system-contracts/interfaces/IPaymaster.sol"; +import {IAccountFactory} from "../interfaces/IAccountFactory.sol"; +import {BOOTLOADER_FORMAL_ADDRESS} from "@matterlabs/zksync-contracts/contracts/system-contracts/Constants.sol"; +import {OwnableRoles} from "solady/src/auth/OwnableRoles.sol"; +import {SafeTransferLib} from "solady/src/utils/ext/zksync/SafeTransferLib.sol"; + +contract ChainOpsPaymaster is OwnableRoles, IPaymaster { + using SafeTransferLib for address; + using SafeTransferLib for address payable; + + error OnlyBootloader(); + error WithdrawalFailed(); + error SponsorshipRefused(); + + uint256 public constant MANAGER_ROLE = _ROLE_0; + + IAccountFactory public immutable AA_FACTORY; + address private immutable _deployer; + + mapping(address from => bool sponsored) public sponsoredAccounts; + mapping(address from => mapping(address to => mapping(bytes4 selector => bool sponsored))) public sponsoredCalls; + + constructor(address owner, address aaFactory) { + AA_FACTORY = IAccountFactory(aaFactory); + _initializeOwner(owner); + _grantRoles(owner, MANAGER_ROLE); + } + + function validateAndPayForPaymasterTransaction(bytes32, bytes32, Transaction calldata _transaction) + external + payable + returns (bytes4 magic, bytes memory context) + { + if (msg.sender != BOOTLOADER_FORMAL_ADDRESS) { + revert OnlyBootloader(); + } + + bool shouldSponsor = false; + + address from = address(uint160(_transaction.from)); + address to = address(uint160(_transaction.to)); + + bytes4 selector; + if (_transaction.data.length > 4) { + selector = bytes4(_transaction.data[0:4]); + } + + if (to == address(AA_FACTORY)) { + if (selector == IAccountFactory.deployAccount.selector) { + if (AA_FACTORY.authorizedDeployers(from)) { + shouldSponsor = true; + } + } + } + + if (!shouldSponsor) { + if (sponsoredAccounts[from]) { + shouldSponsor = true; + } else if (sponsoredCalls[from][to][selector]) { + shouldSponsor = true; + } + } + + if (!shouldSponsor) { + revert SponsorshipRefused(); + } + + context = ""; + magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC; + + uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas; + + BOOTLOADER_FORMAL_ADDRESS.safeTransferETH(requiredETH); + } + + function postTransaction( + bytes calldata _context, + Transaction calldata _transaction, + bytes32 _txHash, + bytes32 _suggestedSignedHash, + ExecutionResult _txResult, + uint256 _maxRefundedGas + ) external payable {} + + function withdraw(address payable to, uint256 amount) external onlyOwner { + uint256 balance = address(this).balance; + if (amount > balance) { + amount = balance; + } + (bool success,) = to.call{value: amount}(""); + if (!success) { + revert WithdrawalFailed(); + } + } + + function rescueERC20(address token, address to, uint256 amount) external onlyOwner { + token.safeTransfer(to, amount); + } + + function rescueERC721(address token, address to, uint256 tokenId) external onlyOwner { + token.safeTransferFrom(address(this), to, tokenId); + } + + function setSponsoredAccount(address from, bool sponsored) external onlyRolesOrOwner(MANAGER_ROLE) { + sponsoredAccounts[from] = sponsored; + } + + function setSponsoredCall(address from, address to, bytes4 selector, bool sponsored) + external + onlyRolesOrOwner(MANAGER_ROLE) + { + sponsoredCalls[from][to][selector] = sponsored; + } + + receive() external payable {} +} diff --git a/deploy/deploy-chain-ops-paymaster.ts b/deploy/deploy-chain-ops-paymaster.ts new file mode 100644 index 0000000..3e9ea6b --- /dev/null +++ b/deploy/deploy-chain-ops-paymaster.ts @@ -0,0 +1,29 @@ +/** + * Copyright Clave - All Rights Reserved + * Unauthorized copying of this file, via any medium is strictly prohibited + * Proprietary and confidential + */ +import * as hre from 'hardhat'; +import { Wallet } from 'zksync-ethers'; +import { create2IfNotExists, getProvider, getWallet } from '../deploy/utils'; +let fundingWallet: Wallet; + +export default async function (): Promise { + fundingWallet = getWallet(hre); + + const provider = getProvider(hre); + + const network = await provider.getNetwork(); + + const implementation = await create2IfNotExists(hre, "ChainOpsPaymaster", [ + "0x6f6426a9b93a7567fCCcBfE5d0d6F26c1085999b", + "0x9B947df68D35281C972511B3E7BC875926f26C1A" + ]); + + await implementation.setSponsoredAccount("0x6f6426a9b93a7567fCCcBfE5d0d6F26c1085999b", true); + if (network.chainId === 2741n) { + await implementation.setSponsoredAccount("0x7f60048318AD245B29A2cE9E87733C22d1f8335E", true); + } else if (network.chainId === 11124n) { + await implementation.setSponsoredAccount("0x84ffdFA5737012752AA9bfc89C6865E22a40c446", true); + } +} diff --git a/foundry.toml b/foundry.toml index 18294e4..38b71f1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ src = "contracts" test = "test" script = "script" out = "out" -libs = ["node_modules"] +libs = ["node_modules", "lib"] remappings = [ "@chainlink/=node_modules/@chainlink/", "@eth-optimism/=node_modules/@chainlink/contracts/node_modules/@eth-optimism/", diff --git a/package-lock.json b/package-lock.json index 30db78b..75964a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@nomad-xyz/excessively-safe-call": "github:nomad-xyz/ExcessivelySafeCall", "@openzeppelin/contracts": "5.1.0", "@openzeppelin/contracts-upgradeable": "5.1.0", + "solady": "^0.1.21", "ts-morph": "^19.0.0" }, "devDependencies": { @@ -10551,6 +10552,12 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/solady": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/solady/-/solady-0.1.21.tgz", + "integrity": "sha512-Pq3UYSwWFBXc+NwsBfOcTwHGbKuaLBLKI78jikBSmvq5h4CArYRp/MYsBKNjP/Zwr8eOGUO3HFKP87F94zMgIw==", + "license": "MIT" + }, "node_modules/solc": { "version": "0.8.26", "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.26.tgz", diff --git a/package.json b/package.json index 992ae70..4d75481 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@nomad-xyz/excessively-safe-call": "github:nomad-xyz/ExcessivelySafeCall", "@openzeppelin/contracts": "5.1.0", "@openzeppelin/contracts-upgradeable": "5.1.0", + "solady": "^0.1.21", "ts-morph": "^19.0.0" }, "devDependencies": {