-
Notifications
You must be signed in to change notification settings - Fork 670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New TokenPaymaster #286
New TokenPaymaster #286
Conversation
} | ||
} | ||
uint256 tokenAmount = weiToToken(preChargeNative, cachedPriceWithMarkup, false); | ||
SafeERC20.safeTransferFrom(token, userOp.sender, address(this), tokenAmount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor, IIRC from our past debug experience, SafeERC20 uses banned selfbalance opcode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I did not know that! Created a small PR in the OpenZeppelin repo, hope this is all there is to this issue:
OpenZeppelin/openzeppelin-contracts#4271
I hope to avoid having to copy SafeERC20.sol
library into the account-abstraction
repo, but maybe we will have no choice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we can use solady in that case https://github.com/Vectorized/solady/blob/f9a6cc652dc8b37387590588a78644abfa4be3ac/src/utils/SafeTransferLib.sol#L238
contracts/samples/TokenPaymaster.sol
Outdated
); | ||
} else { | ||
// If the token amount is not greater than the actual amount needed, revert to remove incentive to cheat | ||
revert("TPM: preCharge was too low"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it's worth of attempting to transfer the owed token (preCharge - actualTokenNeeded) from sender here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this may actually work, will bring this up with the team. So far I did add an attempt to withdraw the owed amount (actualTokenNeeded - preCharge
) from the UserOp sender to the Paymaster.
contracts/samples/TokenPaymaster.sol
Outdated
function setTokenPaymasterConfig( | ||
TokenPaymasterConfig memory _tokenPaymasterConfig | ||
) public onlyOwner { | ||
require(_tokenPaymasterConfig.priceMarkup <= 1900000, "TPM: price markup too high"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, how do we pick the upper bound (1900000)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of these hard-coded parameters are selected yet, but in my opinion the maximum possible value for the markup should just be there to prevent human error or malicious configuration.
For example, Pimlico Paymaster used 120e4
which sounds to me like it may be a bit too limiting:
https://github.com/pimlicolabs/erc20-paymaster-contracts/blob/c9911402a49928398441b07fb9caa9f4b4d0352f/src/PimlicoERC20Paymaster.sol#L68
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe 2e6 is good upper limit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@livingrockrises Agreed. Will set for 200% for now. Will also make it explicitly related to the PRICE_DENOMINATOR
value.
} | ||
|
||
emit UserOperationSponsored(userOpSender, actualTokenNeeded, actualGasCost, cachedPrice); | ||
refillEntryPointDeposit(_cachedPrice); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean router swapping cost is dumped on to the users?
since it is conditional postOp cost would change
if not user who is paying for this computation - paymaster's gas deposit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A single user should not get stuck with the bill for the Paymaster's swap operation on its own.
I am not sure but I think what we should do is to set the POSTOP_COST
parameter such that it kind of spreads the cost of these periodic swaps over all other transactions.
Of course this way it may end up accumulating some loss or profit over time if left completely unattended.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If paymaster is always charging premium, POSTOP_COST can just be everything unaccounted on-chain on entry point and postOp - swap executionGas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@livingrockrises
A paymaster is expected to set refundPostopCost
to actual cost of postOp, with faction of the cost of the uniswap, since the swap is expected to happen only every "N" transaction.
uint256 preCharge = uint256(bytes32(context[0 : 32])); | ||
uint256 maxFeePerGas = uint256(bytes32(context[32 : 64])); | ||
address userOpSender = address(bytes20(context[64 : 84])); | ||
if (mode == PostOpMode.postOpReverted) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This branch cannot be reached since there is a immediate return
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Fixed.
contracts/samples/TokenPaymaster.sol
Outdated
// note: as price is in ether-per-token and we want more tokens increasing it means dividing it by markup | ||
uint256 cachedPriceWithMarkup = _cachedPrice * PRICE_DENOMINATOR / priceMarkup; | ||
// Refund tokens based on actual gas cost | ||
uint256 actualChargeNative = actualGasCost + REFUND_POSTOP_COST * maxFeePerGas; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of using maxFeePerGas, does it make sense to use tx.gasprice ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, thanks! However, we cannot just use tx.gasprice
as the bundler can manipulate this value. I think we should use userOp.gasPrice()
here. Updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
userop.gasPrice() touches BASEFEE which forbidden opcode. this would be fine in postOp(), but you shouldn't calculate it in validatePaymasterUserOp and pass it in context.
Instead pass maxPriorityFee and maxFee in context and calculate like below in the postOp
uint256 opGasPrice;
unchecked {
if (maxFeePerGas == maxPriorityFeePerGas) {
opGasPrice = maxFeePerGas;
} else {
opGasPrice = Math.min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right 😀
event Received(address indexed sender, uint256 value); | ||
|
||
/// @notice All 'price' variables are multiplied by this value to avoid rounding up | ||
uint256 private constant PRICE_DENOMINATOR = 1e26; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the rationale behind using 1e26?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point constants are actually pretty random and will be finalised after we make some gas usage optimisations. Do you have suggestions on what value should be used and why?
contracts/samples/TokenPaymaster.sol
Outdated
require(paymasterAndDataLength == 0 || paymasterAndDataLength == 32, | ||
"TPM: invalid data length" | ||
); | ||
uint256 gasPrice = userOp.gasPrice(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will revert on the bundler
cc @leekt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup this will revert for sure
…rice' This is the behaviour of the EntryPoint contract as well.
preCharge - actualTokenNeeded | ||
); | ||
} else if (preCharge < actualTokenNeeded) { | ||
// Attempt to cover Paymaster's gas expenses by withdrawing the 'overdraft' from the client |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about this part. Won't this make clientSuppliedPrice
invalid? clientSuppliedPrice
should be the guard for how much token user is willing to pay but this part will end up forcing user to pay the full slippage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, yes, with this implementation the user-supplied price can only increase the pre-charge for the user, but does not set an upper limit of the token price, so once the users approve this paymaster they basically agree to pay for gas at any current market price.
@drortirosh what do you think about this feature?
CC: @huaweigu
contracts/samples/TokenPaymaster.sol
Outdated
uint256 private constant PRICE_DENOMINATOR = 1e26; | ||
|
||
/// @notice Estimated gas cost for refunding tokens after the transaction is completed | ||
uint256 public constant REFUND_POSTOP_COST = 40000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
postop_cost depends on price of token transfer, which varies with different token implementation.
to be accurate, this is better be a constructor parameter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
contracts/samples/TokenPaymaster.sol
Outdated
} | ||
|
||
function setOracleConfiguration( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need setters? if the data is immutable, it the logic much cheaper.
There is no harm at re-deploy the paymaster with different configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This paymaster will accumulate client base by getting 'approval' in ERC-20 token used. Deploying a new paymaster just to switch to a different price oracle will lose all existing clients until they migrate their approval.
Do you want me to make the fields immutable and make the paymaster an upgradeable proxy instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@drortirosh
Bumping this one. Should we?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a user, I trust the config values the paymaster have - but I really can't fully trust it, since it can change its configuration at will (e.g. increase the postOp cost, or switch to a malicious oracle)
I want to trust an address given by some authority, and know this address can't change.
Better yet - I want to know this address define all parameters values.
Also other parameters (like minEPbalance) are not expected to be modified much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
at Biconomy we are returning a fee quote to the sender and allow them to update userop.calldata with the approval transaction with necessary amount each time. then partialUserOp is filled with paymasterAndData and signature. SDK account APIs can facilitate this for decoding calldata and append approval transaction in a batch and have new callData, callGasLimit
contracts/samples/TokenPaymaster.sol
Outdated
uint256 tokenAmount = weiToToken(preChargeNative, cachedPriceWithMarkup); | ||
SafeERC20.safeTransferFrom(token, userOp.sender, address(this), tokenAmount); | ||
context = abi.encodePacked(tokenAmount, userOp.maxFeePerGas, userOp.maxPriorityFeePerGas, userOp.sender); | ||
validationResult = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better return validUntil as cachedPriceTimestamp + maxCacheAge
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
contracts/samples/TokenPaymaster.sol
Outdated
function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) internal override { | ||
unchecked { | ||
uint256 priceMarkup = tokenPaymasterConfig.priceMarkup; | ||
uint256 preCharge = uint256(bytes32(context[0 : 32])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
abi.decode()
is that bad?
it surely much clearer to understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. I was trying to same some bytes by doing abi.encodePacked
.
IOracle nativeOracle = oracleHelperConfig.nativeOracle; | ||
|
||
uint256 cacheAge = block.timestamp - cachedPriceTimestamp; | ||
if (!force && cacheAge <= cacheTimeToLive) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better be moved up, and avoid loading storage fields before the quick bail-out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should also add gas-check test in gas-calc/
context = abi.encode(tokenAmount, userOp.maxFeePerGas, userOp.maxPriorityFeePerGas, userOp.sender); | ||
validationResult = _packValidationData( | ||
false, | ||
uint48(cachedPriceTimestamp + tokenPaymasterConfig.priceMaxAge), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
storage optimization: instead referencing 2 storage slots on each call, use
cachedPriceExpirationTimestamp = timestamp + priceMaxAge
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please elaborate where does the timestamp
variable come from?
@@ -102,6 +102,8 @@ describe.only('OracleHelper', function () { | |||
const owner = await ethersSigner.getAddress() | |||
|
|||
const tokenPaymasterConfig: TokenPaymaster.TokenPaymasterConfigStruct = { | |||
priceMaxAge: 86400, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^^ remove "only" from describe.only('OracleHelper')
@@ -1,2 +0,0 @@ | |||
export { UserOperationStruct } from './types/EntryPoint' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true, should be removed (along with the code to use it during "yarn publish"), but unrelated to TokenPaymaster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was messing up the environment for me though.
|
||
/// @notice Simplified code copied from here: | ||
/// https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/PeripheryPayments.sol#L19 | ||
function unwrapWETH9(uint256 amountMinimum, address recipient) public payable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unused?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not? This is used by the UniswapHelper.sol
file:
function unwrapWeth(uint256 amount) internal {
IPeripheryPayments(address(uniswap)).unwrapWETH9(amount, address(this));
}
contracts/samples/TokenPaymaster.sol
Outdated
); | ||
} | ||
|
||
emit UserOperationSponsored(userOpSender, actualTokenNeeded, actualGasCost, cachedPrice); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is _cachedPrice
, not cachedPrice
} | ||
|
||
emit UserOperationSponsored(userOpSender, actualTokenNeeded, actualGasCost, cachedPrice); | ||
refillEntryPointDeposit(_cachedPrice); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@livingrockrises
A paymaster is expected to set refundPostopCost
to actual cost of postOp, with faction of the cost of the uniswap, since the swap is expected to happen only every "N" transaction.
contracts/samples/TokenPaymaster.sol
Outdated
} | ||
|
||
function setOracleConfiguration( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a user, I trust the config values the paymaster have - but I really can't fully trust it, since it can change its configuration at will (e.g. increase the postOp cost, or switch to a malicious oracle)
I want to trust an address given by some authority, and know this address can't change.
Better yet - I want to know this address define all parameters values.
Also other parameters (like minEPbalance) are not expected to be modified much.
1. Remove 'setOracleConfiguration' - only constructor can set oracles 2. Use '_cachedPrice' instead of 'cachedPrice' to avoid extra state read 3. Remove 'only' from TokenPaymaster related tests
@@ -67,7 +67,7 @@ abstract contract OracleHelper { | |||
|
|||
function _setOracleConfiguration( | |||
OracleHelperConfig memory _oracleHelperConfig | |||
) internal { | |||
) private { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leaving this method (called only from constructor) means that we still pay the SLOAD when using these parameters.
(and you can't make a member "immutable" if it is set from a private method)
Currently, the paymaster uses 15 slots (14, if we exclude "owner")
(checked with solc --storage-layout
)
So it uses 14 storage slots on each operation, which is at minimum 14*2100 = 29400 gas .
But I suggest adding a "gascalc" test before optimizing it.
No description provided.