From 5f13394c0676f397db29f9e247636a7f4edf4b7a Mon Sep 17 00:00:00 2001 From: Kevin Cheng Date: Fri, 22 Mar 2024 13:39:35 -0700 Subject: [PATCH] Add max payment cost to Paycall (#185) The max payment cost is specified in wei and is used to revert the script if the payment token amount exceeds it. --- src/quark-core-scripts/src/Paycall.sol | 10 ++++- test/quark-core-scripts/Paycall.t.sol | 60 ++++++++++++++++++++++---- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/quark-core-scripts/src/Paycall.sol b/src/quark-core-scripts/src/Paycall.sol index 7b2ee7c4..4e4cfe7a 100644 --- a/src/quark-core-scripts/src/Paycall.sol +++ b/src/quark-core-scripts/src/Paycall.sol @@ -14,6 +14,7 @@ contract Paycall { using SafeERC20 for IERC20; error InvalidCallContext(); + error TransactionTooExpensive(); /// @notice This contract's address address internal immutable scriptAddress; @@ -53,9 +54,13 @@ contract Paycall { * @notice Execute delegatecall on a contract and pay tx.origin for gas * @param callContract Contract to call * @param callData Encoded calldata for call + * @param maxPaymentCost The maximum amount of payment tokens allowed for this transaction * @return Return data from call */ - function run(address callContract, bytes calldata callData) external returns (bytes memory) { + function run(address callContract, bytes calldata callData, uint256 maxPaymentCost) + external + returns (bytes memory) + { uint256 gasInitial = gasleft(); // Ensures that this script cannot be called directly and self-destructed if (address(this) == scriptAddress) { @@ -72,6 +77,9 @@ contract Paycall { (, int256 price,,,) = AggregatorV3Interface(ethBasedPriceFeedAddress).latestRoundData(); uint256 gasUsed = gasInitial - gasleft() + GAS_OVERHEAD; uint256 paymentAmount = gasUsed * tx.gasprice * uint256(price) / divisorScale; + if (paymentAmount > maxPaymentCost) { + revert TransactionTooExpensive(); + } IERC20(paymentTokenAddress).safeTransfer(tx.origin, paymentAmount); return returnData; diff --git a/test/quark-core-scripts/Paycall.t.sol b/test/quark-core-scripts/Paycall.t.sol index 77fd5797..6a73cd90 100644 --- a/test/quark-core-scripts/Paycall.t.sol +++ b/test/quark-core-scripts/Paycall.t.sol @@ -115,7 +115,8 @@ contract PaycallTest is Test { address(counter), abi.encodeCall(Counter.setNumber, (1)), 0 // value - ) + ), + 10e6 ); } @@ -155,7 +156,8 @@ contract PaycallTest is Test { abi.encodeWithSelector( Paycall.run.selector, multicallAddress, - abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas) + abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), + 10e6 ), ScriptType.ScriptSource ); @@ -188,7 +190,8 @@ contract PaycallTest is Test { USDC, abi.encodeWithSignature("transfer(address,uint256)", address(this), 10e6), 0 - ) + ), + 20e6 ), ScriptType.ScriptSource ); @@ -245,7 +248,8 @@ contract PaycallTest is Test { abi.encodeWithSelector( Paycall.run.selector, multicallAddress, - abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas) + abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), + 20e6 ), ScriptType.ScriptSource ); @@ -275,7 +279,8 @@ contract PaycallTest is Test { ethcallAddress, abi.encodeWithSelector( Ethcall.run.selector, address(counter), abi.encodeWithSignature("decrement(uint256)", (1)), 0 - ) + ), + 20e6 ), ScriptType.ScriptSource ); @@ -297,7 +302,7 @@ contract PaycallTest is Test { vm.txGasPrice(32 gwei); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); - // Deal some USDT and WBTC + // Deal some USDT and WETH deal(USDT, address(wallet), 1000e6); deal(WETH, address(wallet), 1 ether); @@ -313,7 +318,8 @@ contract PaycallTest is Test { WETH, abi.encodeWithSignature("transfer(address,uint256)", address(this), 1 ether), 0 - ) + ), + 10e6 ), ScriptType.ScriptSource ); @@ -331,7 +337,7 @@ contract PaycallTest is Test { vm.txGasPrice(32 gwei); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); - // Deal some USDT and WBTC + // Deal some WBTC and WETH deal(WBTC, address(wallet), 1e8); deal(WETH, address(wallet), 1 ether); @@ -347,7 +353,8 @@ contract PaycallTest is Test { WETH, abi.encodeWithSignature("transfer(address,uint256)", address(this), 1 ether), 0 - ) + ), + 30e3 ), ScriptType.ScriptSource ); @@ -359,4 +366,39 @@ contract PaycallTest is Test { // Fees in WBTC will be around ~ 0.00021 WBTC assertApproxEqAbs(IERC20(WBTC).balanceOf(address(wallet)), 99979e3, 1e3); } + + function testRevertsWhenCostIsMoreThanMaxPaymentCost() public { + vm.pauseGasMetering(); + vm.txGasPrice(32 gwei); + QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); + + // Deal some USDC and WETH + deal(USDC, address(wallet), 1000e6); + deal(WETH, address(wallet), 1 ether); + + // Pay with USDC + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + wallet, + paycall, + abi.encodeWithSelector( + Paycall.run.selector, + ethcallAddress, + abi.encodeWithSelector( + Ethcall.run.selector, + WETH, + abi.encodeWithSignature("transfer(address,uint256)", address(this), 1 ether), + 0 + ), + 5e6 + ), + ScriptType.ScriptSource + ); + (uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, wallet, op); + + vm.resumeGasMetering(); + vm.expectRevert(abi.encodeWithSelector(Paycall.TransactionTooExpensive.selector)); + wallet.executeQuarkOperation(op, v, r, s); + + assertEq(IERC20(USDC).balanceOf(address(wallet)), 1000e6); + } }