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:
Dror Tirosh
2022-01-01 13:06:41 +02:00
committed by GitHub
parent a899fcc246
commit b1fe37766d
17 changed files with 409 additions and 281 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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.
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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();
}
}

View File

@@ -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 {

View File

@@ -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
View 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
// );
}
})

View File

@@ -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 () => {

View File

@@ -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')
})

View File

@@ -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")
});
})

View File

@@ -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)

View File

@@ -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)