diff --git a/contracts/samples/DepositPaymaster.sol b/contracts/samples/DepositPaymaster.sol deleted file mode 100644 index 89a107df0..000000000 --- a/contracts/samples/DepositPaymaster.sol +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.12; - -/* solhint-disable reason-string */ - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "../core/BasePaymaster.sol"; -import "../core/UserOperationLib.sol"; -import "./IOracle.sol"; - -/** - * A token-based paymaster that accepts token deposits - * The deposit is only a safeguard: the user pays with his token balance. - * only if the user didn't approve() the paymaster, or if the token balance is not enough, the deposit will be used. - * thus the required deposit is to cover just one method call. - * The deposit is locked for the current block: the user must issue unlockTokenDeposit() to be allowed to withdraw - * (but can't use the deposit for this or further operations) - * - * paymasterAndData holds the paymaster address followed by the token address to use. - * @notice This paymaster will be rejected by the standard rules of EIP4337, as it uses an external oracle. - * (the standard rules ban accessing data of an external contract) - * It can only be used if it is "whitelisted" by the bundler. - * (technically, it can be used by an "oracle" which returns a static value, without accessing any storage) - */ -contract DepositPaymaster is BasePaymaster { - - using UserOperationLib for UserOperation; - using SafeERC20 for IERC20; - - //calculated cost of the postOp - uint256 constant public COST_OF_POST = 35000; - - IOracle private constant NULL_ORACLE = IOracle(address(0)); - mapping(IERC20 => IOracle) public oracles; - mapping(IERC20 => mapping(address => uint256)) public balances; - mapping(address => uint256) public unlockBlock; - - constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) { - //owner account is unblocked, to allow withdraw of paid tokens; - unlockTokenDeposit(); - } - - /** - * owner of the paymaster should add supported tokens - */ - function addToken(IERC20 token, IOracle tokenPriceOracle) external onlyOwner { - require(oracles[token] == NULL_ORACLE, "Token already set"); - oracles[token] = tokenPriceOracle; - } - - /** - * deposit tokens that a specific account can use to pay for gas. - * The sender must first approve this paymaster to withdraw these tokens (they are only withdrawn in this method). - * Note depositing the tokens is equivalent to transferring them to the "account" - only the account can later - * use them - either as gas, or using withdrawTo() - * - * @param token the token to deposit. - * @param account the account to deposit for. - * @param amount the amount of token to deposit. - */ - function addDepositFor(IERC20 token, address account, uint256 amount) external { - require(oracles[token] != NULL_ORACLE, "unsupported token"); - //(sender must have approval for the paymaster) - token.safeTransferFrom(msg.sender, address(this), amount); - balances[token][account] += amount; - if (msg.sender == account) { - lockTokenDeposit(); - } - } - - /** - * @return amount - the amount of given token deposited to the Paymaster. - * @return _unlockBlock - the block height at which the deposit can be withdrawn. - */ - function depositInfo(IERC20 token, address account) public view returns (uint256 amount, uint256 _unlockBlock) { - amount = balances[token][account]; - _unlockBlock = unlockBlock[account]; - } - - /** - * unlock deposit, so that it can be withdrawn. - * can't be called in the same block as withdrawTo() - */ - function unlockTokenDeposit() public { - unlockBlock[msg.sender] = block.number; - } - - /** - * lock the tokens deposited for this account so they can be used to pay for gas. - * after calling unlockTokenDeposit(), the account can't use this paymaster until the deposit is locked. - */ - function lockTokenDeposit() public { - unlockBlock[msg.sender] = 0; - } - - /** - * withdraw tokens. - * can only be called after unlock() is called in a previous block. - * @param token the token deposit to withdraw - * @param target address to send to - * @param amount amount to withdraw - */ - function withdrawTokensTo(IERC20 token, address target, uint256 amount) public { - require(unlockBlock[msg.sender] != 0 && block.number > unlockBlock[msg.sender], "DepositPaymaster: must unlockTokenDeposit"); - balances[token][msg.sender] -= amount; - token.safeTransfer(target, amount); - } - - /** - * translate the given eth value to token amount - * @param token the token to use - * @param ethBought the required eth value we want to "buy" - * @return requiredTokens the amount of tokens required to get this amount of eth - */ - function getTokenValueOfEth(IERC20 token, uint256 ethBought) internal view virtual returns (uint256 requiredTokens) { - IOracle oracle = oracles[token]; - require(oracle != NULL_ORACLE, "DepositPaymaster: unsupported token"); - return oracle.getTokenValueOfEth(ethBought); - } - - /** - * Validate the request: - * The sender should have enough deposit to pay the max possible cost. - * Note that the sender's balance is not checked. If it fails to pay from its balance, - * this deposit will be used to compensate the paymaster for the transaction. - */ - function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) - internal view override returns (bytes memory context, uint256 validationData) { - - (userOpHash); - // verificationGasLimit is dual-purposed, as gas limit for postOp. make sure it is high enough - require(userOp.verificationGasLimit > COST_OF_POST, "DepositPaymaster: gas too low for postOp"); - - bytes calldata paymasterAndData = userOp.paymasterAndData; - require(paymasterAndData.length == 20+20, "DepositPaymaster: paymasterAndData must specify token"); - IERC20 token = IERC20(address(bytes20(paymasterAndData[20:]))); - address account = userOp.getSender(); - uint256 maxTokenCost = getTokenValueOfEth(token, maxCost); - uint256 gasPriceUserOp = userOp.gasPrice(); - require(unlockBlock[account] == 0, "DepositPaymaster: deposit not locked"); - require(balances[token][account] >= maxTokenCost, "DepositPaymaster: deposit too low"); - return (abi.encode(account, token, gasPriceUserOp, maxTokenCost, maxCost),0); - } - - /** - * perform the post-operation to charge the sender for the gas. - * in normal mode, use transferFrom to withdraw enough tokens from the sender's balance. - * in case the transferFrom fails, the _postOp reverts and the entryPoint will call it again, - * this time in *postOpReverted* mode. - * In this mode, we use the deposit to pay (which we validated to be large enough) - */ - function _postOp(PostOpMode, bytes calldata context, uint256 actualGasCost) internal override { - - (address account, IERC20 token, uint256 gasPricePostOp, uint256 maxTokenCost, uint256 maxCost) = abi.decode(context, (address, IERC20, uint256, uint256, uint256)); - //use same conversion rate as used for validation. - uint256 actualTokenCost = (actualGasCost + COST_OF_POST * gasPricePostOp) * maxTokenCost / maxCost; - // attempt to pay with tokens: - token.safeTransferFrom(account, address(this), actualTokenCost); - balances[token][owner()] += actualTokenCost; - } -} diff --git a/test/deposit-paymaster.test.ts b/test/deposit-paymaster.test.ts deleted file mode 100644 index 2e7ca959c..000000000 --- a/test/deposit-paymaster.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import './aa.init' -import { ethers } from 'hardhat' -import { expect } from 'chai' -import { - SimpleAccount, - EntryPoint, - DepositPaymaster, - DepositPaymaster__factory, - TestOracle__factory, - TestCounter, - TestCounter__factory, - TestToken, - TestToken__factory -} from '../typechain' -import { - AddressZero, createAddress, - createAccountOwner, - deployEntryPoint, FIVE_ETH, ONE_ETH, userOpsWithoutAgg, createAccount -} from './testutils' -import { fillAndSign, simulateValidation } from './UserOp' -import { hexConcat, hexZeroPad, parseEther } from 'ethers/lib/utils' - -// TODO: fails after unrelated change in the repo -describe.skip('DepositPaymaster', () => { - let entryPoint: EntryPoint - const ethersSigner = ethers.provider.getSigner() - let token: TestToken - let paymaster: DepositPaymaster - before(async function () { - entryPoint = await deployEntryPoint() - - paymaster = await new DepositPaymaster__factory(ethersSigner).deploy(entryPoint.address) - await paymaster.addStake(1, { value: parseEther('2') }) - await entryPoint.depositTo(paymaster.address, { value: parseEther('1') }) - - token = await new TestToken__factory(ethersSigner).deploy() - const testOracle = await new TestOracle__factory(ethersSigner).deploy() - await paymaster.addToken(token.address, testOracle.address) - - await token.mint(await ethersSigner.getAddress(), FIVE_ETH) - await token.approve(paymaster.address, ethers.constants.MaxUint256) - }) - - describe('deposit', () => { - let account: SimpleAccount - - before(async () => { - ({ proxy: account } = await createAccount(ethersSigner, await ethersSigner.getAddress(), entryPoint.address)) - }) - it('should deposit and read balance', async () => { - await paymaster.addDepositFor(token.address, account.address, 100) - expect(await paymaster.depositInfo(token.address, account.address)).to.eql({ amount: 100 }) - }) - it('should fail to withdraw without unlock', async () => { - const paymasterWithdraw = await paymaster.populateTransaction.withdrawTokensTo(token.address, AddressZero, 1).then(tx => tx.data!) - - await expect( - account.execute(paymaster.address, 0, paymasterWithdraw) - ).to.revertedWith('DepositPaymaster: must unlockTokenDeposit') - }) - it('should fail to withdraw within the same block ', async () => { - const paymasterUnlock = await paymaster.populateTransaction.unlockTokenDeposit().then(tx => tx.data!) - const paymasterWithdraw = await paymaster.populateTransaction.withdrawTokensTo(token.address, AddressZero, 1).then(tx => tx.data!) - - await expect( - account.executeBatch([paymaster.address, paymaster.address], [], [paymasterUnlock, paymasterWithdraw]) - ).to.be.revertedWith('DepositPaymaster: must unlockTokenDeposit') - }) - it('should succeed to withdraw after unlock', async () => { - const paymasterUnlock = await paymaster.populateTransaction.unlockTokenDeposit().then(tx => tx.data!) - const target = createAddress() - const paymasterWithdraw = await paymaster.populateTransaction.withdrawTokensTo(token.address, target, 1).then(tx => tx.data!) - await account.execute(paymaster.address, 0, paymasterUnlock) - await account.execute(paymaster.address, 0, paymasterWithdraw) - expect(await token.balanceOf(target)).to.eq(1) - }) - }) - - describe('#validatePaymasterUserOp', () => { - let account: SimpleAccount - const gasPrice = 1e9 - - before(async () => { - ({ proxy: account } = await createAccount(ethersSigner, await ethersSigner.getAddress(), entryPoint.address)) - }) - - it('should fail if no token', async () => { - const userOp = await fillAndSign({ - sender: account.address, - paymasterAndData: paymaster.address - }, ethersSigner, entryPoint) - await expect(simulateValidation(userOp, entryPoint.address)).to.be.revertedWith('paymasterAndData must specify token') - }) - - it('should fail with wrong token', async () => { - const userOp = await fillAndSign({ - sender: account.address, - paymasterAndData: hexConcat([paymaster.address, hexZeroPad('0x1234', 20)]) - }, ethersSigner, entryPoint) - await expect(simulateValidation(userOp, entryPoint.address, { gasPrice })).to.be.revertedWith('DepositPaymaster: unsupported token') - }) - - it('should reject if no deposit', async () => { - const userOp = await fillAndSign({ - sender: account.address, - paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)]) - }, ethersSigner, entryPoint) - await expect(simulateValidation(userOp, entryPoint.address, { gasPrice })).to.be.revertedWith('DepositPaymaster: deposit too low') - }) - - it('should reject if deposit is not locked', async () => { - await paymaster.addDepositFor(token.address, account.address, ONE_ETH) - - const paymasterUnlock = await paymaster.populateTransaction.unlockTokenDeposit().then(tx => tx.data!) - await account.execute(paymaster.address, 0, paymasterUnlock) - - const userOp = await fillAndSign({ - sender: account.address, - paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)]) - }, ethersSigner, entryPoint) - await expect(simulateValidation(userOp, entryPoint.address, { gasPrice })).to.be.revertedWith('not locked') - }) - - it('succeed with valid deposit', async () => { - // needed only if previous test did unlock. - const paymasterLockTokenDeposit = await paymaster.populateTransaction.lockTokenDeposit().then(tx => tx.data!) - await account.execute(paymaster.address, 0, paymasterLockTokenDeposit) - - const userOp = await fillAndSign({ - sender: account.address, - paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)]) - }, ethersSigner, entryPoint) - await simulateValidation(userOp, entryPoint.address) - }) - }) - describe('#handleOps', () => { - let account: SimpleAccount - const accountOwner = createAccountOwner() - let counter: TestCounter - let callData: string - before(async () => { - ({ proxy: account } = await createAccount(ethersSigner, await accountOwner.getAddress(), entryPoint.address)) - counter = await new TestCounter__factory(ethersSigner).deploy() - const counterJustEmit = await counter.populateTransaction.justemit().then(tx => tx.data!) - callData = await account.populateTransaction.execute(counter.address, 0, counterJustEmit).then(tx => tx.data!) - - await paymaster.addDepositFor(token.address, account.address, ONE_ETH) - }) - it('should pay with deposit (and revert user\'s call) if user can\'t pay with tokens', async () => { - const beneficiary = createAddress() - const userOp = await fillAndSign({ - sender: account.address, - paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)]), - callData - }, accountOwner, entryPoint) - - await entryPoint.handleAggregatedOps(userOpsWithoutAgg([userOp]), beneficiary) - - const [log] = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent()) - expect(log.args.success).to.eq(false) - expect(await counter.queryFilter(counter.filters.CalledFrom())).to.eql([]) - expect(await ethers.provider.getBalance(beneficiary)).to.be.gt(0) - }) - - it('should pay with tokens if available', async () => { - const beneficiary = createAddress() - const beneficiary1 = createAddress() - const initialTokens = parseEther('1') - await token.mint(account.address, initialTokens) - - // need to "approve" the paymaster to use the tokens. we issue a UserOp for that (which uses the deposit to execute) - const tokenApprovePaymaster = await token.populateTransaction.approve(paymaster.address, ethers.constants.MaxUint256).then(tx => tx.data!) - const execApprove = await account.populateTransaction.execute(token.address, 0, tokenApprovePaymaster).then(tx => tx.data!) - const userOp1 = await fillAndSign({ - sender: account.address, - paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)]), - callData: execApprove - }, accountOwner, entryPoint) - await entryPoint.handleAggregatedOps(userOpsWithoutAgg([userOp1]), beneficiary1) - - const userOp = await fillAndSign({ - sender: account.address, - paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)]), - callData - }, accountOwner, entryPoint) - await entryPoint.handleAggregatedOps(userOpsWithoutAgg([userOp]), beneficiary) - - const [log] = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(), await ethers.provider.getBlockNumber()) - expect(log.args.success).to.eq(true) - const charge = log.args.actualGasCost - expect(await ethers.provider.getBalance(beneficiary)).to.eq(charge) - - const targetLogs = await counter.queryFilter(counter.filters.CalledFrom()) - expect(targetLogs.length).to.eq(1) - }) - }) -})