diff --git a/.gitmodules b/.gitmodules index e69de29..f008824 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/nexus"] + path = lib/nexus + url = https://github.com/bcnmy/nexus +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/.solhint.json b/.solhint.json index a41dc0f..06fbb26 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,7 +1,7 @@ { "extends": "solhint:recommended", "rules": { - "compiler-version": ["error", "^0.8.24"], + "compiler-version": ["error", "^0.8.26"], "func-visibility": ["warn", { "ignoreConstructors": true }], "reentrancy": "error", "state-visibility": "error", diff --git a/contracts/base/BasePaymaster.sol b/contracts/base/BasePaymaster.sol index 25ca1a6..8a6f1a0 100644 --- a/contracts/base/BasePaymaster.sol +++ b/contracts/base/BasePaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; /* solhint-disable reason-string */ @@ -13,6 +13,7 @@ import "account-abstraction/contracts/core/UserOperationLib.sol"; * provides helper methods for staking. * Validates that the postOp is called only by the entryPoint. */ + abstract contract BasePaymaster is IPaymaster, SoladyOwnable { IEntryPoint public immutable entryPoint; @@ -25,10 +26,44 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { entryPoint = _entryPoint; } - //sanity check: make sure this EntryPoint was compiled against the same - // IEntryPoint of this paymaster - function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual { - require(IERC165(address(_entryPoint)).supportsInterface(type(IEntryPoint).interfaceId), "IEntryPoint interface mismatch"); + /** + * Add stake for this paymaster. + * This method can also carry eth value to add to the current stake. + * @param unstakeDelaySec - The unstake delay for this paymaster. Can only be increased. + */ + function addStake(uint32 unstakeDelaySec) external payable onlyOwner { + entryPoint.addStake{ value: msg.value }(unstakeDelaySec); + } + + /** + * Unlock the stake, in order to withdraw it. + * The paymaster can't serve requests once unlocked, until it calls addStake again + */ + function unlockStake() external onlyOwner { + entryPoint.unlockStake(); + } + + /** + * Withdraw the entire paymaster's stake. + * stake must be unlocked first (and then wait for the unstakeDelay to be over) + * @param withdrawAddress - The address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external onlyOwner { + entryPoint.withdrawStake(withdrawAddress); + } + + /// @inheritdoc IPaymaster + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) + external + override + { + _requireFromEntryPoint(); + _postOp(mode, context, actualGasCost, actualUserOpFeePerGas); } /// @inheritdoc IPaymaster @@ -36,11 +71,47 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost - ) external override returns (bytes memory context, uint256 validationData) { + ) + external + override + returns (bytes memory context, uint256 validationData) + { _requireFromEntryPoint(); return _validatePaymasterUserOp(userOp, userOpHash, maxCost); } + /** + * Add a deposit for this paymaster, used for paying for transaction fees. + */ + function deposit() external payable virtual { + entryPoint.depositTo{ value: msg.value }(address(this)); + } + + /** + * Withdraw value from the deposit. + * @param withdrawAddress - Target to send to. + * @param amount - Amount to withdraw. + */ + function withdrawTo(address payable withdrawAddress, uint256 amount) external virtual onlyOwner { + entryPoint.withdrawTo(withdrawAddress, amount); + } + + /** + * Return current paymaster's deposit on the entryPoint. + */ + function getDeposit() public view returns (uint256) { + return entryPoint.balanceOf(address(this)); + } + + //sanity check: make sure this EntryPoint was compiled against the same + // IEntryPoint of this paymaster + function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual { + require( + IERC165(address(_entryPoint)).supportsInterface(type(IEntryPoint).interfaceId), + "IEntryPoint interface mismatch" + ); + } + /** * Validate a user operation. * @param userOp - The user operation. @@ -51,18 +122,10 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost - ) internal virtual returns (bytes memory context, uint256 validationData); - - /// @inheritdoc IPaymaster - function postOp( - PostOpMode mode, - bytes calldata context, - uint256 actualGasCost, - uint256 actualUserOpFeePerGas - ) external override { - _requireFromEntryPoint(); - _postOp(mode, context, actualGasCost, actualUserOpFeePerGas); - } + ) + internal + virtual + returns (bytes memory context, uint256 validationData); /** * Post-operation handler. @@ -84,68 +147,27 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas - ) internal virtual { + ) + internal + virtual + { (mode, context, actualGasCost, actualUserOpFeePerGas); // unused params // subclass must override this method if validatePaymasterUserOp returns a context revert("must override"); } - /** - * Add a deposit for this paymaster, used for paying for transaction fees. - */ - function deposit() public virtual payable { - entryPoint.depositTo{value: msg.value}(address(this)); - } - - /** - * Withdraw value from the deposit. - * @param withdrawAddress - Target to send to. - * @param amount - Amount to withdraw. - */ - function withdrawTo( - address payable withdrawAddress, - uint256 amount - ) public virtual onlyOwner { - entryPoint.withdrawTo(withdrawAddress, amount); - } - - /** - * Add stake for this paymaster. - * This method can also carry eth value to add to the current stake. - * @param unstakeDelaySec - The unstake delay for this paymaster. Can only be increased. - */ - function addStake(uint32 unstakeDelaySec) external payable onlyOwner { - entryPoint.addStake{value: msg.value}(unstakeDelaySec); - } - - /** - * Return current paymaster's deposit on the entryPoint. - */ - function getDeposit() public view returns (uint256) { - return entryPoint.balanceOf(address(this)); - } - - /** - * Unlock the stake, in order to withdraw it. - * The paymaster can't serve requests once unlocked, until it calls addStake again - */ - function unlockStake() external onlyOwner { - entryPoint.unlockStake(); - } - - /** - * Withdraw the entire paymaster's stake. - * stake must be unlocked first (and then wait for the unstakeDelay to be over) - * @param withdrawAddress - The address to send withdrawn value. - */ - function withdrawStake(address payable withdrawAddress) external onlyOwner { - entryPoint.withdrawStake(withdrawAddress); - } - /** * Validate the call is made from a valid entrypoint */ function _requireFromEntryPoint() internal virtual { require(msg.sender == address(entryPoint), "Sender not EntryPoint"); } -} \ No newline at end of file + + function isContract(address _addr) internal view returns (bool) { + uint256 size; + assembly ("memory-safe") { + size := extcodesize(_addr) + } + return size > 0; + } +} diff --git a/contracts/common/Errors.sol b/contracts/common/Errors.sol index 045a5f1..fdaddb8 100644 --- a/contracts/common/Errors.sol +++ b/contracts/common/Errors.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; contract BiconomySponsorshipPaymasterErrors { - /** * @notice Throws when the paymaster address provided is address(0) */ - error PaymasterIdCannotBeZero(); + error PaymasterIdCanNotBeZero(); /** * @notice Throws when the 0 has been provided as deposit @@ -16,26 +15,60 @@ contract BiconomySponsorshipPaymasterErrors { /** * @notice Throws when the verifiying signer address provided is address(0) */ - error VerifyingSignerCannotBeZero(); + error VerifyingSignerCanNotBeZero(); /** * @notice Throws when the fee collector address provided is address(0) */ - error FeeCollectorCannotBeZero(); + error FeeCollectorCanNotBeZero(); /** * @notice Throws when the fee collector address provided is a deployed contract */ - error FeeCollectorCannotBeContract(); + error FeeCollectorCanNotBeContract(); /** * @notice Throws when the fee collector address provided is a deployed contract */ - error VerifyingSignerCannotBeContract(); + error VerifyingSignerCanNotBeContract(); + + /** + * @notice Throws when ETH withdrawal fails + */ + error WithdrawalFailed(); + + /** + * @notice Throws when insufficient funds to withdraw + */ + error InsufficientFundsInGasTank(); + + /** + * @notice Throws when invalid signature length in paymasterAndData + */ + error InvalidSignatureLength(); + + /** + * @notice Throws when invalid signature length in paymasterAndData + */ + error InvalidDynamicAdjustment(); + + /** + * @notice Throws when insufficient funds for paymasterid + */ + error InsufficientFundsForPaymasterId(); + + /** + * @notice Throws when calling deposit() + */ + error UseDepositForInstead(); /** * @notice Throws when trying to withdraw to address(0) */ error CanNotWithdrawToZeroAddress(); -} \ No newline at end of file + /** + * @notice Throws when trying unaccountedGas is too high + */ + error UnaccountedGasTooHigh(); +} diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index f90955c..00a4c39 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -1,19 +1,17 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; interface IBiconomySponsorshipPaymaster { - event PostopCostChanged(uint256 indexed _oldValue, uint256 indexed _newValue); - event FixedPriceMarkupChanged(uint32 indexed _oldValue, uint32 indexed _newValue); + event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); + event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue); - event VerifyingSignerChanged(address indexed _oldSigner, address indexed _newSigner, address indexed _actor); + event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); - event FeeCollectorChanged( - address indexed _oldFeeCollector, address indexed _newFeeCollector, address indexed _actor - ); - event GasDeposited(address indexed _paymasterId, uint256 indexed _value); - 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 FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); + event GasDeposited(address indexed paymasterId, uint256 indexed value); + event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); + event GasBalanceDeducted(address indexed paymasterId, uint256 indexed charge, bytes32 indexed userOpHash); + event DynamicAdjustmentCollected(address indexed paymasterId, uint256 indexed dynamicAdjustment); 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 + event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); +} diff --git a/contracts/mocks/Imports.sol b/contracts/mocks/Imports.sol index 3eb785e..131ae80 100644 --- a/contracts/mocks/Imports.sol +++ b/contracts/mocks/Imports.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; /* solhint-disable reason-string */ @@ -8,4 +8,3 @@ import "account-abstraction/contracts/core/EntryPointSimulations.sol"; import "@biconomy-devx/erc7579-msa/contracts/SmartAccount.sol"; import "@biconomy-devx/erc7579-msa/contracts/factory/AccountFactory.sol"; - diff --git a/contracts/mocks/MockValidator.sol b/contracts/mocks/MockValidator.sol index 5f7bdd9..7682958 100644 --- a/contracts/mocks/MockValidator.sol +++ b/contracts/mocks/MockValidator.sol @@ -1,3 +1,3 @@ -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; -import "@biconomy-devx/erc7579-msa/test/foundry/mocks/MockValidator.sol"; \ No newline at end of file +import "@biconomy-devx/erc7579-msa/test/foundry/mocks/MockValidator.sol"; diff --git a/contracts/references/SampleVerifyingPaymaster.sol b/contracts/references/SampleVerifyingPaymaster.sol index 3fdce99..1522c6e 100644 --- a/contracts/references/SampleVerifyingPaymaster.sol +++ b/contracts/references/SampleVerifyingPaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; /* solhint-disable reason-string */ /* solhint-disable no-inline-assembly */ @@ -20,7 +20,6 @@ import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; * - the account checks a signature to prove identity and account ownership. */ contract VerifyingPaymaster is BasePaymaster { - using UserOperationLib for PackedUserOperation; address public immutable verifyingSigner; @@ -40,19 +39,25 @@ contract VerifyingPaymaster is BasePaymaster { * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", * which will carry the signature itself. */ - function getHash(PackedUserOperation calldata userOp, uint48 validUntil, uint48 validAfter) - public view returns (bytes32) { + function getHash( + PackedUserOperation calldata userOp, + uint48 validUntil, + uint48 validAfter + ) + public + view + returns (bytes32) + { //can't use userOp.hash(), since it contains also the paymasterAndData itself. address sender = userOp.getSender(); - return - keccak256( + 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])), + uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_DATA_OFFSET])), userOp.preVerificationGas, userOp.gasFees, block.chainid, @@ -63,6 +68,15 @@ contract VerifyingPaymaster is BasePaymaster { ); } + function parsePaymasterAndData(bytes calldata paymasterAndData) + public + pure + returns (uint48 validUntil, uint48 validAfter, bytes calldata signature) + { + (validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET:], (uint48, uint48)); + signature = paymasterAndData[SIGNATURE_OFFSET:]; + } + /** * verify our external signer signed this request. * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params @@ -70,14 +84,27 @@ contract VerifyingPaymaster is BasePaymaster { * paymasterAndData[20:84] : abi.encode(validUntil, validAfter) * paymasterAndData[84:] : signature */ - function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) - internal view override returns (bytes memory context, uint256 validationData) { + function _validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32, /*userOpHash*/ + uint256 requiredPreFund + ) + internal + view + override + returns (bytes memory context, uint256 validationData) + { (requiredPreFund); - (uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData); + (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"); + // 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 = MessageHashUtils.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter)); //don't revert on signature failure: return SIG_VALIDATION_FAILED @@ -89,9 +116,4 @@ contract VerifyingPaymaster is BasePaymaster { // 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 :], (uint48, uint48)); - signature = paymasterAndData[SIGNATURE_OFFSET :]; - } -} \ No newline at end of file +} diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol similarity index 58% rename from contracts/sponsorship/SponsorshipPaymasterWithPremium.sol rename to contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol index 4cdc960..906e9d2 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; /* solhint-disable reason-string */ @@ -19,45 +19,72 @@ import { IBiconomySponsorshipPaymaster } from "../interfaces/IBiconomySponsorshi * @author livingrockrises * @notice Based on Infinitism 'VerifyingPaymaster' contract * @dev This contract is used to sponsor the transaction fees of the user operations - * Uses a verifying signer to provide the signature if predetermined conditions are met - * regarding the user operation calldata. Also this paymaster is Singleton in nature which + * Uses a verifying signer to provide the signature if predetermined conditions are met + * regarding the user operation calldata. Also this paymaster is Singleton in nature which * means multiple Dapps/Wallet clients willing to sponsor the transactions can share this paymaster. - * Maintains it's own accounting of the gas balance for each Dapp/Wallet client + * Maintains it's own accounting of the gas balance for each Dapp/Wallet client * and Manages it's own deposit on the EntryPoint. */ // @Todo: Add more methods in interface -contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, BiconomySponsorshipPaymasterErrors, IBiconomySponsorshipPaymaster { +contract BiconomySponsorshipPaymaster is + BasePaymaster, + ReentrancyGuard, + BiconomySponsorshipPaymasterErrors, + IBiconomySponsorshipPaymaster +{ using UserOperationLib for PackedUserOperation; using SignatureCheckerLib for address; address public verifyingSigner; address public feeCollector; - uint48 public postopCost; + uint48 public unaccountedGas; uint32 private constant PRICE_DENOMINATOR = 1e6; // note: could rename to PAYMASTER_ID_OFFSET uint256 private constant VALID_PND_OFFSET = PAYMASTER_DATA_OFFSET; + // Limit for unaccounted gas cost + uint16 private constant UNACCOUNTED_GAS_LIMIT = 10_000; + mapping(address => uint256) public paymasterIdBalances; - constructor(address _owner, IEntryPoint _entryPoint, address _verifyingSigner, address _feeCollector) BasePaymaster(_owner, _entryPoint) { - // TODO - // Check for zero address + constructor( + address _owner, + IEntryPoint _entryPoint, + address _verifyingSigner, + address _feeCollector, + uint48 _unaccountedGas + ) + BasePaymaster(_owner, _entryPoint) + { + if (_verifyingSigner == address(0)) { + revert VerifyingSignerCanNotBeZero(); + } else if (_feeCollector == address(0)) { + revert FeeCollectorCanNotBeZero(); + } else if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { + revert UnaccountedGasTooHigh(); + } verifyingSigner = _verifyingSigner; feeCollector = _feeCollector; + unaccountedGas = _unaccountedGas; + } + + receive() external payable { + emit Received(msg.sender, msg.value); } /** - * @dev Add a deposit for this paymaster and given paymasterId (Dapp Depositor address), used for paying for transaction fees + * @dev Add a deposit for this paymaster and given paymasterId (Dapp Depositor address), used for paying for + * transaction fees * @param paymasterId dapp identifier for which deposit is being made */ function depositFor(address paymasterId) external payable nonReentrant { - if (paymasterId == address(0)) revert PaymasterIdCannotBeZero(); + if (paymasterId == address(0)) revert PaymasterIdCanNotBeZero(); if (msg.value == 0) revert DepositCanNotBeZero(); paymasterIdBalances[paymasterId] += msg.value; - entryPoint.depositTo{value: msg.value}(address(this)); + entryPoint.depositTo{ value: msg.value }(address(this)); emit GasDeposited(paymasterId, msg.value); } @@ -68,16 +95,13 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * @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(); + function setSigner(address _newVerifyingSigner) external payable onlyOwner { + if (isContract(_newVerifyingSigner)) revert VerifyingSignerCanNotBeContract(); + if (_newVerifyingSigner == address(0)) { + revert VerifyingSignerCanNotBeZero(); + } address oldSigner = verifyingSigner; - assembly { + assembly ("memory-safe") { sstore(verifyingSigner.slot, _newVerifyingSigner) } emit VerifyingSignerChanged(oldSigner, _newVerifyingSigner, msg.sender); @@ -90,14 +114,10 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * @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(); + function setFeeCollector(address _newFeeCollector) external payable onlyOwner { + if (_newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero(); address oldFeeCollector = feeCollector; - assembly { - sstore(feeCollector.slot, _newFeeCollector) - } + feeCollector = _newFeeCollector; emit FeeCollectorChanged(oldFeeCollector, _newFeeCollector, msg.sender); } @@ -106,49 +126,64 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * @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); + function setUnaccountedGas(uint48 value) external payable onlyOwner { + if (value > UNACCOUNTED_GAS_LIMIT) { + revert UnaccountedGasTooHigh(); + } + uint256 oldValue = unaccountedGas; + unaccountedGas = value; + emit UnaccountedGasChanged(oldValue, value); } /** - * @dev get the current deposit for paymasterId (Dapp Depositor address) - * @param paymasterId dapp identifier + * @dev Override the default implementation. */ - function getBalance( - address paymasterId - ) external view returns (uint256 balance) { - balance = paymasterIdBalances[paymasterId]; + function deposit() external payable virtual override { + revert UseDepositForInstead(); } /** - @dev Override the default implementation. + * @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 deposit() public payable virtual override { - revert("Use depositFor() instead"); + function withdrawERC20(IERC20 token, address target, uint256 amount) external payable onlyOwner nonReentrant { + _withdrawERC20(token, target, amount); } /** - * @dev Withdraws the specified amount of gas tokens from the paymaster's balance and transfers them to the specified address. + * @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 { + function withdrawTo(address payable withdrawAddress, uint256 amount) external 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"); + if (amount > currentBalance) { + revert InsufficientFundsInGasTank(); + } paymasterIdBalances[msg.sender] = currentBalance - amount; entryPoint.withdrawTo(withdrawAddress, amount); emit GasWithdrawn(msg.sender, withdrawAddress, amount); } + function withdrawEth(address payable recipient, uint256 amount) external onlyOwner nonReentrant { + (bool success,) = recipient.call{ value: amount }(""); + if (!success) { + revert WithdrawalFailed(); + } + } + + /** + * @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]; + } + /** * 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. @@ -156,19 +191,27 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * 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) { + function getHash( + PackedUserOperation calldata userOp, + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 dynamicAdjustment + ) + public + view + returns (bytes32) + { //can't use userOp.hash(), since it contains also the paymasterAndData itself. address sender = userOp.getSender(); - return - keccak256( + 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])), + uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_DATA_OFFSET])), userOp.preVerificationGas, userOp.gasFees, block.chainid, @@ -176,11 +219,69 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom paymasterId, validUntil, validAfter, - priceMarkup + dynamicAdjustment ) ); } + function parsePaymasterAndData(bytes calldata paymasterAndData) + public + pure + returns ( + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 dynamicAdjustment, + bytes calldata signature + ) + { + unchecked { + 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])); + dynamicAdjustment = uint32(bytes4(paymasterAndData[VALID_PND_OFFSET + 32:VALID_PND_OFFSET + 36])); + signature = paymasterAndData[VALID_PND_OFFSET + 36:]; + } + } + + /// @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. + function _postOp( + PostOpMode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) + internal + override + { + unchecked { + (address paymasterId, uint32 dynamicAdjustment, bytes32 userOpHash) = + abi.decode(context, (address, uint32, bytes32)); + + // Include unaccountedGas since EP doesn't include this in actualGasCost + // unaccountedGas = postOpGas + EP overhead gas + estimated penalty + actualGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas); + // Apply the dynamic adjustment + uint256 adjustedGasCost = (actualGasCost * dynamicAdjustment) / PRICE_DENOMINATOR; + + // Deduct the adjusted cost + paymasterIdBalances[paymasterId] -= adjustedGasCost; + + if (adjustedGasCost > actualGasCost) { + // Apply dynamicAdjustment to fee collector balance + uint256 premium = adjustedGasCost - actualGasCost; + paymasterIdBalances[feeCollector] += premium; + // Review if we should emit adjustedGasCost as well + emit DynamicAdjustmentCollected(paymasterId, premium); + } + + emit GasBalanceDeducted(paymasterId, adjustedGasCost, userOpHash); + } + } + /** * verify our external signer signed this request. * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params @@ -188,24 +289,30 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * paymasterAndData[52:72] : paymasterId (dappDepositor) * paymasterAndData[72:78] : validUntil * paymasterAndData[78:84] : validAfter - * paymasterAndData[84:88] : priceMarkup + * paymasterAndData[84:88] : dynamicAdjustment * 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); + 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 dynamicAdjustment, 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"); + // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and + // not "ECDSA" + if (signature.length != 64 && signature.length != 65) { + revert InvalidSignatureLength(); + } bool validSig = verifyingSigner.isValidSignatureNow( - ECDSA_solady.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup)), + ECDSA_solady.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter, dynamicAdjustment)), signature ); @@ -214,105 +321,28 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom return ("", _packValidationData(true, validUntil, validAfter)); } - require(priceMarkup <= 2e6 && priceMarkup > 0, "Sponsorship Paymaster: Invalid markup %"); - - uint256 maxFeePerGas = userOp.unpackMaxFeePerGas(); + if (dynamicAdjustment > 2e6 || dynamicAdjustment == 0) { + revert InvalidDynamicAdjustment(); + } // Send 1e6 for No markup // Send between 0 and 1e6 for discount - uint256 effectiveCost = ((requiredPreFund + (postopCost * maxFeePerGas)) * priceMarkup) / - PRICE_DENOMINATOR; + uint256 effectiveCost = (requiredPreFund * dynamicAdjustment) / PRICE_DENOMINATOR; - require(effectiveCost <= paymasterIdBalances[paymasterId], "Sponsorship Paymaster: paymasterId does not have enough deposit"); + if (effectiveCost > paymasterIdBalances[paymasterId]) { + revert InsufficientFundsForPaymasterId(); + } - context = abi.encode( - paymasterId, - priceMarkup, - userOpHash - ); + context = abi.encode(paymasterId, dynamicAdjustment, 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 +} diff --git a/contracts/test/Foo.sol b/contracts/test/Foo.sol deleted file mode 100644 index f419123..0000000 --- a/contracts/test/Foo.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.24; - -/** - * @title Foo - * @dev A simple contract demonstrating a pure function in Solidity. - */ -contract Foo { - /** - * @notice Returns the input value unchanged. - * @dev A pure function that does not alter or interact with contract state. - * @param value The uint256 value to be returned. - * @return uint256 The same value that was input. - */ - function id(uint256 value) external pure returns (uint256) { - return value; - } -} diff --git a/contracts/test/Lock.sol b/contracts/test/Lock.sol deleted file mode 100644 index d11302f..0000000 --- a/contracts/test/Lock.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -/** - * @title Lock - * @dev Implements a time-locked wallet that only allows withdrawals after a certain date. - */ -contract Lock { - uint256 public unlockTime; - address payable public owner; - - /** - * @dev Emitted when funds are withdrawn from the contract. - * @param amount The amount of Ether withdrawn. - * @param when The timestamp of the withdrawal. - */ - event Withdrawal(uint256 amount, uint256 when); - - /** - * @notice Creates a locked wallet. - * @param unlockTime_ The timestamp after which withdrawals can be made. - */ - constructor(uint256 unlockTime_) payable { - require(block.timestamp < unlockTime_, "Wrong Unlock time"); - - unlockTime = unlockTime_; - owner = payable(msg.sender); - } - - /** - * @notice Allows funds to be received via direct transfers. - */ - receive() external payable { } - - /** - * @notice Withdraws all funds if the unlock time has passed and the caller is the owner. - */ - function withdraw() public { - require(block.timestamp > unlockTime, "You can't withdraw yet"); - require(msg.sender == owner, "You aren't the owner"); - - emit Withdrawal(address(this).balance, block.timestamp); - - owner.transfer(address(this).balance); - } -} diff --git a/contracts/utils/SoladyOwnable.sol b/contracts/utils/SoladyOwnable.sol index 9589b3d..8b680d3 100644 --- a/contracts/utils/SoladyOwnable.sol +++ b/contracts/utils/SoladyOwnable.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; -import {Ownable} from "solady/src/auth/Ownable.sol"; +import { Ownable } from "solady/src/auth/Ownable.sol"; contract SoladyOwnable is Ownable { constructor(address _owner) Ownable() { _initializeOwner(_owner); } -} \ No newline at end of file +} diff --git a/foundry.toml b/foundry.toml index e3480d4..80fe03b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,13 +5,12 @@ block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT bytecode_hash = "none" evm_version = "paris" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode - fuzz = { runs = 1_000 } gas_reports = ["*"] optimizer = true optimizer_runs = 1_000_000 out = "out" script = "scripts" - solc = "0.8.24" + solc = "0.8.26" src = "contracts" test = "test" cache_path = "cache_forge" @@ -19,6 +18,10 @@ gas_reports_ignore = ["LockTest"] via_ir = true +[fuzz] + runs = 1_000 + max_test_rejects = 1_000_000 + [profile.ci] fuzz = { runs = 10_000 } verbosity = 4 diff --git a/hardhat.config.ts b/hardhat.config.ts index 3e7fdf2..e139ab6 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -5,7 +5,7 @@ import "@bonadocs/docgen"; const config: HardhatUserConfig = { solidity: { - version: "0.8.24", + version: "0.8.26", settings: { optimizer: { enabled: true, diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..8948d45 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8948d45d3d9022c508b83eb5d26fd3a7a93f2f32 diff --git a/lib/nexus b/lib/nexus new file mode 160000 index 0000000..ab9616b --- /dev/null +++ b/lib/nexus @@ -0,0 +1 @@ +Subproject commit ab9616bd71fcd51048e834f87a7b60dccbfc0adb diff --git a/lib/nexus.git b/lib/nexus.git new file mode 160000 index 0000000..5d81e53 --- /dev/null +++ b/lib/nexus.git @@ -0,0 +1 @@ +Subproject commit 5d81e533941b49194fbc469b09b182c6c5d0e9d9 diff --git a/package.json b/package.json index 1227124..a59cac8 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "chai": "^4.3.7", "codecov": "^3.8.3", "ethers": "^6.11.1", - "forge-std": "github:foundry-rs/forge-std#v1.7.6", "hardhat-deploy": "^0.11.45", "hardhat-deploy-ethers": "^0.4.1", "hardhat-gas-reporter": "^1.0.10", diff --git a/remappings.txt b/remappings.txt index af5268a..6710ed5 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,7 +1,8 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/test/=node_modules/@prb/test/ -forge-std/=node_modules/forge-std/ +nexus/=lib/nexus/ +forge-std/=lib/forge-std/ account-abstraction=node_modules/account-abstraction/ modulekit/=node_modules/modulekit/src/ sentinellist/=node_modules/sentinellist/ -solady/=node_modules/solady \ No newline at end of file +solady/=node_modules/solady diff --git a/scripts/foundry/Deploy.s.sol b/scripts/foundry/Deploy.s.sol deleted file mode 100644 index 5a55826..0000000 --- a/scripts/foundry/Deploy.s.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.23 <0.9.0; - -import { Foo } from "../../contracts/test/Foo.sol"; - -import { BaseScript } from "./Base.s.sol"; - -/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting -contract Deploy is BaseScript { - function run() public broadcast returns (Foo foo) { - foo = new Foo(); - } - - function test() public pure returns (uint256) { - return 0; - } -} diff --git a/test/foundry/Lock.t.sol b/test/foundry/Lock.t.sol deleted file mode 100644 index 5782e2d..0000000 --- a/test/foundry/Lock.t.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.24 <0.9.0; - -import { PRBTest } from "@prb/test/src/PRBTest.sol"; -import { Lock } from "../../contracts/test/Lock.sol"; -import { StdCheats } from "forge-std/src/StdCheats.sol"; - -contract LockTest is PRBTest, StdCheats { - Lock public lock; - address payable owner; - - receive() external payable { } - - function setUp() public { - owner = payable(address(this)); - uint256 unlockTime = block.timestamp + 1 days; // Set unlock time to 1 day from now - lock = new Lock{ value: 1 ether }(unlockTime); - } - - function testInitialOwner() public { - assertEq(lock.owner(), owner); - } - - function testWithdrawal() public { - // Fast forward time to surpass the unlockTime - vm.warp(block.timestamp + 2 days); - - uint256 initialBalance = address(this).balance; - lock.withdraw(); - uint256 finalBalance = address(this).balance; - - // Check if the contract's balance was transferred to the owner - assertGt(finalBalance, initialBalance); - } - - function testWithdrawTooEarly() public { - // This test is expected to fail as the withdrawal is too early - vm.expectRevert(bytes("You can't withdraw yet")); - lock.withdraw(); - } - - function testWithdrawByNonOwner() public { - // Change the sender to someone other than the owner - vm.warp(block.timestamp + 2 days); - vm.prank(address(0x123)); - vm.expectRevert(bytes("You aren't the owner")); - lock.withdraw(); - } -} diff --git a/test/foundry/base/BaseEventsAndErrors.sol b/test/foundry/base/BaseEventsAndErrors.sol new file mode 100644 index 0000000..497366e --- /dev/null +++ b/test/foundry/base/BaseEventsAndErrors.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.26; + +import { EventsAndErrors } from "nexus/test/foundry/utils/EventsAndErrors.sol"; +import { BiconomySponsorshipPaymasterErrors } from "./../../../contracts/common/Errors.sol"; + +contract BaseEventsAndErrors is EventsAndErrors, BiconomySponsorshipPaymasterErrors { + // ========================== + // Events + // ========================== + event OwnershipTransferred(address indexed oldOwner, address indexed newOwner); + + // ========================== + // Errors + // ========================== + error NewOwnerIsZeroAddress(); +} diff --git a/test/foundry/base/TestBase.sol b/test/foundry/base/TestBase.sol new file mode 100644 index 0000000..69eaceb --- /dev/null +++ b/test/foundry/base/TestBase.sol @@ -0,0 +1,550 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Test } from "forge-std/src/Test.sol"; +import { Vm } from "forge-std/src/Vm.sol"; + +import "solady/src/utils/ECDSA.sol"; + +import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; +import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { IAccount } from "account-abstraction/contracts/interfaces/IAccount.sol"; +import { Exec } from "account-abstraction/contracts/utils/Exec.sol"; +import { IPaymaster } from "account-abstraction/contracts/interfaces/IPaymaster.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; + +import { Nexus } from "nexus/contracts/Nexus.sol"; +import { NexusAccountFactory } from "nexus/contracts/factory/NexusAccountFactory.sol"; +import { BiconomyMetaFactory } from "nexus/contracts/factory/BiconomyMetaFactory.sol"; +import { MockValidator } from "nexus/contracts/mocks/MockValidator.sol"; +import { BootstrapLib } from "nexus/contracts/lib/BootstrapLib.sol"; +import { Bootstrap, BootstrapConfig } from "nexus/contracts/utils/Bootstrap.sol"; +import { CheatCodes } from "nexus/test/foundry/utils/CheatCodes.sol"; +import { BaseEventsAndErrors } from "./BaseEventsAndErrors.sol"; + +import { BiconomySponsorshipPaymaster } from + "../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; + +abstract contract TestBase is CheatCodes, BaseEventsAndErrors { + // ----------------------------------------- + // State Variables + // ----------------------------------------- + + Vm.Wallet internal DEPLOYER; + Vm.Wallet internal ALICE; + Vm.Wallet internal BOB; + Vm.Wallet internal CHARLIE; + Vm.Wallet internal DAN; + Vm.Wallet internal EMMA; + Vm.Wallet internal BUNDLER; + Vm.Wallet internal PAYMASTER_OWNER; + Vm.Wallet internal PAYMASTER_SIGNER; + Vm.Wallet internal PAYMASTER_FEE_COLLECTOR; + Vm.Wallet internal DAPP_ACCOUNT; + Vm.Wallet internal FACTORY_OWNER; + + address internal ALICE_ADDRESS; + address internal BOB_ADDRESS; + address internal CHARLIE_ADDRESS; + address internal DAN_ADDRESS; + address internal EMMA_ADDRESS; + + Nexus internal ALICE_ACCOUNT; + Nexus internal BOB_ACCOUNT; + Nexus internal CHARLIE_ACCOUNT; + Nexus internal DAN_ACCOUNT; + Nexus internal EMMA_ACCOUNT; + + address constant ENTRYPOINT_ADDRESS = address(0x0000000071727De22E5E9d8BAf0edAc6f37da032); + IEntryPoint internal ENTRYPOINT; + + NexusAccountFactory internal FACTORY; + BiconomyMetaFactory internal META_FACTORY; + MockValidator internal VALIDATOR_MODULE; + Nexus internal ACCOUNT_IMPLEMENTATION; + Bootstrap internal BOOTSTRAPPER; + + // Used to buffer user op gas limits + // GAS_LIMIT = (ESTIMATED_GAS * GAS_BUFFER_RATIO) / 100 + uint8 private constant GAS_BUFFER_RATIO = 110; + + // ----------------------------------------- + // Modifiers + // ----------------------------------------- + modifier prankModifier(address pranker) { + startPrank(pranker); + _; + stopPrank(); + } + + // ----------------------------------------- + // Setup Functions + // ----------------------------------------- + /// @notice Initializes the testing environment with wallets, contracts, and accounts + function setupTestEnvironment() internal virtual { + /// Initializes the testing environment + setupPredefinedWallets(); + deployTestContracts(); + deployNexusForPredefinedWallets(); + } + + function createAndFundWallet(string memory name, uint256 amount) internal returns (Vm.Wallet memory) { + Vm.Wallet memory wallet = newWallet(name); + vm.deal(wallet.addr, amount); + return wallet; + } + + function setupPredefinedWallets() internal { + DEPLOYER = createAndFundWallet("DEPLOYER", 1000 ether); + BUNDLER = createAndFundWallet("BUNDLER", 1000 ether); + + ALICE = createAndFundWallet("ALICE", 1000 ether); + BOB = createAndFundWallet("BOB", 1000 ether); + CHARLIE = createAndFundWallet("CHARLIE", 1000 ether); + DAN = createAndFundWallet("DAN", 1000 ether); + EMMA = createAndFundWallet("EMMA", 1000 ether); + + ALICE_ADDRESS = ALICE.addr; + BOB_ADDRESS = BOB.addr; + CHARLIE_ADDRESS = CHARLIE.addr; + DAN_ADDRESS = DAN.addr; + EMMA_ADDRESS = EMMA.addr; + + PAYMASTER_OWNER = createAndFundWallet("PAYMASTER_OWNER", 1000 ether); + PAYMASTER_SIGNER = createAndFundWallet("PAYMASTER_SIGNER", 1000 ether); + PAYMASTER_FEE_COLLECTOR = createAndFundWallet("PAYMASTER_FEE_COLLECTOR", 1000 ether); + DAPP_ACCOUNT = createAndFundWallet("DAPP_ACCOUNT", 1000 ether); + FACTORY_OWNER = createAndFundWallet("FACTORY_OWNER", 1000 ether); + } + + function deployTestContracts() internal { + ENTRYPOINT = new EntryPoint(); + vm.etch(ENTRYPOINT_ADDRESS, address(ENTRYPOINT).code); + ENTRYPOINT = IEntryPoint(ENTRYPOINT_ADDRESS); + ACCOUNT_IMPLEMENTATION = new Nexus(address(ENTRYPOINT)); + FACTORY = new NexusAccountFactory(address(ACCOUNT_IMPLEMENTATION), address(FACTORY_OWNER.addr)); + META_FACTORY = new BiconomyMetaFactory(address(FACTORY_OWNER.addr)); + vm.prank(FACTORY_OWNER.addr); + META_FACTORY.addFactoryToWhitelist(address(FACTORY)); + VALIDATOR_MODULE = new MockValidator(); + BOOTSTRAPPER = new Bootstrap(); + } + + // ----------------------------------------- + // Account Deployment Functions + // ----------------------------------------- + /// @notice Deploys an account with a specified wallet, deposit amount, and optional custom validator + /// @param wallet The wallet to deploy the account for + /// @param deposit The deposit amount + /// @param validator The custom validator address, if not provided uses default + /// @return The deployed Nexus account + function deployNexus(Vm.Wallet memory wallet, uint256 deposit, address validator) internal returns (Nexus) { + address payable accountAddress = calculateAccountAddress(wallet.addr, validator); + bytes memory initCode = buildInitCode(wallet.addr, validator); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = buildUserOpWithInitAndCalldata(wallet, initCode, "", validator); + + ENTRYPOINT.depositTo{ value: deposit }(address(accountAddress)); + ENTRYPOINT.handleOps(userOps, payable(wallet.addr)); + assertTrue(MockValidator(validator).isOwner(accountAddress, wallet.addr)); + return Nexus(accountAddress); + } + + /// @notice Deploys Nexus accounts for predefined wallets + function deployNexusForPredefinedWallets() internal { + BOB_ACCOUNT = deployNexus(BOB, 100 ether, address(VALIDATOR_MODULE)); + vm.label(address(BOB_ACCOUNT), "BOB_ACCOUNT"); + ALICE_ACCOUNT = deployNexus(ALICE, 100 ether, address(VALIDATOR_MODULE)); + vm.label(address(ALICE_ACCOUNT), "ALICE_ACCOUNT"); + CHARLIE_ACCOUNT = deployNexus(CHARLIE, 100 ether, address(VALIDATOR_MODULE)); + vm.label(address(CHARLIE_ACCOUNT), "CHARLIE_ACCOUNT"); + DAN_ACCOUNT = deployNexus(DAN, 100 ether, address(VALIDATOR_MODULE)); + vm.label(address(DAN_ACCOUNT), "DAN_ACCOUNT"); + EMMA_ACCOUNT = deployNexus(EMMA, 100 ether, address(VALIDATOR_MODULE)); + vm.label(address(EMMA_ACCOUNT), "EMMA_ACCOUNT"); + } + // ----------------------------------------- + // Utility Functions + // ----------------------------------------- + + /// @notice Calculates the address of a new account + /// @param owner The address of the owner + /// @param validator The address of the validator + /// @return account The calculated account address + function calculateAccountAddress( + address owner, + address validator + ) + internal + view + returns (address payable account) + { + bytes memory moduleInstallData = abi.encodePacked(owner); + + BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(validator, moduleInstallData); + BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); + bytes memory saDeploymentIndex = "0"; + + // Create initcode and salt to be sent to Factory + bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook); + bytes32 salt = keccak256(saDeploymentIndex); + + account = FACTORY.computeAccountAddress(_initData, salt); + return account; + } + + /// @notice Prepares the init code for account creation with a validator + /// @param ownerAddress The address of the owner + /// @param validator The address of the validator + /// @return initCode The prepared init code + function buildInitCode(address ownerAddress, address validator) internal view returns (bytes memory initCode) { + bytes memory moduleInitData = abi.encodePacked(ownerAddress); + + BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(validator, moduleInitData); + BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); + + bytes memory saDeploymentIndex = "0"; + + // Create initcode and salt to be sent to Factory + bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook); + + bytes32 salt = keccak256(saDeploymentIndex); + + bytes memory factoryData = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, salt); + + // Prepend the factory address to the encoded function call to form the initCode + initCode = abi.encodePacked( + address(META_FACTORY), + abi.encodeWithSelector(META_FACTORY.deployWithFactory.selector, address(FACTORY), factoryData) + ); + } + + /// @notice Prepares a user operation with init code and call data + /// @param wallet The wallet for which the user operation is prepared + /// @param initCode The init code + /// @param callData The call data + /// @param validator The validator address + /// @return userOp The prepared user operation + function buildUserOpWithInitAndCalldata( + Vm.Wallet memory wallet, + bytes memory initCode, + bytes memory callData, + address validator + ) + internal + view + returns (PackedUserOperation memory userOp) + { + userOp = buildUserOpWithCalldata(wallet, callData, validator); + userOp.initCode = initCode; + + bytes memory signature = signUserOp(wallet, userOp); + userOp.signature = signature; + } + + /// @notice Prepares a user operation with call data and a validator + /// @param wallet The wallet for which the user operation is prepared + /// @param callData The call data + /// @param validator The validator address + /// @return userOp The prepared user operation + function buildUserOpWithCalldata( + Vm.Wallet memory wallet, + bytes memory callData, + address validator + ) + internal + view + returns (PackedUserOperation memory userOp) + { + address payable account = calculateAccountAddress(wallet.addr, validator); + uint256 nonce = getNonce(account, validator); + userOp = buildPackedUserOp(account, nonce); + userOp.callData = callData; + + bytes memory signature = signUserOp(wallet, userOp); + userOp.signature = signature; + } + + /// @notice Retrieves the nonce for a given account and validator + /// @param account The account address + /// @param validator The validator address + /// @return nonce The retrieved nonce + function getNonce(address account, address validator) internal view returns (uint256 nonce) { + uint192 key = uint192(bytes24(bytes20(address(validator)))); + nonce = ENTRYPOINT.getNonce(address(account), key); + } + + /// @notice Signs a user operation + /// @param wallet The wallet to sign the operation + /// @param userOp The user operation to sign + /// @return The signed user operation + function signUserOp( + Vm.Wallet memory wallet, + PackedUserOperation memory userOp + ) + internal + view + returns (bytes memory) + { + bytes32 opHash = ENTRYPOINT.getUserOpHash(userOp); + return signMessage(wallet, opHash); + } + + // ----------------------------------------- + // Utility Functions + // ----------------------------------------- + + /// @notice Modifies the address of a deployed contract in a test environment + /// @param originalAddress The original address of the contract + /// @param newAddress The new address to replace the original + function changeContractAddress(address originalAddress, address newAddress) internal { + vm.etch(newAddress, originalAddress.code); + } + + /// @notice Builds a user operation struct for account abstraction tests + /// @param sender The sender address + /// @param nonce The nonce + /// @return userOp The built user operation + function buildPackedUserOp(address sender, uint256 nonce) internal pure returns (PackedUserOperation memory) { + return PackedUserOperation({ + sender: sender, + nonce: nonce, + initCode: "", + callData: "", + accountGasLimits: bytes32(abi.encodePacked(uint128(3e6), uint128(3e6))), // verification and call gas limit + preVerificationGas: 3e5, // Adjusted preVerificationGas + gasFees: bytes32(abi.encodePacked(uint128(3e6), uint128(3e6))), // maxFeePerGas and maxPriorityFeePerGas + paymasterAndData: "", + signature: "" + }); + } + + /// @notice Signs a message and packs r, s, v into bytes + /// @param wallet The wallet to sign the message + /// @param messageHash The hash of the message to sign + /// @return signature The packed signature + function signMessage(Vm.Wallet memory wallet, bytes32 messageHash) internal pure returns (bytes memory signature) { + bytes32 userOpHash = ECDSA.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, userOpHash); + signature = abi.encodePacked(r, s, v); + } + + /// @notice Pre-funds a smart account and asserts success + /// @param sa The smart account address + /// @param prefundAmount The amount to pre-fund + function prefundSmartAccountAndAssertSuccess(address sa, uint256 prefundAmount) internal { + (bool res,) = sa.call{ value: prefundAmount }(""); // Pre-funding the account contract + assertTrue(res, "Pre-funding account should succeed"); + } + + function estimateUserOpGasCosts(PackedUserOperation memory userOp) + internal + prankModifier(ENTRYPOINT_ADDRESS) + returns (uint256 verificationGasUsed, uint256 callGasUsed, uint256 verificationGasLimit, uint256 callGasLimit) + { + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + verificationGasUsed = gasleft(); + IAccount(userOp.sender).validateUserOp(userOp, userOpHash, 0); + verificationGasUsed = verificationGasUsed - gasleft(); + + callGasUsed = gasleft(); + bool success = Exec.call(userOp.sender, 0, userOp.callData, 3e6); + callGasUsed = callGasUsed - gasleft(); + assert(success); + + verificationGasLimit = (verificationGasUsed * GAS_BUFFER_RATIO) / 100; + callGasLimit = (callGasUsed * GAS_BUFFER_RATIO) / 100; + } + + function estimatePaymasterGasCosts( + BiconomySponsorshipPaymaster paymaster, + PackedUserOperation memory userOp, + uint256 requiredPreFund + ) + internal + prankModifier(ENTRYPOINT_ADDRESS) + returns (uint256 validationGasUsed, uint256 postopGasUsed, uint256 validationGasLimit, uint256 postopGasLimit) + { + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + // Warm up accounts to get more accurate gas estimations + (bytes memory context,) = paymaster.validatePaymasterUserOp(userOp, userOpHash, requiredPreFund); + paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 1e12, 3e6); + + // Estimate gas used + validationGasUsed = gasleft(); + (context,) = paymaster.validatePaymasterUserOp(userOp, userOpHash, requiredPreFund); + validationGasUsed = validationGasUsed - gasleft(); + + postopGasUsed = gasleft(); + paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 1e12, 3e6); + postopGasUsed = (postopGasUsed - gasleft()); + + validationGasLimit = (validationGasUsed * GAS_BUFFER_RATIO) / 100; + postopGasLimit = (postopGasUsed * GAS_BUFFER_RATIO) / 100; + } + + function createUserOp( + Vm.Wallet memory sender, + BiconomySponsorshipPaymaster paymaster, + uint32 dynamicAdjustment + ) + internal + returns (PackedUserOperation memory userOp, bytes32 userOpHash) + { + // Create userOp with no gas estimates + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + userOp = buildUserOpWithCalldata(sender, "", address(VALIDATOR_MODULE)); + + (userOp.paymasterAndData,) = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, paymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, dynamicAdjustment + ); + userOp.signature = signUserOp(sender, userOp); + + (,, uint256 verificationGasLimit, uint256 callGasLimit) = estimateUserOpGasCosts(userOp); + // Estimate paymaster gas limits + (, uint256 postopGasUsed, uint256 validationGasLimit, uint256 postopGasLimit) = + estimatePaymasterGasCosts(paymaster, userOp, 5e4); + + vm.startPrank(paymaster.owner()); + // Set unaccounted gas to be gas used in postop + 1000 for EP overhead and penalty + paymaster.setUnaccountedGas(uint48(postopGasUsed + 1000)); + vm.stopPrank(); + + // Ammend the userop to have new gas limits and signature + userOp.accountGasLimits = bytes32(abi.encodePacked(uint128(verificationGasLimit), uint128(callGasLimit))); + (userOp.paymasterAndData,) = generateAndSignPaymasterData( + userOp, + PAYMASTER_SIGNER, + paymaster, + uint128(validationGasLimit), + uint128(postopGasLimit), + DAPP_ACCOUNT.addr, + validUntil, + validAfter, + dynamicAdjustment + ); + userOp.signature = signUserOp(sender, userOp); + userOpHash = ENTRYPOINT.getUserOpHash(userOp); + } + + /// @notice Generates and signs the paymaster data for a user operation. + /// @dev This function prepares the `paymasterAndData` field for a `PackedUserOperation` with the correct signature. + /// @param userOp The user operation to be signed. + /// @param signer The wallet that will sign the paymaster hash. + /// @param paymaster The paymaster contract. + /// @return finalPmData Full Pm Data. + /// @return signature Pm Signature on Data. + function generateAndSignPaymasterData( + PackedUserOperation memory userOp, + Vm.Wallet memory signer, + BiconomySponsorshipPaymaster paymaster, + uint128 paymasterValGasLimit, + uint128 paymasterPostOpGasLimit, + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 dynamicAdjustment + ) + internal + view + returns (bytes memory finalPmData, bytes memory signature) + { + // Initial paymaster data with zero signature + bytes memory initialPmData = abi.encodePacked( + address(paymaster), + paymasterValGasLimit, + paymasterPostOpGasLimit, + paymasterId, + validUntil, + validAfter, + dynamicAdjustment, + new bytes(65) // Zero signature + ); + + // Update user operation with initial paymaster data + userOp.paymasterAndData = initialPmData; + + // Generate hash to be signed + bytes32 paymasterHash = paymaster.getHash(userOp, paymasterId, validUntil, validAfter, dynamicAdjustment); + + // Sign the hash + signature = signMessage(signer, paymasterHash); + require(signature.length == 65, "Invalid Paymaster Signature length"); + + // Final paymaster data with the actual signature + finalPmData = abi.encodePacked( + address(paymaster), + paymasterValGasLimit, + paymasterPostOpGasLimit, + paymasterId, + validUntil, + validAfter, + dynamicAdjustment, + signature + ); + } + + function excludeLastNBytes(bytes memory data, uint256 n) internal pure returns (bytes memory) { + require(data.length > n, "Input data is too short"); + bytes memory result = new bytes(data.length - n); + for (uint256 i = 0; i < data.length - n; i++) { + result[i] = data[i]; + } + return result; + } + + function getDynamicAdjustments( + BiconomySponsorshipPaymaster paymaster, + uint256 initialDappPaymasterBalance, + uint256 initialFeeCollectorBalance, + uint32 dynamicAdjustment + ) + internal + view + returns (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) + { + uint256 resultingDappPaymasterBalance = paymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 resultingFeeCollectorPaymasterBalance = paymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + + uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; + + if (dynamicAdjustment >= 1e6) { + //dynamicAdjustment + expectedDynamicAdjustment = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / dynamicAdjustment); + actualDynamicAdjustment = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; + } else { + revert("DynamicAdjustment must be more than 1e6"); + } + } + + function calculateAndAssertAdjustments( + BiconomySponsorshipPaymaster bicoPaymaster, + uint256 initialDappPaymasterBalance, + uint256 initialFeeCollectorBalance, + uint256 initialBundlerBalance, + uint256 initialPaymasterEpBalance, + uint32 dynamicAdjustment + ) + internal + { + (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( + bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment + ); + uint256 totalGasFeePaid = BUNDLER.addr.balance - initialBundlerBalance; + uint256 gasPaidByDapp = initialDappPaymasterBalance - bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + + // Assert that what paymaster paid is the same as what the bundler received + assertEq(totalGasFeePaid, initialPaymasterEpBalance - bicoPaymaster.getDeposit()); + // Assert that adjustment collected (if any) is correct + assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); + // Gas paid by dapp is higher than paymaster + // Guarantees that EP always has sufficient deposit to pay back dapps + assertGt(gasPaidByDapp, BUNDLER.addr.balance - initialBundlerBalance); + // Ensure that max 1% difference between total gas paid + the adjustment premium and gas paid by dapp (from + // paymaster) + assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18); + } +} diff --git a/test/foundry/mocks/Counter.sol b/test/foundry/mocks/Counter.sol deleted file mode 100644 index 5807161..0000000 --- a/test/foundry/mocks/Counter.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -contract Counter { - uint256 private _number; - - function incrementNumber() public { - _number++; - } - - function decrementNumber() public { - _number--; - } - - function getNumber() public view returns (uint256) { - return _number; - } - - function revertOperation() public pure { - revert("Counter: Revert operation"); - } - - function test_() public pure { - // This function is used to ignore file in coverage report - } -} diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol new file mode 100644 index 0000000..a77dbb1 --- /dev/null +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.26; + +import { TestBase } from "../../base/TestBase.sol"; +import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; +import { BiconomySponsorshipPaymaster } from + "../../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/core/UserOperationLib.sol"; +import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; + +contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { + BiconomySponsorshipPaymaster public bicoPaymaster; + + function setUp() public { + setupTestEnvironment(); + // Deploy Sponsorship Paymaster + bicoPaymaster = new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 + ); + } + + function test_Deploy() external { + BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 + ); + assertEq(testArtifact.owner(), PAYMASTER_OWNER.addr); + assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); + assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); + assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + assertEq(testArtifact.unaccountedGas(), 7e3); + } + + function test_RevertIf_DeployWithSignerSetToZero() external { + vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeZero.selector)); + new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, address(0), PAYMASTER_FEE_COLLECTOR.addr, 7e3 + ); + } + + function test_RevertIf_DeployWithFeeCollectorSetToZero() external { + vm.expectRevert(abi.encodeWithSelector(FeeCollectorCanNotBeZero.selector)); + new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(0), 7e3); + } + + function test_RevertIf_DeployWithUnaccountedGasCostTooHigh() external { + vm.expectRevert(abi.encodeWithSelector(UnaccountedGasTooHigh.selector)); + new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 10_001 + ); + } + + function test_CheckInitialPaymasterState() external view { + assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); + assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); + assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + assertEq(bicoPaymaster.unaccountedGas(), 7e3); + } + + function test_OwnershipTransfer() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit OwnershipTransferred(PAYMASTER_OWNER.addr, DAN_ADDRESS); + bicoPaymaster.transferOwnership(DAN_ADDRESS); + assertEq(bicoPaymaster.owner(), DAN_ADDRESS); + } + + function test_RevertIf_OwnershipTransferToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(abi.encodeWithSelector(NewOwnerIsZeroAddress.selector)); + bicoPaymaster.transferOwnership(address(0)); + } + + function test_RevertIf_UnauthorizedOwnershipTransfer() external { + vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); + bicoPaymaster.transferOwnership(DAN_ADDRESS); + } + + function test_SetVerifyingSigner() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.VerifyingSignerChanged( + PAYMASTER_SIGNER.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr + ); + bicoPaymaster.setSigner(DAN_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), DAN_ADDRESS); + } + + function test_RevertIf_SetVerifyingSignerToContract() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeContract.selector)); + bicoPaymaster.setSigner(ENTRYPOINT_ADDRESS); + } + + function test_RevertIf_SetVerifyingSignerToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeZero.selector)); + bicoPaymaster.setSigner(address(0)); + } + + function test_RevertIf_UnauthorizedSetVerifyingSigner() external { + vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); + bicoPaymaster.setSigner(DAN_ADDRESS); + } + + function test_SetFeeCollector() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.FeeCollectorChanged( + PAYMASTER_FEE_COLLECTOR.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr + ); + bicoPaymaster.setFeeCollector(DAN_ADDRESS); + assertEq(bicoPaymaster.feeCollector(), DAN_ADDRESS); + } + + function test_RevertIf_SetFeeCollectorToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(abi.encodeWithSelector(FeeCollectorCanNotBeZero.selector)); + bicoPaymaster.setFeeCollector(address(0)); + } + + function test_RevertIf_UnauthorizedSetFeeCollector() external { + vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); + bicoPaymaster.setFeeCollector(DAN_ADDRESS); + } + + function test_SetUnaccountedGas() external prankModifier(PAYMASTER_OWNER.addr) { + uint48 initialUnaccountedGas = bicoPaymaster.unaccountedGas(); + uint48 newUnaccountedGas = 5000; + + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.UnaccountedGasChanged(initialUnaccountedGas, newUnaccountedGas); + bicoPaymaster.setUnaccountedGas(newUnaccountedGas); + + uint48 resultingUnaccountedGas = bicoPaymaster.unaccountedGas(); + assertEq(resultingUnaccountedGas, newUnaccountedGas); + } + + function test_RevertIf_SetUnaccountedGasToHigh() external prankModifier(PAYMASTER_OWNER.addr) { + uint48 newUnaccountedGas = 10_001; + vm.expectRevert(abi.encodeWithSelector(UnaccountedGasTooHigh.selector)); + bicoPaymaster.setUnaccountedGas(newUnaccountedGas); + } + + function test_DepositFor() external { + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 depositAmount = 10 ether; + assertEq(dappPaymasterBalance, 0 ether); + + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_ACCOUNT.addr, depositAmount); + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + + dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, depositAmount); + } + + function test_RevertIf_DepositForZeroAddress() external { + vm.expectRevert(abi.encodeWithSelector(PaymasterIdCanNotBeZero.selector)); + bicoPaymaster.depositFor{ value: 1 ether }(address(0)); + } + + function test_RevertIf_DepositForZeroValue() external { + vm.expectRevert(abi.encodeWithSelector(DepositCanNotBeZero.selector)); + bicoPaymaster.depositFor{ value: 0 ether }(DAPP_ACCOUNT.addr); + } + + function test_RevertIf_DepositCalled() external { + vm.expectRevert(abi.encodeWithSelector(UseDepositForInstead.selector)); + bicoPaymaster.deposit{ value: 1 ether }(); + } + + function test_WithdrawTo() external prankModifier(DAPP_ACCOUNT.addr) { + uint256 depositAmount = 10 ether; + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + uint256 danInitialBalance = DAN_ADDRESS.balance; + + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, depositAmount); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), depositAmount); + + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, 0 ether); + uint256 expectedDanBalance = danInitialBalance + depositAmount; + assertEq(DAN_ADDRESS.balance, expectedDanBalance); + } + + function test_RevertIf_WithdrawToZeroAddress() external prankModifier(DAPP_ACCOUNT.addr) { + vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawToZeroAddress.selector)); + bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); + } + + function test_RevertIf_WithdrawToExceedsBalance() external prankModifier(DAPP_ACCOUNT.addr) { + vm.expectRevert(abi.encodeWithSelector(InsufficientFundsInGasTank.selector)); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); + } + + function test_ValidatePaymasterAndPostOpWithoutDynamicAdjustment() external prankModifier(DAPP_ACCOUNT.addr) { + bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); + // No adjustment + uint32 dynamicAdjustment = 1e6; + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); + ops[0] = userOp; + + uint256 initialBundlerBalance = BUNDLER.addr.balance; + uint256 initialPaymasterEpBalance = bicoPaymaster.getDeposit(); + uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + + // submit userops + vm.expectEmit(true, false, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + + // Calculate and assert dynamic adjustments and gas payments + calculateAndAssertAdjustments( + bicoPaymaster, + initialDappPaymasterBalance, + initialFeeCollectorBalance, + initialBundlerBalance, + initialPaymasterEpBalance, + dynamicAdjustment + ); + } + + function test_ValidatePaymasterAndPostOpWithDynamicAdjustment() external { + bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); + // 10% dynamicAdjustment on gas cost + uint32 dynamicAdjustment = 1e6 + 1e5; + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); + ops[0] = userOp; + + uint256 initialBundlerBalance = BUNDLER.addr.balance; + uint256 initialPaymasterEpBalance = bicoPaymaster.getDeposit(); + uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + + // submit userops + vm.expectEmit(true, false, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.DynamicAdjustmentCollected(DAPP_ACCOUNT.addr, 0); + vm.expectEmit(true, false, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + + // Calculate and assert dynamic adjustments and gas payments + calculateAndAssertAdjustments( + bicoPaymaster, + initialDappPaymasterBalance, + initialFeeCollectorBalance, + initialBundlerBalance, + initialPaymasterEpBalance, + dynamicAdjustment + ); + } + + function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + (userOp.paymasterAndData,) = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.paymasterAndData = excludeLastNBytes(userOp.paymasterAndData, 2); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + } + + function test_RevertIf_ValidatePaymasterUserOpWithInvalidPriceMarkUp() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + (userOp.paymasterAndData,) = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + } + + function test_RevertIf_ValidatePaymasterUserOpWithInsufficientDeposit() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + (userOp.paymasterAndData,) = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + } + + function test_Receive() external prankModifier(ALICE_ADDRESS) { + uint256 initialPaymasterBalance = address(bicoPaymaster).balance; + uint256 sendAmount = 10 ether; + + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.Received(ALICE_ADDRESS, sendAmount); + (bool success,) = address(bicoPaymaster).call{ value: sendAmount }(""); + + assert(success); + uint256 resultingPaymasterBalance = address(bicoPaymaster).balance; + assertEq(resultingPaymasterBalance, initialPaymasterBalance + sendAmount); + } + + function test_WithdrawEth() external prankModifier(PAYMASTER_OWNER.addr) { + uint256 initialAliceBalance = ALICE_ADDRESS.balance; + uint256 ethAmount = 10 ether; + vm.deal(address(bicoPaymaster), ethAmount); + + bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); + vm.stopPrank(); + + assertEq(ALICE_ADDRESS.balance, initialAliceBalance + ethAmount); + assertEq(address(bicoPaymaster).balance, 0 ether); + } + + function test_RevertIf_WithdrawEthExceedsBalance() external prankModifier(PAYMASTER_OWNER.addr) { + uint256 ethAmount = 10 ether; + vm.expectRevert(abi.encodeWithSelector(WithdrawalFailed.selector)); + bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); + } + + function test_WithdrawErc20() external prankModifier(PAYMASTER_OWNER.addr) { + MockToken token = new MockToken("Token", "TKN"); + uint256 mintAmount = 10 * (10 ** token.decimals()); + token.mint(address(bicoPaymaster), mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), mintAmount); + assertEq(token.balanceOf(ALICE_ADDRESS), 0); + + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.TokensWithdrawn( + address(token), ALICE_ADDRESS, mintAmount, PAYMASTER_OWNER.addr + ); + bicoPaymaster.withdrawERC20(token, ALICE_ADDRESS, mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), 0); + assertEq(token.balanceOf(ALICE_ADDRESS), mintAmount); + } + + function test_RevertIf_WithdrawErc20ToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { + MockToken token = new MockToken("Token", "TKN"); + uint256 mintAmount = 10 * (10 ** token.decimals()); + token.mint(address(bicoPaymaster), mintAmount); + + vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawToZeroAddress.selector)); + bicoPaymaster.withdrawERC20(token, address(0), mintAmount); + } + + function test_ParsePaymasterAndData() external view { + address paymasterId = DAPP_ACCOUNT.addr; + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + uint32 dynamicAdjustment = 1e6; + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + (bytes memory paymasterAndData, bytes memory signature) = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, dynamicAdjustment + ); + + ( + address parsedPaymasterId, + uint48 parsedValidUntil, + uint48 parsedValidAfter, + uint32 parsedDynamicAdjustment, + bytes memory parsedSignature + ) = bicoPaymaster.parsePaymasterAndData(paymasterAndData); + + assertEq(paymasterId, parsedPaymasterId); + assertEq(validUntil, parsedValidUntil); + assertEq(validAfter, parsedValidAfter); + assertEq(dynamicAdjustment, parsedDynamicAdjustment); + assertEq(signature, parsedSignature); + } +} diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol new file mode 100644 index 0000000..5fb3416 --- /dev/null +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.26; + +import { TestBase } from "../../base/TestBase.sol"; +import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; +import { BiconomySponsorshipPaymaster } from + "../../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; +import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; + +contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { + BiconomySponsorshipPaymaster public bicoPaymaster; + + function setUp() public { + setupTestEnvironment(); + // Deploy Sponsorship Paymaster + bicoPaymaster = new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 + ); + } + + function testFuzz_DepositFor(uint256 depositAmount) external { + vm.assume(depositAmount <= 1000 ether && depositAmount > 0 ether); + vm.deal(DAPP_ACCOUNT.addr, depositAmount); + + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, 0 ether); + + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_ACCOUNT.addr, depositAmount); + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + + dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, depositAmount); + } + + function testFuzz_WithdrawTo(uint256 withdrawAmount) external prankModifier(DAPP_ACCOUNT.addr) { + vm.assume(withdrawAmount <= 1000 ether && withdrawAmount > 0 ether); + vm.deal(DAPP_ACCOUNT.addr, withdrawAmount); + + bicoPaymaster.depositFor{ value: withdrawAmount }(DAPP_ACCOUNT.addr); + uint256 danInitialBalance = DAN_ADDRESS.balance; + + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, withdrawAmount); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), withdrawAmount); + + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, 0 ether); + uint256 expectedDanBalance = danInitialBalance + withdrawAmount; + assertEq(DAN_ADDRESS.balance, expectedDanBalance); + } + + function testFuzz_Receive(uint256 ethAmount) external prankModifier(ALICE_ADDRESS) { + vm.assume(ethAmount <= 1000 ether && ethAmount > 0 ether); + uint256 initialPaymasterBalance = address(bicoPaymaster).balance; + + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.Received(ALICE_ADDRESS, ethAmount); + (bool success,) = address(bicoPaymaster).call{ value: ethAmount }(""); + + assert(success); + uint256 resultingPaymasterBalance = address(bicoPaymaster).balance; + assertEq(resultingPaymasterBalance, initialPaymasterBalance + ethAmount); + } + + function testFuzz_WithdrawEth(uint256 ethAmount) external prankModifier(PAYMASTER_OWNER.addr) { + vm.assume(ethAmount <= 1000 ether && ethAmount > 0 ether); + vm.deal(address(bicoPaymaster), ethAmount); + uint256 initialAliceBalance = ALICE_ADDRESS.balance; + + bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); + + assertEq(ALICE_ADDRESS.balance, initialAliceBalance + ethAmount); + assertEq(address(bicoPaymaster).balance, 0 ether); + } + + function testFuzz_WithdrawErc20(address target, uint256 amount) external prankModifier(PAYMASTER_OWNER.addr) { + vm.assume(target != address(0) && amount <= 1_000_000 * (10 ** 18)); + MockToken token = new MockToken("Token", "TKN"); + uint256 mintAmount = amount; + token.mint(address(bicoPaymaster), mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), mintAmount); + assertEq(token.balanceOf(ALICE_ADDRESS), 0); + + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.TokensWithdrawn( + address(token), ALICE_ADDRESS, mintAmount, PAYMASTER_OWNER.addr + ); + bicoPaymaster.withdrawERC20(token, ALICE_ADDRESS, mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), 0); + assertEq(token.balanceOf(ALICE_ADDRESS), mintAmount); + } + + function testFuzz_ValidatePaymasterAndPostOpWithDynamicAdjustment(uint32 dynamicAdjustment) external { + vm.assume(dynamicAdjustment <= 2e6 && dynamicAdjustment > 1e6); + bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); + ops[0] = userOp; + + uint256 initialBundlerBalance = BUNDLER.addr.balance; + uint256 initialPaymasterEpBalance = bicoPaymaster.getDeposit(); + uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + + // submit userops + vm.expectEmit(true, false, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.DynamicAdjustmentCollected(DAPP_ACCOUNT.addr, 0); + vm.expectEmit(true, false, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + + // Calculate and assert dynamic adjustments and gas payments + calculateAndAssertAdjustments( + bicoPaymaster, + initialDappPaymasterBalance, + initialFeeCollectorBalance, + initialBundlerBalance, + initialPaymasterEpBalance, + dynamicAdjustment + ); + } + + function testFuzz_ParsePaymasterAndData( + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 dynamicAdjustment + ) + external + view + { + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + (bytes memory paymasterAndData, bytes memory signature) = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, dynamicAdjustment + ); + + ( + address parsedPaymasterId, + uint48 parsedValidUntil, + uint48 parsedValidAfter, + uint32 parsedDynamicAdjustment, + bytes memory parsedSignature + ) = bicoPaymaster.parsePaymasterAndData(paymasterAndData); + + assertEq(paymasterId, parsedPaymasterId); + assertEq(validUntil, parsedValidUntil); + assertEq(validAfter, parsedValidAfter); + assertEq(dynamicAdjustment, parsedDynamicAdjustment); + assertEq(signature, parsedSignature); + } +} diff --git a/test/hardhat/Lock.ts b/test/hardhat/Lock.ts deleted file mode 100644 index 8e49635..0000000 --- a/test/hardhat/Lock.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - time, - loadFixture, -} from "@nomicfoundation/hardhat-toolbox/network-helpers"; -import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; -import { expect } from "chai"; -import { ethers } from "hardhat"; - -describe("Lock", function () { - // We define a fixture to reuse the same setup in every test. - // We use loadFixture to run this setup once, snapshot that state, - // and reset Hardhat Network to that snapshot in every test. - async function deployOneYearLockFixture() { - const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; - const ONE_GWEI = 1_000_000_000; - - const lockedAmount = ONE_GWEI; - const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; - - // Contracts are deployed using the first signer/account by default - const [owner, otherAccount] = await ethers.getSigners(); - - const Lock = await ethers.getContractFactory("Lock"); - const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); - - const SmartAccount = await ethers.getContractFactory("SmartAccount"); - const smartAccount = await SmartAccount.deploy(); - - return { lock, unlockTime, lockedAmount, owner, otherAccount }; - } - - describe("Deployment", function () { - it("Should set the right unlockTime", async function () { - const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); - - expect(await lock.unlockTime()).to.equal(unlockTime); - }); - - it("Should set the right owner", async function () { - const { lock, owner } = await loadFixture(deployOneYearLockFixture); - - expect(await lock.owner()).to.equal(owner.address); - }); - - it("Should receive and store the funds to lock", async function () { - const { lock, lockedAmount } = await loadFixture( - deployOneYearLockFixture, - ); - - expect(await ethers.provider.getBalance(lock.target)).to.equal( - lockedAmount, - ); - }); - - it("Should fail if the unlockTime is not in the future", async function () { - // We don't use the fixture here because we want a different deployment - const latestTime = await time.latest(); - const Lock = await ethers.getContractFactory("Lock"); - await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( - "Wrong Unlock time", - ); - }); - }); - - describe("Withdrawals", function () { - describe("Validations", function () { - it("Should revert with the right error if called too soon", async function () { - const { lock } = await loadFixture(deployOneYearLockFixture); - - await expect(lock.withdraw()).to.be.revertedWith( - "You can't withdraw yet", - ); - }); - - it("Should revert with the right error if called from another account", async function () { - const { lock, unlockTime, otherAccount } = await loadFixture( - deployOneYearLockFixture, - ); - - // We can increase the time in Hardhat Network - await time.increaseTo(unlockTime); - - // We use lock.connect() to send a transaction from another account - await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( - "You aren't the owner", - ); - }); - - it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { - const { lock, unlockTime } = await loadFixture( - deployOneYearLockFixture, - ); - - // Transactions are sent using the first signer by default - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()).not.to.be.reverted; - }); - }); - - describe("Events", function () { - it("Should emit an event on withdrawals", async function () { - const { lock, unlockTime, lockedAmount } = await loadFixture( - deployOneYearLockFixture, - ); - - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()) - .to.emit(lock, "Withdrawal") - .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg - }); - }); - - describe("Transfers", function () { - it("Should transfer the funds to the owner", async function () { - const { lock, unlockTime, lockedAmount, owner } = await loadFixture( - deployOneYearLockFixture, - ); - - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()).to.changeEtherBalances( - [owner, lock], - [lockedAmount, -lockedAmount], - ); - }); - }); - }); -}); diff --git a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts index dbfabb1..c3a48d4 100644 --- a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts +++ b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts @@ -1,23 +1,35 @@ import { ethers } from "hardhat"; import { expect } from "chai"; -import { AbiCoder, AddressLike, BytesLike, Signer, parseEther, toBeHex } from "ethers"; -import { - EntryPoint, - EntryPoint__factory, - MockValidator, - MockValidator__factory, - SmartAccount, - SmartAccount__factory, - AccountFactory, - AccountFactory__factory, - BiconomySponsorshipPaymaster, - BiconomySponsorshipPaymaster__factory +import { + AbiCoder, + AddressLike, + BytesLike, + Signer, + parseEther, + toBeHex, +} from "ethers"; +import { + EntryPoint, + EntryPoint__factory, + MockValidator, + MockValidator__factory, + SmartAccount, + SmartAccount__factory, + AccountFactory, + AccountFactory__factory, + BiconomySponsorshipPaymaster, + BiconomySponsorshipPaymaster__factory, } from "../../typechain-types"; -import { DefaultsForUserOp, fillAndSign, fillSignAndPack, packUserOp, simulateValidation } from './utils/userOpHelpers' +import { + DefaultsForUserOp, + fillAndSign, + fillSignAndPack, + packUserOp, + simulateValidation, +} from "./utils/userOpHelpers"; import { parseValidationData } from "./utils/testUtils"; - export const AddressZero = ethers.ZeroAddress; const MOCK_VALID_UNTIL = "0x00000000deadbeef"; @@ -25,148 +37,174 @@ const MOCK_VALID_AFTER = "0x0000000000001234"; const MARKUP = 1100000; export const ENTRY_POINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; -const coder = AbiCoder.defaultAbiCoder() +const coder = AbiCoder.defaultAbiCoder(); export async function deployEntryPoint( - provider = ethers.provider - ): Promise { - const epf = await (await ethers.getContractFactory("EntryPoint")).deploy(); - // Retrieve the deployed contract bytecode - const deployedCode = await ethers.provider.getCode( - await epf.getAddress(), - ); - - // Use hardhat_setCode to set the contract code at the specified address - await ethers.provider.send("hardhat_setCode", [ENTRY_POINT_V7, deployedCode]); - - return epf.attach(ENTRY_POINT_V7) as EntryPoint; + provider = ethers.provider, +): Promise { + const epf = await (await ethers.getContractFactory("EntryPoint")).deploy(); + // Retrieve the deployed contract bytecode + const deployedCode = await ethers.provider.getCode(await epf.getAddress()); + + // Use hardhat_setCode to set the contract code at the specified address + await ethers.provider.send("hardhat_setCode", [ENTRY_POINT_V7, deployedCode]); + + return epf.attach(ENTRY_POINT_V7) as EntryPoint; } describe("EntryPoint with Biconomy Sponsorship Paymaster", function () { - let entryPoint: EntryPoint; - let depositorSigner: Signer; - let walletOwner: Signer; - let walletAddress: string, paymasterAddress: string; - let paymasterDepositorId: string; - let ethersSigner: Signer[]; - let offchainSigner: Signer, deployer: Signer, feeCollector: Signer; - let paymaster: BiconomySponsorshipPaymaster; - let smartWalletImp: SmartAccount; - let ecdsaModule: MockValidator; - let walletFactory: AccountFactory; - - beforeEach(async function () { - ethersSigner = await ethers.getSigners(); - entryPoint = await deployEntryPoint(); - - deployer = ethersSigner[0]; - offchainSigner = ethersSigner[1]; - depositorSigner = ethersSigner[2]; - feeCollector = ethersSigner[3]; - walletOwner = deployer; - - paymasterDepositorId = await depositorSigner.getAddress(); - - const offchainSignerAddress = await offchainSigner.getAddress(); - const walletOwnerAddress = await walletOwner.getAddress(); - const feeCollectorAddess = await feeCollector.getAddress(); - - ecdsaModule = await new MockValidator__factory( - deployer - ).deploy(); - - paymaster = - await new BiconomySponsorshipPaymaster__factory(deployer).deploy( - await deployer.getAddress(), - await entryPoint.getAddress(), - offchainSignerAddress, - feeCollectorAddess - ); - - smartWalletImp = await new SmartAccount__factory( - deployer - ).deploy(); - - walletFactory = await new AccountFactory__factory(deployer).deploy( - await smartWalletImp.getAddress(), - ); - - await walletFactory - .connect(deployer) - .addStake( 86400, { value: parseEther("2") }); - - const smartAccountDeploymentIndex = 0; - - // Module initialization data, encoded - const moduleInstallData = ethers.solidityPacked(["address"], [walletOwnerAddress]); - - await walletFactory.createAccount( - await ecdsaModule.getAddress(), - moduleInstallData, - smartAccountDeploymentIndex - ); - - const expected = await walletFactory.getCounterFactualAddress( - await ecdsaModule.getAddress(), - moduleInstallData, - smartAccountDeploymentIndex - ); - - walletAddress = expected; - - paymasterAddress = await paymaster.getAddress(); - - await paymaster - .connect(deployer) - .addStake(86400, { value: parseEther("2") }); - - await paymaster.depositFor(paymasterDepositorId, { value: parseEther("1") }); - - await entryPoint.depositTo(paymasterAddress, { value: parseEther("1") }); - - await deployer.sendTransaction({to: expected, value: parseEther("1"), data: '0x'}); - }); - - describe("Deployed Account : #validatePaymasterUserOp and #sendEmptySponsoredTx", () => { - it("succeed with valid signature", async () => { - const nonceKey = ethers.zeroPadBytes(await ecdsaModule.getAddress(), 24); - const userOp1 = await fillAndSign({ - sender: walletAddress, - paymaster: paymasterAddress, - paymasterData: ethers.concat([ - ethers.zeroPadValue(paymasterDepositorId, 20), - ethers.zeroPadValue(toBeHex(MOCK_VALID_UNTIL), 6), - ethers.zeroPadValue(toBeHex(MOCK_VALID_AFTER), 6), - ethers.zeroPadValue(toBeHex(MARKUP), 4), - '0x' + '00'.repeat(65) - ]), - paymasterPostOpGasLimit: 40_000, - }, walletOwner, entryPoint, 'getNonce', nonceKey) - const hash = await paymaster.getHash(packUserOp(userOp1), paymasterDepositorId, MOCK_VALID_UNTIL, MOCK_VALID_AFTER, MARKUP) - const sig = await offchainSigner.signMessage(ethers.getBytes(hash)) - const userOp = await fillSignAndPack({ - ...userOp1, - paymaster: paymasterAddress, - paymasterData: ethers.concat([ - ethers.zeroPadValue(paymasterDepositorId, 20), - ethers.zeroPadValue(toBeHex(MOCK_VALID_UNTIL), 6), - ethers.zeroPadValue(toBeHex(MOCK_VALID_AFTER), 6), - ethers.zeroPadValue(toBeHex(MARKUP), 4), - sig - ]), - paymasterPostOpGasLimit: 40_000, - }, walletOwner, entryPoint, 'getNonce', nonceKey) - // const parsedPnD = await paymaster.parsePaymasterAndData(userOp.paymasterAndData) - const res = await simulateValidation(userOp, await entryPoint.getAddress()) - const validationData = parseValidationData(res.returnInfo.paymasterValidationData) - expect(validationData).to.eql({ - aggregator: AddressZero, - validAfter: parseInt(MOCK_VALID_AFTER), - validUntil: parseInt(MOCK_VALID_UNTIL) - }) - - await entryPoint.handleOps([userOp], await deployer.getAddress()) - }); + let entryPoint: EntryPoint; + let depositorSigner: Signer; + let walletOwner: Signer; + let walletAddress: string, paymasterAddress: string; + let paymasterDepositorId: string; + let ethersSigner: Signer[]; + let offchainSigner: Signer, deployer: Signer, feeCollector: Signer; + let paymaster: BiconomySponsorshipPaymaster; + let smartWalletImp: SmartAccount; + let ecdsaModule: MockValidator; + let walletFactory: AccountFactory; + + beforeEach(async function () { + ethersSigner = await ethers.getSigners(); + entryPoint = await deployEntryPoint(); + + deployer = ethersSigner[0]; + offchainSigner = ethersSigner[1]; + depositorSigner = ethersSigner[2]; + feeCollector = ethersSigner[3]; + walletOwner = deployer; + + paymasterDepositorId = await depositorSigner.getAddress(); + + const offchainSignerAddress = await offchainSigner.getAddress(); + const walletOwnerAddress = await walletOwner.getAddress(); + const feeCollectorAddess = await feeCollector.getAddress(); + + ecdsaModule = await new MockValidator__factory(deployer).deploy(); + + paymaster = await new BiconomySponsorshipPaymaster__factory( + deployer, + ).deploy( + await deployer.getAddress(), + await entryPoint.getAddress(), + offchainSignerAddress, + feeCollectorAddess, + ); + + smartWalletImp = await new SmartAccount__factory(deployer).deploy(); + + walletFactory = await new AccountFactory__factory(deployer).deploy( + await smartWalletImp.getAddress(), + ); + + await walletFactory + .connect(deployer) + .addStake(86400, { value: parseEther("2") }); + + const smartAccountDeploymentIndex = 0; + + // Module initialization data, encoded + const moduleInstallData = ethers.solidityPacked( + ["address"], + [walletOwnerAddress], + ); + + await walletFactory.createAccount( + await ecdsaModule.getAddress(), + moduleInstallData, + smartAccountDeploymentIndex, + ); + + const expected = await walletFactory.getCounterFactualAddress( + await ecdsaModule.getAddress(), + moduleInstallData, + smartAccountDeploymentIndex, + ); + + walletAddress = expected; + + paymasterAddress = await paymaster.getAddress(); + + await paymaster + .connect(deployer) + .addStake(86400, { value: parseEther("2") }); + + await paymaster.depositFor(paymasterDepositorId, { + value: parseEther("1"), }); -}) + await entryPoint.depositTo(paymasterAddress, { value: parseEther("1") }); + + await deployer.sendTransaction({ + to: expected, + value: parseEther("1"), + data: "0x", + }); + }); + + describe("Deployed Account : #validatePaymasterUserOp and #sendEmptySponsoredTx", () => { + it("succeed with valid signature", async () => { + const nonceKey = ethers.zeroPadBytes(await ecdsaModule.getAddress(), 24); + const userOp1 = await fillAndSign( + { + sender: walletAddress, + paymaster: paymasterAddress, + paymasterData: ethers.concat([ + ethers.zeroPadValue(paymasterDepositorId, 20), + ethers.zeroPadValue(toBeHex(MOCK_VALID_UNTIL), 6), + ethers.zeroPadValue(toBeHex(MOCK_VALID_AFTER), 6), + ethers.zeroPadValue(toBeHex(MARKUP), 4), + "0x" + "00".repeat(65), + ]), + paymasterPostOpGasLimit: 40_000, + }, + walletOwner, + entryPoint, + "getNonce", + nonceKey, + ); + const hash = await paymaster.getHash( + packUserOp(userOp1), + paymasterDepositorId, + MOCK_VALID_UNTIL, + MOCK_VALID_AFTER, + MARKUP, + ); + const sig = await offchainSigner.signMessage(ethers.getBytes(hash)); + const userOp = await fillSignAndPack( + { + ...userOp1, + paymaster: paymasterAddress, + paymasterData: ethers.concat([ + ethers.zeroPadValue(paymasterDepositorId, 20), + ethers.zeroPadValue(toBeHex(MOCK_VALID_UNTIL), 6), + ethers.zeroPadValue(toBeHex(MOCK_VALID_AFTER), 6), + ethers.zeroPadValue(toBeHex(MARKUP), 4), + sig, + ]), + paymasterPostOpGasLimit: 40_000, + }, + walletOwner, + entryPoint, + "getNonce", + nonceKey, + ); + // const parsedPnD = await paymaster.parsePaymasterAndData(userOp.paymasterAndData) + const res = await simulateValidation( + userOp, + await entryPoint.getAddress(), + ); + const validationData = parseValidationData( + res.returnInfo.paymasterValidationData, + ); + expect(validationData).to.eql({ + aggregator: AddressZero, + validAfter: parseInt(MOCK_VALID_AFTER), + validUntil: parseInt(MOCK_VALID_UNTIL), + }); + + await entryPoint.handleOps([userOp], await deployer.getAddress()); + }); + }); +}); diff --git a/test/hardhat/utils/deployment.ts b/test/hardhat/utils/deployment.ts index 282831d..18ebef0 100644 --- a/test/hardhat/utils/deployment.ts +++ b/test/hardhat/utils/deployment.ts @@ -1,6 +1,12 @@ import { BytesLike, HDNodeWallet, Signer } from "ethers"; import { deployments, ethers } from "hardhat"; -import { AccountFactory, BiconomySponsorshipPaymaster, EntryPoint, MockValidator, SmartAccount } from "../../../typechain-types"; +import { + AccountFactory, + BiconomySponsorshipPaymaster, + EntryPoint, + MockValidator, + SmartAccount, +} from "../../../typechain-types"; import { TASK_DEPLOY } from "hardhat-deploy"; import { DeployResult } from "hardhat-deploy/dist/types"; @@ -14,39 +20,39 @@ export const ENTRY_POINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; * @returns A promise that resolves to the deployed contract instance. */ export async function deployContract( - contractName: string, - deployer: Signer, - ): Promise { - const ContractFactory = await ethers.getContractFactory( - contractName, - deployer, - ); - const contract = await ContractFactory.deploy(); - await contract.waitForDeployment(); - return contract as T; + contractName: string, + deployer: Signer, +): Promise { + const ContractFactory = await ethers.getContractFactory( + contractName, + deployer, + ); + const contract = await ContractFactory.deploy(); + await contract.waitForDeployment(); + return contract as T; } /** * Deploys the EntryPoint contract with a deterministic deployment. * @returns A promise that resolves to the deployed EntryPoint contract instance. */ -export async function getDeployedEntrypoint() : Promise { - const [deployer] = await ethers.getSigners(); - - // Deploy the contract normally to get its bytecode - const EntryPoint = await ethers.getContractFactory("EntryPoint"); - const entryPoint = await EntryPoint.deploy(); - await entryPoint.waitForDeployment(); - - // Retrieve the deployed contract bytecode - const deployedCode = await ethers.provider.getCode( - await entryPoint.getAddress(), - ); - - // Use hardhat_setCode to set the contract code at the specified address - await ethers.provider.send("hardhat_setCode", [ENTRY_POINT_V7, deployedCode]); - - return EntryPoint.attach(ENTRY_POINT_V7) as EntryPoint; +export async function getDeployedEntrypoint(): Promise { + const [deployer] = await ethers.getSigners(); + + // Deploy the contract normally to get its bytecode + const EntryPoint = await ethers.getContractFactory("EntryPoint"); + const entryPoint = await EntryPoint.deploy(); + await entryPoint.waitForDeployment(); + + // Retrieve the deployed contract bytecode + const deployedCode = await ethers.provider.getCode( + await entryPoint.getAddress(), + ); + + // Use hardhat_setCode to set the contract code at the specified address + await ethers.provider.send("hardhat_setCode", [ENTRY_POINT_V7, deployedCode]); + + return EntryPoint.attach(ENTRY_POINT_V7) as EntryPoint; } /** @@ -54,18 +60,18 @@ export async function getDeployedEntrypoint() : Promise { * @returns A promise that resolves to the deployed SA implementation contract instance. */ export async function getDeployedMSAImplementation(): Promise { - const accounts: Signer[] = await ethers.getSigners(); - const addresses = await Promise.all( - accounts.map((account) => account.getAddress()), - ); - - const SmartAccount = await ethers.getContractFactory("SmartAccount"); - const deterministicMSAImpl = await deployments.deploy("SmartAccount", { - from: addresses[0], - deterministicDeployment: true, - }); - - return SmartAccount.attach(deterministicMSAImpl.address) as SmartAccount; + const accounts: Signer[] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const SmartAccount = await ethers.getContractFactory("SmartAccount"); + const deterministicMSAImpl = await deployments.deploy("SmartAccount", { + from: addresses[0], + deterministicDeployment: true, + }); + + return SmartAccount.attach(deterministicMSAImpl.address) as SmartAccount; } /** @@ -73,27 +79,27 @@ export async function getDeployedMSAImplementation(): Promise { * @returns A promise that resolves to the deployed EntryPoint contract instance. */ export async function getDeployedAccountFactory( - implementationAddress: string, - // Note: this could be converted to dto so that additional args can easily be passed - ): Promise { - const accounts: Signer[] = await ethers.getSigners(); - const addresses = await Promise.all( - accounts.map((account) => account.getAddress()), - ); - - const AccountFactory = await ethers.getContractFactory("AccountFactory"); - const deterministicAccountFactory = await deployments.deploy( - "AccountFactory", - { - from: addresses[0], - deterministicDeployment: true, - args: [implementationAddress], - }, - ); - - return AccountFactory.attach( - deterministicAccountFactory.address, - ) as AccountFactory; + implementationAddress: string, + // Note: this could be converted to dto so that additional args can easily be passed +): Promise { + const accounts: Signer[] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const AccountFactory = await ethers.getContractFactory("AccountFactory"); + const deterministicAccountFactory = await deployments.deploy( + "AccountFactory", + { + from: addresses[0], + deterministicDeployment: true, + args: [implementationAddress], + }, + ); + + return AccountFactory.attach( + deterministicAccountFactory.address, + ) as AccountFactory; } /** @@ -101,41 +107,50 @@ export async function getDeployedAccountFactory( * @returns A promise that resolves to the deployed MockValidator contract instance. */ export async function getDeployedMockValidator(): Promise { - const accounts: Signer[] = await ethers.getSigners(); - const addresses = await Promise.all( - accounts.map((account) => account.getAddress()), - ); - - const MockValidator = await ethers.getContractFactory("MockValidator"); - const deterministicMockValidator = await deployments.deploy("MockValidator", { - from: addresses[0], - deterministicDeployment: true, - }); - - return MockValidator.attach( - deterministicMockValidator.address, - ) as MockValidator; + const accounts: Signer[] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const MockValidator = await ethers.getContractFactory("MockValidator"); + const deterministicMockValidator = await deployments.deploy("MockValidator", { + from: addresses[0], + deterministicDeployment: true, + }); + + return MockValidator.attach( + deterministicMockValidator.address, + ) as MockValidator; } /** * Deploys the MockValidator contract with a deterministic deployment. * @returns A promise that resolves to the deployed MockValidator contract instance. */ -export async function getDeployedSponsorshipPaymaster(owner: string, entryPoint: string, verifyingSigner: string, feeCollector: string): Promise { - const accounts: Signer[] = await ethers.getSigners(); - const addresses = await Promise.all( - accounts.map((account) => account.getAddress()), - ); - - const BiconomySponsorshipPaymaster = await ethers.getContractFactory("BiconomySponsorshipPaymaster"); - const deterministicSponsorshipPaymaster = await deployments.deploy("BiconomySponsorshipPaymaster", { +export async function getDeployedSponsorshipPaymaster( + owner: string, + entryPoint: string, + verifyingSigner: string, + feeCollector: string, +): Promise { + const accounts: Signer[] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const BiconomySponsorshipPaymaster = await ethers.getContractFactory( + "BiconomySponsorshipPaymaster", + ); + const deterministicSponsorshipPaymaster = await deployments.deploy( + "BiconomySponsorshipPaymaster", + { from: addresses[0], deterministicDeployment: true, args: [owner, entryPoint, verifyingSigner, feeCollector], - }); - - return BiconomySponsorshipPaymaster.attach( + }, + ); + + return BiconomySponsorshipPaymaster.attach( deterministicSponsorshipPaymaster.address, - ) as BiconomySponsorshipPaymaster; + ) as BiconomySponsorshipPaymaster; } - diff --git a/test/hardhat/utils/testUtils.ts b/test/hardhat/utils/testUtils.ts index 06c4218..abe1776 100644 --- a/test/hardhat/utils/testUtils.ts +++ b/test/hardhat/utils/testUtils.ts @@ -1,6 +1,15 @@ -import { AbiCoder, AddressLike, BigNumberish, Contract, Interface, dataSlice, parseEther, toBeHex } from 'ethers'; -import { ethers } from 'hardhat' -import { EntryPoint__factory, IERC20 } from '../../../typechain-types'; +import { + AbiCoder, + AddressLike, + BigNumberish, + Contract, + Interface, + dataSlice, + parseEther, + toBeHex, +} from "ethers"; +import { ethers } from "hardhat"; +import { EntryPoint__factory, IERC20 } from "../../../typechain-types"; // define mode and exec type enums export const CALLTYPE_SINGLE = "0x00"; // 1 byte @@ -13,171 +22,189 @@ export const UNUSED = "0x00000000"; // 4 bytes export const MODE_PAYLOAD = "0x00000000000000000000000000000000000000000000"; // 22 bytes export const AddressZero = ethers.ZeroAddress; -export const HashZero = ethers.ZeroHash -export const ONE_ETH = parseEther('1') -export const TWO_ETH = parseEther('2') -export const FIVE_ETH = parseEther('5') -export const maxUint48 = (2 ** 48) - 1 +export const HashZero = ethers.ZeroHash; +export const ONE_ETH = parseEther("1"); +export const TWO_ETH = parseEther("2"); +export const FIVE_ETH = parseEther("5"); +export const maxUint48 = 2 ** 48 - 1; -export const tostr = (x: any): string => x != null ? x.toString() : 'null' +export const tostr = (x: any): string => (x != null ? x.toString() : "null"); -const coder = AbiCoder.defaultAbiCoder() +const coder = AbiCoder.defaultAbiCoder(); export interface ValidationData { - aggregator: string - validAfter: number - validUntil: number + aggregator: string; + validAfter: number; + validUntil: number; } export const panicCodes: { [key: number]: string } = { - // from https://docs.soliditylang.org/en/v0.8.0/control-structures.html - 0x01: 'assert(false)', - 0x11: 'arithmetic overflow/underflow', - 0x12: 'divide by zero', - 0x21: 'invalid enum value', - 0x22: 'storage byte array that is incorrectly encoded', - 0x31: '.pop() on an empty array.', - 0x32: 'array sout-of-bounds or negative index', - 0x41: 'memory overflow', - 0x51: 'zero-initialized variable of internal function type' -} + // from https://docs.soliditylang.org/en/v0.8.0/control-structures.html + 0x01: "assert(false)", + 0x11: "arithmetic overflow/underflow", + 0x12: "divide by zero", + 0x21: "invalid enum value", + 0x22: "storage byte array that is incorrectly encoded", + 0x31: ".pop() on an empty array.", + 0x32: "array sout-of-bounds or negative index", + 0x41: "memory overflow", + 0x51: "zero-initialized variable of internal function type", +}; export const Erc20 = [ - "function transfer(address _receiver, uint256 _value) public returns (bool success)", - "function transferFrom(address, address, uint256) public returns (bool)", - "function approve(address _spender, uint256 _value) public returns (bool success)", - "function allowance(address _owner, address _spender) public view returns (uint256 remaining)", - "function balanceOf(address _owner) public view returns (uint256 balance)", - "event Approval(address indexed _owner, address indexed _spender, uint256 _value)", - ]; - + "function transfer(address _receiver, uint256 _value) public returns (bool success)", + "function transferFrom(address, address, uint256) public returns (bool)", + "function approve(address _spender, uint256 _value) public returns (bool success)", + "function allowance(address _owner, address _spender) public view returns (uint256 remaining)", + "function balanceOf(address _owner) public view returns (uint256 balance)", + "event Approval(address indexed _owner, address indexed _spender, uint256 _value)", +]; + export const Erc20Interface = new ethers.Interface(Erc20); export const encodeTransfer = ( - target: string, - amount: string | number - ): string => { - return Erc20Interface.encodeFunctionData("transfer", [target, amount]); + target: string, + amount: string | number, +): string => { + return Erc20Interface.encodeFunctionData("transfer", [target, amount]); }; export const encodeTransferFrom = ( - from: string, - target: string, - amount: string | number - ): string => { - return Erc20Interface.encodeFunctionData("transferFrom", [ - from, - target, - amount, - ]); + from: string, + target: string, + amount: string | number, +): string => { + return Erc20Interface.encodeFunctionData("transferFrom", [ + from, + target, + amount, + ]); }; // rethrow "cleaned up" exception. // - stack trace goes back to method (or catch) line, not inner provider // - attempt to parse revert data (needed for geth) // use with ".catch(rethrow())", so that current source file/line is meaningful. -export function rethrow (): (e: Error) => void { - const callerStack = new Error().stack!.replace(/Error.*\n.*at.*\n/, '').replace(/.*at.* \(internal[\s\S]*/, '') +export function rethrow(): (e: Error) => void { + const callerStack = new Error() + .stack!.replace(/Error.*\n.*at.*\n/, "") + .replace(/.*at.* \(internal[\s\S]*/, ""); if (arguments[0] != null) { - throw new Error('must use .catch(rethrow()), and NOT .catch(rethrow)') + throw new Error("must use .catch(rethrow()), and NOT .catch(rethrow)"); } return function (e: Error) { - const solstack = e.stack!.match(/((?:.* at .*\.sol.*\n)+)/) - const stack = (solstack != null ? solstack[1] : '') + callerStack + const solstack = e.stack!.match(/((?:.* at .*\.sol.*\n)+)/); + const stack = (solstack != null ? solstack[1] : "") + callerStack; // const regex = new RegExp('error=.*"data":"(.*?)"').compile() - const found = /error=.*?"data":"(.*?)"/.exec(e.message) - let message: string + const found = /error=.*?"data":"(.*?)"/.exec(e.message); + let message: string; if (found != null) { - const data = found[1] - message = decodeRevertReason(data) ?? e.message + ' - ' + data.slice(0, 100) + const data = found[1]; + message = + decodeRevertReason(data) ?? e.message + " - " + data.slice(0, 100); } else { - message = e.message + message = e.message; } - const err = new Error(message) - err.stack = 'Error: ' + message + '\n' + stack - throw err - } + const err = new Error(message); + err.stack = "Error: " + message + "\n" + stack; + throw err; + }; } const decodeRevertReasonContracts = new Interface([ ...EntryPoint__factory.createInterface().fragments, - 'error ECDSAInvalidSignature()' -]) // .filter(f => f.type === 'error')) - -export function decodeRevertReason (data: string | Error, nullIfNoMatch = true): string | null { - if (typeof data !== 'string') { - const err = data as any - data = (err.data ?? err.error?.data) as string - if (typeof data !== 'string') throw err + "error ECDSAInvalidSignature()", +]); // .filter(f => f.type === 'error')) + +export function decodeRevertReason( + data: string | Error, + nullIfNoMatch = true, +): string | null { + if (typeof data !== "string") { + const err = data as any; + data = (err.data ?? err.error?.data) as string; + if (typeof data !== "string") throw err; } - const methodSig = data.slice(0, 10) - const dataParams = '0x' + data.slice(10) + const methodSig = data.slice(0, 10); + const dataParams = "0x" + data.slice(10); // can't add Error(string) to xface... - if (methodSig === '0x08c379a0') { - const [err] = coder.decode(['string'], dataParams) + if (methodSig === "0x08c379a0") { + const [err] = coder.decode(["string"], dataParams); // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `Error(${err})` - } else if (methodSig === '0x4e487b71') { - const [code] = coder.decode(['uint256'], dataParams) - return `Panic(${panicCodes[code] ?? code} + ')` + return `Error(${err})`; + } else if (methodSig === "0x4e487b71") { + const [code] = coder.decode(["uint256"], dataParams); + return `Panic(${panicCodes[code] ?? code} + ')`; } try { - const err = decodeRevertReasonContracts.parseError(data) + const err = decodeRevertReasonContracts.parseError(data); // treat any error "bytes" argument as possible error to decode (e.g. FailedOpWithRevert, PostOpReverted) const args = err!.args.map((arg: any, index) => { switch (err?.fragment.inputs[index].type) { - case 'bytes' : return decodeRevertReason(arg) - case 'string': return `"${(arg as string)}"` - default: return arg + case "bytes": + return decodeRevertReason(arg); + case "string": + return `"${arg as string}"`; + default: + return arg; } - }) - return `${err!.name}(${args.join(',')})` + }); + return `${err!.name}(${args.join(",")})`; } catch (e) { // throw new Error('unsupported errorSig ' + data) if (!nullIfNoMatch) { - return data + return data; } - return null + return null; } } -export function tonumber (x: any): number { +export function tonumber(x: any): number { try { - return parseFloat(x.toString()) + return parseFloat(x.toString()); } catch (e: any) { - console.log('=== failed to parseFloat:', x, (e).message) - return NaN + console.log("=== failed to parseFloat:", x, e.message); + return NaN; } } // just throw 1eth from account[0] to the given address (or contract instance) -export async function fund (contractOrAddress: string | Contract, amountEth = '1'): Promise { - let address: string - if (typeof contractOrAddress === 'string') { - address = contractOrAddress - } else { - address = await contractOrAddress.getAddress() - } - const [firstSigner] = await ethers.getSigners(); - await firstSigner.sendTransaction({ to: address, value: parseEther(amountEth) }) +export async function fund( + contractOrAddress: string | Contract, + amountEth = "1", +): Promise { + let address: string; + if (typeof contractOrAddress === "string") { + address = contractOrAddress; + } else { + address = await contractOrAddress.getAddress(); + } + const [firstSigner] = await ethers.getSigners(); + await firstSigner.sendTransaction({ + to: address, + value: parseEther(amountEth), + }); } -export async function getBalance (address: string): Promise { - const balance = await ethers.provider.getBalance(address) - return parseInt(balance.toString()) +export async function getBalance(address: string): Promise { + const balance = await ethers.provider.getBalance(address); + return parseInt(balance.toString()); } -export async function getTokenBalance (token: IERC20, address: string): Promise { - const balance = await token.balanceOf(address) - return parseInt(balance.toString()) +export async function getTokenBalance( + token: IERC20, + address: string, +): Promise { + const balance = await token.balanceOf(address); + return parseInt(balance.toString()); } -export async function isDeployed (addr: string): Promise { - const code = await ethers.provider.getCode(addr) - return code.length > 2 +export async function isDeployed(addr: string): Promise { + const code = await ethers.provider.getCode(addr); + return code.length > 2; } // Getting initcode for AccountFactory which accepts one validator (with ECDSA owner required for installation) @@ -202,28 +229,29 @@ export async function getInitCode( return factoryAddress + factoryDeploymentData; } -export function callDataCost (data: string): number { - return ethers.getBytes(data) - .map(x => x === 0 ? 4 : 16) - .reduce((sum, x) => sum + x) +export function callDataCost(data: string): number { + return ethers + .getBytes(data) + .map((x) => (x === 0 ? 4 : 16)) + .reduce((sum, x) => sum + x); } -export function parseValidationData (validationData: BigNumberish): ValidationData { - const data = ethers.zeroPadValue(toBeHex(validationData), 32) +export function parseValidationData( + validationData: BigNumberish, +): ValidationData { + const data = ethers.zeroPadValue(toBeHex(validationData), 32); // string offsets start from left (msb) - const aggregator = dataSlice(data, 32 - 20) - let validUntil = parseInt(dataSlice(data, 32 - 26, 32 - 20)) + const aggregator = dataSlice(data, 32 - 20); + let validUntil = parseInt(dataSlice(data, 32 - 26, 32 - 20)); if (validUntil === 0) { - validUntil = maxUint48 + validUntil = maxUint48; } - const validAfter = parseInt(dataSlice(data, 0, 6)) + const validAfter = parseInt(dataSlice(data, 0, 6)); return { aggregator, validAfter, - validUntil - } + validUntil, + }; } - - diff --git a/test/hardhat/utils/types.ts b/test/hardhat/utils/types.ts index 791fc10..7dd52fa 100644 --- a/test/hardhat/utils/types.ts +++ b/test/hardhat/utils/types.ts @@ -1,34 +1,30 @@ -import { - AddressLike, - BigNumberish, - BytesLike, - } from "ethers"; +import { AddressLike, BigNumberish, BytesLike } from "ethers"; export interface UserOperation { - sender: AddressLike; // Or string - nonce?: BigNumberish; - initCode?: BytesLike; - callData?: BytesLike; - callGasLimit?: BigNumberish; - verificationGasLimit?: BigNumberish; - preVerificationGas?: BigNumberish; - maxFeePerGas?: BigNumberish; - maxPriorityFeePerGas?: BigNumberish; - paymaster?: AddressLike; // Or string - paymasterVerificationGasLimit?: BigNumberish; - paymasterPostOpGasLimit?: BigNumberish; - paymasterData?: BytesLike; - signature?: BytesLike; - } + sender: AddressLike; // Or string + nonce?: BigNumberish; + initCode?: BytesLike; + callData?: BytesLike; + callGasLimit?: BigNumberish; + verificationGasLimit?: BigNumberish; + preVerificationGas?: BigNumberish; + maxFeePerGas?: BigNumberish; + maxPriorityFeePerGas?: BigNumberish; + paymaster?: AddressLike; // Or string + paymasterVerificationGasLimit?: BigNumberish; + paymasterPostOpGasLimit?: BigNumberish; + paymasterData?: BytesLike; + signature?: BytesLike; +} - export interface PackedUserOperation { - sender: AddressLike; // Or string - nonce: BigNumberish; - initCode: BytesLike; - callData: BytesLike; - accountGasLimits: BytesLike; - preVerificationGas: BigNumberish; - gasFees: BytesLike; - paymasterAndData: BytesLike; - signature: BytesLike; - } \ No newline at end of file +export interface PackedUserOperation { + sender: AddressLike; // Or string + nonce: BigNumberish; + initCode: BytesLike; + callData: BytesLike; + accountGasLimits: BytesLike; + preVerificationGas: BigNumberish; + gasFees: BytesLike; + paymasterAndData: BytesLike; + signature: BytesLike; +} diff --git a/test/hardhat/utils/userOpHelpers.ts b/test/hardhat/utils/userOpHelpers.ts index 8dc582c..50fccd5 100644 --- a/test/hardhat/utils/userOpHelpers.ts +++ b/test/hardhat/utils/userOpHelpers.ts @@ -1,157 +1,230 @@ import { ethers } from "hardhat"; -import { EntryPoint, EntryPointSimulations__factory, IEntryPointSimulations } from "../../../typechain-types"; +import { + EntryPoint, + EntryPointSimulations__factory, + IEntryPointSimulations, +} from "../../../typechain-types"; import { PackedUserOperation, UserOperation } from "./types"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { TransactionRequest } from '@ethersproject/abstract-provider' -import { AbiCoder, BigNumberish, BytesLike, Contract, Signer, dataSlice, keccak256, toBeHex } from "ethers"; +import { TransactionRequest } from "@ethersproject/abstract-provider"; +import { + AbiCoder, + BigNumberish, + BytesLike, + Contract, + Signer, + dataSlice, + keccak256, + toBeHex, +} from "ethers"; import { toGwei } from "./general"; import { callDataCost, decodeRevertReason, rethrow } from "./testUtils"; -import EntryPointSimulationsJson from '../../../artifacts/account-abstraction/contracts/core/EntryPointSimulations.sol/EntryPointSimulations.json' +import EntryPointSimulationsJson from "../../../artifacts/account-abstraction/contracts/core/EntryPointSimulations.sol/EntryPointSimulations.json"; const AddressZero = ethers.ZeroAddress; -const coder = AbiCoder.defaultAbiCoder() +const coder = AbiCoder.defaultAbiCoder(); -export function packUserOp (userOp: UserOperation): PackedUserOperation { +export function packUserOp(userOp: UserOperation): PackedUserOperation { + const { + sender, + nonce, + initCode = "0x", + callData = "0x", + callGasLimit = 1_500_000, + verificationGasLimit = 1_500_000, + preVerificationGas = 2_000_000, + maxFeePerGas = toGwei("20"), + maxPriorityFeePerGas = toGwei("10"), + paymaster = ethers.ZeroAddress, + paymasterData = "0x", + paymasterVerificationGasLimit = 3_00_000, + paymasterPostOpGasLimit = 0, + signature = "0x", + } = userOp; - const { - sender, - nonce, - initCode = "0x", - callData = "0x", - callGasLimit = 1_500_000, - verificationGasLimit = 1_500_000, - preVerificationGas = 2_000_000, - maxFeePerGas = toGwei("20"), - maxPriorityFeePerGas = toGwei("10"), - paymaster = ethers.ZeroAddress, - paymasterData = "0x", - paymasterVerificationGasLimit = 3_00_000, - paymasterPostOpGasLimit = 0, - signature = "0x", - } = userOp; - - const accountGasLimits = packAccountGasLimits(verificationGasLimit, callGasLimit) - const gasFees = packAccountGasLimits(maxPriorityFeePerGas, maxFeePerGas) - let paymasterAndData = '0x' - if (paymaster.toString().length >= 20 && paymaster !== ethers.ZeroAddress) { - paymasterAndData = packPaymasterData( - userOp.paymaster as string, - paymasterVerificationGasLimit, - paymasterPostOpGasLimit, - paymasterData as string, - ) as string; - } - return { - sender: userOp.sender, - nonce: userOp.nonce || 0, - callData: userOp.callData || '0x', - accountGasLimits, - initCode: userOp.initCode || '0x', - preVerificationGas: userOp.preVerificationGas || 50000, - gasFees, - paymasterAndData, - signature: userOp.signature || '0x' - } + const accountGasLimits = packAccountGasLimits( + verificationGasLimit, + callGasLimit, + ); + const gasFees = packAccountGasLimits(maxPriorityFeePerGas, maxFeePerGas); + let paymasterAndData = "0x"; + if (paymaster.toString().length >= 20 && paymaster !== ethers.ZeroAddress) { + paymasterAndData = packPaymasterData( + userOp.paymaster as string, + paymasterVerificationGasLimit, + paymasterPostOpGasLimit, + paymasterData as string, + ) as string; + } + return { + sender: userOp.sender, + nonce: userOp.nonce || 0, + callData: userOp.callData || "0x", + accountGasLimits, + initCode: userOp.initCode || "0x", + preVerificationGas: userOp.preVerificationGas || 50000, + gasFees, + paymasterAndData, + signature: userOp.signature || "0x", + }; } -export function encodeUserOp (userOp: UserOperation, forSignature = true): string { - const packedUserOp = packUserOp(userOp) - if (forSignature) { - return coder.encode( - ['address', 'uint256', 'bytes32', 'bytes32', - 'bytes32', 'uint256', 'bytes32', - 'bytes32'], - [packedUserOp.sender, packedUserOp.nonce, keccak256(packedUserOp.initCode), keccak256(packedUserOp.callData), - packedUserOp.accountGasLimits, packedUserOp.preVerificationGas, packedUserOp.gasFees, - keccak256(packedUserOp.paymasterAndData)]) - } else { - // for the purpose of calculating gas cost encode also signature (and no keccak of bytes) - return coder.encode( - ['address', 'uint256', 'bytes', 'bytes', - 'bytes32', 'uint256', 'bytes32', - 'bytes', 'bytes'], - [packedUserOp.sender, packedUserOp.nonce, packedUserOp.initCode, packedUserOp.callData, - packedUserOp.accountGasLimits, packedUserOp.preVerificationGas, packedUserOp.gasFees, - packedUserOp.paymasterAndData, packedUserOp.signature]) - } +export function encodeUserOp( + userOp: UserOperation, + forSignature = true, +): string { + const packedUserOp = packUserOp(userOp); + if (forSignature) { + return coder.encode( + [ + "address", + "uint256", + "bytes32", + "bytes32", + "bytes32", + "uint256", + "bytes32", + "bytes32", + ], + [ + packedUserOp.sender, + packedUserOp.nonce, + keccak256(packedUserOp.initCode), + keccak256(packedUserOp.callData), + packedUserOp.accountGasLimits, + packedUserOp.preVerificationGas, + packedUserOp.gasFees, + keccak256(packedUserOp.paymasterAndData), + ], + ); + } else { + // for the purpose of calculating gas cost encode also signature (and no keccak of bytes) + return coder.encode( + [ + "address", + "uint256", + "bytes", + "bytes", + "bytes32", + "uint256", + "bytes32", + "bytes", + "bytes", + ], + [ + packedUserOp.sender, + packedUserOp.nonce, + packedUserOp.initCode, + packedUserOp.callData, + packedUserOp.accountGasLimits, + packedUserOp.preVerificationGas, + packedUserOp.gasFees, + packedUserOp.paymasterAndData, + packedUserOp.signature, + ], + ); + } } // Can be moved to testUtils export function packPaymasterData( - paymaster: string, - paymasterVerificationGasLimit: BigNumberish, - postOpGasLimit: BigNumberish, - paymasterData: BytesLike, - ): BytesLike { - return ethers.concat([ - paymaster, - ethers.zeroPadValue(toBeHex(Number(paymasterVerificationGasLimit)), 16), - ethers.zeroPadValue(toBeHex(Number(postOpGasLimit)), 16), - paymasterData, - ]); + paymaster: string, + paymasterVerificationGasLimit: BigNumberish, + postOpGasLimit: BigNumberish, + paymasterData: BytesLike, +): BytesLike { + return ethers.concat([ + paymaster, + ethers.zeroPadValue(toBeHex(Number(paymasterVerificationGasLimit)), 16), + ethers.zeroPadValue(toBeHex(Number(postOpGasLimit)), 16), + paymasterData, + ]); } // Can be moved to testUtils -export function packAccountGasLimits (verificationGasLimit: BigNumberish, callGasLimit: BigNumberish): string { - return ethers.concat([ - ethers.zeroPadValue(toBeHex(Number(verificationGasLimit)), 16), ethers.zeroPadValue(toBeHex(Number(callGasLimit)), 16) - ]) +export function packAccountGasLimits( + verificationGasLimit: BigNumberish, + callGasLimit: BigNumberish, +): string { + return ethers.concat([ + ethers.zeroPadValue(toBeHex(Number(verificationGasLimit)), 16), + ethers.zeroPadValue(toBeHex(Number(callGasLimit)), 16), + ]); } // Can be moved to testUtils -export function unpackAccountGasLimits (accountGasLimits: string): { verificationGasLimit: number, callGasLimit: number } { - return { verificationGasLimit: parseInt(accountGasLimits.slice(2, 34), 16), callGasLimit: parseInt(accountGasLimits.slice(34), 16) } +export function unpackAccountGasLimits(accountGasLimits: string): { + verificationGasLimit: number; + callGasLimit: number; +} { + return { + verificationGasLimit: parseInt(accountGasLimits.slice(2, 34), 16), + callGasLimit: parseInt(accountGasLimits.slice(34), 16), + }; } -export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: number): string { - const userOpHash = keccak256(encodeUserOp(op, true)) - const enc = coder.encode( - ['bytes32', 'address', 'uint256'], - [userOpHash, entryPoint, chainId]) - return keccak256(enc) +export function getUserOpHash( + op: UserOperation, + entryPoint: string, + chainId: number, +): string { + const userOpHash = keccak256(encodeUserOp(op, true)); + const enc = coder.encode( + ["bytes32", "address", "uint256"], + [userOpHash, entryPoint, chainId], + ); + return keccak256(enc); } export const DefaultsForUserOp: UserOperation = { - sender: AddressZero, - nonce: 0, - initCode: '0x', - callData: '0x', - callGasLimit: 0, - verificationGasLimit: 150000, // default verification gas. will add create2 cost (3200+200*length) if initCode exists - preVerificationGas: 21000, // should also cover calldata cost. - maxFeePerGas: 0, - maxPriorityFeePerGas: 1e9, - paymaster: AddressZero, - paymasterData: '0x', - paymasterVerificationGasLimit: 3e5, - paymasterPostOpGasLimit: 0, - signature: '0x' -} + sender: AddressZero, + nonce: 0, + initCode: "0x", + callData: "0x", + callGasLimit: 0, + verificationGasLimit: 150000, // default verification gas. will add create2 cost (3200+200*length) if initCode exists + preVerificationGas: 21000, // should also cover calldata cost. + maxFeePerGas: 0, + maxPriorityFeePerGas: 1e9, + paymaster: AddressZero, + paymasterData: "0x", + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 0, + signature: "0x", +}; // Different compared to infinitism utils -export async function signUserOp (op: UserOperation, signer: Signer, entryPoint: string, chainId: number): Promise { - const message = getUserOpHash(op, entryPoint, chainId) +export async function signUserOp( + op: UserOperation, + signer: Signer, + entryPoint: string, + chainId: number, +): Promise { + const message = getUserOpHash(op, entryPoint, chainId); - const signature = await signer.signMessage(ethers.getBytes(message)); - - return { - ...op, - signature: signature - } + const signature = await signer.signMessage(ethers.getBytes(message)); + + return { + ...op, + signature: signature, + }; } -export function fillUserOpDefaults (op: Partial, defaults = DefaultsForUserOp): UserOperation { - const partial: any = { ...op } - // we want "item:undefined" to be used from defaults, and not override defaults, so we must explicitly - // remove those so "merge" will succeed. - for (const key in partial) { - if (partial[key] == null) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete partial[key] - } +export function fillUserOpDefaults( + op: Partial, + defaults = DefaultsForUserOp, +): UserOperation { + const partial: any = { ...op }; + // we want "item:undefined" to be used from defaults, and not override defaults, so we must explicitly + // remove those so "merge" will succeed. + for (const key in partial) { + if (partial[key] == null) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete partial[key]; } - const filled = { ...defaults, ...partial } - return filled + } + const filled = { ...defaults, ...partial }; + return filled; } // helper to fill structure: @@ -166,112 +239,151 @@ export function fillUserOpDefaults (op: Partial, defaults = Defau // sender - only in case of construction: fill sender from initCode. // callGasLimit: VERY crude estimation (by estimating call to account, and add rough entryPoint overhead // verificationGasLimit: hard-code default at 100k. should add "create2" cost -export async function fillUserOp (op: Partial, entryPoint?: EntryPoint, getNonceFunction = 'getNonce', nonceKey = "0"): Promise { - const op1 = { ...op } - const provider = ethers.provider - if (op.initCode != null && op.initCode !== "0x" ) { - const initAddr = dataSlice(op1.initCode!, 0, 20) - const initCallData = dataSlice(op1.initCode!, 20) - if (op1.nonce == null) op1.nonce = 0 - if (op1.sender == null) { - if (provider == null) throw new Error('no entrypoint/provider') - op1.sender = await entryPoint!.getSenderAddress(op1.initCode!).catch(e => e.errorArgs.sender) - } - if (op1.verificationGasLimit == null) { - if (provider == null) throw new Error('no entrypoint/provider') - const initEstimate = await provider.estimateGas({ - from: await entryPoint?.getAddress(), - to: initAddr, - data: initCallData, - gasLimit: 10e6 - }) - op1.verificationGasLimit = Number(DefaultsForUserOp.verificationGasLimit!) + Number(initEstimate) +export async function fillUserOp( + op: Partial, + entryPoint?: EntryPoint, + getNonceFunction = "getNonce", + nonceKey = "0", +): Promise { + const op1 = { ...op }; + const provider = ethers.provider; + if (op.initCode != null && op.initCode !== "0x") { + const initAddr = dataSlice(op1.initCode!, 0, 20); + const initCallData = dataSlice(op1.initCode!, 20); + if (op1.nonce == null) op1.nonce = 0; + if (op1.sender == null) { + if (provider == null) throw new Error("no entrypoint/provider"); + op1.sender = await entryPoint! + .getSenderAddress(op1.initCode!) + .catch((e) => e.errorArgs.sender); } + if (op1.verificationGasLimit == null) { + if (provider == null) throw new Error("no entrypoint/provider"); + const initEstimate = await provider.estimateGas({ + from: await entryPoint?.getAddress(), + to: initAddr, + data: initCallData, + gasLimit: 10e6, + }); + op1.verificationGasLimit = + Number(DefaultsForUserOp.verificationGasLimit!) + Number(initEstimate); } - if (op1.nonce == null) { - // TODO: nonce should be fetched from entrypoint based on key + } + if (op1.nonce == null) { + // TODO: nonce should be fetched from entrypoint based on key // if (provider == null) throw new Error('must have entryPoint to autofill nonce') // const c = new Contract(op.sender! as string, [`function ${getNonceFunction}() view returns(uint256)`], provider) // op1.nonce = await c[getNonceFunction]().catch(rethrow()) const nonce = await entryPoint?.getNonce(op1.sender!, nonceKey); op1.nonce = nonce ?? 0n; + } + if (op1.callGasLimit == null && op.callData != null) { + if (provider == null) + throw new Error("must have entryPoint for callGasLimit estimate"); + const gasEtimated = await provider.estimateGas({ + from: await entryPoint?.getAddress(), + to: op1.sender, + data: op1.callData as string, + }); + + // console.log('estim', op1.sender,'len=', op1.callData!.length, 'res=', gasEtimated) + // estimateGas assumes direct call from entryPoint. add wrapper cost. + op1.callGasLimit = gasEtimated; // .add(55000) + } + if (op1.paymaster != null) { + if (op1.paymasterVerificationGasLimit == null) { + op1.paymasterVerificationGasLimit = + DefaultsForUserOp.paymasterVerificationGasLimit; } - if (op1.callGasLimit == null && op.callData != null) { - if (provider == null) throw new Error('must have entryPoint for callGasLimit estimate') - const gasEtimated = await provider.estimateGas({ - from: await entryPoint?.getAddress(), - to: op1.sender, - data: op1.callData as string - }) - - // console.log('estim', op1.sender,'len=', op1.callData!.length, 'res=', gasEtimated) - // estimateGas assumes direct call from entryPoint. add wrapper cost. - op1.callGasLimit = gasEtimated // .add(55000) - } - if (op1.paymaster != null) { - if (op1.paymasterVerificationGasLimit == null) { - op1.paymasterVerificationGasLimit = DefaultsForUserOp.paymasterVerificationGasLimit - } - if (op1.paymasterPostOpGasLimit == null) { - op1.paymasterPostOpGasLimit = DefaultsForUserOp.paymasterPostOpGasLimit - } - } - if (op1.maxFeePerGas == null) { - if (provider == null) throw new Error('must have entryPoint to autofill maxFeePerGas') - const block = await provider.getBlock('latest') - op1.maxFeePerGas = Number(block!.baseFeePerGas!) + Number(op1.maxPriorityFeePerGas ?? DefaultsForUserOp.maxPriorityFeePerGas) - } - // TODO: this is exactly what fillUserOp below should do - but it doesn't. - // adding this manually - if (op1.maxPriorityFeePerGas == null) { - op1.maxPriorityFeePerGas = DefaultsForUserOp.maxPriorityFeePerGas - } - const op2 = fillUserOpDefaults(op1) - // if(op2 === undefined || op2 === null) { - // throw new Error('op2 is undefined or null') - // } - // eslint-disable-next-line @typescript-eslint/no-base-to-string - if (op2?.preVerificationGas?.toString() === '0') { - // TODO: we don't add overhead, which is ~21000 for a single TX, but much lower in a batch. - op2.preVerificationGas = callDataCost(encodeUserOp(op2, false)) + if (op1.paymasterPostOpGasLimit == null) { + op1.paymasterPostOpGasLimit = DefaultsForUserOp.paymasterPostOpGasLimit; } - return op2; + } + if (op1.maxFeePerGas == null) { + if (provider == null) + throw new Error("must have entryPoint to autofill maxFeePerGas"); + const block = await provider.getBlock("latest"); + op1.maxFeePerGas = + Number(block!.baseFeePerGas!) + + Number( + op1.maxPriorityFeePerGas ?? DefaultsForUserOp.maxPriorityFeePerGas, + ); + } + // TODO: this is exactly what fillUserOp below should do - but it doesn't. + // adding this manually + if (op1.maxPriorityFeePerGas == null) { + op1.maxPriorityFeePerGas = DefaultsForUserOp.maxPriorityFeePerGas; + } + const op2 = fillUserOpDefaults(op1); + // if(op2 === undefined || op2 === null) { + // throw new Error('op2 is undefined or null') + // } + // eslint-disable-next-line @typescript-eslint/no-base-to-string + if (op2?.preVerificationGas?.toString() === "0") { + // TODO: we don't add overhead, which is ~21000 for a single TX, but much lower in a batch. + op2.preVerificationGas = callDataCost(encodeUserOp(op2, false)); + } + return op2; } -export async function fillAndPack (op: Partial, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { - const userOp = await fillUserOp(op, entryPoint, getNonceFunction); - if(userOp === undefined) { - throw new Error('userOp is undefined') - } - return packUserOp(userOp) +export async function fillAndPack( + op: Partial, + entryPoint?: EntryPoint, + getNonceFunction = "getNonce", +): Promise { + const userOp = await fillUserOp(op, entryPoint, getNonceFunction); + if (userOp === undefined) { + throw new Error("userOp is undefined"); + } + return packUserOp(userOp); } -export async function fillAndSign (op: Partial, signer: Signer | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce', nonceKey = "0"): Promise { - const provider = ethers.provider - const op2 = await fillUserOp(op, entryPoint, getNonceFunction, nonceKey) - if(op2 === undefined) { - throw new Error('op2 is undefined') - } - - const chainId = await provider!.getNetwork().then(net => net.chainId) - const message = ethers.getBytes(getUserOpHash(op2, await entryPoint!.getAddress(), Number(chainId))) - - let signature - try { - signature = await signer.signMessage(message) - } catch (err: any) { - // attempt to use 'eth_sign' instead of 'personal_sign' which is not supported by Foundry Anvil - signature = await (signer as any)._legacySignMessage(message) - } - return { - ...op2, - signature - } +export async function fillAndSign( + op: Partial, + signer: Signer | Signer, + entryPoint?: EntryPoint, + getNonceFunction = "getNonce", + nonceKey = "0", +): Promise { + const provider = ethers.provider; + const op2 = await fillUserOp(op, entryPoint, getNonceFunction, nonceKey); + if (op2 === undefined) { + throw new Error("op2 is undefined"); + } + + const chainId = await provider!.getNetwork().then((net) => net.chainId); + const message = ethers.getBytes( + getUserOpHash(op2, await entryPoint!.getAddress(), Number(chainId)), + ); + + let signature; + try { + signature = await signer.signMessage(message); + } catch (err: any) { + // attempt to use 'eth_sign' instead of 'personal_sign' which is not supported by Foundry Anvil + signature = await (signer as any)._legacySignMessage(message); + } + return { + ...op2, + signature, + }; } - - export async function fillSignAndPack (op: Partial, signer: Signer | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce', nonceKey = "0"): Promise { - const filledAndSignedOp = await fillAndSign(op, signer, entryPoint, getNonceFunction, nonceKey) - return packUserOp(filledAndSignedOp) + +export async function fillSignAndPack( + op: Partial, + signer: Signer | Signer, + entryPoint?: EntryPoint, + getNonceFunction = "getNonce", + nonceKey = "0", +): Promise { + const filledAndSignedOp = await fillAndSign( + op, + signer, + entryPoint, + getNonceFunction, + nonceKey, + ); + return packUserOp(filledAndSignedOp); } /** @@ -281,67 +393,94 @@ export async function fillAndSign (op: Partial, signer: Signer | * @param entryPointAddress * @param txOverrides */ -export async function simulateValidation ( - userOp: PackedUserOperation, - entryPointAddress: string, - txOverrides?: any): Promise { - const entryPointSimulations = EntryPointSimulations__factory.createInterface() - const data = entryPointSimulations.encodeFunctionData('simulateValidation', [userOp]) - const tx: TransactionRequest = { - to: entryPointAddress, - data, - ...txOverrides - } - const stateOverride = { - [entryPointAddress]: { - code: EntryPointSimulationsJson.deployedBytecode - } - } - try { - const simulationResult = await ethers.provider.send('eth_call', [tx, 'latest', stateOverride]) - const res = entryPointSimulations.decodeFunctionResult('simulateValidation', simulationResult) - // note: here collapsing the returned "tuple of one" into a single value - will break for returning actual tuples - return res[0] - } catch (error: any) { - const revertData = error?.data - if (revertData != null) { - // note: this line throws the revert reason instead of returning it - entryPointSimulations.decodeFunctionResult('simulateValidation', revertData) - } - throw error +export async function simulateValidation( + userOp: PackedUserOperation, + entryPointAddress: string, + txOverrides?: any, +): Promise { + const entryPointSimulations = + EntryPointSimulations__factory.createInterface(); + const data = entryPointSimulations.encodeFunctionData("simulateValidation", [ + userOp, + ]); + const tx: TransactionRequest = { + to: entryPointAddress, + data, + ...txOverrides, + }; + const stateOverride = { + [entryPointAddress]: { + code: EntryPointSimulationsJson.deployedBytecode, + }, + }; + try { + const simulationResult = await ethers.provider.send("eth_call", [ + tx, + "latest", + stateOverride, + ]); + const res = entryPointSimulations.decodeFunctionResult( + "simulateValidation", + simulationResult, + ); + // note: here collapsing the returned "tuple of one" into a single value - will break for returning actual tuples + return res[0]; + } catch (error: any) { + const revertData = error?.data; + if (revertData != null) { + // note: this line throws the revert reason instead of returning it + entryPointSimulations.decodeFunctionResult( + "simulateValidation", + revertData, + ); } + throw error; + } } // TODO: this code is very much duplicated but "encodeFunctionData" is based on 20 overloads // TypeScript is not able to resolve overloads with variables: https://github.com/microsoft/TypeScript/issues/14107 -export async function simulateHandleOp ( - userOp: PackedUserOperation, - target: string, - targetCallData: string, - entryPointAddress: string, - txOverrides?: any): Promise { - const entryPointSimulations = EntryPointSimulations__factory.createInterface() - const data = entryPointSimulations.encodeFunctionData('simulateHandleOp', [userOp, target, targetCallData]) - const tx: TransactionRequest = { - to: entryPointAddress, - data, - ...txOverrides - } - const stateOverride = { - [entryPointAddress]: { - code: EntryPointSimulationsJson.deployedBytecode - } - } - try { - const simulationResult = await ethers.provider.send('eth_call', [tx, 'latest', stateOverride]) - const res = entryPointSimulations.decodeFunctionResult('simulateHandleOp', simulationResult) - // note: here collapsing the returned "tuple of one" into a single value - will break for returning actual tuples - return res[0] - } catch (error: any) { - const err = decodeRevertReason(error) - if (err != null) { - throw new Error(err) - } - throw error +export async function simulateHandleOp( + userOp: PackedUserOperation, + target: string, + targetCallData: string, + entryPointAddress: string, + txOverrides?: any, +): Promise { + const entryPointSimulations = + EntryPointSimulations__factory.createInterface(); + const data = entryPointSimulations.encodeFunctionData("simulateHandleOp", [ + userOp, + target, + targetCallData, + ]); + const tx: TransactionRequest = { + to: entryPointAddress, + data, + ...txOverrides, + }; + const stateOverride = { + [entryPointAddress]: { + code: EntryPointSimulationsJson.deployedBytecode, + }, + }; + try { + const simulationResult = await ethers.provider.send("eth_call", [ + tx, + "latest", + stateOverride, + ]); + const res = entryPointSimulations.decodeFunctionResult( + "simulateHandleOp", + simulationResult, + ); + // note: here collapsing the returned "tuple of one" into a single value - will break for returning actual tuples + return res[0]; + } catch (error: any) { + const err = decodeRevertReason(error); + if (err != null) { + throw new Error(err); } + throw error; } +}