mirror of
https://github.com/getwax/zk-account-abstraction.git
synced 2026-01-09 20:47:58 -05:00
Use wallet deposit (#21)
- simplified stake/deposit mechanism - send eth to EntryPoint adds to deposit - remove "walletEth" mode: always pay from wallet deposit. - user is not automatically refunded after the call (deposit is left for future calls) - can use "withdrawTo" call to retrieve its leftover deposit. - add chainId to hash before signature. - refactor: name methods as per the EIP (verifyUserOp=>validateUserOp) redeemer => beneficiary redeem() => compensate
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -18,4 +18,4 @@ jobs:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
- run: yarn install
|
||||
- run: yarn run ci
|
||||
- run: FORCE_COLOR=1 yarn run ci
|
||||
|
||||
@@ -16,8 +16,7 @@ contract EntryPoint is StakeManager {
|
||||
|
||||
enum PaymentMode {
|
||||
paymasterStake, // if paymaster is set, use paymaster's stake to pay.
|
||||
walletStake, // wallet has enough stake to pay for request.
|
||||
walletEth // wallet has no stake. paying with eth.
|
||||
walletStake // pay with wallet deposit.
|
||||
}
|
||||
|
||||
uint public immutable paymasterStake;
|
||||
@@ -31,7 +30,7 @@ contract EntryPoint is StakeManager {
|
||||
//handleOps reverts with this error struct, to mark the offending op
|
||||
// NOTE: if simulateOp passes successfully, there should be no reason for handleOps to fail on it.
|
||||
// @param opIndex - index into the array of ops to the failed one (in simulateOp, this is always zero)
|
||||
// @param paymaster - if paymaster.verifyPaymasterUserOp fails, this will be the paymaster's address. if verifyUserOp failed,
|
||||
// @param paymaster - if paymaster.validatePaymasterUserOp fails, this will be the paymaster's address. if validateUserOp failed,
|
||||
// this value will be zero (since it failed before accessing the paymaster)
|
||||
// @param reason - revert reason
|
||||
// only to aid troubleshooting of wallet/paymaster reverts
|
||||
@@ -47,14 +46,12 @@ contract EntryPoint is StakeManager {
|
||||
paymasterStake = _paymasterStake;
|
||||
}
|
||||
|
||||
receive() external payable {}
|
||||
|
||||
/**
|
||||
* Execute the given UserOperation.
|
||||
* @param op the operation to execute
|
||||
* @param redeemer the contract to redeem the fee
|
||||
* @param beneficiary the address to receive the fees
|
||||
*/
|
||||
function handleOp(UserOperation calldata op, address payable redeemer) public {
|
||||
function handleOp(UserOperation calldata op, address payable beneficiary) public {
|
||||
|
||||
uint preGas = gasleft();
|
||||
|
||||
@@ -70,14 +67,20 @@ contract EntryPoint is StakeManager {
|
||||
actualGasCost = handlePostOp(IPaymaster.PostOpMode.postOpReverted, op, context, actualGas, prefund, paymentMode);
|
||||
}
|
||||
|
||||
redeem(redeemer, actualGasCost);
|
||||
compensate(beneficiary, actualGasCost);
|
||||
}
|
||||
|
||||
function redeem(address payable redeemer, uint amount) internal {
|
||||
redeemer.transfer(amount);
|
||||
function compensate(address payable beneficiary, uint amount) internal {
|
||||
(bool success,) = beneficiary.call{value : amount}("");
|
||||
require(success);
|
||||
}
|
||||
|
||||
function handleOps(UserOperation[] calldata ops, address payable redeemer) public {
|
||||
/**
|
||||
* Execute a batch of UserOperation.
|
||||
* @param ops the operations to execute
|
||||
* @param beneficiary the address to receive the fees
|
||||
*/
|
||||
function handleOps(UserOperation[] calldata ops, address payable beneficiary) public {
|
||||
|
||||
uint opslen = ops.length;
|
||||
uint256[] memory preOpGas = new uint256[](opslen);
|
||||
@@ -117,7 +120,7 @@ contract EntryPoint is StakeManager {
|
||||
}
|
||||
}
|
||||
|
||||
redeem(redeemer, collected);
|
||||
compensate(beneficiary, collected);
|
||||
}
|
||||
|
||||
function internalHandleOp(UserOperation calldata op, bytes calldata context, uint preOpGas, uint prefund, PaymentMode paymentMode) external returns (uint actualGasCost) {
|
||||
@@ -128,8 +131,10 @@ contract EntryPoint is StakeManager {
|
||||
if (op.callData.length > 0) {
|
||||
|
||||
(bool success,bytes memory result) = address(op.getSender()).call{gas : op.callGas}(op.callData);
|
||||
if (!success && result.length > 0) {
|
||||
emit UserOperationRevertReason(op.getSender(), op.nonce, result);
|
||||
if (!success) {
|
||||
if (result.length > 0) {
|
||||
emit UserOperationRevertReason(op.getSender(), op.nonce, result);
|
||||
}
|
||||
mode = IPaymaster.PostOpMode.opReverted;
|
||||
}
|
||||
}
|
||||
@@ -139,9 +144,11 @@ contract EntryPoint is StakeManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a call to wallet.verifyUserOp and paymaster.verifyPaymasterUserOp
|
||||
* Simulate a call to wallet.validateUserOp and paymaster.validatePaymasterUserOp.
|
||||
* Validation succeeds of the call doesn't revert.
|
||||
* The node must also verify it doesn't use banned opcodes, and that it doesn't reference storage outside the wallet's data
|
||||
* @dev The node must also verify it doesn't use banned opcodes, and that it doesn't reference storage outside the wallet's data.
|
||||
* In order to split the running opcodes of the wallet (validateUserOp) from the paymaster's validatePaymasterUserOp,
|
||||
* it should look for the NUMBER opcode at depth=1 (which itself is a banned opcode)
|
||||
*/
|
||||
function simulateValidation(UserOperation calldata userOp) external {
|
||||
_validatePrepayment(0, userOp);
|
||||
@@ -149,62 +156,57 @@ contract EntryPoint is StakeManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a call for wallet.verifyUserOp.
|
||||
* Simulate a call for wallet.validateUserOp.
|
||||
* Call must not revert.
|
||||
* @return gasUsedByPayForSelfOp - gas used by the validation, to pass into simulatePaymasterValidation.
|
||||
* The node must also verify it doesn't use banned opcode, and that it doesn't reference storage outside the wallet's data
|
||||
* @dev The node must also verify it doesn't use banned opcode, and that it doesn't reference storage outside the wallet's data
|
||||
* @return gasUsedByValidateUserOp - gas used by the validation, to pass into simulatePaymasterValidation.
|
||||
*/
|
||||
function simulateWalletValidation(UserOperation calldata userOp) external returns (uint gasUsedByPayForSelfOp){
|
||||
function simulateWalletValidation(UserOperation calldata userOp) external returns (uint gasUsedByValidateUserOp){
|
||||
require(msg.sender == address(0), "must be called off-chain with from=zero-addr");
|
||||
(uint requiredPreFund, PaymentMode paymentMode) = getPaymentInfo(userOp);
|
||||
(gasUsedByPayForSelfOp,) = _validateWalletPrepayment(0, userOp, requiredPreFund, paymentMode);
|
||||
(gasUsedByValidateUserOp,) = _validateWalletPrepayment(0, userOp, requiredPreFund, paymentMode);
|
||||
}
|
||||
|
||||
function getPaymentInfo(UserOperation calldata userOp) internal view returns (uint requiredPrefund, PaymentMode paymentMode) {
|
||||
requiredPrefund = userOp.requiredPreFund();
|
||||
if (userOp.hasPaymaster()) {
|
||||
paymentMode = PaymentMode.paymasterStake;
|
||||
} else if (isStaked(userOp.getSender(), requiredPrefund, 0)) {
|
||||
paymentMode = PaymentMode.walletStake;
|
||||
} else {
|
||||
paymentMode = PaymentMode.walletEth;
|
||||
paymentMode = PaymentMode.walletStake;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a call to paymaster.verifyPaymasterUserOp
|
||||
* Simulate a call to paymaster.validatePaymasterUserOp.
|
||||
* do nothing if has no paymaster.
|
||||
* @dev The node must also verify it doesn't use banned opcode, and that it doesn't reference storage outside the paymaster's data
|
||||
* @param userOp the user operation to validate.
|
||||
* @param gasUsedByPayForSelfOp - the gas returned by simulateWalletValidation, as these 2 calls should share
|
||||
* @param gasUsedByValidateUserOp - the gas returned by simulateWalletValidation, as these 2 calls should share
|
||||
* the same userOp.validationGas quota.
|
||||
* The node must also verify it doesn't use banned opcode, and that it doesn't reference storage outside the paymaster's data
|
||||
*/
|
||||
function simulatePaymasterValidation(UserOperation calldata userOp, uint gasUsedByPayForSelfOp) external view returns (bytes memory context, uint gasUsedByPayForOp){
|
||||
function simulatePaymasterValidation(UserOperation calldata userOp, uint gasUsedByValidateUserOp) external view returns (bytes memory context, uint gasUsedByPayForOp){
|
||||
(uint requiredPreFund, PaymentMode paymentMode) = getPaymentInfo(userOp);
|
||||
if (paymentMode != PaymentMode.paymasterStake) {
|
||||
return ("", 0);
|
||||
}
|
||||
return _validatePaymasterPrepayment(0, userOp, requiredPreFund, gasUsedByPayForSelfOp);
|
||||
return _validatePaymasterPrepayment(0, userOp, requiredPreFund, gasUsedByValidateUserOp);
|
||||
}
|
||||
|
||||
// get the sender address, or use "create2" to create it.
|
||||
// note that the gas allocation for this creation is deterministic (by the size of callData),
|
||||
// so it is not checked on-chain, and adds to the gas used by verifyUserOp
|
||||
// create the sender's contract if needed.
|
||||
function _createSenderIfNeeded(UserOperation calldata op) internal {
|
||||
if (op.initCode.length != 0) {
|
||||
//its a create operation. run the create2
|
||||
// note that we're still under the gas limit of validate, so probably
|
||||
// this create2 creates a proxy account.
|
||||
// appending signer makes the request unique, so no one else can make this request.
|
||||
//nonce is meaningless during create, so we re-purpose it as salt
|
||||
// @dev initCode must be unique (e.g. contains the signer address), to make sure
|
||||
// it can only be executed from the entryPoint, and called with its initialization code (callData)
|
||||
address sender1 = ICreate2Deployer(create2factory).deploy(op.initCode, bytes32(op.nonce));
|
||||
require(sender1 != address(0), "create2 failed");
|
||||
require(sender1 == op.getSender(), "sender doesn't match create2 address");
|
||||
}
|
||||
}
|
||||
|
||||
//get counterfactual sender address.
|
||||
// use the initCode and salt in the UserOperation tot create this sender contract
|
||||
/// Get counterfactual sender address.
|
||||
/// Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation.
|
||||
function getSenderAddress(bytes memory initCode, uint _salt) public view returns (address) {
|
||||
bytes32 hash = keccak256(
|
||||
abi.encodePacked(
|
||||
@@ -219,57 +221,44 @@ contract EntryPoint is StakeManager {
|
||||
return address(uint160(uint256(hash)));
|
||||
}
|
||||
|
||||
//call wallet.verifyUserOp, and validate that it paid as needed.
|
||||
//call wallet.validateUserOp, and validate that it paid as needed.
|
||||
// return actual value sent from wallet to "this"
|
||||
function _validateWalletPrepayment(uint opIndex, UserOperation calldata op, uint requiredPrefund, PaymentMode paymentMode) internal returns (uint gasUsedByPayForSelfOp, uint prefund) {
|
||||
function _validateWalletPrepayment(uint opIndex, UserOperation calldata op, uint requiredPrefund, PaymentMode paymentMode) internal returns (uint gasUsedByValidateUserOp, uint prefund) {
|
||||
uint preGas = gasleft();
|
||||
_createSenderIfNeeded(op);
|
||||
uint preBalance = address(this).balance;
|
||||
uint requiredEthPrefund = 0;
|
||||
if (paymentMode == PaymentMode.walletEth) {
|
||||
requiredEthPrefund = requiredPrefund;
|
||||
} else if (paymentMode == PaymentMode.walletStake) {
|
||||
_prefundFromSender(op, requiredPrefund);
|
||||
} else {
|
||||
// paymaster pays in handlePostOp
|
||||
uint missingWalletFunds = 0;
|
||||
address sender = op.getSender();
|
||||
if (paymentMode != PaymentMode.paymasterStake) {
|
||||
uint bal = balanceOf(sender);
|
||||
missingWalletFunds = bal > requiredPrefund ? 0 : requiredPrefund - bal;
|
||||
}
|
||||
try IWallet(op.getSender()).verifyUserOp{gas : op.verificationGas}(op, requiredEthPrefund) {
|
||||
try IWallet(sender).validateUserOp{gas : op.verificationGas}(op, missingWalletFunds) {
|
||||
} catch Error(string memory revertReason) {
|
||||
revert FailedOp(opIndex, address(0), revertReason);
|
||||
} catch {
|
||||
revert FailedOp(opIndex, address(0), "");
|
||||
}
|
||||
uint actualEthPrefund = address(this).balance - preBalance;
|
||||
|
||||
if (paymentMode == PaymentMode.walletEth) {
|
||||
if (actualEthPrefund < requiredEthPrefund) {
|
||||
if (paymentMode != PaymentMode.paymasterStake) {
|
||||
if (requiredPrefund > balanceOf(sender)) {
|
||||
revert FailedOp(opIndex, address(0), "wallet didn't pay prefund");
|
||||
}
|
||||
prefund = actualEthPrefund;
|
||||
} else if (paymentMode == PaymentMode.walletStake) {
|
||||
if (actualEthPrefund != 0) {
|
||||
revert FailedOp(opIndex, address(0), "using wallet stake but wallet paid eth");
|
||||
}
|
||||
internalDecrementDeposit(sender, requiredPrefund);
|
||||
prefund = requiredPrefund;
|
||||
} else {
|
||||
if (actualEthPrefund != 0) {
|
||||
revert FailedOp(opIndex, address(0), "has paymaster but wallet paid");
|
||||
}
|
||||
prefund = requiredPrefund;
|
||||
prefund = 0;
|
||||
}
|
||||
|
||||
gasUsedByPayForSelfOp = preGas - gasleft();
|
||||
gasUsedByValidateUserOp = preGas - gasleft();
|
||||
}
|
||||
|
||||
//validate paymaster.verifyPaymasterUserOp
|
||||
function _validatePaymasterPrepayment(uint opIndex, UserOperation calldata op, uint requiredPreFund, uint gasUsedByPayForSelfOp) internal view returns (bytes memory context, uint gasUsedByPayForOp) {
|
||||
//validate paymaster.validatePaymasterUserOp
|
||||
function _validatePaymasterPrepayment(uint opIndex, UserOperation calldata op, uint requiredPreFund, uint gasUsedByValidateUserOp) internal view returns (bytes memory context, uint gasUsedByPayForOp) {
|
||||
uint preGas = gasleft();
|
||||
if (!isValidStake(op, requiredPreFund)) {
|
||||
revert FailedOp(opIndex, op.paymaster, "not enough stake");
|
||||
}
|
||||
//no pre-pay from paymaster
|
||||
uint gas = op.verificationGas - gasUsedByPayForSelfOp;
|
||||
try IPaymaster(op.paymaster).verifyPaymasterUserOp{gas : gas}(op, requiredPreFund) returns (bytes memory _context){
|
||||
uint gas = op.verificationGas - gasUsedByValidateUserOp;
|
||||
try IPaymaster(op.paymaster).validatePaymasterUserOp{gas : gas}(op, requiredPreFund) returns (bytes memory _context){
|
||||
context = _context;
|
||||
} catch Error(string memory revertReason) {
|
||||
revert FailedOp(opIndex, op.paymaster, revertReason);
|
||||
@@ -282,15 +271,20 @@ contract EntryPoint is StakeManager {
|
||||
function _validatePrepayment(uint opIndex, UserOperation calldata userOp) private returns (uint prefund, PaymentMode paymentMode, bytes memory context){
|
||||
|
||||
uint preGas = gasleft();
|
||||
uint gasUsedByPayForSelfOp;
|
||||
uint gasUsedByValidateUserOp;
|
||||
uint requiredPreFund;
|
||||
(requiredPreFund, paymentMode) = getPaymentInfo(userOp);
|
||||
|
||||
(gasUsedByPayForSelfOp, prefund) = _validateWalletPrepayment(opIndex, userOp, requiredPreFund, paymentMode);
|
||||
(gasUsedByValidateUserOp, prefund) = _validateWalletPrepayment(opIndex, userOp, requiredPreFund, paymentMode);
|
||||
|
||||
//a "marker" where wallet opcode validation is done, by paymaster opcode validation is about to start
|
||||
// (used only by off-chain simulateValidation)
|
||||
uint marker = block.number;
|
||||
(marker);
|
||||
|
||||
uint gasUsedByPayForOp = 0;
|
||||
if (paymentMode == PaymentMode.paymasterStake) {
|
||||
(context, gasUsedByPayForOp) = _validatePaymasterPrepayment(opIndex, userOp, requiredPreFund, gasUsedByPayForSelfOp);
|
||||
(context, gasUsedByPayForOp) = _validatePaymasterPrepayment(opIndex, userOp, requiredPreFund, gasUsedByValidateUserOp);
|
||||
} else {
|
||||
context = "";
|
||||
}
|
||||
@@ -304,7 +298,7 @@ contract EntryPoint is StakeManager {
|
||||
function getPaymastersStake(address[] calldata paymasters) external view returns (uint[] memory _stakes) {
|
||||
_stakes = new uint[](paymasters.length);
|
||||
for (uint i = 0; i < paymasters.length; i++) {
|
||||
_stakes[i] = stakes[paymasters[i]].stake;
|
||||
_stakes[i] = deposits[paymasters[i]].amount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,11 +311,7 @@ contract EntryPoint is StakeManager {
|
||||
revert ("wallet prefund below actualGasCost");
|
||||
}
|
||||
uint refund = prefund - actualGasCost;
|
||||
if (paymentMode == PaymentMode.walletStake) {
|
||||
_refundSenderStake(op, refund);
|
||||
} else {
|
||||
_refundSender(op, refund);
|
||||
}
|
||||
internalIncrementDeposit(op.getSender(), refund);
|
||||
} else {
|
||||
if (context.length > 0) {
|
||||
//if paymaster.postOp reverts:
|
||||
@@ -336,29 +326,12 @@ contract EntryPoint is StakeManager {
|
||||
actualGas += preGas - gasleft();
|
||||
actualGasCost = actualGas * gasPrice;
|
||||
//paymaster balance known to be high enough, and to be locked for this block
|
||||
stakes[op.paymaster].stake -= uint96(actualGasCost);
|
||||
internalDecrementDeposit(op.paymaster, actualGasCost);
|
||||
}
|
||||
_emitLog(op, actualGasCost, gasPrice, mode == IPaymaster.PostOpMode.opSucceeded);
|
||||
}
|
||||
|
||||
function _emitLog(UserOperation calldata op, uint actualGasCost, uint gasPrice, bool success) internal {
|
||||
bool success = mode == IPaymaster.PostOpMode.opSucceeded;
|
||||
emit UserOperationEvent(op.getSender(), op.paymaster, op.nonce, actualGasCost, gasPrice, success);
|
||||
}
|
||||
|
||||
function _prefundFromSender(UserOperation calldata userOp, uint requiredPrefund) internal {
|
||||
stakes[userOp.getSender()].stake -= uint96(requiredPrefund);
|
||||
}
|
||||
|
||||
function _refundSender(UserOperation calldata userOp, uint refund) internal {
|
||||
//NOTE: deliberately ignoring revert: wallet should accept refund.
|
||||
bool sendOk = payable(userOp.getSender()).send(refund);
|
||||
(sendOk);
|
||||
}
|
||||
|
||||
function _refundSenderStake(UserOperation calldata userOp, uint refund) internal {
|
||||
stakes[userOp.getSender()].stake += uint96(refund);
|
||||
}
|
||||
|
||||
//validate a paymaster has enough stake (including for payment for this TX)
|
||||
// NOTE: when submitting a batch, caller has to make sure a paymaster has enough stake to cover
|
||||
// all its transactions in the batch.
|
||||
|
||||
@@ -3,14 +3,12 @@ pragma solidity ^0.8.7;
|
||||
|
||||
import "./UserOperation.sol";
|
||||
|
||||
/**
|
||||
* the interface exposed by a paymaster contract, who agrees to pay the gas for user's operations.
|
||||
* a paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction.
|
||||
*/
|
||||
interface IPaymaster {
|
||||
|
||||
enum PostOpMode {
|
||||
opSucceeded, // user op succeeded
|
||||
opReverted, // user op reverted. still has to pay for gas.
|
||||
postOpReverted //user op succeeded, but caused postOp to revert. Now its a 2nd call, after user's op was deliberately reverted.
|
||||
}
|
||||
|
||||
/**
|
||||
* payment validation: check if paymaster agree to pay (using its stake)
|
||||
* revert to reject this request.
|
||||
@@ -20,7 +18,7 @@ interface IPaymaster {
|
||||
* @return context value to send to a postOp
|
||||
* zero length to signify postOp is not required.
|
||||
*/
|
||||
function verifyPaymasterUserOp(UserOperation calldata userOp, uint maxCost) external view returns (bytes memory context);
|
||||
function validatePaymasterUserOp(UserOperation calldata userOp, uint maxCost) external view returns (bytes memory context);
|
||||
|
||||
/**
|
||||
* post-operation handler.
|
||||
@@ -30,8 +28,14 @@ interface IPaymaster {
|
||||
* opReverted - user op reverted. still has to pay for gas.
|
||||
* postOpReverted - user op succeeded, but caused postOp (in mode=opSucceeded) to revert.
|
||||
* Now this is the 2nd call, after user's op was deliberately reverted.
|
||||
* @param context - the context value returned by verifyPaymasterUserOp
|
||||
* @param context - the context value returned by validatePaymasterUserOp
|
||||
* @param actualGasCost - actual gas used so far (without this postOp call).
|
||||
*/
|
||||
function postOp(PostOpMode mode, bytes calldata context, uint actualGasCost) external;
|
||||
|
||||
enum PostOpMode {
|
||||
opSucceeded, // user op succeeded
|
||||
opReverted, // user op reverted. still has to pay for gas.
|
||||
postOpReverted //user op succeeded, but caused postOp to revert. Now its a 2nd call, after user's op was deliberately reverted.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ import "./UserOperation.sol";
|
||||
interface IWallet {
|
||||
|
||||
/**
|
||||
* validate user's signature and nonce
|
||||
* must accept calls ONLY from entrypoint
|
||||
* Validate user's signature and nonce
|
||||
* the entryPoint will make the call to the recipient only if this validation call returns successfuly.
|
||||
*
|
||||
* @dev Must validate caller is the entryPoint.
|
||||
* Must validate the signature and nonce
|
||||
* @param userOp the operation that is about to be executed.
|
||||
* @param requiredPrefund how much this wallet should pre-fund the transaction.
|
||||
* Should send this amount to sender (entrypoint)
|
||||
* After execution, the excess is sent back to the wallet.
|
||||
* @dev if requiredPrefund is zero, the wallet MUST NOT send anything (the paymaster pays)
|
||||
* @param requiredPrefund the minimum amount to transfer to the sender(entryPoint) to be able to make the call.
|
||||
* The excess is left as a deposit in the entrypoint, for future calls.
|
||||
* can be withdrawn anytime using "entryPoint.withdrawTo()"
|
||||
* In case there is a paymaster in the request (or the current deposit is high enough), this value will be zero.
|
||||
*/
|
||||
function verifyUserOp(UserOperation calldata userOp, uint requiredPrefund) external;
|
||||
function validateUserOp(UserOperation calldata userOp, uint requiredPrefund) external;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ pragma solidity ^0.8;
|
||||
|
||||
import "hardhat/console.sol";
|
||||
|
||||
/**
|
||||
* manage deposit of sender or paymaster, to pay for gas.
|
||||
* paymaster must stake some of the deposit.
|
||||
*/
|
||||
contract StakeManager {
|
||||
|
||||
/// minimum number of blocks to after 'unlock' before amount can be withdrawn.
|
||||
@@ -12,94 +16,130 @@ contract StakeManager {
|
||||
unstakeDelaySec = _unstakeDelaySec;
|
||||
}
|
||||
|
||||
event StakeAdded(
|
||||
address indexed paymaster,
|
||||
uint256 totalStake,
|
||||
event Deposited(
|
||||
address indexed account,
|
||||
uint256 totalDeposit,
|
||||
uint256 unstakeDelaySec
|
||||
);
|
||||
|
||||
|
||||
/// Emitted once a stake is scheduled for withdrawal
|
||||
event StakeUnlocking(
|
||||
address indexed paymaster,
|
||||
event DepositUnstaked(
|
||||
address indexed account,
|
||||
uint256 withdrawTime
|
||||
);
|
||||
|
||||
event StakeWithdrawn(
|
||||
address indexed paymaster,
|
||||
event Withdrawn(
|
||||
address indexed account,
|
||||
address withdrawAddress,
|
||||
uint256 amount
|
||||
uint256 withdrawAmount
|
||||
);
|
||||
|
||||
/// @param stake - amount of ether staked for this paymaster
|
||||
/// @param withdrawStake - once 'unlocked' the value is no longer staked.
|
||||
/// @param withdrawTime - first block timestamp where 'withdraw' will be callable, or zero if the unlock has not been called
|
||||
struct StakeInfo {
|
||||
uint96 stake;
|
||||
/// @param amount of ether deposited for this account
|
||||
/// @param unstakeDelaySec - time the deposit is locked, after calling unlock (or zero if deposit is not locked)
|
||||
/// @param withdrawTime - first block timestamp where 'withdrawTo' will be callable, or zero if not locked
|
||||
struct DepositInfo {
|
||||
uint112 amount;
|
||||
uint32 unstakeDelaySec;
|
||||
uint96 withdrawStake;
|
||||
uint32 withdrawTime;
|
||||
uint64 withdrawTime;
|
||||
}
|
||||
|
||||
/// maps relay managers to their stakes
|
||||
mapping(address => StakeInfo) public stakes;
|
||||
/// maps accounts to their deposits
|
||||
mapping(address => DepositInfo) public deposits;
|
||||
|
||||
function getStakeInfo(address paymaster) external view returns (StakeInfo memory stakeInfo) {
|
||||
return stakes[paymaster];
|
||||
function getDepositInfo(address account) external view returns (DepositInfo memory info) {
|
||||
return deposits[account];
|
||||
}
|
||||
|
||||
function balanceOf(address account) public view returns (uint) {
|
||||
return deposits[account].amount;
|
||||
}
|
||||
|
||||
receive() external payable {
|
||||
depositTo(msg.sender);
|
||||
}
|
||||
|
||||
function internalIncrementDeposit(address account, uint amount) internal {
|
||||
deposits[account].amount += uint112(amount);
|
||||
}
|
||||
|
||||
function internalDecrementDeposit(address account, uint amount) internal {
|
||||
deposits[account].amount -= uint112(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* add a deposit (just like stake, but with lock=0
|
||||
* cancel any pending unlock
|
||||
* add to the deposit of the given account
|
||||
*/
|
||||
function addDeposit() external payable {
|
||||
addStake(0);
|
||||
}
|
||||
|
||||
//add deposit to another account (doesn't change lock status)
|
||||
function addDepositTo(address target) external payable {
|
||||
stakes[target].stake += uint96(msg.value);
|
||||
function depositTo(address account) public payable {
|
||||
internalIncrementDeposit(account, msg.value);
|
||||
DepositInfo storage info = deposits[account];
|
||||
emit Deposited(msg.sender, info.amount, info.unstakeDelaySec);
|
||||
}
|
||||
|
||||
/**
|
||||
* add stake value for this paymaster.
|
||||
* cancel any pending unlock
|
||||
* stake the account's deposit.
|
||||
* any pending unstakeDeposit is first cancelled.
|
||||
* can also set (or increase) the deposit with call.
|
||||
* @param _unstakeDelaySec the new lock time before the deposit can be withdrawn.
|
||||
*/
|
||||
function addStake(uint32 _unstakeDelaySec) public payable {
|
||||
require(_unstakeDelaySec >= stakes[msg.sender].unstakeDelaySec, "cannot decrease unstake time");
|
||||
uint96 stake = uint96(stakes[msg.sender].stake + msg.value + stakes[msg.sender].withdrawStake);
|
||||
stakes[msg.sender] = StakeInfo(
|
||||
stake,
|
||||
DepositInfo storage info = deposits[msg.sender];
|
||||
require(_unstakeDelaySec >= info.unstakeDelaySec, "cannot decrease unstake time");
|
||||
uint112 amount = deposits[msg.sender].amount + uint112(msg.value);
|
||||
deposits[msg.sender] = DepositInfo(
|
||||
amount,
|
||||
_unstakeDelaySec,
|
||||
0,
|
||||
0);
|
||||
emit StakeAdded(msg.sender, stake, _unstakeDelaySec);
|
||||
emit Deposited(msg.sender, amount, _unstakeDelaySec);
|
||||
}
|
||||
|
||||
function unlockStake() external {
|
||||
StakeInfo storage info = stakes[msg.sender];
|
||||
require(info.withdrawTime == 0, "already pending");
|
||||
require(info.stake != 0 && info.unstakeDelaySec != 0, "no stake to unlock");
|
||||
uint32 withdrawTime = uint32(block.timestamp) + info.unstakeDelaySec;
|
||||
/**
|
||||
* attempt to unstake the deposit.
|
||||
* the value can be withdrawn (using withdrawTo) after the unstake delay.
|
||||
*/
|
||||
function unstakeDeposit() external {
|
||||
DepositInfo storage info = deposits[msg.sender];
|
||||
require(info.withdrawTime == 0, "already unstaking");
|
||||
require(info.unstakeDelaySec != 0, "not staked");
|
||||
uint64 withdrawTime = uint64(block.timestamp) + info.unstakeDelaySec;
|
||||
info.withdrawTime = withdrawTime;
|
||||
info.withdrawStake = info.stake;
|
||||
info.stake = 0;
|
||||
emit StakeUnlocking(msg.sender, withdrawTime);
|
||||
emit DepositUnstaked(msg.sender, withdrawTime);
|
||||
}
|
||||
|
||||
function withdrawStake(address payable withdrawAddress) external {
|
||||
StakeInfo memory info = stakes[msg.sender];
|
||||
/**
|
||||
* withdraw from the deposit.
|
||||
* will fail if the deposit is already staked or too low.
|
||||
* after a paymaster unlocks and withdraws some of the value, it must call addStake() to stake the value again.
|
||||
* @param withdrawAddress the address to send withdrawn value.
|
||||
* @param withdrawAmount the amount to withdraw.
|
||||
*/
|
||||
function withdrawTo(address payable withdrawAddress, uint withdrawAmount) external {
|
||||
DepositInfo memory info = deposits[msg.sender];
|
||||
if (info.unstakeDelaySec != 0) {
|
||||
require(info.withdrawStake > 0, "no unlocked stake");
|
||||
require(info.withdrawTime > 0, "must call unstakeDeposit() first");
|
||||
require(info.withdrawTime <= block.timestamp, "Withdrawal is not due");
|
||||
}
|
||||
uint256 amount = info.withdrawStake + info.stake;
|
||||
stakes[msg.sender] = StakeInfo(0, info.unstakeDelaySec, 0, 0);
|
||||
withdrawAddress.transfer(amount);
|
||||
emit StakeWithdrawn(msg.sender, withdrawAddress, amount);
|
||||
require(withdrawAmount <= info.amount, "Withdraw amount too large");
|
||||
|
||||
// store the remaining value, with stake info cleared.
|
||||
deposits[msg.sender] = DepositInfo(
|
||||
info.amount - uint112(withdrawAmount),
|
||||
0,
|
||||
0);
|
||||
withdrawAddress.transfer(withdrawAmount);
|
||||
emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount);
|
||||
}
|
||||
|
||||
function isStaked(address paymaster, uint requiredStake, uint requiredDelaySec) public view returns (bool) {
|
||||
StakeInfo memory stakeInfo = stakes[paymaster];
|
||||
return stakeInfo.stake >= requiredStake && stakeInfo.unstakeDelaySec >= requiredDelaySec;
|
||||
/**
|
||||
* check if the given account is staked and didn't unlock it yet.
|
||||
* @param account the account (paymaster) to check
|
||||
* @param requiredStake the minimum deposit
|
||||
* @param requiredDelaySec the minimum required stake time.
|
||||
*/
|
||||
function isStaked(address account, uint requiredStake, uint requiredDelaySec) public view returns (bool) {
|
||||
DepositInfo memory info = deposits[account];
|
||||
return info.amount >= requiredStake &&
|
||||
info.unstakeDelaySec >= requiredDelaySec &&
|
||||
info.withdrawTime == 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@ library UserOperationLib {
|
||||
uint maxPriorityFeePerGas = userOp.maxPriorityFeePerGas;
|
||||
if (maxFeePerGas == maxPriorityFeePerGas) {
|
||||
//legacy mode (for networks that don't support basefee opcode)
|
||||
return maxFeePerGas;
|
||||
return min(tx.gasprice, maxFeePerGas);
|
||||
}
|
||||
return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
|
||||
return min(tx.gasprice, min(maxFeePerGas, maxPriorityFeePerGas + block.basefee));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,9 @@ library UserOperationLib {
|
||||
);
|
||||
}
|
||||
|
||||
function hash(UserOperation calldata userOp) internal pure returns (bytes32) {
|
||||
function hash(UserOperation calldata userOp) internal view returns (bytes32) {
|
||||
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32",
|
||||
keccak256(pack(userOp))));
|
||||
keccak256(abi.encodePacked(pack(userOp), block.chainid))));
|
||||
}
|
||||
|
||||
function min(uint a, uint b) internal pure returns (uint) {
|
||||
|
||||
@@ -63,7 +63,7 @@ contract SimpleWallet is IWallet {
|
||||
require(msg.sender == address(entryPoint), "wallet: not from EntryPoint");
|
||||
}
|
||||
|
||||
function verifyUserOp(UserOperation calldata userOp, uint requiredPrefund) external override {
|
||||
function validateUserOp(UserOperation calldata userOp, uint requiredPrefund) external override {
|
||||
_requireFromEntryPoint();
|
||||
_validateSignature(userOp);
|
||||
_validateAndIncrementNonce(userOp);
|
||||
@@ -72,13 +72,12 @@ contract SimpleWallet is IWallet {
|
||||
|
||||
function _payPrefund(uint requiredPrefund) internal {
|
||||
if (requiredPrefund != 0) {
|
||||
(bool success) = payable(msg.sender).send(requiredPrefund);
|
||||
(success);
|
||||
//ignore failure (its EntryPoint's job to verify, not wallet.)
|
||||
(bool success,) = payable(msg.sender).call{value : requiredPrefund}("");
|
||||
(success); //ignore failure (its EntryPoint's job to verify, not wallet.)
|
||||
}
|
||||
}
|
||||
|
||||
//called by entryPoint, only after verifyUserOp succeeded.
|
||||
//called by entryPoint, only after validateUserOp succeeded.
|
||||
function execFromEntryPoint(address dest, uint value, bytes calldata func) external {
|
||||
_requireFromEntryPoint();
|
||||
_call(dest, value, func);
|
||||
@@ -123,11 +122,17 @@ contract SimpleWallet is IWallet {
|
||||
}
|
||||
}
|
||||
|
||||
function addDeposit() public payable {
|
||||
entryPoint.addDeposit{value : msg.value}();
|
||||
function getDeposit() public view returns (uint) {
|
||||
return entryPoint.balanceOf(address(this));
|
||||
}
|
||||
|
||||
function withdrawDeposit(address payable withdrawAddress) public {
|
||||
entryPoint.withdrawStake(withdrawAddress);
|
||||
function addDeposit() public payable {
|
||||
|
||||
(bool req,) = address(entryPoint).call{value : msg.value}("");
|
||||
require(req);
|
||||
}
|
||||
|
||||
function withdrawDepsitTo(address payable withdrawAddress, uint amount) public {
|
||||
entryPoint.withdrawTo(withdrawAddress, amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ contract TokenPaymaster is Ownable, ERC20, IPaymaster {
|
||||
_mint(recipient, amount);
|
||||
}
|
||||
|
||||
//owner should call and put eth into it.
|
||||
/**
|
||||
* add stake for this paymaster, using the unstake delay defined by the entryPoint.
|
||||
*/
|
||||
function addStake() external payable {
|
||||
entryPoint.addStake{value : msg.value}(entryPoint.unstakeDelaySec());
|
||||
}
|
||||
@@ -44,7 +46,7 @@ contract TokenPaymaster is Ownable, ERC20, IPaymaster {
|
||||
}
|
||||
|
||||
// verify that the user has enough tokens.
|
||||
function verifyPaymasterUserOp(UserOperation calldata userOp, uint requiredPreFund) external view override returns (bytes memory context) {
|
||||
function validatePaymasterUserOp(UserOperation calldata userOp, uint requiredPreFund) external view override returns (bytes memory context) {
|
||||
uint tokenPrefund = ethToToken(requiredPreFund);
|
||||
|
||||
if (userOp.initCode.length != 0) {
|
||||
|
||||
@@ -31,7 +31,7 @@ contract VerifyingPaymaster is IPaymaster {
|
||||
|
||||
// verify our external signer signed this request.
|
||||
// the "paymasterData" is supposed to be a signature over the entire request params
|
||||
function verifyPaymasterUserOp(UserOperation calldata userOp, uint requiredPreFund) external view override returns (bytes memory context) {
|
||||
function validatePaymasterUserOp(UserOperation calldata userOp, uint requiredPreFund) external view override returns (bytes memory context) {
|
||||
(requiredPreFund);
|
||||
|
||||
bytes32 hash = userOp.hash();
|
||||
@@ -46,7 +46,7 @@ contract VerifyingPaymaster is IPaymaster {
|
||||
}
|
||||
|
||||
function postOp(PostOpMode, bytes calldata, uint) external pure override {
|
||||
//should never get called. returned "0" from verifyPaymasterUserOp
|
||||
//should never get called. returned "0" from validatePaymasterUserOp
|
||||
revert();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {arrayify, defaultAbiCoder, keccak256} from "ethers/lib/utils";
|
||||
import {BigNumber, Contract, Signer, Wallet} from "ethers";
|
||||
import {BigNumber, Contract, ethers, Signer, Wallet} from "ethers";
|
||||
import {AddressZero, callDataCost, rethrow} from "./testutils";
|
||||
import {ecsign, toRpcSig, keccak256 as keccak256_buffer} from "ethereumjs-util";
|
||||
import {
|
||||
@@ -119,8 +119,10 @@ export const DefaultsForUserOp: UserOperation = {
|
||||
signature: '0x'
|
||||
}
|
||||
|
||||
export function signUserOp(op: UserOperation, signer: Wallet): UserOperation {
|
||||
let packed = packUserOp(op);
|
||||
export function signUserOp(op: UserOperation, signer: Wallet, chainId: number): UserOperation {
|
||||
const chainIdPadded = chainId!.toString(16).padStart(64,'0')
|
||||
let packed = packUserOp(op) + chainIdPadded
|
||||
|
||||
let message = Buffer.from(arrayify(keccak256(packed)));
|
||||
let msg1 = Buffer.concat([
|
||||
Buffer.from("\x19Ethereum Signed Message:\n32", 'ascii'),
|
||||
@@ -210,7 +212,9 @@ export async function fillAndSign(op: Partial<UserOperation>, signer: Wallet | S
|
||||
op2.preVerificationGas = callDataCost(packUserOp(op2, false))
|
||||
}
|
||||
|
||||
let packed = packUserOp(op2);
|
||||
const chainId = await provider?.getNetwork().then(net=>net.chainId)
|
||||
const chainIdPadded = chainId!.toString(16).padStart(64,'0')
|
||||
let packed = packUserOp(op2).concat(chainIdPadded)
|
||||
let message = Buffer.from(arrayify(keccak256(packed)));
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,3 +3,4 @@ export const inspect_custom_symbol = Symbol.for('nodejs.util.inspect.custom')
|
||||
// @ts-ignore
|
||||
ethers.BigNumber.prototype[inspect_custom_symbol] = function() { return `BigNumber ${parseInt(this._hex)}`}
|
||||
|
||||
import './chaiHelper'
|
||||
65
test/chaiHelper.ts
Normal file
65
test/chaiHelper.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// remap "eql" function to work nicely with EVM values.
|
||||
|
||||
// cleanup "Result" object (returned on web3/ethers calls)
|
||||
// remove "array" members, convert values to strings.
|
||||
// so Result obj like
|
||||
// { '0': "a", '1': 20, first: "a", second: 20 }
|
||||
// becomes:
|
||||
// { first: "a", second: "20" }
|
||||
// map values inside object using mapping func.
|
||||
import chai from 'chai'
|
||||
|
||||
export function objValues (obj: { [key: string]: any }, mapFunc: (val: any, key?: string) => any): any {
|
||||
return Object.keys(obj)
|
||||
.filter(key => key.match(/^[\d_]/) == null)
|
||||
.reduce((set, key) => ({
|
||||
...set,
|
||||
[key]: mapFunc(obj[key], key)
|
||||
}), {})
|
||||
}
|
||||
|
||||
/**
|
||||
* cleanup a value of an object, for easier testing.
|
||||
* - Result: this is an array which also contains named members.
|
||||
* - obj.length*2 == Object.keys().length
|
||||
* - remove the array elements, use just the named ones.
|
||||
* - recursively handle inner members of object, arrays.
|
||||
* - attempt toString. but if no normal value, recurse into fields.
|
||||
*/
|
||||
export function cleanValue (val: any): any {
|
||||
if (val == null) return val
|
||||
if (Array.isArray(val)) {
|
||||
if (val.length * 2 === Object.keys(val).length) {
|
||||
// "looks" like a Result object.
|
||||
return objValues(val, cleanValue)
|
||||
}
|
||||
// its a plain array. map each array element
|
||||
return val.map(val1 => cleanValue(val1))
|
||||
}
|
||||
|
||||
const str = val.toString()
|
||||
if (str !== '[object Object]') { return str }
|
||||
|
||||
return objValues(val, cleanValue)
|
||||
}
|
||||
|
||||
// use cleanValue for comparing. MUCH easier, since numbers compare well with bignumbers, etc
|
||||
|
||||
chai.Assertion.overwriteMethod('eql', (original) => {
|
||||
// @ts-ignore
|
||||
return function (this: any, expected: any) {
|
||||
const _actual = cleanValue(this._obj)
|
||||
const _expected = cleanValue(expected)
|
||||
// original.apply(this,arguments)
|
||||
this._obj = _actual
|
||||
original.apply(this, [_expected])
|
||||
// assert.deepEqual(_actual, _expected)
|
||||
// ctx.assert(
|
||||
// _actual == _expected,
|
||||
// 'expected #{act} to equal #{exp}',
|
||||
// 'expected #{act} to be different from #{exp}',
|
||||
// _expected,
|
||||
// _actual
|
||||
// );
|
||||
}
|
||||
})
|
||||
@@ -24,13 +24,13 @@ import {
|
||||
ONE_ETH,
|
||||
TWO_ETH,
|
||||
deployEntryPoint,
|
||||
getBalance
|
||||
getBalance, FIVE_ETH
|
||||
} from "./testutils";
|
||||
import {fillAndSign} from "./UserOp";
|
||||
import {UserOperation} from "./UserOperation";
|
||||
import {PopulatedTransaction} from "ethers/lib/ethers";
|
||||
import {ethers} from 'hardhat'
|
||||
import {parseEther} from "ethers/lib/utils";
|
||||
import {formatEther, parseEther} from "ethers/lib/utils";
|
||||
import {debugTransaction} from './debugTx';
|
||||
|
||||
describe("EntryPoint", function () {
|
||||
@@ -43,7 +43,7 @@ describe("EntryPoint", function () {
|
||||
let ethersSigner = ethers.provider.getSigner();
|
||||
let wallet: SimpleWallet
|
||||
|
||||
const unstakeDelaySec = 2
|
||||
const globalUnstakeDelaySec = 2
|
||||
const paymasterStake = ethers.utils.parseEther('1')
|
||||
|
||||
before(async function () {
|
||||
@@ -51,7 +51,7 @@ describe("EntryPoint", function () {
|
||||
await checkForGeth()
|
||||
|
||||
testUtil = await new TestUtil__factory(ethersSigner).deploy()
|
||||
entryPoint = await deployEntryPoint(paymasterStake, unstakeDelaySec)
|
||||
entryPoint = await deployEntryPoint(paymasterStake, globalUnstakeDelaySec)
|
||||
|
||||
//static call must come from address zero, to validate it can only be called off-chain.
|
||||
entryPointView = entryPoint.connect(ethers.provider.getSigner(AddressZero))
|
||||
@@ -66,12 +66,23 @@ describe("EntryPoint", function () {
|
||||
addr = await ethersSigner.getAddress()
|
||||
})
|
||||
|
||||
it('should deposit for transfer into EntryPoint', async () => {
|
||||
let signer2 = ethers.provider.getSigner(2);
|
||||
await signer2.sendTransaction({to: entryPoint.address, value: ONE_ETH})
|
||||
expect(await entryPoint.balanceOf(await signer2.getAddress())).to.eql(ONE_ETH)
|
||||
expect(await entryPoint.getDepositInfo(await signer2.getAddress())).to.eql({
|
||||
amount: ONE_ETH,
|
||||
unstakeDelaySec: 0,
|
||||
withdrawTime: 0
|
||||
})
|
||||
});
|
||||
|
||||
describe('without stake', () => {
|
||||
it('should return no stake', async () => {
|
||||
expect(await entryPoint.isPaymasterStaked(addr, TWO_ETH)).to.eq(false)
|
||||
})
|
||||
it('should fail to unlock', async () => {
|
||||
await expect(entryPoint.unlockStake()).to.revertedWith('no stake')
|
||||
await expect(entryPoint.unstakeDeposit()).to.revertedWith('not staked')
|
||||
})
|
||||
})
|
||||
describe('with stake of 2 eth', () => {
|
||||
@@ -80,47 +91,46 @@ describe("EntryPoint", function () {
|
||||
})
|
||||
it('should report "staked" state', async () => {
|
||||
expect(await entryPoint.isPaymasterStaked(addr, 0)).to.eq(true)
|
||||
const {stake, withdrawStake, withdrawTime} = await entryPoint.getStakeInfo(addr)
|
||||
expect({stake, withdrawStake, withdrawTime}).to.eql({
|
||||
stake: parseEther('2'),
|
||||
withdrawStake: BigNumber.from(0),
|
||||
const {amount, unstakeDelaySec, withdrawTime} = await entryPoint.getDepositInfo(addr)
|
||||
expect({amount, unstakeDelaySec, withdrawTime}).to.eql({
|
||||
amount: parseEther('2'),
|
||||
unstakeDelaySec: 2,
|
||||
withdrawTime: 0
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
it('should succeed to stake again', async () => {
|
||||
const {stake} = await entryPoint.getStakeInfo(addr)
|
||||
expect(stake).to.eq(TWO_ETH)
|
||||
const {amount} = await entryPoint.getDepositInfo(addr)
|
||||
await entryPoint.addStake(2, {value: ONE_ETH})
|
||||
const {stake: stakeAfter} = await entryPoint.getStakeInfo(addr)
|
||||
expect(stakeAfter).to.eq(parseEther('3'))
|
||||
const {amount: amountAfter} = await entryPoint.getDepositInfo(addr)
|
||||
expect(amountAfter).to.eq(amount.add(ONE_ETH))
|
||||
})
|
||||
it('should fail to withdraw before unlock', async () => {
|
||||
await expect(entryPoint.withdrawStake(AddressZero)).to.revertedWith('no unlocked stake')
|
||||
await expect(entryPoint.withdrawTo(AddressZero, ONE_ETH)).to.revertedWith('must call unstakeDeposit() first')
|
||||
})
|
||||
describe('with unlocked stake', () => {
|
||||
before(async () => {
|
||||
await entryPoint.unlockStake()
|
||||
await entryPoint.unstakeDeposit()
|
||||
})
|
||||
it('should report as "not staked"', async () => {
|
||||
expect(await entryPoint.isPaymasterStaked(addr, TWO_ETH)).to.eq(false)
|
||||
})
|
||||
it('should report unstake state', async () => {
|
||||
const withdrawTime1 = await ethers.provider.getBlock('latest').then(block => block.timestamp) + unstakeDelaySec
|
||||
const {stake, withdrawStake, withdrawTime} = await entryPoint.getStakeInfo(addr)
|
||||
expect({stake, withdrawStake, withdrawTime}).to.eql({
|
||||
stake: BigNumber.from(0),
|
||||
withdrawStake: parseEther('3'),
|
||||
const withdrawTime1 = await ethers.provider.getBlock('latest').then(block => block.timestamp) + globalUnstakeDelaySec
|
||||
const {amount, unstakeDelaySec, withdrawTime} = await entryPoint.getDepositInfo(addr)
|
||||
expect({amount, unstakeDelaySec, withdrawTime}).to.eql({
|
||||
amount: parseEther('3'),
|
||||
unstakeDelaySec: 2,
|
||||
withdrawTime: withdrawTime1
|
||||
})
|
||||
expect(await entryPoint.isPaymasterStaked(addr, TWO_ETH)).to.eq(false)
|
||||
})
|
||||
it('should fail to withdraw before unlock timeout', async () => {
|
||||
await expect(entryPoint.withdrawStake(AddressZero)).to.revertedWith('Withdrawal is not due')
|
||||
await expect(entryPoint.withdrawTo(AddressZero, ONE_ETH)).to.revertedWith('Withdrawal is not due')
|
||||
})
|
||||
it('should fail to unlock again', async () => {
|
||||
await expect(entryPoint.unlockStake()).to.revertedWith('already pending')
|
||||
await expect(entryPoint.unstakeDeposit()).to.revertedWith('already unstaking')
|
||||
})
|
||||
describe('after unstake delay', () => {
|
||||
before(async () => {
|
||||
@@ -135,10 +145,10 @@ describe("EntryPoint", function () {
|
||||
|
||||
await ethersSigner.sendTransaction({to: addr})
|
||||
await entryPoint.addStake(2, {value: ONE_ETH})
|
||||
const {stake, withdrawStake, withdrawTime} = await entryPoint.getStakeInfo(addr)
|
||||
expect({stake, withdrawStake, withdrawTime}).to.eql({
|
||||
stake: parseEther('4'),
|
||||
withdrawStake: parseEther('0'),
|
||||
const {amount, unstakeDelaySec, withdrawTime} = await entryPoint.getDepositInfo(addr)
|
||||
expect({amount, unstakeDelaySec, withdrawTime}).to.eql({
|
||||
amount: parseEther('4'),
|
||||
unstakeDelaySec: 2,
|
||||
withdrawTime: 0
|
||||
})
|
||||
} finally {
|
||||
@@ -150,23 +160,36 @@ describe("EntryPoint", function () {
|
||||
expect(await entryPoint.isPaymasterStaked(addr, TWO_ETH)).to.eq(false)
|
||||
})
|
||||
it('should fail to unlock again', async () => {
|
||||
await expect(entryPoint.unlockStake()).to.revertedWith('already pending')
|
||||
await expect(entryPoint.unstakeDeposit()).to.revertedWith('already unstaking')
|
||||
})
|
||||
it('should succeed to withdraw', async () => {
|
||||
const {withdrawStake} = await entryPoint.getStakeInfo(addr)
|
||||
it( 'should fail to withdraw too much ', async()=>{
|
||||
await expect(entryPoint.withdrawTo(AddressZero, FIVE_ETH)).to.revertedWith('Withdraw amount too large')
|
||||
})
|
||||
it('should succeed to withdraw some deposit', async () => {
|
||||
const {amount} = await entryPoint.getDepositInfo(addr)
|
||||
const addr1 = createWalletOwner().address
|
||||
await entryPoint.withdrawStake(addr1)
|
||||
expect(await ethers.provider.getBalance(addr1)).to.eq(withdrawStake)
|
||||
const {stake, withdrawStake: withdrawStakeAfter, withdrawTime} = await entryPoint.getStakeInfo(addr)
|
||||
await entryPoint.withdrawTo(addr1, ONE_ETH)
|
||||
expect(await ethers.provider.getBalance(addr1)).to.eq(ONE_ETH)
|
||||
const {amount: amountAfter, withdrawTime, unstakeDelaySec} = await entryPoint.getDepositInfo(addr)
|
||||
|
||||
expect({stake, withdrawStakeAfter, withdrawTime}).to.eql({
|
||||
stake: BigNumber.from(0),
|
||||
withdrawStakeAfter: BigNumber.from(0),
|
||||
expect({amountAfter, withdrawTime, unstakeDelaySec}).to.eql({
|
||||
amountAfter: amount.sub(ONE_ETH),
|
||||
unstakeDelaySec: 0,
|
||||
withdrawTime: 0
|
||||
})
|
||||
})
|
||||
it('should fail to withdraw again', async () => {
|
||||
await expect(entryPoint.withdrawStake(AddressZero)).to.revertedWith('no unlocked stake')
|
||||
it('should succeed to withdraw the rest', async () => {
|
||||
const {amount} = await entryPoint.getDepositInfo(addr)
|
||||
const addr1 = createWalletOwner().address
|
||||
await entryPoint.withdrawTo(addr1, amount)
|
||||
expect(await ethers.provider.getBalance(addr1)).to.eq(amount)
|
||||
const {amount: amountAfter, withdrawTime, unstakeDelaySec} = await entryPoint.getDepositInfo(addr)
|
||||
|
||||
expect({amountAfter, withdrawTime, unstakeDelaySec}).to.eql({
|
||||
amountAfter: BigNumber.from(0),
|
||||
unstakeDelaySec: 0,
|
||||
withdrawTime: 0
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -180,16 +203,17 @@ describe("EntryPoint", function () {
|
||||
await wallet.addDeposit({value: ONE_ETH})
|
||||
expect(await getBalance(wallet.address)).to.equal(0)
|
||||
})
|
||||
it('should fail to unlock deposit (its not locked)', async () => {
|
||||
it('should fail to unstakeDeposit if not staked', async () => {
|
||||
//wallet doesn't have "unlock" api, so we test it with static call.
|
||||
await expect(entryPoint.connect(wallet.address).callStatic.unlockStake()).to.revertedWith('no stake')
|
||||
await expect(entryPoint.connect(wallet.address).callStatic.unstakeDeposit()).to.revertedWith('not staked')
|
||||
})
|
||||
it('should withdraw with no unlock', async () => {
|
||||
await wallet.withdrawDeposit(wallet.address)
|
||||
await wallet.withdrawDepsitTo(wallet.address, ONE_ETH)
|
||||
expect(await getBalance(wallet.address)).to.equal(1e18)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#simulateWalletValidation', () => {
|
||||
const walletOwner1 = createWalletOwner()
|
||||
let wallet1: SimpleWallet
|
||||
@@ -201,13 +225,13 @@ describe("EntryPoint", function () {
|
||||
const op = await fillAndSign({sender: wallet1.address}, walletOwner1, entryPoint)
|
||||
await expect(entryPoint.simulateWalletValidation(op)).to.revertedWith('must be called off-chain')
|
||||
});
|
||||
it('should fail if verifyUserOp fails', async () => {
|
||||
it('should fail if validateUserOp fails', async () => {
|
||||
//using wrong owner for wallet1
|
||||
const op = await fillAndSign({sender: wallet1.address}, walletOwner, entryPoint)
|
||||
await expect(entryPointView.callStatic.simulateWalletValidation(op).catch(rethrow())).to
|
||||
.revertedWith('wrong signature')
|
||||
});
|
||||
it('should succeed if verifyUserOp succeeds', async () => {
|
||||
it('should succeed if validateUserOp succeeds', async () => {
|
||||
const op = await fillAndSign({sender: wallet1.address}, walletOwner1, entryPoint)
|
||||
await fund(wallet1)
|
||||
const ret = await entryPointView.callStatic.simulateWalletValidation(op).catch(rethrow())
|
||||
@@ -230,7 +254,7 @@ describe("EntryPoint", function () {
|
||||
await entryPointView.callStatic.simulateWalletValidation(op1).catch(rethrow())
|
||||
})
|
||||
|
||||
it('should not use banned ops during simulateWalletValidation', async () => {
|
||||
it('should not use banned ops during simulateValidation', async () => {
|
||||
const op1 = await fillAndSign({
|
||||
initCode: WalletConstructor(entryPoint.address, walletOwner1.address),
|
||||
}, walletOwner1, entryPoint)
|
||||
@@ -262,15 +286,15 @@ describe("EntryPoint", function () {
|
||||
verificationGas: 1e6,
|
||||
callGas: 1e6
|
||||
}, walletOwner, entryPoint)
|
||||
const redeemerAddress = Wallet.createRandom().address
|
||||
const beneficiaryAddress = Wallet.createRandom().address
|
||||
|
||||
const countBefore = await counter.counters(wallet.address)
|
||||
//for estimateGas, must specify maxFeePerGas, otherwise our gas check fails
|
||||
console.log(' == est gas=', await entryPoint.estimateGas.handleOps([op], redeemerAddress, {maxFeePerGas: 1e9}).then(tostr))
|
||||
console.log(' == est gas=', await entryPoint.estimateGas.handleOps([op], beneficiaryAddress, {maxFeePerGas: 1e9}).then(tostr))
|
||||
|
||||
//must specify at least on of maxFeePerGas, gasLimit
|
||||
// (gasLimit, to prevent estimateGas to fail on missing maxFeePerGas, see above..)
|
||||
const rcpt = await entryPoint.handleOps([op], redeemerAddress, {
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiaryAddress, {
|
||||
maxFeePerGas: 1e9,
|
||||
gasLimit: 1e7
|
||||
}).then(t => t.wait())
|
||||
@@ -279,7 +303,7 @@ describe("EntryPoint", function () {
|
||||
expect(countAfter.toNumber()).to.equal(countBefore.toNumber() + 1)
|
||||
console.log('rcpt.gasUsed=', rcpt.gasUsed.toString(), rcpt.transactionHash)
|
||||
|
||||
await calcGasUsage(rcpt, entryPoint, redeemerAddress)
|
||||
await calcGasUsage(rcpt, entryPoint, beneficiaryAddress)
|
||||
});
|
||||
|
||||
it('legacy mode (maxPriorityFee==maxFeePerGas) should not use "basefee" opcode', async function () {
|
||||
@@ -291,10 +315,10 @@ describe("EntryPoint", function () {
|
||||
verificationGas: 1e6,
|
||||
callGas: 1e6
|
||||
}, walletOwner, entryPoint)
|
||||
const redeemerAddress = Wallet.createRandom().address
|
||||
const beneficiaryAddress = Wallet.createRandom().address
|
||||
|
||||
// (gasLimit, to prevent estimateGas to fail on missing maxFeePerGas, see above..)
|
||||
const rcpt = await entryPoint.handleOps([op], redeemerAddress, {
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiaryAddress, {
|
||||
maxFeePerGas: 1e9,
|
||||
gasLimit: 1e7
|
||||
}).then(t => t.wait())
|
||||
@@ -312,17 +336,17 @@ describe("EntryPoint", function () {
|
||||
verificationGas: 1e6,
|
||||
callGas: 1e6
|
||||
}, walletOwner, entryPoint)
|
||||
const redeemerAddress = Wallet.createRandom().address
|
||||
const beneficiaryAddress = Wallet.createRandom().address
|
||||
|
||||
const countBefore = await counter.counters(wallet.address)
|
||||
//for estimateGas, must specify maxFeePerGas, otherwise our gas check fails
|
||||
console.log(' == est gas=', await entryPoint.estimateGas.handleOps([op], redeemerAddress, {maxFeePerGas: 1e9}).then(tostr))
|
||||
console.log(' == est gas=', await entryPoint.estimateGas.handleOps([op], beneficiaryAddress, {maxFeePerGas: 1e9}).then(tostr))
|
||||
|
||||
const balBefore = await getBalance(wallet.address)
|
||||
const stakeBefore = await entryPoint.getStakeInfo(wallet.address).then(info => info.stake)
|
||||
const depositBefore = await entryPoint.balanceOf(wallet.address)
|
||||
//must specify at least one of maxFeePerGas, gasLimit
|
||||
// (gasLimit, to prevent estimateGas to fail on missing maxFeePerGas, see above..)
|
||||
const rcpt = await entryPoint.handleOps([op], redeemerAddress, {
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiaryAddress, {
|
||||
maxFeePerGas: 1e9,
|
||||
gasLimit: 1e7
|
||||
}).then(t => t.wait())
|
||||
@@ -332,16 +356,19 @@ describe("EntryPoint", function () {
|
||||
console.log('rcpt.gasUsed=', rcpt.gasUsed.toString(), rcpt.transactionHash)
|
||||
|
||||
const balAfter = await getBalance(wallet.address)
|
||||
const stakeAfter = await entryPoint.getStakeInfo(wallet.address).then(info => info.stake)
|
||||
const depositAfter = await entryPoint.balanceOf(wallet.address)
|
||||
expect(balAfter).to.equal(balBefore, 'should pay from stake, not balance')
|
||||
let stakeUsed = stakeBefore.sub(stakeAfter)
|
||||
expect(await ethers.provider.getBalance(redeemerAddress)).to.equal(stakeUsed)
|
||||
let depositUsed = depositBefore.sub(depositAfter)
|
||||
console.log('redeemer', (await getBalance(beneficiaryAddress))/1e9)
|
||||
// @ts-ignore
|
||||
console.log('depused=', depositUsed/1e9, depositAfter/1e9, depositBefore/1e9)
|
||||
expect(await ethers.provider.getBalance(beneficiaryAddress)).to.equal(depositUsed)
|
||||
|
||||
await calcGasUsage(rcpt, entryPoint, redeemerAddress)
|
||||
await calcGasUsage(rcpt, entryPoint, beneficiaryAddress)
|
||||
});
|
||||
|
||||
it('#handleOp (single)', async () => {
|
||||
const redeemerAddress = Wallet.createRandom().address
|
||||
const beneficiaryAddress = Wallet.createRandom().address
|
||||
|
||||
const op = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
@@ -349,14 +376,14 @@ describe("EntryPoint", function () {
|
||||
}, walletOwner, entryPoint)
|
||||
|
||||
const countBefore = await counter.counters(wallet.address)
|
||||
const rcpt = await entryPoint.handleOp(op, redeemerAddress, {
|
||||
const rcpt = await entryPoint.handleOp(op, beneficiaryAddress, {
|
||||
gasLimit: 1e7
|
||||
}).then(t => t.wait())
|
||||
const countAfter = await counter.counters(wallet.address)
|
||||
expect(countAfter.toNumber()).to.equal(countBefore.toNumber() + 1)
|
||||
|
||||
console.log('rcpt.gasUsed=', rcpt.gasUsed.toString(), rcpt.transactionHash)
|
||||
await calcGasUsage(rcpt, entryPoint, redeemerAddress)
|
||||
await calcGasUsage(rcpt, entryPoint, beneficiaryAddress)
|
||||
|
||||
});
|
||||
})
|
||||
@@ -364,7 +391,7 @@ describe("EntryPoint", function () {
|
||||
describe('create account', () => {
|
||||
let createOp: UserOperation
|
||||
let created = false
|
||||
let redeemerAddress = Wallet.createRandom().address //1
|
||||
let beneficiaryAddress = Wallet.createRandom().address //1
|
||||
|
||||
it('should reject create if sender address is wrong', async () => {
|
||||
|
||||
@@ -374,7 +401,7 @@ describe("EntryPoint", function () {
|
||||
sender: '0x'.padEnd(42, '1')
|
||||
}, walletOwner, entryPoint)
|
||||
|
||||
await expect(entryPoint.callStatic.handleOps([op], redeemerAddress, {
|
||||
await expect(entryPoint.callStatic.handleOps([op], beneficiaryAddress, {
|
||||
gasLimit: 1e7
|
||||
})).to.revertedWith('sender doesn\'t match create2 address')
|
||||
});
|
||||
@@ -388,8 +415,9 @@ describe("EntryPoint", function () {
|
||||
|
||||
expect(await ethers.provider.getBalance(op.sender)).to.eq(0)
|
||||
|
||||
await expect(entryPoint.callStatic.handleOps([op], redeemerAddress, {
|
||||
gasLimit: 1e7
|
||||
await expect(entryPoint.callStatic.handleOps([op], beneficiaryAddress, {
|
||||
gasLimit: 1e7,
|
||||
gasPrice: await ethers.provider.getGasPrice()
|
||||
})).to.revertedWith('didn\'t pay prefund')
|
||||
|
||||
// await expect(await ethers.provider.getCode(op.sender).then(x => x.length)).to.equal(2, "wallet exists before creation")
|
||||
@@ -407,11 +435,11 @@ describe("EntryPoint", function () {
|
||||
}, walletOwner, entryPoint)
|
||||
|
||||
await expect(await ethers.provider.getCode(preAddr).then(x => x.length)).to.equal(2, "wallet exists before creation")
|
||||
const rcpt = await entryPoint.handleOps([createOp], redeemerAddress, {
|
||||
const rcpt = await entryPoint.handleOps([createOp], beneficiaryAddress, {
|
||||
gasLimit: 1e7,
|
||||
}).then(tx => tx.wait()).catch(rethrow())
|
||||
created = true
|
||||
await calcGasUsage(rcpt!, entryPoint, redeemerAddress)
|
||||
await calcGasUsage(rcpt!, entryPoint, beneficiaryAddress)
|
||||
});
|
||||
|
||||
it('should reject if account already created', async function () {
|
||||
@@ -419,7 +447,7 @@ describe("EntryPoint", function () {
|
||||
if (await ethers.provider.getCode(preAddr).then(x => x.length) == 2)
|
||||
this.skip()
|
||||
|
||||
await expect(entryPoint.callStatic.handleOps([createOp], redeemerAddress, {
|
||||
await expect(entryPoint.callStatic.handleOps([createOp], beneficiaryAddress, {
|
||||
gasLimit: 1e7
|
||||
})).to.revertedWith('create2 failed')
|
||||
});
|
||||
@@ -434,7 +462,7 @@ describe("EntryPoint", function () {
|
||||
*/
|
||||
let counter: TestCounter
|
||||
let walletExecCounterFromEntryPoint: PopulatedTransaction
|
||||
const redeemerAddress = Wallet.createRandom().address
|
||||
const beneficiaryAddress = Wallet.createRandom().address
|
||||
const walletOwner1 = createWalletOwner()
|
||||
let wallet1: string
|
||||
let walletOwner2 = createWalletOwner()
|
||||
@@ -473,7 +501,7 @@ describe("EntryPoint", function () {
|
||||
await fund(wallet2.address)
|
||||
prebalance1 = await ethers.provider.getBalance((wallet1))
|
||||
prebalance2 = await ethers.provider.getBalance((wallet2.address))
|
||||
await entryPoint.handleOps([op1!, op2], redeemerAddress).catch((rethrow())).then(r => r!.wait())
|
||||
await entryPoint.handleOps([op1!, op2], beneficiaryAddress).catch((rethrow())).then(r => r!.wait())
|
||||
// console.log(ret.events!.map(e=>({ev:e.event, ...objdump(e.args!)})))
|
||||
})
|
||||
it('should execute', async () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("EntryPoint with paymaster", function () {
|
||||
let walletOwner: Wallet
|
||||
let ethersSigner = ethers.provider.getSigner();
|
||||
let wallet: SimpleWallet
|
||||
let redeemerAddress = '0x'.padEnd(42, '1')
|
||||
let beneficiaryAddress = '0x'.padEnd(42, '1')
|
||||
|
||||
before(async function () {
|
||||
await checkForGeth()
|
||||
@@ -65,10 +65,10 @@ describe("EntryPoint with paymaster", function () {
|
||||
paymaster: paymaster.address,
|
||||
callData: calldata
|
||||
}, walletOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.handleOps([op], redeemerAddress, {
|
||||
await expect(entryPoint.callStatic.handleOps([op], beneficiaryAddress, {
|
||||
gasLimit: 1e7,
|
||||
}).catch(rethrow())).to.revertedWith('TokenPaymaster: no balance')
|
||||
await expect(entryPoint.handleOps([op], redeemerAddress, {
|
||||
await expect(entryPoint.handleOps([op], beneficiaryAddress, {
|
||||
gasLimit: 1e7,
|
||||
}).catch(rethrow())).to.revertedWith('TokenPaymaster: no balance')
|
||||
});
|
||||
@@ -77,7 +77,7 @@ describe("EntryPoint with paymaster", function () {
|
||||
describe('create account', () => {
|
||||
let createOp: UserOperation
|
||||
let created = false
|
||||
const redeemerAddress = Wallet.createRandom().address
|
||||
const beneficiaryAddress = Wallet.createRandom().address
|
||||
|
||||
it('should reject if account not funded', async () => {
|
||||
const op = await fillAndSign({
|
||||
@@ -85,7 +85,7 @@ describe("EntryPoint with paymaster", function () {
|
||||
verificationGas: 1e7,
|
||||
paymaster: paymaster.address
|
||||
}, walletOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.handleOps([op], redeemerAddress, {
|
||||
await expect(entryPoint.callStatic.handleOps([op], beneficiaryAddress, {
|
||||
gasLimit: 1e7,
|
||||
}).catch(rethrow())).to.revertedWith('TokenPaymaster: no balance')
|
||||
});
|
||||
@@ -107,7 +107,7 @@ describe("EntryPoint with paymaster", function () {
|
||||
const [tx] = await ethers.provider.getBlock('latest').then(block=>block.transactions)
|
||||
await checkForBannedOps(tx, true)
|
||||
|
||||
const rcpt = await entryPoint.handleOps([createOp], redeemerAddress, {
|
||||
const rcpt = await entryPoint.handleOps([createOp], beneficiaryAddress, {
|
||||
gasLimit: 1e7,
|
||||
}).catch(rethrow()).then(tx => tx!.wait())
|
||||
console.log('\t== create gasUsed=', rcpt!.gasUsed.toString())
|
||||
@@ -118,7 +118,7 @@ describe("EntryPoint with paymaster", function () {
|
||||
it('account should pay for its creation (in tst)', async function () {
|
||||
if (!created) this.skip()
|
||||
//TODO: calculate needed payment
|
||||
const ethRedeemed = await getBalance(redeemerAddress)
|
||||
const ethRedeemed = await getBalance(beneficiaryAddress)
|
||||
expect(ethRedeemed).to.above(100000)
|
||||
|
||||
const walletAddr = await entryPoint.getSenderAddress(WalletConstructor(entryPoint.address, walletOwner.address), 0)
|
||||
@@ -128,7 +128,7 @@ describe("EntryPoint with paymaster", function () {
|
||||
|
||||
it('should reject if account already created', async function () {
|
||||
if (!created) this.skip()
|
||||
await expect(entryPoint.callStatic.handleOps([createOp], redeemerAddress, {
|
||||
await expect(entryPoint.callStatic.handleOps([createOp], beneficiaryAddress, {
|
||||
gasLimit: 1e7,
|
||||
}).catch(rethrow())).to.revertedWith('create2 failed')
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("SimpleWallet", function () {
|
||||
expect(await testUtil.packUserOp(op)).to.equal(packed)
|
||||
});
|
||||
|
||||
describe('#verifyUserOp', () => {
|
||||
describe('#validateUserOp', () => {
|
||||
let wallet: SimpleWallet
|
||||
let userOp: UserOperation
|
||||
let preBalance: number
|
||||
@@ -64,16 +64,18 @@ describe("SimpleWallet", function () {
|
||||
const callGas = 200000
|
||||
const verificationGas = 100000
|
||||
const maxFeePerGas = 3e9
|
||||
const chainId = await ethers.provider.getNetwork().then(net=>net.chainId)
|
||||
|
||||
userOp = signUserOp(fillUserOp({
|
||||
sender: wallet.address,
|
||||
callGas,
|
||||
verificationGas,
|
||||
maxFeePerGas,
|
||||
}), walletOwner)
|
||||
}), walletOwner, chainId)
|
||||
expectedPay = actualGasPrice * (callGas + verificationGas)
|
||||
|
||||
preBalance = await getBalance(wallet.address)
|
||||
const ret = await wallet.verifyUserOp(userOp, expectedPay, {gasPrice: actualGasPrice})
|
||||
const ret = await wallet.validateUserOp(userOp, expectedPay, {gasPrice: actualGasPrice})
|
||||
await ret.wait()
|
||||
})
|
||||
|
||||
@@ -89,7 +91,7 @@ describe("SimpleWallet", function () {
|
||||
expect(await wallet.nonce()).to.equal(1)
|
||||
});
|
||||
it('should reject same TX on nonce error', async () => {
|
||||
await expect(wallet.verifyUserOp(userOp, 0)).to.revertedWith("invalid nonce")
|
||||
await expect(wallet.validateUserOp(userOp, 0)).to.revertedWith("invalid nonce")
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ export const AddressZero = ethers.constants.AddressZero
|
||||
export const HashZero = ethers.constants.HashZero
|
||||
export const ONE_ETH = parseEther('1');
|
||||
export const TWO_ETH = parseEther('2');
|
||||
export const FIVE_ETH = parseEther('5');
|
||||
|
||||
export const tostr = (x: any) => x != null ? x.toString() : 'null'
|
||||
|
||||
@@ -63,7 +64,7 @@ export function callDataCost(data: string): number {
|
||||
.reduce((sum, x) => sum + x)
|
||||
}
|
||||
|
||||
export async function calcGasUsage(rcpt: ContractReceipt, entryPoint: EntryPoint, redeemerAddress?: string) {
|
||||
export async function calcGasUsage(rcpt: ContractReceipt, entryPoint: EntryPoint, beneficiaryAddress?: string) {
|
||||
const actualGas = await rcpt.gasUsed
|
||||
const logs = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(), rcpt.blockHash)
|
||||
const {actualGasCost, actualGasPrice} = logs[0].args
|
||||
@@ -72,8 +73,8 @@ export async function calcGasUsage(rcpt: ContractReceipt, entryPoint: EntryPoint
|
||||
console.log('\t== calculated gasUsed (paid to redeemer)=', calculatedGasUsed)
|
||||
const tx = await ethers.provider.getTransaction(rcpt.transactionHash)
|
||||
console.log('\t== gasDiff', actualGas.toNumber() - calculatedGasUsed - callDataCost(tx.data))
|
||||
if (redeemerAddress != null) {
|
||||
expect(await getBalance(redeemerAddress)).to.eq(actualGasCost.toNumber())
|
||||
if (beneficiaryAddress != null) {
|
||||
expect(await getBalance(beneficiaryAddress)).to.eq(actualGasCost.toNumber())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,10 +189,10 @@ export async function checkForBannedOps(txHash: string, checkPaymaster: boolean)
|
||||
|
||||
const tx = await debugTransaction(txHash)
|
||||
const logs = tx.structLogs
|
||||
const balanceOfs = logs.map((op, index) => ({op: op.op, index})).filter(op => op.op == 'SELFBALANCE')
|
||||
expect(balanceOfs.length).to.equal(2, "expected exactly 2 calls to SELFBALANCE (Before and after validateUserOp)")
|
||||
const validateWalletOps = logs.slice(0, balanceOfs[1].index - 1)
|
||||
const validatePaymasterOps = logs.slice(balanceOfs[1].index + 1)
|
||||
const blockHash = logs.map((op, index) => ({op: op.op, index})).filter(op => op.op == 'NUMBER')
|
||||
expect(blockHash.length).to.equal(1, "expected exactly 1 call to NUMBER (Just before validatePaymasterUserOp)")
|
||||
const validateWalletOps = logs.slice(0, blockHash[0].index - 1)
|
||||
const validatePaymasterOps = logs.slice(blockHash[0].index + 1)
|
||||
const ops = validateWalletOps.filter(log => log.depth > 1).map(log => log.op)
|
||||
const paymasterOps = validatePaymasterOps.filter(log => log.depth > 1).map(log => log.op)
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("Batch gas testing", function () {
|
||||
let counter: TestCounter
|
||||
let walletExecCounterFromEntryPoint: PopulatedTransaction
|
||||
let execCounterCount: PopulatedTransaction
|
||||
const redeemerAddress = Wallet.createRandom().address
|
||||
const beneficiaryAddress = Wallet.createRandom().address
|
||||
|
||||
before(async () => {
|
||||
counter = await new TestCounter__factory(ethersSigner).deploy()
|
||||
@@ -145,8 +145,8 @@ describe("Batch gas testing", function () {
|
||||
to: wallet.address,
|
||||
data: execCounterCount.data!
|
||||
}), 'datacost=', callDataCost(execCounterCount.data!));
|
||||
console.log('through handleOps:', await entryPoint.estimateGas.handleOps([op1], redeemerAddress))
|
||||
console.log('through single handleOp:', await entryPoint.estimateGas.handleOp(op1, redeemerAddress))
|
||||
console.log('through handleOps:', await entryPoint.estimateGas.handleOps([op1], beneficiaryAddress))
|
||||
console.log('through single handleOp:', await entryPoint.estimateGas.handleOp(op1, beneficiaryAddress))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -206,10 +206,10 @@ describe("Batch gas testing", function () {
|
||||
})
|
||||
|
||||
async function call_handleOps_and_stats(title: string, ops: UserOperation[], count: number) {
|
||||
const redeemerAddress = createWalletOwner().address
|
||||
const beneficiaryAddress = createWalletOwner().address
|
||||
const sender = ethersSigner // ethers.provider.getSigner(5)
|
||||
const senderPrebalance = await ethers.provider.getBalance(await sender.getAddress())
|
||||
const entireTxEncoded = toBuffer(await entryPoint.populateTransaction.handleOps(ops, redeemerAddress).then(tx => tx.data))
|
||||
const entireTxEncoded = toBuffer(await entryPoint.populateTransaction.handleOps(ops, beneficiaryAddress).then(tx => tx.data))
|
||||
|
||||
function callDataCost(data: Buffer | string): number {
|
||||
if (typeof data == 'string') {
|
||||
@@ -230,7 +230,7 @@ describe("Batch gas testing", function () {
|
||||
//for slack testing, we set TX priority same as UserOp
|
||||
//(real miner may create tx with priorityFee=0, to avoid paying from the "sender" to coinbase)
|
||||
const {maxPriorityFeePerGas} = ops[0]
|
||||
const ret = await entryPoint.connect(sender).handleOps(ops, redeemerAddress, {
|
||||
const ret = await entryPoint.connect(sender).handleOps(ops, beneficiaryAddress, {
|
||||
gasLimit: 13e6,
|
||||
maxPriorityFeePerGas
|
||||
}).catch((rethrow())).then(r => r!.wait())
|
||||
@@ -250,7 +250,7 @@ describe("Batch gas testing", function () {
|
||||
const totalEventsGasCost = parseInt(events1.map(x => x.args!.actualGasCost).reduce((sum, x) => sum.add(x)).toString())
|
||||
|
||||
const senderPaid = parseInt(senderPrebalance.sub(await ethers.provider.getBalance(await sender.getAddress())).toString())
|
||||
let senderRedeemed = await ethers.provider.getBalance(redeemerAddress).then(tonumber)
|
||||
let senderRedeemed = await ethers.provider.getBalance(beneficiaryAddress).then(tonumber)
|
||||
|
||||
expect(senderRedeemed).to.equal(totalEventsGasCost)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user