Skip to content
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

Merged
merged 25 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eabd560
Bring over PimlicoERC20Paymaster as new TokenPaymaster
forshtat May 18, 2023
e814e97
Pass local lint and original tests
forshtat May 18, 2023
70ce950
Fix bug in denominators calculation; streamline tests; use OZ SafeERC20
forshtat May 18, 2023
82bf877
Reuse 'updatePrice' logic and test 'TokenPriceUpdated'
forshtat May 19, 2023
d78b712
Extract oracle-specific logic to the OracleHelper library
forshtat May 19, 2023
664e65f
Bring over necessary UniswapHelper code - solidity compiles
forshtat May 20, 2023
e39e9a3
Some cleanup
forshtat May 20, 2023
23312c6
Deploy TokenPaymaster witout Create2 factory for the unit tests
forshtat May 20, 2023
06c130c
REPRO: out of gas in 'handleOps'
forshtat May 21, 2023
5e0cca2
Add 'TestUniswap' to silence errors; fix price-related errors
forshtat May 21, 2023
03efff5
Add token price override and test for it
forshtat May 21, 2023
5a4f340
WIP: Implemented TestUniswap stub logic and deposit to EntryPoint logic
forshtat May 22, 2023
dd896a0
Remove all 'console.log'
forshtat May 22, 2023
aae7c9a
Fix accidental change in EP
forshtat May 22, 2023
63cf750
TBD: charge the sender account in postOp if 'refund' is negative ('ov…
forshtat May 23, 2023
39a1e41
WIP: Check new price calculation function; use 1e26 as 'price denomin…
forshtat May 25, 2023
a047984
Use 'userOp.gasPrice' instead of 'maxFeePerGas' to calculate precharg…
forshtat Jun 4, 2023
6ac12fe
Avoid duplicate return from '_postOp'
forshtat Jun 4, 2023
c2c775d
Correction: pre-charge based on 'maxFeePerGas', refund based on 'gasP…
forshtat Jun 4, 2023
41ee2ec
Add support for legacy chains (avoid 'basefee' opcode)
forshtat Jun 4, 2023
28916ad
Switch from 'abi.encodePacked' to 'abi.encode' for better readability
forshtat Jun 16, 2023
821ada7
Make 'refundPostopCost' a part of 'TokenPaymasterConfig'
forshtat Jun 16, 2023
d461522
Return price invalidation time to prevent ops using against outdated …
forshtat Jun 16, 2023
36633a5
Reorder lines of code to make the fast way out even more faster
forshtat Jun 16, 2023
eeaf42a
Address PR comments (see list)
forshtat Jun 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- run: yarn install
- run: yarn compile
- run: yarn tsc

- run: yarn run ci

Expand Down
2 changes: 0 additions & 2 deletions contracts/index.ts

This file was deleted.

3 changes: 2 additions & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"devDependencies": {
"@gnosis.pm/safe-contracts": "^1.3.0",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1"
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@uniswap/v3-periphery": "^1.4.3"
}
}
113 changes: 113 additions & 0 deletions contracts/samples/LegacyTokenPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

/* solhint-disable reason-string */

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../core/BasePaymaster.sol";

/**
* A sample paymaster that defines itself as a token to pay for gas.
* The paymaster IS the token to use, since a paymaster cannot use an external contract.
* Also, the exchange rate has to be fixed, since it can't reference an external Uniswap or other exchange contract.
* subclass should override "getTokenValueOfEth" to provide actual token exchange rate, settable by the owner.
* Known Limitation: this paymaster is exploitable when put into a batch with multiple ops (of different accounts):
* - while a single op can't exploit the paymaster (if postOp fails to withdraw the tokens, the user's op is reverted,
* and then we know we can withdraw the tokens), multiple ops with different senders (all using this paymaster)
* in a batch can withdraw funds from 2nd and further ops, forcing the paymaster itself to pay (from its deposit)
* - Possible workarounds are either use a more complex paymaster scheme (e.g. the DepositPaymaster) or
* to whitelist the account and the called method ids.
*/
contract LegacyTokenPaymaster is BasePaymaster, ERC20 {

//calculated cost of the postOp
uint256 constant public COST_OF_POST = 15000;

address public immutable theFactory;

constructor(address accountFactory, string memory _symbol, IEntryPoint _entryPoint) ERC20(_symbol, _symbol) BasePaymaster(_entryPoint) {
theFactory = accountFactory;
//make it non-empty
_mint(address(this), 1);

//owner is allowed to withdraw tokens from the paymaster's balance
_approve(address(this), msg.sender, type(uint).max);
}


/**
* helpers for owner, to mint and withdraw tokens.
* @param recipient - the address that will receive the minted tokens.
* @param amount - the amount it will receive.
*/
function mintTokens(address recipient, uint256 amount) external onlyOwner {
_mint(recipient, amount);
}

/**
* transfer paymaster ownership.
* owner of this paymaster is allowed to withdraw funds (tokens transferred to this paymaster's balance)
* when changing owner, the old owner's withdrawal rights are revoked.
*/
function transferOwnership(address newOwner) public override virtual onlyOwner {
// remove allowance of current owner
_approve(address(this), owner(), 0);
super.transferOwnership(newOwner);
// new owner is allowed to withdraw tokens from the paymaster's balance
_approve(address(this), newOwner, type(uint).max);
}

//Note: this method assumes a fixed ratio of token-to-eth. subclass should override to supply oracle
// or a setter.
function getTokenValueOfEth(uint256 valueEth) internal view virtual returns (uint256 valueToken) {
return valueEth / 100;
}

/**
* validate the request:
* if this is a constructor call, make sure it is a known account.
* verify the sender has enough tokens.
* (since the paymaster is also the token, there is no notion of "approval")
*/
function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund)
internal view override returns (bytes memory context, uint256 validationData) {
uint256 tokenPrefund = getTokenValueOfEth(requiredPreFund);

// verificationGasLimit is dual-purposed, as gas limit for postOp. make sure it is high enough
// make sure that verificationGasLimit is high enough to handle postOp
require(userOp.verificationGasLimit > COST_OF_POST, "TokenPaymaster: gas too low for postOp");

if (userOp.initCode.length != 0) {
_validateConstructor(userOp);
require(balanceOf(userOp.sender) >= tokenPrefund, "TokenPaymaster: no balance (pre-create)");
} else {

require(balanceOf(userOp.sender) >= tokenPrefund, "TokenPaymaster: no balance");
}

return (abi.encode(userOp.sender), 0);
}

// when constructing an account, validate constructor code and parameters
// we trust our factory (and that it doesn't have any other public methods)
function _validateConstructor(UserOperation calldata userOp) internal virtual view {
address factory = address(bytes20(userOp.initCode[0 : 20]));
require(factory == theFactory, "TokenPaymaster: wrong account factory");
}

/**
* actual charge of user.
* this method will be called just after the user's TX with mode==OpSucceeded|OpReverted (account pays in both cases)
* BUT: if the user changed its balance in a way that will cause postOp to revert, then it gets called again, after reverting
* the user's TX , back to the state it was before the transaction started (before the validatePaymasterUserOp),
* and the transaction should succeed there.
*/
function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) internal override {
//we don't really care about the mode, we just pay the gas with the user's tokens.
(mode);
address sender = abi.decode(context, (address));
uint256 charge = getTokenValueOfEth(actualGasCost + COST_OF_POST);
//actualGasCost is known to be no larger than the above requiredPreFund, so the transfer should succeed.
_transfer(sender, address(this), charge);
}
}
Loading