Combined updates - #11
Conversation
|
New Idea for UserOp. I'm thinking of having a When the The Having a Reverse Dutch Auction will make fillers consider if they want to frontrun other fillers and accept a lower payment, incentivizing lower filling fees for users. |
| bytes32 structHash = EfficientHashLib.hash( | ||
| uint256(EXECUTE_TYPEHASH), | ||
| nonce & 1, | ||
| uint256(a.hash()), | ||
| nonce, | ||
| _getDelegationStorage().nonceSalt | ||
| ); | ||
| return nonce & 1 > 0 ? _hashTypedDataSansChainId(structHash) : _hashTypedData(structHash); |
There was a problem hiding this comment.
Love this and we should document this technique really well IMO!
| function execute(bytes32 mode, bytes calldata executionData) public payable virtual override { | ||
| if (bytes1(mode) == 0xff) { | ||
| address target = address(bytes20(LibBytes.loadCalldata(executionData, 0x00))); | ||
| if (!_getDelegationStorage().approvedImplementations.contains(target)) { | ||
| revert Unauthorized(); | ||
| } | ||
| bytes calldata data = LibBytes.sliceCalldata(executionData, 0x14); | ||
| assembly ("memory-safe") { | ||
| let m := mload(0x40) | ||
| calldatacopy(m, data.offset, data.length) | ||
| if iszero(delegatecall(gas(), target, m, data.length, codesize(), 0x00)) { | ||
| returndatacopy(m, 0x00, returndatasize()) | ||
| revert(m, returndatasize()) | ||
| } | ||
| } | ||
| return; | ||
| } | ||
| super.execute(mode, executionData); | ||
| } | ||
|
|
||
| function supportsExecutionMode(bytes32 mode) public view virtual override returns (bool) { | ||
| return LibBit.or(bytes1(mode) == 0xff, super.supportsExecutionMode(mode)); | ||
| } |
There was a problem hiding this comment.
Is the goal of doing it with ERC7579 interface here to make it compatible with https://github.com/zkemail/email-recovery/blob/65b525af2e9dfbc8338b08b708b8912f1b81725e/src/modules/EmailRecoveryModule.sol#L97? Otherwise why do we need the mode flag? I originally thought this might be execute(address target,bytes calldata)
And we're basically reserving the 0xff mode for code delegation?
There was a problem hiding this comment.
The ERC7579 standard has reserved 0xff it for delegate calls.
| /// @dev The combined gas limit for payment, verification, and calling the EOA. | ||
| uint256 combinedGas; |
There was a problem hiding this comment.
cleaner
| /// @dev The combined gas limit for payment, verification, and calling the EOA. | |
| uint256 combinedGas; | |
| /// @dev The combined gas limit for payment, verification, and calling the EOA. | |
| uint256 gasLimit; |
| uint256 paymentMaxAmount; | ||
| /// @dev The gas limit for the payment. | ||
| uint256 paymentGas; | ||
| /// @dev The gas limit for the verification. | ||
| uint256 verificationGas; | ||
| /// @dev The gas limit for calling the EOA. | ||
| uint256 callGas; | ||
| /// @dev The amount of ERC20 to pay per gas spent. For calculation of refunds. | ||
| /// If this is left at zero, it will be treated as infinity (i.e. no refunds). | ||
| uint256 paymentPerGas; |
There was a problem hiding this comment.
I was thinking we could remove the need for paymentAmount / paymentMaxAmount and only have the exchange rate. Still thinking about it though.
There was a problem hiding this comment.
having a max is for safety reasons. Suppose someone wants to call a contract that uses very little gas in the general case, but spikes in gas usage in certain edge cases. (e.g. a sale contract that does a expensive migration once a certain target has been hit).
| // `_verifyAndCall()`. | ||
| if (s == 0xe235a92a) { | ||
| require(msg.sender == address(this)); | ||
| _execute(_calldataUserOp(), _calldataKeyHash()); | ||
| assembly ("memory-safe") { | ||
| mstore(0x00, 1) | ||
| return(0x00, 0x20) | ||
| } | ||
| } | ||
| // `_computeDigest()`. | ||
| if (s == 0x693e53c3) { | ||
| bytes32 digest = _computeDigest(_calldataUserOp()); | ||
| assembly ("memory-safe") { | ||
| mstore(0x00, digest) | ||
| return(0x00, 0x20) | ||
| } | ||
| (bool isValid, bytes32 keyHash) = _verify(userOp); | ||
| if (!isValid) revert VerificationError(); | ||
| _execute(userOp, keyHash); | ||
| return; | ||
| } |
There was a problem hiding this comment.
i like this: userop gives back "is this OK and the keyhash" and then you execute the user op with the keyhash
| // 1. Pay. | ||
| mstore(m, 0x1a3de5c3) // `_pay()`. | ||
| mstore(0x00, 0) // Zeroize the return slot. | ||
| let gCapped := xor(g, mul(xor(g, _PAYMENT_GAS_CAP), lt(_PAYMENT_GAS_CAP, g))) // `min`. | ||
| if iszero(call(gCapped, address(), 0, s, n, 0x00, 0x20)) { | ||
| err := mload(0x00) | ||
| if iszero(returndatasize()) { err := shl(224, 0xbff2584f) } // `PaymentError()`. | ||
| break | ||
| } |
There was a problem hiding this comment.
I was wondering from the Slack thread if instead we can just move the _pay() call after _verifyAndCall(), and skip doing any refund logic altogether, wdyt?
There was a problem hiding this comment.
we run the risk that the gas has been burned and the payer doesn’t have enough tokens to pay.
There was a problem hiding this comment.
Right but then we'd just revert the UserOp? Can't we do that?
| uint256 gUsed = Math.rawSub(gStart, gasleft()); | ||
| uint256 paymentPerGas = u.paymentPerGas; | ||
| if (paymentPerGas == uint256(0)) paymentPerGas = type(uint256).max; | ||
| uint256 finalPaymentAmount = Math.min( | ||
| paymentAmount, | ||
| Math.saturatingMul(paymentPerGas, Math.saturatingAdd(gUsed, _REFUND_GAS)) | ||
| ); | ||
| address paymentRecipient = u.paymentRecipient; | ||
| if (paymentRecipient == address(0)) paymentRecipient = address(this); | ||
| if (LibBit.and(finalPaymentAmount != 0, paymentRecipient != address(this))) { | ||
| TokenTransferLib.safeTransfer(u.paymentToken, paymentRecipient, finalPaymentAmount); | ||
| } | ||
| if (paymentAmount > finalPaymentAmount) { | ||
| uint256 toRefund = Math.rawSub(paymentAmount, finalPaymentAmount); | ||
| TokenTransferLib.safeTransfer(u.paymentToken, u.eoa, toRefund); | ||
| } |
There was a problem hiding this comment.
So instead of doing this, the amount you're _pay()-ing is always the finalPaymentAmount? paymentPerGas * gasUsed.
| // To prevent griefing, we need to do two non-reverting gas-limited calls. | ||
| // Even if the verify and call fails, which the gas will be burned, | ||
| // the payment has already been made and can't be reverted. |
There was a problem hiding this comment.
I don't understand, why? I was thnking as I write below "you execute, measure the gas executed, multiply it by the exchange rate, and pay in erc20 only the exact amount needed at the end"
gakonst
left a comment
There was a problem hiding this comment.
Merging to unblock work at the key rotation & gas estimation, have more thoughts for me & Vectorized to go over for the gas sponsorship model
| /// If the nonce is odd, the digest will be computed without the chain ID. | ||
| /// Otherwise, the digest will be computed with the chain ID. |
There was a problem hiding this comment.
@jxom we should bake this in the client-side API
| { | ||
| /// @dev Pays `paymentAmount` of `paymentToken` to the Entry Point. | ||
| /// The last address argument is `onBehalfOf`, which we don't use here. | ||
| function payEntryPoint(address paymentToken, uint256 paymentAmount, address) public virtual { |
There was a problem hiding this comment.
Can this 3rd address arg be removed? What do we need it for?
|
|
||
| /// @dev Override to allow for a delegate call workflow. | ||
| /// Any implementation contract used in the delegate call workflow must be approved first. | ||
| function execute(bytes32 mode, bytes calldata executionData) public payable virtual override { |
There was a problem hiding this comment.
Interesting that this is not onlyThis, do you envision a third party calling into this to e.g. trigger some kind of recurring operation on behalf of the user? Which is why we need to gate the delegates?
| let s := add(m, 0x1c) // Start of the calldata in memory to pass to the self call. | ||
| let n := add(encodedUserOp.length, 0x24) // Length of the calldata to the self call. | ||
|
|
||
| // To prevent griefing, we need to do two non-reverting gas-limited calls. |
There was a problem hiding this comment.
What's the griefing vector?
| // 1. Pay. | ||
| mstore(m, 0x1a3de5c3) // `_pay()`. | ||
| mstore(0x00, 0) // Zeroize the return slot. | ||
| let gCapped := xor(g, mul(xor(g, _PAYMENT_GAS_CAP), lt(_PAYMENT_GAS_CAP, g))) // `min`. | ||
| // Perform the gas-limited self call. | ||
| switch call(gCapped, address(), 0, s, n, 0x00, 0x20) | ||
| case 0 { | ||
| err := mload(0x00) | ||
| if iszero(returndatasize()) { err := shl(224, 0xbff2584f) } // `PaymentError()`. | ||
| } |
There was a problem hiding this comment.
Maybe dumb Q but why aren't we just calling the internal _pay() function here and doing it with a gas-limited self-call via the fallback?
| uint256 paymentAmount; | ||
| assembly ("memory-safe") { | ||
| // Check if there's sufficient gas left for the gas-limited self calls | ||
| // via the 63/64 rule. This is for gas estimation. If the total amount of gas |
There was a problem hiding this comment.
What is for gas estimation?
| if iszero(returndatasize()) { err := shl(224, 0xad4db224) } // `VerifiedCallError()`. | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
So all of the above is doing is _pay(op); _verify(op); but in assembly and with the self-call vs internal calls.
| if (LibBit.and(finalPaymentAmount != 0, paymentRecipient != address(this))) { | ||
| TokenTransferLib.safeTransfer(u.paymentToken, paymentRecipient, finalPaymentAmount); | ||
| } |
There was a problem hiding this comment.
I don't understand, given we are doing this stuff, why do we even call _pay(u) above? Let's just call payEntrypoint(...) here with finalPaymentAmount to paymentRecipient instead of first pulling the funds in from the account to the entrypoint, and then from the entrypoint to the payment recipient?
compensatefunction on Delegation for direct payment.msg.senderis appropriate. If the key is revoked, this entire mapping is discarded (we'll bump some storage pointer).