From 625970fb09207f3cc99e31b94177cfd1292fe003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joseph-Andr=C3=A9=20Turk?= Date: Thu, 29 Aug 2024 19:40:03 +0200 Subject: [PATCH] feat: added fallback on origin, AA compatibility, tests --- codegen/payments.ts | 90 +++++++++++-- examples/PaymentLimit.sol | 40 ++++++ examples/Regression1.sol | 4 +- examples/SmartAccount.sol | 22 +++ examples/TracingSubCalls.sol | 5 +- lib/FHEPayment.sol | 78 ++++++++++- test/paymentUtils.ts | 13 ++ test/payments/payments.ts | 252 +++++++++++++++++++++++++++++++++++ 8 files changed, 484 insertions(+), 20 deletions(-) create mode 100644 examples/PaymentLimit.sol create mode 100644 examples/SmartAccount.sol create mode 100644 test/paymentUtils.ts create mode 100644 test/payments/payments.ts diff --git a/codegen/payments.ts b/codegen/payments.ts index e5e796a2..38e237f3 100644 --- a/codegen/payments.ts +++ b/codegen/payments.ts @@ -23,6 +23,10 @@ export function generateFHEPayment(priceData: PriceData): string { error RecoveryFailed(); error WithdrawalFailed(); error AccountNotEnoughFunded(); + error AlreadyAuthorizedAllContracts(); + error AlreadyWhitelistedContract(); + error AllContractsNotAuthorized(); + error ContractNotWhitelisted(); contract FHEPayment is Ownable2Step { /// @notice Name of the contract @@ -43,6 +47,8 @@ export function generateFHEPayment(priceData: PriceData): string { uint256 public claimableUsedFHEGas; mapping(address payer => uint256 depositedAmount) private depositsETH; + mapping(address user => bool allowedAllContracts) private allowedAll; + mapping(address user => mapping(address dappContract => bool isWhitelisted)) private whitelistedDapps; constructor() Ownable(msg.sender) {} @@ -67,19 +73,81 @@ export function generateFHEPayment(priceData: PriceData): string { return depositsETH[account]; } - function updateFunding(address payer, uint256 paidAmountGas) private { - uint256 ratio_gas = (tx.gasprice*FHE_GASPRICE_NATIVE_RATIO)/1_000_000; - uint256 effective_fhe_gasPrice = ratio_gas > MIN_FHE_GASPRICE ? ratio_gas : MIN_FHE_GASPRICE; - uint256 paidAmountWei = effective_fhe_gasPrice*paidAmountGas; - uint256 depositedAmount = depositsETH[payer]; - if(paidAmountWei>depositedAmount) revert AccountNotEnoughFunded(); - unchecked{ - depositsETH[payer] = depositedAmount - paidAmountWei; + function didAuthorizeAllContracts(address account) external view returns (bool) { + return allowedAll[account]; + } + + function didWhitelistContract(address user, address dappContract) external view returns (bool) { + return whitelistedDapps[user][dappContract]; + } + + function authorizeAllContracts() external { + if (allowedAll[msg.sender]) revert AlreadyAuthorizedAllContracts(); + allowedAll[msg.sender] = true; + } + + function whitelistContract(address dappContract) external { + if (whitelistedDapps[msg.sender][dappContract]) revert AlreadyWhitelistedContract(); + whitelistedDapps[msg.sender][dappContract] = true; + } + + function removeAuthorizationAllContracts() external { + if (!allowedAll[msg.sender]) revert AllContractsNotAuthorized(); + allowedAll[msg.sender] = false; + } + + function removeWhitelistedContract(address dappContract) external { + if (!whitelistedDapps[msg.sender][dappContract]) revert ContractNotWhitelisted(); + whitelistedDapps[msg.sender][dappContract] = false; + } + + // @notice: to be used in the context of account abstraction, before an FHE tx, to make the contract address replace tx.origin as a spender + function becomeTransientSpender() external { + assembly { + tstore(0, caller()) } - currentBlockConsumption += paidAmountGas; - claimableUsedFHEGas += paidAmountWei; } - + + // @notice: to be used in the context of account abstraction, after an FHE tx, to avoid issues if batched with other userOps + function stopBeingTransientSpender() external { + assembly { + tstore(0, 0) + } + } + + function updateFunding(address payer, uint256 paidAmountGas) private { + uint256 ratio_gas = (tx.gasprice * FHE_GASPRICE_NATIVE_RATIO) / 1_000_000; + uint256 effective_fhe_gasPrice = ratio_gas > MIN_FHE_GASPRICE ? ratio_gas : MIN_FHE_GASPRICE; + uint256 paidAmountWei = effective_fhe_gasPrice * paidAmountGas; + uint256 depositedAmount = depositsETH[payer]; + if (paidAmountWei > depositedAmount) { + // if dApp is not enough funded, fallbacks to user (tx.origin by default, in case of an EOA, + // otherwise a smart contract account should call \`becomeTransientSpender\` before, in the same tx + address spender; + assembly { + spender := tload(0) + } + spender = spender == address(0) ? tx.origin : spender; + if (allowedAll[spender] || whitelistedDapps[spender][payer]) { + uint256 depositedAmountUser = depositsETH[spender]; + if (paidAmountWei > depositedAmountUser) revert AccountNotEnoughFunded(); + unchecked { + depositsETH[spender] = depositedAmountUser - paidAmountWei; + } + currentBlockConsumption += paidAmountGas; + claimableUsedFHEGas += paidAmountWei; + } else { + revert AccountNotEnoughFunded(); + } + } else { + unchecked { + depositsETH[payer] = depositedAmount - paidAmountWei; + } + currentBlockConsumption += paidAmountGas; + claimableUsedFHEGas += paidAmountWei; + } + } + function checkIfNewBlock() private { uint256 lastBlock_ = block.number; if (block.number > lastBlock) { diff --git a/examples/PaymentLimit.sol b/examples/PaymentLimit.sol new file mode 100644 index 00000000..bbe8e13a --- /dev/null +++ b/examples/PaymentLimit.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity ^0.8.24; + +import "../lib/TFHE.sol"; +import "@openzeppelin/contracts/access/Ownable2Step.sol"; +import "../payment/Payment.sol"; + +contract PaymentLimit { + constructor() payable { + Payment.depositForThis(msg.value); + } + + function wayunderBlockFHEGasLimit() external { + // should pass if only tx in block + euint64 x = TFHE.asEuint64(2); + euint64 result; + for (uint256 i; i < 3; i++) { + result = TFHE.mul(result, x); + } + } + + function underBlockFHEGasLimit() external { + // should pass if only tx in block + euint64 x = TFHE.asEuint64(2); + euint64 result; + for (uint256 i; i < 15; i++) { + result = TFHE.mul(result, x); + } + } + + function aboveBlockFHEGasLimit() external { + // should revert due to exceeding block fheGas limit + euint64 x = TFHE.asEuint64(2); + euint64 result; + for (uint256 i; i < 16; i++) { + result = TFHE.mul(result, x); + } + } +} diff --git a/examples/Regression1.sol b/examples/Regression1.sol index 23e29507..b8ae90eb 100644 --- a/examples/Regression1.sol +++ b/examples/Regression1.sol @@ -1,5 +1,5 @@ -/* SPDX-License-Identifier: MIT */ -pragma solidity ^0.8.20; +// SPDX-License-Identifier: BSD-3-Clause-Clear +pragma solidity ^0.8.24; contract Regression1 { error IndexOutOfBound(); diff --git a/examples/SmartAccount.sol b/examples/SmartAccount.sol new file mode 100644 index 00000000..b7749eac --- /dev/null +++ b/examples/SmartAccount.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/access/Ownable2Step.sol"; + +contract SmartAccount is Ownable2Step { + struct Transaction { + address target; + uint256 value; + bytes data; + } + + constructor() Ownable(msg.sender) {} + + function executeBatch(Transaction[] memory transactions) public payable onlyOwner { + for (uint i = 0; i < transactions.length; i++) { + Transaction memory transaction = transactions[i]; + (bool success, ) = transaction.target.call{value: transaction.value}(transaction.data); + require(success, "Transaction failed"); + } + } +} diff --git a/examples/TracingSubCalls.sol b/examples/TracingSubCalls.sol index a4dc8f69..65f9c243 100644 --- a/examples/TracingSubCalls.sol +++ b/examples/TracingSubCalls.sol @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity ^0.8.24; import "../lib/TFHE.sol"; contract TracingSubCalls { diff --git a/lib/FHEPayment.sol b/lib/FHEPayment.sol index 7ff23c67..5dd56e8e 100644 --- a/lib/FHEPayment.sol +++ b/lib/FHEPayment.sol @@ -13,6 +13,10 @@ error OnlyNonScalarOperationsAreSupported(); error RecoveryFailed(); error WithdrawalFailed(); error AccountNotEnoughFunded(); +error AlreadyAuthorizedAllContracts(); +error AlreadyWhitelistedContract(); +error AllContractsNotAuthorized(); +error ContractNotWhitelisted(); contract FHEPayment is Ownable2Step { /// @notice Name of the contract @@ -33,6 +37,8 @@ contract FHEPayment is Ownable2Step { uint256 public claimableUsedFHEGas; mapping(address payer => uint256 depositedAmount) private depositsETH; + mapping(address user => bool allowedAllContracts) private allowedAll; + mapping(address user => mapping(address dappContract => bool isWhitelisted)) private whitelistedDapps; constructor() Ownable(msg.sender) {} @@ -57,17 +63,79 @@ contract FHEPayment is Ownable2Step { return depositsETH[account]; } + function didAuthorizeAllContracts(address account) external view returns (bool) { + return allowedAll[account]; + } + + function didWhitelistContract(address user, address dappContract) external view returns (bool) { + return whitelistedDapps[user][dappContract]; + } + + function authorizeAllContracts() external { + if (allowedAll[msg.sender]) revert AlreadyAuthorizedAllContracts(); + allowedAll[msg.sender] = true; + } + + function whitelistContract(address dappContract) external { + if (whitelistedDapps[msg.sender][dappContract]) revert AlreadyWhitelistedContract(); + whitelistedDapps[msg.sender][dappContract] = true; + } + + function removeAuthorizationAllContracts() external { + if (!allowedAll[msg.sender]) revert AllContractsNotAuthorized(); + allowedAll[msg.sender] = false; + } + + function removeWhitelistedContract(address dappContract) external { + if (!whitelistedDapps[msg.sender][dappContract]) revert ContractNotWhitelisted(); + whitelistedDapps[msg.sender][dappContract] = false; + } + + // @notice: to be used in the context of account abstraction, before an FHE tx, to make the contract address replace tx.origin as a spender + function becomeTransientSpender() external { + assembly { + tstore(0, caller()) + } + } + + // @notice: to be used in the context of account abstraction, after an FHE tx, to avoid issues if batched with other userOps + function stopBeingTransientSpender() external { + assembly { + tstore(0, 0) + } + } + function updateFunding(address payer, uint256 paidAmountGas) private { uint256 ratio_gas = (tx.gasprice * FHE_GASPRICE_NATIVE_RATIO) / 1_000_000; uint256 effective_fhe_gasPrice = ratio_gas > MIN_FHE_GASPRICE ? ratio_gas : MIN_FHE_GASPRICE; uint256 paidAmountWei = effective_fhe_gasPrice * paidAmountGas; uint256 depositedAmount = depositsETH[payer]; - if (paidAmountWei > depositedAmount) revert AccountNotEnoughFunded(); - unchecked { - depositsETH[payer] = depositedAmount - paidAmountWei; + if (paidAmountWei > depositedAmount) { + // if dApp is not enough funded, fallbacks to user (tx.origin by default, in case of an EOA, + // otherwise a smart contract account should call `becomeTransientSpender` before, in the same tx + address spender; + assembly { + spender := tload(0) + } + spender = spender == address(0) ? tx.origin : spender; + if (allowedAll[spender] || whitelistedDapps[spender][payer]) { + uint256 depositedAmountUser = depositsETH[spender]; + if (paidAmountWei > depositedAmountUser) revert AccountNotEnoughFunded(); + unchecked { + depositsETH[spender] = depositedAmountUser - paidAmountWei; + } + currentBlockConsumption += paidAmountGas; + claimableUsedFHEGas += paidAmountWei; + } else { + revert AccountNotEnoughFunded(); + } + } else { + unchecked { + depositsETH[payer] = depositedAmount - paidAmountWei; + } + currentBlockConsumption += paidAmountGas; + claimableUsedFHEGas += paidAmountWei; } - currentBlockConsumption += paidAmountGas; - claimableUsedFHEGas += paidAmountWei; } function checkIfNewBlock() private { diff --git a/test/paymentUtils.ts b/test/paymentUtils.ts new file mode 100644 index 00000000..26f4f57a --- /dev/null +++ b/test/paymentUtils.ts @@ -0,0 +1,13 @@ +import dotenv from 'dotenv'; +import fs from 'fs'; +import { ethers } from 'hardhat'; + +export async function initializeFHEPayment() { + const fhePaymentFactory = await ethers.getContractFactory('FHEPayment'); + const parsedFHEPayment = dotenv.parse(fs.readFileSync('lib/.env.fhepayment')); + const fhePayment = fhePaymentFactory.attach(parsedFHEPayment.FHE_PAYMENT_CONTRACT_ADDRESS); + return fhePayment; +} + +export const FHE_GASPRICE_NATIVE_RATIO = 1000n; +export const MIN_FHE_GASPRICE = 10_000_000n; diff --git a/test/payments/payments.ts b/test/payments/payments.ts new file mode 100644 index 00000000..a7ef4c38 --- /dev/null +++ b/test/payments/payments.ts @@ -0,0 +1,252 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +import { createInstances } from '../instance'; +import { FHE_GASPRICE_NATIVE_RATIO, MIN_FHE_GASPRICE, initializeFHEPayment } from '../paymentUtils'; +import { getSigners, initSigners } from '../signers'; + +describe('TestFHEPayment', function () { + before(async function () { + await initSigners(2); + this.signers = await getSigners(); + this.fhePayment = await initializeFHEPayment(); + }); + + beforeEach(async function () { + this.instances = await createInstances(this.signers); + }); + + it('contract which is not funded cannot be used by non-depositor', async function () { + const contractFactory = await ethers.getContractFactory('EncryptedERC20'); + const contract = await contractFactory.connect(this.signers.alice).deploy('Naraggara', 'NARA', { + value: ethers.parseEther('0'), // don't fund contract + }); + await contract.waitForDeployment(); + await expect(contract.mint(1000)).to.be.revertedWithCustomError(this.fhePayment, 'AccountNotEnoughFunded'); + }); + + it('contract with enough deposits burns the correct amount of fheGas', async function () { + const contractFactory = await ethers.getContractFactory('EncryptedERC20'); + const contract = await contractFactory.connect(this.signers.alice).deploy('Naraggara', 'NARA', { + value: ethers.parseEther('0.001'), + }); + const initialDeposit = await this.fhePayment.getAvailableDepositsETH(await contract.getAddress()); + await contract.waitForDeployment(); + const tx = await contract.mint(1000n); + const rcpt = await tx.wait(); + const ratioGas = (rcpt!.gasPrice * FHE_GASPRICE_NATIVE_RATIO) / 1_000_000n; + const effectiveFheGasPrice = ratioGas > MIN_FHE_GASPRICE ? ratioGas : MIN_FHE_GASPRICE; + const remainingDeposit = await this.fhePayment.getAvailableDepositsETH(await contract.getAddress()); + const consumedFheGas = (initialDeposit - remainingDeposit) / effectiveFheGasPrice; + expect(consumedFheGas).to.equal(188000n + 600n); // scalarFheAdd(euint64) + trivialEncrypt(euint64) + }); + + it('contract which is not funded can be used by depositor if he whitelisted dApp contract', async function () { + const contractFactory = await ethers.getContractFactory('EncryptedERC20'); + const contract = await contractFactory.connect(this.signers.alice).deploy('Naraggara', 'NARA', { + value: ethers.parseEther('0'), // don't fund contract + }); + await contract.waitForDeployment(); + const tx = await this.fhePayment.depositETH(this.signers.alice, { value: ethers.parseEther('0.001') }); + await tx.wait(); + const initialDeposit = await this.fhePayment.getAvailableDepositsETH(this.signers.alice); + const txbis = await this.fhePayment.whitelistContract(contract); + await txbis.wait(); + const tx2 = await contract.mint(1000); + const rcpt = await tx2.wait(); + const ratioGas = (rcpt!.gasPrice * FHE_GASPRICE_NATIVE_RATIO) / 1_000_000n; + const effectiveFheGasPrice = ratioGas > MIN_FHE_GASPRICE ? ratioGas : MIN_FHE_GASPRICE; + const remainingDeposit = await this.fhePayment.getAvailableDepositsETH(this.signers.alice); + const consumedFheGas = (initialDeposit - remainingDeposit) / effectiveFheGasPrice; + expect(consumedFheGas).to.equal(188000n + 600n); // scalarFheAdd(euint64) + trivialEncrypt(euint64) + }); + + it('contract which is not funded can be used by depositor if he authorized all contracts', async function () { + const contractFactory = await ethers.getContractFactory('EncryptedERC20'); + const contract = await contractFactory.connect(this.signers.alice).deploy('Naraggara', 'NARA', { + value: ethers.parseEther('0'), // don't fund contract + }); + await contract.waitForDeployment(); + const tx = await this.fhePayment.depositETH(this.signers.alice, { value: ethers.parseEther('0.001') }); + await tx.wait(); + const initialDeposit = await this.fhePayment.getAvailableDepositsETH(this.signers.alice); + const txbis = await this.fhePayment.authorizeAllContracts(contract); + await txbis.wait(); + const tx2 = await contract.mint(1000); + const rcpt = await tx2.wait(); + const ratioGas = (rcpt!.gasPrice * FHE_GASPRICE_NATIVE_RATIO) / 1_000_000n; + const effectiveFheGasPrice = ratioGas > MIN_FHE_GASPRICE ? ratioGas : MIN_FHE_GASPRICE; + const remainingDeposit = await this.fhePayment.getAvailableDepositsETH(this.signers.alice); + const consumedFheGas = (initialDeposit - remainingDeposit) / effectiveFheGasPrice; + expect(consumedFheGas).to.equal(188000n + 600n); // scalarFheAdd(euint64) + trivialEncrypt(euint64) + }); + + it('contract which is not funded cannot be used by depositor if he did not authorize all contracts nor whitelisted dapp contract', async function () { + const contractFactory = await ethers.getContractFactory('EncryptedERC20'); + const contract = await contractFactory.connect(this.signers.alice).deploy('Naraggara', 'NARA', { + value: ethers.parseEther('0'), // don't fund contract + }); + await contract.waitForDeployment(); + const tx = await this.fhePayment.depositETH(this.signers.alice, { value: ethers.parseEther('0.001') }); + await tx.wait(); + const initialDeposit = await this.fhePayment.getAvailableDepositsETH(this.signers.alice); + const txbis = await this.fhePayment.removeAuthorizationAllContracts(); + await txbis.wait(); + await expect(contract.mint(1000)).to.be.revertedWithCustomError(this.fhePayment, 'AccountNotEnoughFunded'); + const remainingDeposit = await this.fhePayment.getAvailableDepositsETH(this.signers.alice); + expect(remainingDeposit).to.equal(initialDeposit); + }); + + it('tx succeeds if under block fheGas limit', async function () { + const contractFactory = await ethers.getContractFactory('PaymentLimit'); + const contract = await contractFactory.connect(this.signers.alice).deploy(); + await contract.waitForDeployment(); + const initialDeposit = await this.fhePayment.getAvailableDepositsETH(this.signers.alice); + const txbis = await this.fhePayment.authorizeAllContracts(); + await txbis.wait(); + const tx2 = await contract.underBlockFHEGasLimit(); + const rcpt = await tx2.wait(); + const ratioGas = (rcpt!.gasPrice * FHE_GASPRICE_NATIVE_RATIO) / 1_000_000n; + const effectiveFheGasPrice = ratioGas > MIN_FHE_GASPRICE ? ratioGas : MIN_FHE_GASPRICE; + const remainingDeposit = await this.fhePayment.getAvailableDepositsETH(this.signers.alice); + const consumedFheGas = (initialDeposit - remainingDeposit) / effectiveFheGasPrice; + expect(consumedFheGas).to.equal(15n * 641000n + 2n * 600n); // 15*FheMul(euint64) + 2*trivialEncrypt(euint64) + }); + + it('tx reverts if above block fheGas limit', async function () { + const contractFactory = await ethers.getContractFactory('PaymentLimit'); + const contract = await contractFactory.connect(this.signers.alice).deploy(); + await contract.waitForDeployment(); + await expect(contract.aboveBlockFHEGasLimit()).revertedWithCustomError(this.fhePayment, 'FHEGasBlockLimitExceeded'); + }); + + it('a smart account becomes spender by calling becomeTransientSpender', async function () { + const contractFactory = await ethers.getContractFactory('SmartAccount'); + const smartAccount = await contractFactory.connect(this.signers.bob).deploy(); + await smartAccount.waitForDeployment(); + const tx = await this.fhePayment + .connect(this.signers.bob) + .depositETH(await smartAccount.getAddress(), { value: ethers.parseEther('0.001') }); + await tx.wait(); + + const initialDeposit = await this.fhePayment.getAvailableDepositsETH(await smartAccount.getAddress()); + + const contractFactory2 = await ethers.getContractFactory('PaymentLimit'); + const contract = await contractFactory2.connect(this.signers.alice).deploy(); + await contract.waitForDeployment(); + + const allowTx = [ + { + target: await this.fhePayment.getAddress(), + data: this.fhePayment.interface.encodeFunctionData('authorizeAllContracts'), + value: 0, + }, + ]; + + const txSmartAllow = await smartAccount.connect(this.signers.bob).executeBatch(allowTx); + await txSmartAllow.wait(); + + const FHETx = [ + { + target: await this.fhePayment.getAddress(), + data: this.fhePayment.interface.encodeFunctionData('becomeTransientSpender'), + value: 0, + }, + { + target: await contract.getAddress(), + data: contract.interface.encodeFunctionData('underBlockFHEGasLimit'), + value: 0, + }, + ]; + + // Execute the batched transaction + const txSmartFHE = await smartAccount.connect(this.signers.bob).executeBatch(FHETx); + + const rcpt = await txSmartFHE.wait(); + const ratioGas = (rcpt!.gasPrice * FHE_GASPRICE_NATIVE_RATIO) / 1_000_000n; + const effectiveFheGasPrice = ratioGas > MIN_FHE_GASPRICE ? ratioGas : MIN_FHE_GASPRICE; + const remainingDeposit = await this.fhePayment.getAvailableDepositsETH(await smartAccount.getAddress()); + const consumedFheGas = (initialDeposit - remainingDeposit) / effectiveFheGasPrice; + expect(consumedFheGas).to.equal(15n * 641000n + 2n * 600n); // 15*FheMul(euint64) + 2*trivialEncrypt(euint64) + }); + + it('batching a user-paid tx with a dapp-sponsored tx via a smart account', async function () { + const contractFactory = await ethers.getContractFactory('SmartAccount'); + const smartAccount = await contractFactory.connect(this.signers.bob).deploy(); + await smartAccount.waitForDeployment(); + const tx = await this.fhePayment + .connect(this.signers.bob) + .depositETH(await smartAccount.getAddress(), { value: ethers.parseEther('0.001') }); + await tx.wait(); + + const contractFactory2 = await ethers.getContractFactory('PaymentLimit'); + const contract = await contractFactory2.connect(this.signers.alice).deploy(); // non-sponsored dApp + await contract.waitForDeployment(); + + const contract2 = await contractFactory2.connect(this.signers.alice).deploy({ value: ethers.parseEther('0.001') }); // sponsored dApp + await contract2.waitForDeployment(); + + const initialDepositSmartAccount = await this.fhePayment.getAvailableDepositsETH(await smartAccount.getAddress()); + const initialDepositSponsoredDapp = await this.fhePayment.getAvailableDepositsETH(await contract2.getAddress()); + + const allowTx = [ + { + target: await this.fhePayment.getAddress(), + data: this.fhePayment.interface.encodeFunctionData('authorizeAllContracts'), + value: 0, + }, + ]; + + const txSmartAllow = await smartAccount.connect(this.signers.bob).executeBatch(allowTx); + await txSmartAllow.wait(); + + const FHETx = [ + { + target: await this.fhePayment.getAddress(), + data: this.fhePayment.interface.encodeFunctionData('becomeTransientSpender'), + value: 0, + }, + { + target: await contract.getAddress(), + data: contract.interface.encodeFunctionData('wayunderBlockFHEGasLimit'), + value: 0, + }, + { + target: await this.fhePayment.getAddress(), + data: this.fhePayment.interface.encodeFunctionData('stopBeingTransientSpender'), + value: 0, + }, + { + target: await contract2.getAddress(), + data: contract2.interface.encodeFunctionData('wayunderBlockFHEGasLimit'), + value: 0, + }, + ]; + + // Execute the batched transaction + const txSmartFHE = await smartAccount.connect(this.signers.bob).executeBatch(FHETx); + + const rcpt = await txSmartFHE.wait(); + const ratioGas = (rcpt!.gasPrice * FHE_GASPRICE_NATIVE_RATIO) / 1_000_000n; + const effectiveFheGasPrice = ratioGas > MIN_FHE_GASPRICE ? ratioGas : MIN_FHE_GASPRICE; + const remainingDepositSmartAccount = await this.fhePayment.getAvailableDepositsETH(await smartAccount.getAddress()); + const remainingDepositSponsoredDapp = await this.fhePayment.getAvailableDepositsETH(await contract2.getAddress()); + + const consumedFheGasSmartAccount = + (initialDepositSmartAccount - remainingDepositSmartAccount) / effectiveFheGasPrice; + expect(consumedFheGasSmartAccount).to.equal(3n * 641000n + 2n * 600n); // 3*FheMul(euint64) + 2*trivialEncrypt(euint64) + const consumedFheGasSponsoredDapp = + (initialDepositSponsoredDapp - remainingDepositSponsoredDapp) / effectiveFheGasPrice; + expect(consumedFheGasSponsoredDapp).to.equal(3n * 641000n + 2n * 600n); // 3*FheMul(euint64) + 2*trivialEncrypt(euint64) + }); + + it('user can withdraw his unburnt deposited funds', async function () { + const depositValue = await this.fhePayment.getAvailableDepositsETH(this.signers.alice); + expect(depositValue).to.be.greaterThan(0); + const balBobBefore = await ethers.provider.getBalance(this.signers.bob); + const tx = await this.fhePayment.withdrawETH(depositValue, this.signers.bob); + await tx.wait(); + const balBobAfter = await ethers.provider.getBalance(this.signers.bob); + expect(balBobAfter - balBobBefore).to.equal(depositValue); + }); +});