Skip to content

Commit

Permalink
Add Paycall flag for reverting and add PayForGas event (#195)
Browse files Browse the repository at this point in the history
This adds a constructor arg to `Paycall` to toggle between propagating and swallowing reverts. I also add a new `PayForGas` event.
  • Loading branch information
kevincheng96 authored Apr 29, 2024
1 parent ec22aa6 commit 6b22766
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 58 deletions.
57 changes: 10 additions & 47 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
ApproveAndSwapTest:testSwap() (gas: 286242)
ApproveAndSwapTest:testSwapFailsIfWeExpectedTooMuch() (gas: 365553)
ApproveAndSwapTest:testSwapFailsWithNoApproval() (gas: 123118)
BatchExecutorTest:testBatchExecuteWithPartialFailures() (gas: 129689)
BatchExecutorTest:testBatchExecuteWithPartialFailuresDoesNotRevertIfAnyCallsRevert() (gas: 123262)
BatchExecutorTest:testBatchExecuteWithoutPartialFailures() (gas: 129953)
Expand Down Expand Up @@ -30,11 +27,6 @@ CodeJarTest:testCodeJarStoresSelfReference() (gas: 107362)
CodeJarTest:testCodeJarTickCounter() (gas: 176903)
CodeJarTest:testRevertsOnConstructorRevert() (gas: 85881)
CodeJarTest:testRevertsOnSelfDestructingConstructor() (gas: 54396)
CometClaimRewardsTest:testClaimComp() (gas: 132187)
CometRepayAndWithdrawMultipleAssetsTest:testInvalidInput() (gas: 68856)
CometRepayAndWithdrawMultipleAssetsTest:testRepayAndWithdrawMultipleAssets() (gas: 154648)
CometSupplyMultipleAssetsAndBorrowTest:testInvalidInput() (gas: 68807)
CometSupplyMultipleAssetsAndBorrowTest:testSupplyMultipleAssetsAndBorrow() (gas: 296281)
ConditionalMulticallTest:testConditionalRunEmptyInputIsValid() (gas: 52025)
ConditionalMulticallTest:testConditionalRunInvalidInput() (gas: 69668)
ConditionalMulticallTest:testConditionalRunMulticallError() (gas: 317487)
Expand All @@ -61,7 +53,6 @@ EthcallTest:testEthcallSupplyUSDCToComet() (gas: 173400)
EthcallTest:testEthcallWithdrawUSDCFromComet() (gas: 298210)
ExecutorTest:testExecutorCanDirectCall() (gas: 132195)
ExecutorTest:testExecutorCanDirectCallBySig() (gas: 118698)
GetDripTest:testDrip() (gas: 121447)
MulticallTest:testCallcodeToMulticallSucceedsWhenUninitialized() (gas: 84259)
MulticallTest:testCreateSubWalletAndExecute() (gas: 602979)
MulticallTest:testEmptyInputIsValid() (gas: 51541)
Expand All @@ -75,15 +66,16 @@ MulticallTest:testSupplyWETHWithdrawUSDCOnComet() (gas: 260802)
NonceTest:testIsSet() (gas: 30425)
NonceTest:testNextUnusedNonce() (gas: 40315)
NonceTest:testNonLinearNonce() (gas: 30757)
PaycallTest:testInitializeProperlyFromConstructor() (gas: 6581)
PaycallTest:testPaycallForPayWithUSDT() (gas: 122194)
PaycallTest:testPaycallForPayWithWBTC() (gas: 116673)
PaycallTest:testReturnCallResult() (gas: 91007)
PaycallTest:testRevertsForInvalidCallContext() (gas: 16770)
PaycallTest:testRevertsWhenCostIsMoreThanMaxPaymentCost() (gas: 120332)
PaycallTest:testSimpleCounterAndPayWithUSDC() (gas: 145103)
PaycallTest:testSimpleTransferTokenAndPayWithUSDC() (gas: 140647)
PaycallTest:testSupplyWETHWithdrawUSDCOnCometAndPayWithUSDC() (gas: 301424)
PaycallTest:testInitializeProperlyFromConstructor() (gas: 6606)
PaycallTest:testPaycallForPayWithUSDT() (gas: 126144)
PaycallTest:testPaycallForPayWithWBTC() (gas: 120640)
PaycallTest:testPaycallNonRevertyDoesNotRevertWhenCallReverts() (gas: 112591)
PaycallTest:testReturnCallResult() (gas: 93192)
PaycallTest:testRevertsForInvalidCallContext() (gas: 16767)
PaycallTest:testRevertsWhenCostIsMoreThanMaxPaymentCost() (gas: 120368)
PaycallTest:testSimpleCounterAndPayWithUSDC() (gas: 150073)
PaycallTest:testSimpleTransferTokenAndPayWithUSDC() (gas: 145617)
PaycallTest:testSupplyWETHWithdrawUSDCOnCometAndPayWithUSDC() (gas: 306416)
QuarkFactoryTest:testInvariantAddressesBetweenNonces() (gas: 3359064)
QuarkFactoryTest:testQuarkFactoryDeployToDeterministicAddresses() (gas: 3358797)
QuarkFactoryTest:testQuarkFactoryDeployTwice() (gas: 3452021)
Expand Down Expand Up @@ -157,24 +149,6 @@ RevertsTest:testRevertsInteger() (gas: 66969)
RevertsTest:testRevertsInvalidOpcode() (gas: 8391762413094949948)
RevertsTest:testRevertsOutOfGas() (gas: 295979)
RevertsTest:testRevertsWhenDividingByZero() (gas: 66815)
SupplyActionsTest:testInvalidInput() (gas: 68436)
SupplyActionsTest:testRepayBorrow() (gas: 90532)
SupplyActionsTest:testSupply() (gas: 132714)
SupplyActionsTest:testSupplyFrom() (gas: 112718)
SupplyActionsTest:testSupplyMultipleCollateral() (gas: 271636)
SupplyActionsTest:testSupplyTo() (gas: 132409)
TransferActionsTest:testRevertsForTransferERC777ReentrancyAttackWithReentrancyGuard() (gas: 150272)
TransferActionsTest:testRevertsForTransferReentrancyAttackWithReentrancyGuard() (gas: 131552)
TransferActionsTest:testRevertsForTransferReentrancyAttackWithoutCallbackEnabled() (gas: 100266)
TransferActionsTest:testRevertsForTransferReentrantAttackWithStolenSignature() (gas: 111641)
TransferActionsTest:testTransferERC20TokenToEOA() (gas: 70970)
TransferActionsTest:testTransferERC20TokenToQuarkWallet() (gas: 72347)
TransferActionsTest:testTransferERC777SuccessWithEvilReceiverWithoutAttackAttempt() (gas: 107807)
TransferActionsTest:testTransferERC777TokenReentrancyAttackSuccessWithCallbackEnabled() (gas: 145954)
TransferActionsTest:testTransferNativeTokenToEOA() (gas: 80223)
TransferActionsTest:testTransferNativeTokenToQuarkWallet() (gas: 55619)
TransferActionsTest:testTransferReentrancyAttackSuccessWithCallbackEnabled() (gas: 129665)
TransferActionsTest:testTransferSuccessWithEvilReceiverWithoutAttackAttempt() (gas: 88210)
UniswapFlashLoanTest:testFlashLoanForCollateralSwapOnCompound() (gas: 428563)
UniswapFlashLoanTest:testRevertsForInsufficientFundsToRepayFlashLoan() (gas: 194222)
UniswapFlashLoanTest:testRevertsForInvalidCaller() (gas: 71191)
Expand All @@ -184,17 +158,6 @@ UniswapFlashSwapExactOutTest:testInvalidCallerFlashSwap() (gas: 71169)
UniswapFlashSwapExactOutTest:testNotEnoughToPayFlashSwap() (gas: 294961)
UniswapFlashSwapExactOutTest:testRevertsIfCalledDirectly() (gas: 10871)
UniswapFlashSwapExactOutTest:testUniswapFlashSwapExactOutLeverageComet() (gas: 355100)
UniswapSwapActionsTest:testApprovalRefund() (gas: 164763)
UniswapSwapActionsTest:testBuyAssetOneStop() (gas: 252895)
UniswapSwapActionsTest:testBuyAssetTwoStops() (gas: 359452)
UniswapSwapActionsTest:testSellAssetOneStop() (gas: 250230)
UniswapSwapActionsTest:testSellAssetTwoStops() (gas: 363377)
WithdrawActionsTest:testBorrow() (gas: 154760)
WithdrawActionsTest:testInvalidInput() (gas: 68308)
WithdrawActionsTest:testWithdraw() (gas: 84474)
WithdrawActionsTest:testWithdrawFrom() (gas: 84000)
WithdrawActionsTest:testWithdrawMultipleAssets() (gas: 160197)
WithdrawActionsTest:testWithdrawTo() (gas: 84468)
isValidSignatureTest:testIsValidSignatureForEOAOwner() (gas: 13445)
isValidSignatureTest:testReturnsMagicValueForValidSignature() (gas: 9376)
isValidSignatureTest:testRevertsForEmptyContract() (gas: 11305)
Expand Down
22 changes: 15 additions & 7 deletions src/quark-core-scripts/src/Paycall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
contract Paycall {
using SafeERC20 for IERC20;

event PayForGas(address indexed payer, address indexed payee, address indexed paymentToken, uint256 amount);

error InvalidCallContext();
error TransactionTooExpensive();

Expand All @@ -25,8 +27,11 @@ contract Paycall {
/// @notice Payment token address
address public immutable paymentTokenAddress;

/// @notice Flag for indicating if reverts from the call should be propagated or swallowed
bool public immutable propagateReverts;

/// @notice Constant buffer for gas overhead
/// This is a constant to accounted for the gas used by the Paycall contract itself that's not tracked by gasleft()
/// This is a constant to account for the gas used by the Paycall contract itself that's not tracked by gasleft()
uint256 internal constant GAS_OVERHEAD = 75000;

/// @notice Difference in scale between the payment token and ETH, used to scale the payment token.
Expand All @@ -35,12 +40,14 @@ contract Paycall {

/**
* @notice Constructor
* @param ethPriceFeed Eth based price feed address that follows Chainlink's AggregatorV3Interface correlated to the payment token
* @param paymentToken Payment token address
* @param ethBasedPriceFeedAddress_ Eth based price feed address that follows Chainlink's AggregatorV3Interface correlated to the payment token
* @param paymentTokenAddress_ Payment token address
* @param propagateReverts_ Flag for indicating if reverts from the call should be propagated or swallowed
*/
constructor(address ethPriceFeed, address paymentToken) {
ethBasedPriceFeedAddress = ethPriceFeed;
paymentTokenAddress = paymentToken;
constructor(address ethBasedPriceFeedAddress_, address paymentTokenAddress_, bool propagateReverts_) {
ethBasedPriceFeedAddress = ethBasedPriceFeedAddress_;
paymentTokenAddress = paymentTokenAddress_;
propagateReverts = propagateReverts_;
scriptAddress = address(this);

divisorScale = 10
Expand Down Expand Up @@ -68,7 +75,7 @@ contract Paycall {
}

(bool success, bytes memory returnData) = callContract.delegatecall(callData);
if (!success) {
if (!success && propagateReverts) {
assembly {
revert(add(returnData, 32), mload(returnData))
}
Expand All @@ -81,6 +88,7 @@ contract Paycall {
revert TransactionTooExpensive();
}
IERC20(paymentTokenAddress).safeTransfer(tx.origin, paymentAmount);
emit PayForGas(address(this), tx.origin, paymentTokenAddress, paymentAmount);

return returnData;
}
Expand Down
59 changes: 55 additions & 4 deletions test/quark-core-scripts/Paycall.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {Multicall} from "quark-core-scripts/src/Multicall.sol";
import {Paycall} from "quark-core-scripts/src/Paycall.sol";

import {Counter} from "test/lib/Counter.sol";
import {Reverts} from "test/lib/Reverts.sol";

import {YulHelper} from "test/lib/YulHelper.sol";
import {SignatureHelper} from "test/lib/SignatureHelper.sol";
Expand All @@ -26,6 +27,8 @@ import "quark-core-scripts/src/vendor/chainlink/AggregatorV3Interface.sol";
import {IComet, IERC20} from "test/lib/DeFiScripts.sol";

contract PaycallTest is Test {
event PayForGas(address indexed payer, address indexed payee, address indexed paymentToken, uint256 amount);

QuarkWalletProxyFactory public factory;
Counter public counter;
uint256 alicePrivateKey = 0xa11ce;
Expand All @@ -48,10 +51,12 @@ contract PaycallTest is Test {
address constant uniswapRouter = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
bytes multicall = new YulHelper().getCode("Multicall.sol/Multicall.json");
bytes ethcall = new YulHelper().getCode("Ethcall.sol/Ethcall.json");
bytes reverts = new YulHelper().getCode("Reverts.sol/Reverts.json");
// Paycall has its contructor with 2 parameters
bytes paycall;
bytes paycallUSDT;
bytes paycallWBTC;
bytes paycallNonReverty;

bytes legendCometSupplyScript = new YulHelper().getCode("DeFiScripts.sol/CometSupplyActions.json");

Expand All @@ -61,9 +66,11 @@ contract PaycallTest is Test {

address ethcallAddress;
address multicallAddress;
address revertsAddress;
address paycallAddress;
address paycallUSDTAddress;
address paycallWBTCAddress;
address paycallNonRevertyAddress;
address legendCometSupplyScriptAddress;
address legendCometWithdrawScriptAddress;
address legendUniswapSwapScriptAddress;
Expand All @@ -82,13 +89,16 @@ contract PaycallTest is Test {
codeJar = QuarkWallet(payable(factory.walletImplementation())).codeJar();
ethcallAddress = codeJar.saveCode(ethcall);
multicallAddress = codeJar.saveCode(multicall);
paycall = abi.encodePacked(type(Paycall).creationCode, abi.encode(ETH_USD_PRICE_FEED, USDC));
paycallUSDT = abi.encodePacked(type(Paycall).creationCode, abi.encode(ETH_USD_PRICE_FEED, USDT));
paycallWBTC = abi.encodePacked(type(Paycall).creationCode, abi.encode(ETH_BTC_PRICE_FEED, WBTC));
revertsAddress = codeJar.saveCode(reverts);
paycall = abi.encodePacked(type(Paycall).creationCode, abi.encode(ETH_USD_PRICE_FEED, USDC, true));
paycallUSDT = abi.encodePacked(type(Paycall).creationCode, abi.encode(ETH_USD_PRICE_FEED, USDT, true));
paycallWBTC = abi.encodePacked(type(Paycall).creationCode, abi.encode(ETH_BTC_PRICE_FEED, WBTC, true));
paycallNonReverty = abi.encodePacked(type(Paycall).creationCode, abi.encode(ETH_USD_PRICE_FEED, USDC, false));

paycallAddress = codeJar.saveCode(paycall);
paycallUSDTAddress = codeJar.saveCode(paycallUSDT);
paycallWBTCAddress = codeJar.saveCode(paycallWBTC);
paycallNonRevertyAddress = codeJar.saveCode(paycallNonReverty);

legendCometSupplyScriptAddress = codeJar.saveCode(legendCometSupplyScript);
legendCometWithdrawScriptAddress = codeJar.saveCode(legendCometWithdrawScript);
Expand Down Expand Up @@ -165,7 +175,10 @@ contract PaycallTest is Test {

// gas: meter execute
vm.resumeGasMetering();
vm.expectEmit(true, true, true, false); // We ignore the amount because it will differ based on via-IR
emit PayForGas(address(wallet), tx.origin, USDC, 7_264_471);
wallet.executeQuarkOperation(op, v, r, s);

assertEq(counter.number(), 15);
assertApproxEqAbs(IERC20(USDC).balanceOf(address(wallet)), 992e6, 1e6);
}
Expand Down Expand Up @@ -199,7 +212,10 @@ contract PaycallTest is Test {

// gas: meter execute
vm.resumeGasMetering();
vm.expectEmit(true, true, true, false); // We ignore the amount because it will differ based on via-IR
emit PayForGas(address(wallet), tx.origin, USDC, 7_489_695);
wallet.executeQuarkOperation(op, v, r, s);

assertApproxEqAbs(IERC20(USDC).balanceOf(address(wallet)), 982e6, 1e6);
assertEq(IERC20(USDC).balanceOf(address(this)), 10e6);
}
Expand Down Expand Up @@ -257,7 +273,10 @@ contract PaycallTest is Test {

// gas: meter execute
vm.resumeGasMetering();
vm.expectEmit(true, true, true, false); // We ignore the amount because it will differ based on via-IR
emit PayForGas(address(wallet), tx.origin, USDC, 18_045_902);
wallet.executeQuarkOperation(op, v, r, s);

assertApproxEqAbs(IERC20(USDC).balanceOf(address(wallet)), 981e6, 1e6);
assertEq(IComet(cUSDCv3).collateralBalanceOf(address(wallet), WETH), 100 ether);
assertApproxEqAbs(IComet(cUSDCv3).borrowBalanceOf(address(wallet)), 1000e6, 2);
Expand Down Expand Up @@ -326,7 +345,10 @@ contract PaycallTest is Test {
(uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, wallet, op);

vm.resumeGasMetering();
vm.expectEmit(true, true, true, false); // We ignore the amount because it will differ based on via-IR
emit PayForGas(address(wallet), tx.origin, USDT, 7_056_836);
wallet.executeQuarkOperation(op, v, r, s);

assertEq(IERC20(WETH).balanceOf(address(this)), 1 ether);
// About $8 in USD fees
assertApproxEqAbs(IERC20(USDT).balanceOf(address(wallet)), 992e6, 1e6);
Expand Down Expand Up @@ -361,10 +383,13 @@ contract PaycallTest is Test {
(uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, wallet, op);

vm.resumeGasMetering();
vm.expectEmit(true, true, true, false); // We ignore the amount because it will differ based on via-IR
emit PayForGas(address(wallet), tx.origin, WBTC, 20_332);
wallet.executeQuarkOperation(op, v, r, s);

assertEq(IERC20(WETH).balanceOf(address(this)), 1 ether);
// Fees in WBTC will be around ~ 0.00021 WBTC
assertApproxEqAbs(IERC20(WBTC).balanceOf(address(wallet)), 99979e3, 1e3);
assertApproxEqAbs(IERC20(WBTC).balanceOf(address(wallet)), 99_979_000, 1e3);
}

function testRevertsWhenCostIsMoreThanMaxPaymentCost() public {
Expand Down Expand Up @@ -401,4 +426,30 @@ contract PaycallTest is Test {

assertEq(IERC20(USDC).balanceOf(address(wallet)), 1000e6);
}

function testPaycallNonRevertyDoesNotRevertWhenCallReverts() public {
// gas: do not meter set-up
vm.pauseGasMetering();
vm.txGasPrice(32 gwei);
QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0)));
// Give wallet some USDC for payment
deal(USDC, address(wallet), 1000e6);

// Execute through paycall
QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata(
wallet,
paycallNonReverty,
abi.encodeWithSelector(Paycall.run.selector, revertsAddress, "", 20e6),
ScriptType.ScriptSource
);
(uint8 v, bytes32 r, bytes32 s) = new SignatureHelper().signOp(alicePrivateKey, wallet, op);

// gas: meter execute
vm.resumeGasMetering();
vm.expectEmit(true, true, true, false); // We ignore the amount because it will differ based on via-IR
emit PayForGas(address(wallet), tx.origin, USDC, 5_553_259);
wallet.executeQuarkOperation(op, v, r, s);

assertApproxEqAbs(IERC20(USDC).balanceOf(address(wallet)), 995e6, 1e6);
}
}

0 comments on commit 6b22766

Please sign in to comment.