From b4c7374d2dc3204991994ee2ee2dde01fc7475a4 Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:32:07 +0400 Subject: [PATCH] fulll implementation --- contracts/base/BasePaymaster.sol | 4 +- contracts/common/Errors.sol | 25 ++ .../IBiconomySponsorshipPaymaster.sol | 3 + .../SponsorshipPaymasterWithPremium.sol | 263 +++++++++++++++++- 4 files changed, 289 insertions(+), 6 deletions(-) diff --git a/contracts/base/BasePaymaster.sol b/contracts/base/BasePaymaster.sol index 63eafc3..cbab46b 100644 --- a/contracts/base/BasePaymaster.sol +++ b/contracts/base/BasePaymaster.sol @@ -93,7 +93,7 @@ abstract contract BasePaymaster is IPaymaster, Ownable { /** * Add a deposit for this paymaster, used for paying for transaction fees. */ - function deposit() public payable { + function deposit() public virtual payable { entryPoint.depositTo{value: msg.value}(address(this)); } @@ -105,7 +105,7 @@ abstract contract BasePaymaster is IPaymaster, Ownable { function withdrawTo( address payable withdrawAddress, uint256 amount - ) public onlyOwner { + ) public virtual onlyOwner { entryPoint.withdrawTo(withdrawAddress, amount); } diff --git a/contracts/common/Errors.sol b/contracts/common/Errors.sol index 0454090..045a5f1 100644 --- a/contracts/common/Errors.sol +++ b/contracts/common/Errors.sol @@ -13,4 +13,29 @@ contract BiconomySponsorshipPaymasterErrors { */ error DepositCanNotBeZero(); + /** + * @notice Throws when the verifiying signer address provided is address(0) + */ + error VerifyingSignerCannotBeZero(); + + /** + * @notice Throws when the fee collector address provided is address(0) + */ + error FeeCollectorCannotBeZero(); + + /** + * @notice Throws when the fee collector address provided is a deployed contract + */ + error FeeCollectorCannotBeContract(); + + /** + * @notice Throws when the fee collector address provided is a deployed contract + */ + error VerifyingSignerCannotBeContract(); + + /** + * @notice Throws when trying to withdraw to address(0) + */ + error CanNotWithdrawToZeroAddress(); + } \ No newline at end of file diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index af0feda..f90955c 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.24; interface IBiconomySponsorshipPaymaster { + event PostopCostChanged(uint256 indexed _oldValue, uint256 indexed _newValue); event FixedPriceMarkupChanged(uint32 indexed _oldValue, uint32 indexed _newValue); event VerifyingSignerChanged(address indexed _oldSigner, address indexed _newSigner, address indexed _actor); @@ -13,4 +14,6 @@ interface IBiconomySponsorshipPaymaster { event GasWithdrawn(address indexed _paymasterId, address indexed _to, uint256 indexed _value); event GasBalanceDeducted(address indexed _paymasterId, uint256 indexed _charge, bytes32 indexed userOpHash); event PremiumCollected(address indexed _paymasterId, uint256 indexed _premium); + event Received(address indexed sender, uint256 value); + event TokensWithdrawn(address indexed _token, address indexed _to, uint256 indexed _amount, address actor); } \ No newline at end of file diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index 898e127..7f9b0f6 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -6,12 +6,12 @@ pragma solidity ^0.8.24; import "../base/BasePaymaster.sol"; import "account-abstraction/contracts/core/UserOperationLib.sol"; import "account-abstraction/contracts/core/Helpers.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { SignatureCheckerLib } from "solady/src/utils/SignatureCheckerLib.sol"; import { ECDSA as ECDSA_solady } from "solady/src/utils/ECDSA.sol"; import { BiconomySponsorshipPaymasterErrors } from "../common/Errors.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol"; import { IBiconomySponsorshipPaymaster } from "../interfaces/IBiconomySponsorshipPaymaster.sol"; // possiblity (conflicts with BasePaymaster which is also Ownbale) // either make BasePaymaster SoladyOwnable @@ -29,14 +29,15 @@ import { IBiconomySponsorshipPaymaster } from "../interfaces/IBiconomySponsorshi * and Manages it's own deposit on the EntryPoint. */ -// Todo: Add more methods in interface -// Todo: Add methods to withdraw stuck erc20 tokens and native tokens +// @Todo: Add more methods in interface abstract contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, BiconomySponsorshipPaymasterErrors, IBiconomySponsorshipPaymaster { using UserOperationLib for PackedUserOperation; + using SignatureCheckerLib for address; address public verifyingSigner; address public feeCollector; + uint48 public postopCost; uint32 private constant PRICE_DENOMINATOR = 1e6; // note: could rename to PAYMASTER_ID_OFFSET @@ -68,4 +69,258 @@ abstract contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard emit GasDeposited(paymasterId, msg.value); } + /** + * @dev Set a new verifying signer address. + * Can only be called by the owner of the contract. + * @param _newVerifyingSigner The new address to be set as the verifying signer. + * @notice If _newVerifyingSigner is set to zero address, it will revert with an error. + * After setting the new signer address, it will emit an event VerifyingSignerChanged. + */ + function setSigner( + address _newVerifyingSigner + ) external payable onlyOwner { + uint256 size; + assembly { size := extcodesize(_newVerifyingSigner) } + if(size > 0) revert VerifyingSignerCannotBeContract(); + if (_newVerifyingSigner == address(0)) + revert VerifyingSignerCannotBeZero(); + address oldSigner = verifyingSigner; + assembly { + sstore(verifyingSigner.slot, _newVerifyingSigner) + } + emit VerifyingSignerChanged(oldSigner, _newVerifyingSigner, msg.sender); + } + + /** + * @dev Set a new fee collector address. + * Can only be called by the owner of the contract. + * @param _newFeeCollector The new address to be set as the fee collector. + * @notice If _newFeeCollector is set to zero address, it will revert with an error. + * After setting the new fee collector address, it will emit an event FeeCollectorChanged. + */ + function setFeeCollector( + address _newFeeCollector + ) external payable onlyOwner { + if (_newFeeCollector == address(0)) revert FeeCollectorCannotBeZero(); + address oldFeeCollector = feeCollector; + assembly { + sstore(feeCollector.slot, _newFeeCollector) + } + emit FeeCollectorChanged(oldFeeCollector, _newFeeCollector, msg.sender); + } + + /** + * @dev Set a new unaccountedEPGasOverhead value. + * @param value The new value to be set as the unaccountedEPGasOverhead. + * @notice only to be called by the owner of the contract. + */ + function setPostopCost( + uint48 value + ) external payable onlyOwner { + require(value <= 200000, "Gas overhead too high"); + uint256 oldValue = postopCost; + postopCost = value; + emit PostopCostChanged(oldValue, value); + } + + /** + * @dev get the current deposit for paymasterId (Dapp Depositor address) + * @param paymasterId dapp identifier + */ + function getBalance( + address paymasterId + ) external view returns (uint256 balance) { + balance = paymasterIdBalances[paymasterId]; + } + + /** + @dev Override the default implementation. + */ + function deposit() public payable virtual override { + revert("Use depositFor() instead"); + } + + /** + * @dev Withdraws the specified amount of gas tokens from the paymaster's balance and transfers them to the specified address. + * @param withdrawAddress The address to which the gas tokens should be transferred. + * @param amount The amount of gas tokens to withdraw. + */ + function withdrawTo( + address payable withdrawAddress, + uint256 amount + ) public override nonReentrant { + if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress(); + uint256 currentBalance = paymasterIdBalances[msg.sender]; + require(amount <= currentBalance, "Sponsorship Paymaster: Insufficient funds to withdraw from gas tank"); + paymasterIdBalances[msg.sender] = currentBalance - amount; + entryPoint.withdrawTo(withdrawAddress, amount); + emit GasWithdrawn(msg.sender, withdrawAddress, amount); + } + + /** + * 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(PackedUserOperation calldata userOp, address paymasterId, uint48 validUntil, uint48 validAfter, uint32 priceMarkup) + public view returns (bytes32) { + //can't use userOp.hash(), since it contains also the paymasterAndData itself. + address sender = userOp.getSender(); + return + keccak256( + abi.encode( + sender, + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.accountGasLimits, + uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_DATA_OFFSET])), + userOp.preVerificationGas, + userOp.gasFees, + block.chainid, + address(this), + paymasterId, + validUntil, + validAfter, + priceMarkup + ) + ); + } + + /** + * 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[52:72] : paymasterId (dappDepositor) + * paymasterAndData[72:78] : validUntil + * paymasterAndData[78:84] : validAfter + * paymasterAndData[84:88] : priceMarkup + * paymasterAndData[88:] : signature + */ + function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 requiredPreFund) + internal view override returns (bytes memory context, uint256 validationData) { + ( + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 priceMarkup, + 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"); + + bool validSig = verifyingSigner.isValidSignatureNow( + ECDSA_solady.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup)), + userOp.signature + ); + + //don't revert on signature failure: return SIG_VALIDATION_FAILED + if (!validSig) { + return ("", _packValidationData(true, validUntil, validAfter)); + } + + require(priceMarkup <= 2e6 && priceMarkup > 0, "Sponsorship Paymaster: Invalid markup %"); + + uint256 maxFeePerGas = userOp.unpackMaxFeePerGas(); + + // Send 1e6 for No markup + // Send between 0 and 1e6 for discount + uint256 effectiveCost = ((requiredPreFund + (postopCost * maxFeePerGas)) * priceMarkup) / + PRICE_DENOMINATOR; + + require(effectiveCost <= paymasterIdBalances[paymasterId], "Sponsorship Paymaster: paymasterId does not have enough deposit"); + + context = abi.encode( + paymasterId, + priceMarkup, + userOpHash + ); + + //no need for other on-chain validation: entire UserOp should have been checked + // by the external service prior to signing it. + return (context, _packValidationData(false, validUntil, validAfter)); + } + + /// @notice Performs post-operation tasks, such as deducting the sponsored gas cost from the paymasterId's balance + /// @dev This function is called after a user operation has been executed or reverted. + /// @param context The context containing the token amount and user sender address. + /// @param actualGasCost The actual gas cost of the transaction. + /// @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas + // and maxPriorityFee (and basefee) + // It is not the same as tx.gasprice, which is what the bundler pays. + function _postOp(PostOpMode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas) internal override { + unchecked { + ( + address paymasterId, + uint32 dynamicMarkup, + bytes32 userOpHash + ) = abi.decode(context, (address, uint32, bytes32)); + + uint256 balToDeduct = actualGasCost + + postopCost * + actualUserOpFeePerGas; + + uint256 costIncludingPremium = (balToDeduct * dynamicMarkup) / + PRICE_DENOMINATOR; + + // deduct with premium + paymasterIdBalances[paymasterId] -= costIncludingPremium; + + uint256 actualPremium = costIncludingPremium - balToDeduct; + // "collect" premium + paymasterIdBalances[feeCollector] += actualPremium; + + emit GasBalanceDeducted(paymasterId, costIncludingPremium, userOpHash); + // Review if we should emit balToDeduct as well + emit PremiumCollected(paymasterId, actualPremium); + } + } + + function parsePaymasterAndData( + bytes calldata paymasterAndData + ) + public + pure + returns ( + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 priceMarkup, + bytes calldata signature + ) + { + paymasterId = address(bytes20(paymasterAndData[VALID_PND_OFFSET:VALID_PND_OFFSET+20])); + validUntil = uint48(bytes6(paymasterAndData[VALID_PND_OFFSET+20:VALID_PND_OFFSET+26])); + validAfter = uint48(bytes6(paymasterAndData[VALID_PND_OFFSET+26:VALID_PND_OFFSET+32])); + priceMarkup = uint32(bytes4(paymasterAndData[VALID_PND_OFFSET+32:VALID_PND_OFFSET+36])); + signature = paymasterAndData[VALID_PND_OFFSET+36:]; + } + + receive() external payable { + emit Received(msg.sender, msg.value); + } + + function withdrawEth(address payable recipient, uint256 amount) external onlyOwner { + (bool success,) = recipient.call{value: amount}(""); + require(success, "withdraw failed"); + } + + /** + * @dev pull tokens out of paymaster in case they were sent to the paymaster at any point. + * @param token the token deposit to withdraw + * @param target address to send to + * @param amount amount to withdraw + */ + function withdrawERC20(IERC20 token, address target, uint256 amount) public payable onlyOwner nonReentrant { + _withdrawERC20(token, target, amount); + } + + function _withdrawERC20(IERC20 token, address target, uint256 amount) private { + if (target == address(0)) revert CanNotWithdrawToZeroAddress(); + SafeTransferLib.safeTransfer(address(token), target, amount); + emit TokensWithdrawn(address(token), target, amount, msg.sender); + } } \ No newline at end of file