From 8b39c271f4b116669925b9db929914cb180e007c Mon Sep 17 00:00:00 2001 From: kevincheng96 Date: Mon, 18 Nov 2024 23:46:11 -0800 Subject: [PATCH 1/5] Use wrap up to scripts in QuarkBuilder --- src/builder/BridgeRoutes.sol | 15 ++++++++++ src/builder/QuarkBuilderBase.sol | 33 +++++++++++++++++---- src/builder/TokenWrapper.sol | 46 ++++++++++++++++++---------- src/builder/actions/Actions.sol | 3 +- test/builder/BridgingLogic.t.sol | 51 ++++++++++++++++---------------- test/builder/mocks/AcrossFFI.sol | 12 ++++++-- 6 files changed, 110 insertions(+), 50 deletions(-) diff --git a/src/builder/BridgeRoutes.sol b/src/builder/BridgeRoutes.sol index 8ba3cb8..f2ee88d 100644 --- a/src/builder/BridgeRoutes.sol +++ b/src/builder/BridgeRoutes.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.27; import {AcrossActions} from "src/AcrossScripts.sol"; import {CCTPBridgeActions} from "src/BridgeScripts.sol"; import {Errors} from "src/builder/Errors.sol"; +import {HashMap} from "src/builder/HashMap.sol"; import {QuarkBuilder} from "src/builder/QuarkBuilder.sol"; import "src/builder/Strings.sol"; @@ -196,4 +197,18 @@ library Across { ) ); } + + // Returns whether or not an asset is bridged non-deterministically. This applies to WETH/ETH, where Across will send either ETH or WETH + // to the target address depending on if address is an EOA or contract. + function isNonDeterministicBridgeAction(HashMap.Map memory assetsBridged, string memory assetSymbol) + internal + pure + returns (bool) + { + uint256 bridgedAmount = HashMap.contains(assetsBridged, abi.encode(assetSymbol)) + ? HashMap.getUint256(assetsBridged, abi.encode(assetSymbol)) + : 0; + return bridgedAmount != 0 + && (Strings.stringEqIgnoreCase(assetSymbol, "ETH") || Strings.stringEqIgnoreCase(assetSymbol, "WETH")); + } } diff --git a/src/builder/QuarkBuilderBase.sol b/src/builder/QuarkBuilderBase.sol index 5addbb3..7fa3cde 100644 --- a/src/builder/QuarkBuilderBase.sol +++ b/src/builder/QuarkBuilderBase.sol @@ -5,7 +5,7 @@ import {IQuarkWallet} from "quark-core/src/interfaces/IQuarkWallet.sol"; import {Actions} from "src/builder/actions/Actions.sol"; import {Accounts} from "src/builder/Accounts.sol"; -import {BridgeRoutes} from "src/builder/BridgeRoutes.sol"; +import {Across, BridgeRoutes} from "src/builder/BridgeRoutes.sol"; import {EIP712Helper} from "src/builder/EIP712Helper.sol"; import {Math} from "src/lib/Math.sol"; import {MorphoInfo} from "src/builder/MorphoInfo.sol"; @@ -232,6 +232,15 @@ contract QuarkBuilderBase { uint256 supplementalBalance = HashMap.contains(assetsBridged, abi.encode(assetSymbolOut)) ? HashMap.getUint256(assetsBridged, abi.encode(assetSymbolOut)) : 0; + // Note: Right now, ETH/WETH is only bridged via Across. Across has a weird quirk where it will send ETH to EOAs and + // WETH to contracts. Since the QuarkBuilder cannot know if a QuarkWallet is deployed before the operation is actually + // executed on-chain, it needs to use the "wrap up to" script because it cannot know how much to wrap ahead of time. + bool useWrapUpTo; + if (Across.isNonDeterministicBridgeAction(assetsBridged, assetSymbolOut)) { + useWrapUpTo = true; + supplementalBalance = 0; + } + checkAndInsertWrapOrUnwrapAction({ actions: actions, quarkOperations: quarkOperations, @@ -243,7 +252,8 @@ contract QuarkBuilderBase { chainId: actionIntent.chainId, account: actionIntent.actor, blockTimestamp: actionIntent.blockTimestamp, - useQuotecall: actionIntent.useQuotecall + useQuotecall: actionIntent.useQuotecall, + useWrapUpTo: useWrapUpTo }); } } @@ -410,25 +420,36 @@ contract QuarkBuilderBase { uint256 chainId, address account, uint256 blockTimestamp, - bool useQuotecall + bool useQuotecall, + bool useWrapUpTo ) internal pure { // Check if inserting wrapOrUnwrap action is necessary uint256 assetBalanceOnChain = Accounts.getBalanceOnChain(assetSymbol, chainId, chainAccountsList) + supplementalBalance; if (assetBalanceOnChain < amount && TokenWrapper.hasWrapperContract(chainId, assetSymbol)) { - // If the asset has a wrapper counterpart, wrap/unwrap the token to cover the transferIntent amount + // If the asset has a wrapper counterpart, wrap/unwrap the token to cover the amount needed for the intent string memory counterpartSymbol = TokenWrapper.getWrapperCounterpartSymbol(chainId, assetSymbol); // Wrap/unwrap the token to cover the amount + uint256 amountToWrap; + if (useWrapUpTo) { + // If we are using the "wrap up to script", then the `amountToWrap` should be the entire amount needed + // for the intent + amountToWrap = amount; + } else { + // If we aren't using the "wrap up to" script, then we need to subtract the current balance from the + // amount to wrap + amountToWrap = amount - assetBalanceOnChain; + } (IQuarkWallet.QuarkOperation memory wrapOrUnwrapOperation, Actions.Action memory wrapOrUnwrapAction) = Actions.wrapOrUnwrapAsset( Actions.WrapOrUnwrapAsset({ chainAccountsList: chainAccountsList, assetSymbol: counterpartSymbol, - // NOTE: Wrap/unwrap the amount needed to cover the amount - amount: amount - assetBalanceOnChain, + amount: amountToWrap, chainId: chainId, sender: account, + useWrapUpTo: useWrapUpTo, blockTimestamp: blockTimestamp }), payment, diff --git a/src/builder/TokenWrapper.sol b/src/builder/TokenWrapper.sol index 31798b4..3472fe5 100644 --- a/src/builder/TokenWrapper.sol +++ b/src/builder/TokenWrapper.sol @@ -108,28 +108,36 @@ library TokenWrapper { return Strings.stringEqIgnoreCase(tokenSymbol, getKnownWrapperTokenPair(chainId, tokenSymbol).wrappedSymbol); } - function encodeActionToWrapOrUnwrap(uint256 chainId, string memory tokenSymbol, uint256 amount) + function encodeActionToWrapOrUnwrap(uint256 chainId, string memory tokenSymbol, uint256 amount, bool useWrapUpTo) internal pure returns (bytes memory) { KnownWrapperTokenPair memory pair = getKnownWrapperTokenPair(chainId, tokenSymbol); if (isWrappedToken(chainId, tokenSymbol)) { - return encodeActionToUnwrapToken(chainId, tokenSymbol, amount); + return encodeActionToUnwrapToken(chainId, tokenSymbol, amount, useWrapUpTo); } else { - return encodeActionToWrapToken(chainId, tokenSymbol, pair.underlyingToken, amount); + return encodeActionToWrapToken(chainId, tokenSymbol, pair.underlyingToken, amount, useWrapUpTo); } } - function encodeActionToWrapToken(uint256 chainId, string memory tokenSymbol, address tokenAddress, uint256 amount) - internal - pure - returns (bytes memory) - { + function encodeActionToWrapToken( + uint256 chainId, + string memory tokenSymbol, + address tokenAddress, + uint256 amount, + bool useWrapUpTo + ) internal pure returns (bytes memory) { if (Strings.stringEqIgnoreCase(tokenSymbol, "ETH")) { - return abi.encodeWithSelector( - WrapperActions.wrapETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount - ); + if (useWrapUpTo) { + return abi.encodeWithSelector( + WrapperActions.wrapETHUpTo.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount + ); + } else { + return abi.encodeWithSelector( + WrapperActions.wrapETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount + ); + } } else if (Strings.stringEqIgnoreCase(tokenSymbol, "stETH")) { return abi.encodeWithSelector( WrapperActions.wrapLidoStETH.selector, @@ -141,15 +149,23 @@ library TokenWrapper { revert NotWrappable(); } - function encodeActionToUnwrapToken(uint256 chainId, string memory tokenSymbol, uint256 amount) + function encodeActionToUnwrapToken(uint256 chainId, string memory tokenSymbol, uint256 amount, bool useWrapUpTo) internal pure returns (bytes memory) { if (Strings.stringEqIgnoreCase(tokenSymbol, "WETH")) { - return abi.encodeWithSelector( - WrapperActions.unwrapWETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount - ); + if (useWrapUpTo) { + return abi.encodeWithSelector( + WrapperActions.unwrapWETHUpTo.selector, + getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, + amount + ); + } else { + return abi.encodeWithSelector( + WrapperActions.unwrapWETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount + ); + } } else if (Strings.stringEqIgnoreCase(tokenSymbol, "wstETH")) { return abi.encodeWithSelector( WrapperActions.unwrapLidoWstETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount diff --git a/src/builder/actions/Actions.sol b/src/builder/actions/Actions.sol index 03a5b78..79dc889 100644 --- a/src/builder/actions/Actions.sol +++ b/src/builder/actions/Actions.sol @@ -118,6 +118,7 @@ library Actions { uint256 amount; uint256 chainId; address sender; + bool useWrapUpTo; uint256 blockTimestamp; } @@ -1491,7 +1492,7 @@ library Actions { isReplayable: false, scriptAddress: CodeJarHelper.getCodeAddress(type(WrapperActions).creationCode), scriptCalldata: TokenWrapper.encodeActionToWrapOrUnwrap( - wrapOrUnwrap.chainId, wrapOrUnwrap.assetSymbol, wrapOrUnwrap.amount + wrapOrUnwrap.chainId, wrapOrUnwrap.assetSymbol, wrapOrUnwrap.amount, wrapOrUnwrap.useWrapUpTo ), scriptSources: scriptSources, expiry: wrapOrUnwrap.blockTimestamp + STANDARD_EXPIRY_BUFFER diff --git a/test/builder/BridgingLogic.t.sol b/test/builder/BridgingLogic.t.sol index 2146ac7..dd93e3c 100644 --- a/test/builder/BridgingLogic.t.sol +++ b/test/builder/BridgingLogic.t.sol @@ -23,14 +23,15 @@ import {PaymentInfo} from "src/builder/PaymentInfo.sol"; import {QuarkBuilder} from "src/builder/QuarkBuilder.sol"; import {QuarkBuilderBase} from "src/builder/QuarkBuilderBase.sol"; import {Quotecall} from "src/Quotecall.sol"; +import {TokenWrapper} from "src/builder/TokenWrapper.sol"; import {YulHelper} from "test/lib/YulHelper.sol"; -import {AcrossFFI} from "test/builder/mocks/AcrossFFI.sol"; +import {MockAcrossFFI, MockAcrossFFIConstants} from "test/builder/mocks/AcrossFFI.sol"; contract BridgingLogicTest is Test, QuarkBuilderTest { function setUp() public { // Deploy mock FFI for calling Across API - AcrossFFI mockFFI = new AcrossFFI(); + MockAcrossFFI mockFFI = new MockAcrossFFI(); vm.etch(FFI.ACROSS_FFI_ADDRESS, address(mockFFI).code); } @@ -48,7 +49,7 @@ contract BridgingLogicTest is Test, QuarkBuilderTest { chainAccountsList[1] = Accounts.ChainAccounts({ chainId: 8453, quarkSecrets: quarkSecrets_(address(0xb0b), bytes32(uint256(2))), - assetPositionsList: assetPositionsList_(8453, address(0xb0b), 0e18), + assetPositionsList: assetPositionsList_(8453, address(0xb0b), 0.5e18), cometPositions: emptyCometPositions_(), morphoPositions: emptyMorphoPositions_(), morphoVaultPositions: emptyMorphoVaultPositions_() @@ -70,6 +71,10 @@ contract BridgingLogicTest is Test, QuarkBuilderTest { assertEq(result.paymentCurrency, "usd", "usd currency"); + address multicallAddress = CodeJarHelper.getCodeAddress(type(Multicall).creationCode); + address wrapperActionsAddress = CodeJarHelper.getCodeAddress(type(WrapperActions).creationCode); + address transferActionsAddress = CodeJarHelper.getCodeAddress(type(TransferActions).creationCode); + // Check the quark operations assertEq(result.quarkOperations.length, 2, "two operations"); assertEq( @@ -102,8 +107,8 @@ contract BridgingLogicTest is Test, QuarkBuilderTest { address(0xb0b), // recipient weth_(1), // inputToken weth_(8453), // outputToken - 1e18 * (1e18 + 0.01e18) / 1e18 + 1e6, // inputAmount - 1e18, // outputAmount + 0.5e18 * (1e18 + MockAcrossFFIConstants.VARIABLE_FEE_PCT) / 1e18 + MockAcrossFFIConstants.GAS_FEE, // inputAmount + 0.5e18, // outputAmount 8453, // destinationChainId address(0), // exclusiveRelayer uint32(BLOCK_TIMESTAMP) - Across.QUOTE_TIMESTAMP_BUFFER, // quoteTimestamp @@ -123,28 +128,21 @@ contract BridgingLogicTest is Test, QuarkBuilderTest { assertEq( result.quarkOperations[1].scriptAddress, - address( - uint160( - uint256( - keccak256( - abi.encodePacked( - bytes1(0xff), - /* codeJar address */ - address(CodeJarHelper.CODE_JAR_ADDRESS), - uint256(0), - /* script bytecode */ - keccak256(type(TransferActions).creationCode) - ) - ) - ) - ) - ), - "script address for transfer is correct given the code jar address" + multicallAddress, + "script address for Multicall is correct given the code jar address" ); + address[] memory callContracts = new address[](2); + callContracts[0] = wrapperActionsAddress; + callContracts[1] = transferActionsAddress; + bytes[] memory callDatas = new bytes[](2); + callDatas[0] = abi.encodeWithSelector( + WrapperActions.wrapETHUpTo.selector, TokenWrapper.getKnownWrapperTokenPair(8453, "WETH").wrapper, 1e18 + ); + callDatas[1] = abi.encodeCall(TransferActions.transferERC20Token, (weth_(8453), address(0xceecee), 1e18)); assertEq( result.quarkOperations[1].scriptCalldata, - abi.encodeCall(TransferActions.transferERC20Token, (weth_(8453), address(0xceecee), 1e18)), - "calldata is TransferActions.transferERC20Token(USDC_8453, address(0xceecee), 5e6);" + abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), + "calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.wrapETHUpTo(USDC_8453, 1e18), TransferActions.transferERC20Token(USDC_8453, address(0xceecee), 1e18))" ); assertEq( result.quarkOperations[1].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days" @@ -169,8 +167,9 @@ contract BridgingLogicTest is Test, QuarkBuilderTest { price: WETH_PRICE, token: WETH_1, assetSymbol: "WETH", - inputAmount: 1e18 * (1e18 + 0.01e18) / 1e18 + 1e6, - outputAmount: 1e18, + inputAmount: 0.5e18 * (1e18 + MockAcrossFFIConstants.VARIABLE_FEE_PCT) / 1e18 + + MockAcrossFFIConstants.GAS_FEE, + outputAmount: 0.5e18, chainId: 1, recipient: address(0xb0b), destinationChainId: 8453, diff --git a/test/builder/mocks/AcrossFFI.sol b/test/builder/mocks/AcrossFFI.sol index 15e2324..b60bb31 100644 --- a/test/builder/mocks/AcrossFFI.sol +++ b/test/builder/mocks/AcrossFFI.sol @@ -3,7 +3,15 @@ pragma solidity 0.8.27; import {IAcrossFFI} from "src/interfaces/IAcrossFFI.sol"; -contract AcrossFFI is IAcrossFFI { +library MockAcrossFFIConstants { + uint256 public constant GAS_FEE = 1e6; + uint256 public constant VARIABLE_FEE_PCT = 0.01e18; +} + +contract MockAcrossFFI is IAcrossFFI { + uint256 public constant GAS_FEE = 1e6; + uint256 public constant VARIABLE_FEE_PCT = 0.01e18; + function requestAcrossQuote( address, /* inputToken */ address, /* outputToken */ @@ -11,6 +19,6 @@ contract AcrossFFI is IAcrossFFI { uint256, /* dstChain */ uint256 /* amount */ ) external pure override returns (uint256 gasFee, uint256 variableFeePct) { - return (1e6, 0.01e18); + return (MockAcrossFFIConstants.GAS_FEE, MockAcrossFFIConstants.VARIABLE_FEE_PCT); } } From 57e308e02a594a8cb1ed039874fae8b998029ae5 Mon Sep 17 00:00:00 2001 From: kevincheng96 Date: Wed, 20 Nov 2024 16:16:04 -0800 Subject: [PATCH 2/5] Implement wrap/unwrap all functions; always wrap/unwrap all in QuarkBuilder --- src/WrapperScripts.sol | 31 ++++-- src/builder/BridgeRoutes.sol | 2 +- src/builder/QuarkBuilderBase.sol | 32 ++----- src/builder/TokenWrapper.sol | 53 ++++------- src/builder/actions/Actions.sol | 5 +- src/interfaces/IWstETH.sol | 4 +- test/WrapperScripts.t.sol | 94 ++++++++++--------- test/builder/BridgingLogic.t.sol | 4 +- test/builder/QuarkBuilderCometBorrow.t.sol | 4 +- test/builder/QuarkBuilderCometRepay.t.sol | 4 +- test/builder/QuarkBuilderCometSupply.t.sol | 4 +- test/builder/QuarkBuilderMorphoBorrow.t.sol | 4 +- test/builder/QuarkBuilderMorphoRepay.t.sol | 4 +- .../QuarkBuilderMorphoVaultSupply.t.sol | 4 +- test/builder/QuarkBuilderSwap.t.sol | 4 +- test/builder/QuarkBuilderTransfer.t.sol | 22 ++--- 16 files changed, 131 insertions(+), 144 deletions(-) diff --git a/src/WrapperScripts.sol b/src/WrapperScripts.sol index 66ddf83..c4ed4a1 100644 --- a/src/WrapperScripts.sol +++ b/src/WrapperScripts.sol @@ -10,10 +10,10 @@ contract WrapperActions { IWETH(weth).deposit{value: amount}(); } - function wrapETHUpTo(address weth, uint256 targetAmount) external payable { - uint256 currentBalance = IERC20(weth).balanceOf(address(this)); - if (currentBalance < targetAmount) { - IWETH(weth).deposit{value: targetAmount - currentBalance}(); + function wrapAllETH(address weth) external payable { + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) { + IWETH(weth).deposit{value: ethBalance}(); } } @@ -21,10 +21,10 @@ contract WrapperActions { IWETH(weth).withdraw(amount); } - function unwrapWETHUpTo(address weth, uint256 targetAmount) external { - uint256 currentBalance = address(this).balance; - if (currentBalance < targetAmount) { - IWETH(weth).withdraw(targetAmount - currentBalance); + function unwrapAllWETH(address weth) external payable { + uint256 wethBalance = IERC20(weth).balanceOf(address(this)); + if (wethBalance > 0) { + IWETH(weth).withdraw(wethBalance); } } @@ -33,7 +33,22 @@ contract WrapperActions { IWstETH(wstETH).wrap(amount); } + function wrapAllLidoStETH(address wstETH, address stETH) external payable { + uint256 stETHBalance = IERC20(stETH).balanceOf(address(this)); + if (stETHBalance > 0) { + IERC20(stETH).approve(wstETH, stETHBalance); + IWstETH(wstETH).wrap(stETHBalance); + } + } + function unwrapLidoWstETH(address wstETH, uint256 amount) external { IWstETH(wstETH).unwrap(amount); } + + function unwrapAllLidoWstETH(address wstETH) external { + uint256 wstETHBalance = IERC20(wstETH).balanceOf(address(this)); + if (wstETHBalance > 0) { + IWstETH(wstETH).unwrap(wstETHBalance); + } + } } diff --git a/src/builder/BridgeRoutes.sol b/src/builder/BridgeRoutes.sol index f2ee88d..346a288 100644 --- a/src/builder/BridgeRoutes.sol +++ b/src/builder/BridgeRoutes.sol @@ -208,7 +208,7 @@ library Across { uint256 bridgedAmount = HashMap.contains(assetsBridged, abi.encode(assetSymbol)) ? HashMap.getUint256(assetsBridged, abi.encode(assetSymbol)) : 0; - return bridgedAmount != 0 + return bridgedAmount > 0 && (Strings.stringEqIgnoreCase(assetSymbol, "ETH") || Strings.stringEqIgnoreCase(assetSymbol, "WETH")); } } diff --git a/src/builder/QuarkBuilderBase.sol b/src/builder/QuarkBuilderBase.sol index 7fa3cde..d671a20 100644 --- a/src/builder/QuarkBuilderBase.sol +++ b/src/builder/QuarkBuilderBase.sol @@ -37,7 +37,7 @@ contract QuarkBuilderBase { /* ===== Constants ===== */ - string constant VERSION = "0.2.0"; + string constant VERSION = "0.2.1"; /* ===== Custom Errors ===== */ @@ -234,10 +234,8 @@ contract QuarkBuilderBase { : 0; // Note: Right now, ETH/WETH is only bridged via Across. Across has a weird quirk where it will send ETH to EOAs and // WETH to contracts. Since the QuarkBuilder cannot know if a QuarkWallet is deployed before the operation is actually - // executed on-chain, it needs to use the "wrap up to" script because it cannot know how much to wrap ahead of time. - bool useWrapUpTo; + // executed on-chain, we need to assume there is no supplemental balance that arrives on the destination chain. if (Across.isNonDeterministicBridgeAction(assetsBridged, assetSymbolOut)) { - useWrapUpTo = true; supplementalBalance = 0; } @@ -247,13 +245,12 @@ contract QuarkBuilderBase { chainAccountsList: chainAccountsList, payment: payment, assetSymbol: assetSymbolOut, - amount: actionIntent.amountOuts[i], + amountNeeded: actionIntent.amountOuts[i], supplementalBalance: supplementalBalance, chainId: actionIntent.chainId, account: actionIntent.actor, blockTimestamp: actionIntent.blockTimestamp, - useQuotecall: actionIntent.useQuotecall, - useWrapUpTo: useWrapUpTo + useQuotecall: actionIntent.useQuotecall }); } } @@ -415,41 +412,30 @@ contract QuarkBuilderBase { Accounts.ChainAccounts[] memory chainAccountsList, PaymentInfo.Payment memory payment, string memory assetSymbol, - uint256 amount, + uint256 amountNeeded, uint256 supplementalBalance, uint256 chainId, address account, uint256 blockTimestamp, - bool useQuotecall, - bool useWrapUpTo + bool useQuotecall ) internal pure { // Check if inserting wrapOrUnwrap action is necessary uint256 assetBalanceOnChain = Accounts.getBalanceOnChain(assetSymbol, chainId, chainAccountsList) + supplementalBalance; - if (assetBalanceOnChain < amount && TokenWrapper.hasWrapperContract(chainId, assetSymbol)) { + if (assetBalanceOnChain < amountNeeded && TokenWrapper.hasWrapperContract(chainId, assetSymbol)) { // If the asset has a wrapper counterpart, wrap/unwrap the token to cover the amount needed for the intent string memory counterpartSymbol = TokenWrapper.getWrapperCounterpartSymbol(chainId, assetSymbol); // Wrap/unwrap the token to cover the amount - uint256 amountToWrap; - if (useWrapUpTo) { - // If we are using the "wrap up to script", then the `amountToWrap` should be the entire amount needed - // for the intent - amountToWrap = amount; - } else { - // If we aren't using the "wrap up to" script, then we need to subtract the current balance from the - // amount to wrap - amountToWrap = amount - assetBalanceOnChain; - } (IQuarkWallet.QuarkOperation memory wrapOrUnwrapOperation, Actions.Action memory wrapOrUnwrapAction) = Actions.wrapOrUnwrapAsset( Actions.WrapOrUnwrapAsset({ chainAccountsList: chainAccountsList, assetSymbol: counterpartSymbol, - amount: amountToWrap, + // This is just to indicate we plan to wrap all + amount: type(uint256).max, chainId: chainId, sender: account, - useWrapUpTo: useWrapUpTo, blockTimestamp: blockTimestamp }), payment, diff --git a/src/builder/TokenWrapper.sol b/src/builder/TokenWrapper.sol index 3472fe5..455f201 100644 --- a/src/builder/TokenWrapper.sol +++ b/src/builder/TokenWrapper.sol @@ -108,67 +108,50 @@ library TokenWrapper { return Strings.stringEqIgnoreCase(tokenSymbol, getKnownWrapperTokenPair(chainId, tokenSymbol).wrappedSymbol); } - function encodeActionToWrapOrUnwrap(uint256 chainId, string memory tokenSymbol, uint256 amount, bool useWrapUpTo) + function encodeActionToWrapOrUnwrap(uint256 chainId, string memory tokenSymbol) internal pure returns (bytes memory) { KnownWrapperTokenPair memory pair = getKnownWrapperTokenPair(chainId, tokenSymbol); if (isWrappedToken(chainId, tokenSymbol)) { - return encodeActionToUnwrapToken(chainId, tokenSymbol, amount, useWrapUpTo); + return encodeActionToUnwrapToken(chainId, tokenSymbol); } else { - return encodeActionToWrapToken(chainId, tokenSymbol, pair.underlyingToken, amount, useWrapUpTo); + return encodeActionToWrapToken(chainId, tokenSymbol, pair.underlyingToken); } } - function encodeActionToWrapToken( - uint256 chainId, - string memory tokenSymbol, - address tokenAddress, - uint256 amount, - bool useWrapUpTo - ) internal pure returns (bytes memory) { + function encodeActionToWrapToken(uint256 chainId, string memory tokenSymbol, address tokenAddress) + internal + pure + returns (bytes memory) + { if (Strings.stringEqIgnoreCase(tokenSymbol, "ETH")) { - if (useWrapUpTo) { - return abi.encodeWithSelector( - WrapperActions.wrapETHUpTo.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount - ); - } else { - return abi.encodeWithSelector( - WrapperActions.wrapETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount - ); - } + return abi.encodeWithSelector( + WrapperActions.wrapAllETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper + ); } else if (Strings.stringEqIgnoreCase(tokenSymbol, "stETH")) { return abi.encodeWithSelector( - WrapperActions.wrapLidoStETH.selector, + WrapperActions.wrapAllLidoStETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, - tokenAddress, - amount + tokenAddress ); } revert NotWrappable(); } - function encodeActionToUnwrapToken(uint256 chainId, string memory tokenSymbol, uint256 amount, bool useWrapUpTo) + function encodeActionToUnwrapToken(uint256 chainId, string memory tokenSymbol) internal pure returns (bytes memory) { if (Strings.stringEqIgnoreCase(tokenSymbol, "WETH")) { - if (useWrapUpTo) { - return abi.encodeWithSelector( - WrapperActions.unwrapWETHUpTo.selector, - getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, - amount - ); - } else { - return abi.encodeWithSelector( - WrapperActions.unwrapWETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount - ); - } + return abi.encodeWithSelector( + WrapperActions.unwrapAllWETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper + ); } else if (Strings.stringEqIgnoreCase(tokenSymbol, "wstETH")) { return abi.encodeWithSelector( - WrapperActions.unwrapLidoWstETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount + WrapperActions.unwrapAllLidoWstETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper ); } revert NotUnwrappable(); diff --git a/src/builder/actions/Actions.sol b/src/builder/actions/Actions.sol index 79dc889..51e58da 100644 --- a/src/builder/actions/Actions.sol +++ b/src/builder/actions/Actions.sol @@ -118,7 +118,6 @@ library Actions { uint256 amount; uint256 chainId; address sender; - bool useWrapUpTo; uint256 blockTimestamp; } @@ -1491,9 +1490,7 @@ library Actions { nonce: accountSecret.nonceSecret, isReplayable: false, scriptAddress: CodeJarHelper.getCodeAddress(type(WrapperActions).creationCode), - scriptCalldata: TokenWrapper.encodeActionToWrapOrUnwrap( - wrapOrUnwrap.chainId, wrapOrUnwrap.assetSymbol, wrapOrUnwrap.amount, wrapOrUnwrap.useWrapUpTo - ), + scriptCalldata: TokenWrapper.encodeActionToWrapOrUnwrap(wrapOrUnwrap.chainId, wrapOrUnwrap.assetSymbol), scriptSources: scriptSources, expiry: wrapOrUnwrap.blockTimestamp + STANDARD_EXPIRY_BUFFER }); diff --git a/src/interfaces/IWstETH.sol b/src/interfaces/IWstETH.sol index bc99867..95a2075 100644 --- a/src/interfaces/IWstETH.sol +++ b/src/interfaces/IWstETH.sol @@ -2,6 +2,6 @@ pragma solidity 0.8.27; interface IWstETH { - function wrap(uint256 amount) external returns (uint256); - function unwrap(uint256 amount) external returns (uint256); + function wrap(uint256 stETHAmount) external returns (uint256); + function unwrap(uint256 wstETHAmount) external returns (uint256); } diff --git a/test/WrapperScripts.t.sol b/test/WrapperScripts.t.sol index 2f87c83..667170c 100644 --- a/test/WrapperScripts.t.sol +++ b/test/WrapperScripts.t.sol @@ -65,7 +65,7 @@ contract WrapperScriptsTest is Test { assertEq(address(wallet).balance, 0 ether); } - function testWrapETHUpTo() public { + function testWrapAllETH() public { vm.pauseGasMetering(); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); @@ -75,7 +75,7 @@ contract WrapperScriptsTest is Test { QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( wallet, wrapperScript, - abi.encodeWithSelector(WrapperActions.wrapETHUpTo.selector, WETH, 10 ether), + abi.encodeWithSelector(WrapperActions.wrapAllETH.selector, WETH), ScriptType.ScriptSource ); bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op); @@ -86,33 +86,8 @@ contract WrapperScriptsTest is Test { vm.resumeGasMetering(); wallet.executeQuarkOperation(op, signature); - assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); - assertEq(address(wallet).balance, 7 ether); - } - - function testWrapETHUpToDoesNotWrapIfNotNeeded() public { - vm.pauseGasMetering(); - QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); - - deal(address(wallet), 10 ether); - deal(WETH, address(wallet), 10 ether); - - QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( - wallet, - wrapperScript, - abi.encodeWithSelector(WrapperActions.wrapETHUpTo.selector, WETH, 10 ether), - ScriptType.ScriptSource - ); - bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op); - - assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); - assertEq(address(wallet).balance, 10 ether); - - vm.resumeGasMetering(); - wallet.executeQuarkOperation(op, signature); - - assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); - assertEq(address(wallet).balance, 10 ether); + assertEq(IERC20(WETH).balanceOf(address(wallet)), 17 ether); + assertEq(address(wallet).balance, 0 ether); } function testUnwrapWETH() public { @@ -137,7 +112,7 @@ contract WrapperScriptsTest is Test { assertEq(address(wallet).balance, 10 ether); } - function testUnwrapWETHUpTo() public { + function testUnwrapAllWETH() public { vm.pauseGasMetering(); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); @@ -147,7 +122,7 @@ contract WrapperScriptsTest is Test { QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( wallet, wrapperScript, - abi.encodeWithSelector(WrapperActions.unwrapWETHUpTo.selector, WETH, 10 ether), + abi.encodeWithSelector(WrapperActions.unwrapAllWETH.selector, WETH), ScriptType.ScriptSource ); bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op); @@ -158,36 +133,40 @@ contract WrapperScriptsTest is Test { vm.resumeGasMetering(); wallet.executeQuarkOperation(op, signature); - assertEq(IERC20(WETH).balanceOf(address(wallet)), 7 ether); - assertEq(address(wallet).balance, 10 ether); + assertEq(IERC20(WETH).balanceOf(address(wallet)), 0 ether); + assertEq(address(wallet).balance, 17 ether); } - function testUnwrapWETHUpToDoesNotUnwrapIfNotNeeded() public { + function testWrapStETH() public { vm.pauseGasMetering(); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); - deal(WETH, address(wallet), 10 ether); + // Special balance computation in Lido, have to do regular staking action to not mess up the Lido's contract deal(address(wallet), 10 ether); + vm.startPrank(address(wallet)); + // Call Lido's submit() to stake + IStETH(stETH).submit{value: 10 ether}(address(0)); + vm.stopPrank(); QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( wallet, wrapperScript, - abi.encodeWithSelector(WrapperActions.unwrapWETHUpTo.selector, WETH, 10 ether), + abi.encodeWithSelector(WrapperActions.wrapLidoStETH.selector, wstETH, stETH, 10 ether), ScriptType.ScriptSource ); bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op); - assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); - assertEq(address(wallet).balance, 10 ether); + assertEq(IERC20(wstETH).balanceOf(address(wallet)), 0 ether); + assertApproxEqAbs(IERC20(stETH).balanceOf(address(wallet)), 10 ether, 0.01 ether); vm.resumeGasMetering(); wallet.executeQuarkOperation(op, signature); - assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); - assertEq(address(wallet).balance, 10 ether); + assertEq(IERC20(stETH).balanceOf(address(wallet)), 0 ether); + assertApproxEqAbs(IERC20(wstETH).balanceOf(address(wallet)), 8.74 ether, 0.01 ether); } - function testWrapStETH() public { + function testWrapAllStETH() public { vm.pauseGasMetering(); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); @@ -201,16 +180,19 @@ contract WrapperScriptsTest is Test { QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( wallet, wrapperScript, - abi.encodeWithSelector(WrapperActions.wrapLidoStETH.selector, wstETH, stETH, 10 ether), + abi.encodeWithSelector(WrapperActions.wrapAllLidoStETH.selector, wstETH, stETH), ScriptType.ScriptSource ); bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op); assertEq(IERC20(wstETH).balanceOf(address(wallet)), 0 ether); assertApproxEqAbs(IERC20(stETH).balanceOf(address(wallet)), 10 ether, 0.01 ether); + vm.resumeGasMetering(); wallet.executeQuarkOperation(op, signature); - assertEq(IERC20(stETH).balanceOf(address(wallet)), 0 ether); + + // Due to shares math in wstETH, the user can be left with a tiny amount of stETH + assertApproxEqAbs(IERC20(stETH).balanceOf(address(wallet)), 0 ether, 1); assertApproxEqAbs(IERC20(wstETH).balanceOf(address(wallet)), 8.74 ether, 0.01 ether); } @@ -230,8 +212,34 @@ contract WrapperScriptsTest is Test { assertEq(IERC20(stETH).balanceOf(address(wallet)), 0 ether); assertEq(IERC20(wstETH).balanceOf(address(wallet)), 10 ether); + vm.resumeGasMetering(); wallet.executeQuarkOperation(op, signature); + + assertApproxEqAbs(IERC20(stETH).balanceOf(address(wallet)), 11.44 ether, 0.01 ether); + assertEq(IERC20(wstETH).balanceOf(address(wallet)), 0 ether); + } + + function testUnwrapAllWstETH() public { + vm.pauseGasMetering(); + QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); + + deal(wstETH, address(wallet), 10 ether); + + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + wallet, + wrapperScript, + abi.encodeWithSelector(WrapperActions.unwrapAllLidoWstETH.selector, wstETH), + ScriptType.ScriptSource + ); + bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op); + + assertEq(IERC20(stETH).balanceOf(address(wallet)), 0 ether); + assertEq(IERC20(wstETH).balanceOf(address(wallet)), 10 ether); + + vm.resumeGasMetering(); + wallet.executeQuarkOperation(op, signature); + assertApproxEqAbs(IERC20(stETH).balanceOf(address(wallet)), 11.44 ether, 0.01 ether); assertEq(IERC20(wstETH).balanceOf(address(wallet)), 0 ether); } diff --git a/test/builder/BridgingLogic.t.sol b/test/builder/BridgingLogic.t.sol index dd93e3c..92917fc 100644 --- a/test/builder/BridgingLogic.t.sol +++ b/test/builder/BridgingLogic.t.sol @@ -136,13 +136,13 @@ contract BridgingLogicTest is Test, QuarkBuilderTest { callContracts[1] = transferActionsAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = abi.encodeWithSelector( - WrapperActions.wrapETHUpTo.selector, TokenWrapper.getKnownWrapperTokenPair(8453, "WETH").wrapper, 1e18 + WrapperActions.wrapAllETH.selector, TokenWrapper.getKnownWrapperTokenPair(8453, "WETH").wrapper ); callDatas[1] = abi.encodeCall(TransferActions.transferERC20Token, (weth_(8453), address(0xceecee), 1e18)); assertEq( result.quarkOperations[1].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.wrapETHUpTo(USDC_8453, 1e18), TransferActions.transferERC20Token(USDC_8453, address(0xceecee), 1e18))" + "calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.wrapAllETH(USDC_8453), TransferActions.transferERC20Token(USDC_8453, address(0xceecee), 1e18))" ); assertEq( result.quarkOperations[1].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days" diff --git a/test/builder/QuarkBuilderCometBorrow.t.sol b/test/builder/QuarkBuilderCometBorrow.t.sol index b41fb8a..56d77ec 100644 --- a/test/builder/QuarkBuilderCometBorrow.t.sol +++ b/test/builder/QuarkBuilderCometBorrow.t.sol @@ -264,7 +264,7 @@ contract QuarkBuilderCometBorrowTest is Test, QuarkBuilderTest { callContracts[1] = cometSupplyMultipleAssetsAndBorrowAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = - abi.encodeWithSelector(WrapperActions.wrapETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18); + abi.encodeWithSelector(WrapperActions.wrapAllETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); callDatas[1] = abi.encodeCall( CometSupplyMultipleAssetsAndBorrow.run, (cometUsdc_(1), collateralTokens, collateralAmounts, usdc_(1), 1e6) ); @@ -272,7 +272,7 @@ contract QuarkBuilderCometBorrowTest is Test, QuarkBuilderTest { assertEq( result.quarkOperations[0].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, cometSupplyMultipleAssetsAndBorrowAddress], [WrapperActions.wrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 10e18), CometSupplyMultipleAssetsAndBorrow.run(COMET_1, collateralTokens, collateralAmounts, usdc_(1), 1e6)" + "calldata is Multicall.run([wrapperActionsAddress, cometSupplyMultipleAssetsAndBorrowAddress], [WrapperActions.wrapAllETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), CometSupplyMultipleAssetsAndBorrow.run(COMET_1, collateralTokens, collateralAmounts, usdc_(1), 1e6)" ); assertEq(result.quarkOperations[0].scriptSources.length, 3); assertEq(result.quarkOperations[0].scriptSources[0], type(WrapperActions).creationCode); diff --git a/test/builder/QuarkBuilderCometRepay.t.sol b/test/builder/QuarkBuilderCometRepay.t.sol index 3051f52..a889488 100644 --- a/test/builder/QuarkBuilderCometRepay.t.sol +++ b/test/builder/QuarkBuilderCometRepay.t.sol @@ -292,7 +292,7 @@ contract QuarkBuilderCometRepayTest is Test, QuarkBuilderTest { callContracts[1] = cometRepayAndWithdrawMultipleAssetsAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = - abi.encodeWithSelector(WrapperActions.wrapETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18); + abi.encodeWithSelector(WrapperActions.wrapAllETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); callDatas[1] = abi.encodeCall( CometRepayAndWithdrawMultipleAssets.run, (cometWeth_(1), collateralTokens, collateralAmounts, weth_(1), 1e18) @@ -301,7 +301,7 @@ contract QuarkBuilderCometRepayTest is Test, QuarkBuilderTest { assertEq( result.quarkOperations[0].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, cometRepayAndWithdrawMultipleAssetsAddress], [WrapperActions.wrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18), CometRepayAndWithdrawMultipleAssets.run(COMET_1_WETH, collateralTokens, collateralAmounts, weth_(1), 1e18)" + "calldata is Multicall.run([wrapperActionsAddress, cometRepayAndWithdrawMultipleAssetsAddress], [WrapperActions.wrapAllETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), CometRepayAndWithdrawMultipleAssets.run(COMET_1_WETH, collateralTokens, collateralAmounts, weth_(1), 1e18)" ); assertEq(result.quarkOperations[0].scriptSources.length, 3); assertEq(result.quarkOperations[0].scriptSources[0], type(WrapperActions).creationCode); diff --git a/test/builder/QuarkBuilderCometSupply.t.sol b/test/builder/QuarkBuilderCometSupply.t.sol index 65cd105..effb975 100644 --- a/test/builder/QuarkBuilderCometSupply.t.sol +++ b/test/builder/QuarkBuilderCometSupply.t.sol @@ -306,12 +306,12 @@ contract QuarkBuilderCometSupplyTest is Test, QuarkBuilderTest { callContracts[1] = cometSupplyActionsAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = - abi.encodeWithSelector(WrapperActions.wrapETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18); + abi.encodeWithSelector(WrapperActions.wrapAllETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); callDatas[1] = abi.encodeCall(CometSupplyActions.supply, (COMET_ETH, weth_(1), 1e18)); assertEq( result.quarkOperations[0].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, cometSupplyActionsAddress], [WrapperActions.wrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18), CometSupplyActions.supply(COMET_ETH, weth_(1), 1e18)" + "calldata is Multicall.run([wrapperActionsAddress, cometSupplyActionsAddress], [WrapperActions.wrapAllETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), CometSupplyActions.supply(COMET_ETH, weth_(1), 1e18)" ); assertEq( result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 3 days" diff --git a/test/builder/QuarkBuilderMorphoBorrow.t.sol b/test/builder/QuarkBuilderMorphoBorrow.t.sol index 8ec9774..b66f33b 100644 --- a/test/builder/QuarkBuilderMorphoBorrow.t.sol +++ b/test/builder/QuarkBuilderMorphoBorrow.t.sol @@ -234,7 +234,7 @@ contract QuarkBuilderMorphoBorrowTest is Test, QuarkBuilderTest { callContracts[1] = MorphoActionsAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = abi.encodeWithSelector( - WrapperActions.wrapETH.selector, TokenWrapper.getKnownWrapperTokenPair(8453, "WETH").wrapper, 1e18 + WrapperActions.wrapAllETH.selector, TokenWrapper.getKnownWrapperTokenPair(8453, "WETH").wrapper ); callDatas[1] = abi.encodeCall( MorphoActions.supplyCollateralAndBorrow, @@ -244,7 +244,7 @@ contract QuarkBuilderMorphoBorrowTest is Test, QuarkBuilderTest { assertEq( result.quarkOperations[0].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, MorphoActionsAddress], [WrapperActions.wrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 10e18), MorphoActions.supplyCollateralAndBorrow(MorphoInfo.getMorphoAddress(8453), MorphoInfo.getMarketParams(8453, WETH, USDC), 1e18, 1e6, address(0xa11ce), address(0xa11ce))" + "calldata is Multicall.run([wrapperActionsAddress, MorphoActionsAddress], [WrapperActions.wrapAllETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), MorphoActions.supplyCollateralAndBorrow(MorphoInfo.getMorphoAddress(8453), MorphoInfo.getMarketParams(8453, WETH, USDC), 1e18, 1e6, address(0xa11ce), address(0xa11ce))" ); assertEq(result.quarkOperations[0].scriptSources.length, 3); assertEq(result.quarkOperations[0].scriptSources[0], type(WrapperActions).creationCode); diff --git a/test/builder/QuarkBuilderMorphoRepay.t.sol b/test/builder/QuarkBuilderMorphoRepay.t.sol index d5ca4fd..0e26472 100644 --- a/test/builder/QuarkBuilderMorphoRepay.t.sol +++ b/test/builder/QuarkBuilderMorphoRepay.t.sol @@ -246,7 +246,7 @@ contract QuarkBuilderMorphoRepayTest is Test, QuarkBuilderTest { callContracts[1] = morphoActionsAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = abi.encodeWithSelector( - WrapperActions.wrapETH.selector, TokenWrapper.getKnownWrapperTokenPair(8453, "WETH").wrapper, 1e18 + WrapperActions.wrapAllETH.selector, TokenWrapper.getKnownWrapperTokenPair(8453, "WETH").wrapper ); callDatas[1] = abi.encodeCall( MorphoActions.repayAndWithdrawCollateral, @@ -256,7 +256,7 @@ contract QuarkBuilderMorphoRepayTest is Test, QuarkBuilderTest { assertEq( result.quarkOperations[0].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, morphoActionsAddress], [WrapperActions.wrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18), MorphoActions.repayAndWithdrawCollateral(MorphoInfo.getMorphoAddress(8453), MorphoInfo.getMarketParams(8453, WETH, USDC), 1e18, 0, 0e18, address(0xa11ce), address(0xa11ce))" + "calldata is Multicall.run([wrapperActionsAddress, morphoActionsAddress], [WrapperActions.wrapAllETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), MorphoActions.repayAndWithdrawCollateral(MorphoInfo.getMorphoAddress(8453), MorphoInfo.getMarketParams(8453, WETH, USDC), 1e18, 0, 0e18, address(0xa11ce), address(0xa11ce))" ); assertEq(result.quarkOperations[0].scriptSources.length, 3); assertEq(result.quarkOperations[0].scriptSources[0], type(WrapperActions).creationCode); diff --git a/test/builder/QuarkBuilderMorphoVaultSupply.t.sol b/test/builder/QuarkBuilderMorphoVaultSupply.t.sol index b8d5037..1ba18ee 100644 --- a/test/builder/QuarkBuilderMorphoVaultSupply.t.sol +++ b/test/builder/QuarkBuilderMorphoVaultSupply.t.sol @@ -320,13 +320,13 @@ contract QuarkBuilderMorphoVaultTest is Test, QuarkBuilderTest { callContracts[1] = morphoVaultActionsAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = - abi.encodeWithSelector(WrapperActions.wrapETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18); + abi.encodeWithSelector(WrapperActions.wrapAllETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); callDatas[1] = abi.encodeCall(MorphoVaultActions.deposit, (MorphoInfo.getMorphoVaultAddress(1, "WETH"), weth_(1), 1e18)); assertEq( result.quarkOperations[0].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, morphoVaultActionsAddress], [WrapperActions.wrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18), MorphoVaultActions.deposit(MorphoInfo.getMorphoVaultAddress(1, WETH), weth_(1), 1e18)" + "calldata is Multicall.run([wrapperActionsAddress, morphoVaultActionsAddress], [WrapperActions.wrapAllETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), MorphoVaultActions.deposit(MorphoInfo.getMorphoVaultAddress(1, WETH), weth_(1), 1e18)" ); assertEq( result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 3 days" diff --git a/test/builder/QuarkBuilderSwap.t.sol b/test/builder/QuarkBuilderSwap.t.sol index b3e6af2..d944357 100644 --- a/test/builder/QuarkBuilderSwap.t.sol +++ b/test/builder/QuarkBuilderSwap.t.sol @@ -283,13 +283,13 @@ contract QuarkBuilderSwapTest is Test, QuarkBuilderTest { callContracts[1] = approveAndSwapAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = - abi.encodeWithSelector(WrapperActions.wrapETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18); + abi.encodeWithSelector(WrapperActions.wrapAllETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); callDatas[1] = abi.encodeCall(ApproveAndSwap.run, (ZERO_EX_ENTRY_POINT, WETH_1, 1e18, USDC_1, 3000e6, ZERO_EX_SWAP_DATA)); assertEq( result.quarkOperations[0].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, approveAndSwapAddress], [WrapperActions.wrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18), ApproveAndSwap.run (ZERO_EX_ENTRY_POINT, WETH_1, 1e18, USDC_1, 3000e6, ZERO_EX_SWAP_DATA)]);" + "calldata is Multicall.run([wrapperActionsAddress, approveAndSwapAddress], [WrapperActions.wrapAllETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), ApproveAndSwap.run (ZERO_EX_ENTRY_POINT, WETH_1, 1e18, USDC_1, 3000e6, ZERO_EX_SWAP_DATA)]);" ); assertEq( result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 3 days, "expiry is current blockTimestamp + 3 days" diff --git a/test/builder/QuarkBuilderTransfer.t.sol b/test/builder/QuarkBuilderTransfer.t.sol index e77b983..e939fd3 100644 --- a/test/builder/QuarkBuilderTransfer.t.sol +++ b/test/builder/QuarkBuilderTransfer.t.sol @@ -1093,14 +1093,13 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { callContracts[0] = wrapperActionsAddress; callContracts[1] = transferActionsAddress; bytes[] memory callDatas = new bytes[](2); - callDatas[0] = abi.encodeWithSelector( - WrapperActions.unwrapWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0.5e18 - ); + callDatas[0] = + abi.encodeWithSelector(WrapperActions.unwrapAllWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 1.5e18); assertEq( result.quarkOperations[0].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0.5e18), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]);" + "calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapAllWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]);" ); assertEq( result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days" @@ -1203,9 +1202,8 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { callContracts[0] = wrapperActionsAddress; callContracts[1] = transferActionsAddress; bytes[] memory callDatas = new bytes[](2); - callDatas[0] = abi.encodeWithSelector( - WrapperActions.unwrapWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0.5e18 - ); + callDatas[0] = + abi.encodeWithSelector(WrapperActions.unwrapAllWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 1.5e18); assertEq( result.quarkOperations[0].scriptCalldata, @@ -1215,7 +1213,7 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), 1e5 ), - "calldata is Paycall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0.5e18), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]), 1e5);" + "calldata is Paycall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapAllWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]), 1e5);" ); assertEq( result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days" @@ -1321,7 +1319,7 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { callContracts[1] = transferActionsAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = - abi.encodeWithSelector(WrapperActions.unwrapWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18); + abi.encodeWithSelector(WrapperActions.unwrapAllWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 2e18); assertEq( result.quarkOperations[0].scriptCalldata, @@ -1331,7 +1329,7 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), 1e5 ), - "calldata is Quotecall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18), TransferActions.transferNativeToken(address(0xceecee), 2e18)]), 1e5);" + "calldata is Quotecall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapAllWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), TransferActions.transferNativeToken(address(0xceecee), 2e18)]), 1e5);" ); assertEq( result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days" @@ -1432,13 +1430,13 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { callContracts[1] = transferActionsAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = - abi.encodeWithSelector(WrapperActions.wrapETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0.75e18); + abi.encodeWithSelector(WrapperActions.wrapAllETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); callDatas[1] = abi.encodeWithSelector(TransferActions.transferERC20Token.selector, WETH_1, address(0xceecee), 1.75e18); assertEq( result.quarkOperations[0].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.wrapETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0.75e18), TransferActions.transferERC20Token(WETH_1, address(0xceecee), 1.75e18)]);" + "calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.wrapAllETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), TransferActions.transferERC20Token(WETH_1, address(0xceecee), 1.75e18)]);" ); assertEq( result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days" From ea77ab6d2e3339ff40ec8df30e949236fd12167e Mon Sep 17 00:00:00 2001 From: kevincheng96 Date: Wed, 20 Nov 2024 16:25:48 -0800 Subject: [PATCH 3/5] Fix imports after rebase --- test/builder/QuarkBuilderTransfer.t.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/builder/QuarkBuilderTransfer.t.sol b/test/builder/QuarkBuilderTransfer.t.sol index e939fd3..4bc2a01 100644 --- a/test/builder/QuarkBuilderTransfer.t.sol +++ b/test/builder/QuarkBuilderTransfer.t.sol @@ -20,10 +20,6 @@ import {PaymentInfo} from "src/builder/PaymentInfo.sol"; import {QuarkBuilder} from "src/builder/QuarkBuilder.sol"; import {QuarkBuilderBase} from "src/builder/QuarkBuilderBase.sol"; import {Quotecall} from "src/Quotecall.sol"; -import {AcrossActions} from "src/AcrossScripts.sol"; - -import {FFI} from "src/builder/FFI.sol"; -import {AcrossFFI} from "test/builder/mocks/AcrossFFI.sol"; contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { function transferUsdc_(uint256 chainId, uint256 amount, address recipient, uint256 blockTimestamp) From 8ec798c16a976d3cee59b403bd063f91a059d65c Mon Sep 17 00:00:00 2001 From: kevincheng96 Date: Wed, 20 Nov 2024 17:04:12 -0800 Subject: [PATCH 4/5] 0.3.0 --- src/builder/QuarkBuilderBase.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builder/QuarkBuilderBase.sol b/src/builder/QuarkBuilderBase.sol index d671a20..8cf7cc7 100644 --- a/src/builder/QuarkBuilderBase.sol +++ b/src/builder/QuarkBuilderBase.sol @@ -37,7 +37,7 @@ contract QuarkBuilderBase { /* ===== Constants ===== */ - string constant VERSION = "0.2.1"; + string constant VERSION = "0.3.0"; /* ===== Custom Errors ===== */ From 42a2c1cee70b1dc0d0b5711da8fb7223de5ef3e3 Mon Sep 17 00:00:00 2001 From: kevincheng96 Date: Thu, 21 Nov 2024 14:25:15 -0800 Subject: [PATCH 5/5] Use unwrap up to for WETH --- src/WrapperScripts.sol | 14 ++++ src/builder/QuarkBuilderBase.sol | 4 +- src/builder/TokenWrapper.sol | 11 ++- src/builder/actions/Actions.sol | 4 +- test/WrapperScripts.t.sol | 100 ++++++++++++++++++++++++ test/builder/QuarkBuilderTransfer.t.sol | 33 ++++---- 6 files changed, 144 insertions(+), 22 deletions(-) diff --git a/src/WrapperScripts.sol b/src/WrapperScripts.sol index c4ed4a1..73b4a9b 100644 --- a/src/WrapperScripts.sol +++ b/src/WrapperScripts.sol @@ -10,6 +10,13 @@ contract WrapperActions { IWETH(weth).deposit{value: amount}(); } + function wrapETHUpTo(address weth, uint256 targetAmount) external payable { + uint256 currentBalance = IERC20(weth).balanceOf(address(this)); + if (currentBalance < targetAmount) { + IWETH(weth).deposit{value: targetAmount - currentBalance}(); + } + } + function wrapAllETH(address weth) external payable { uint256 ethBalance = address(this).balance; if (ethBalance > 0) { @@ -21,6 +28,13 @@ contract WrapperActions { IWETH(weth).withdraw(amount); } + function unwrapWETHUpTo(address weth, uint256 targetAmount) external { + uint256 currentBalance = address(this).balance; + if (currentBalance < targetAmount) { + IWETH(weth).withdraw(targetAmount - currentBalance); + } + } + function unwrapAllWETH(address weth) external payable { uint256 wethBalance = IERC20(weth).balanceOf(address(this)); if (wethBalance > 0) { diff --git a/src/builder/QuarkBuilderBase.sol b/src/builder/QuarkBuilderBase.sol index 8cf7cc7..be28f98 100644 --- a/src/builder/QuarkBuilderBase.sol +++ b/src/builder/QuarkBuilderBase.sol @@ -432,8 +432,8 @@ contract QuarkBuilderBase { Actions.WrapOrUnwrapAsset({ chainAccountsList: chainAccountsList, assetSymbol: counterpartSymbol, - // This is just to indicate we plan to wrap all - amount: type(uint256).max, + // Note: The wrapper logic should only "wrap all" or "wrap up to" the amount needed + amount: amountNeeded, chainId: chainId, sender: account, blockTimestamp: blockTimestamp diff --git a/src/builder/TokenWrapper.sol b/src/builder/TokenWrapper.sol index 455f201..f066cae 100644 --- a/src/builder/TokenWrapper.sol +++ b/src/builder/TokenWrapper.sol @@ -108,14 +108,17 @@ library TokenWrapper { return Strings.stringEqIgnoreCase(tokenSymbol, getKnownWrapperTokenPair(chainId, tokenSymbol).wrappedSymbol); } - function encodeActionToWrapOrUnwrap(uint256 chainId, string memory tokenSymbol) + /// Note: We "wrap/unwrap all" for every asset except for ETH/WETH. For ETH/WETH, we will "wrap all ETH" but + /// "unwrap up to X WETH". This is an intentional choice to prefer WETH over ETH since it is much more + /// usable across protocols. + function encodeActionToWrapOrUnwrap(uint256 chainId, string memory tokenSymbol, uint256 amount) internal pure returns (bytes memory) { KnownWrapperTokenPair memory pair = getKnownWrapperTokenPair(chainId, tokenSymbol); if (isWrappedToken(chainId, tokenSymbol)) { - return encodeActionToUnwrapToken(chainId, tokenSymbol); + return encodeActionToUnwrapToken(chainId, tokenSymbol, amount); } else { return encodeActionToWrapToken(chainId, tokenSymbol, pair.underlyingToken); } @@ -140,14 +143,14 @@ library TokenWrapper { revert NotWrappable(); } - function encodeActionToUnwrapToken(uint256 chainId, string memory tokenSymbol) + function encodeActionToUnwrapToken(uint256 chainId, string memory tokenSymbol, uint256 amount) internal pure returns (bytes memory) { if (Strings.stringEqIgnoreCase(tokenSymbol, "WETH")) { return abi.encodeWithSelector( - WrapperActions.unwrapAllWETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper + WrapperActions.unwrapWETHUpTo.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount ); } else if (Strings.stringEqIgnoreCase(tokenSymbol, "wstETH")) { return abi.encodeWithSelector( diff --git a/src/builder/actions/Actions.sol b/src/builder/actions/Actions.sol index 51e58da..03a5b78 100644 --- a/src/builder/actions/Actions.sol +++ b/src/builder/actions/Actions.sol @@ -1490,7 +1490,9 @@ library Actions { nonce: accountSecret.nonceSecret, isReplayable: false, scriptAddress: CodeJarHelper.getCodeAddress(type(WrapperActions).creationCode), - scriptCalldata: TokenWrapper.encodeActionToWrapOrUnwrap(wrapOrUnwrap.chainId, wrapOrUnwrap.assetSymbol), + scriptCalldata: TokenWrapper.encodeActionToWrapOrUnwrap( + wrapOrUnwrap.chainId, wrapOrUnwrap.assetSymbol, wrapOrUnwrap.amount + ), scriptSources: scriptSources, expiry: wrapOrUnwrap.blockTimestamp + STANDARD_EXPIRY_BUFFER }); diff --git a/test/WrapperScripts.t.sol b/test/WrapperScripts.t.sol index 667170c..1b889a9 100644 --- a/test/WrapperScripts.t.sol +++ b/test/WrapperScripts.t.sol @@ -65,6 +65,56 @@ contract WrapperScriptsTest is Test { assertEq(address(wallet).balance, 0 ether); } + function testWrapETHUpTo() public { + vm.pauseGasMetering(); + QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); + + deal(address(wallet), 10 ether); + deal(WETH, address(wallet), 7 ether); + + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + wallet, + wrapperScript, + abi.encodeWithSelector(WrapperActions.wrapETHUpTo.selector, WETH, 10 ether), + ScriptType.ScriptSource + ); + bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op); + + assertEq(IERC20(WETH).balanceOf(address(wallet)), 7 ether); + assertEq(address(wallet).balance, 10 ether); + + vm.resumeGasMetering(); + wallet.executeQuarkOperation(op, signature); + + assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); + assertEq(address(wallet).balance, 7 ether); + } + + function testWrapETHUpToDoesNotWrapIfNotNeeded() public { + vm.pauseGasMetering(); + QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); + + deal(address(wallet), 10 ether); + deal(WETH, address(wallet), 10 ether); + + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + wallet, + wrapperScript, + abi.encodeWithSelector(WrapperActions.wrapETHUpTo.selector, WETH, 10 ether), + ScriptType.ScriptSource + ); + bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op); + + assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); + assertEq(address(wallet).balance, 10 ether); + + vm.resumeGasMetering(); + wallet.executeQuarkOperation(op, signature); + + assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); + assertEq(address(wallet).balance, 10 ether); + } + function testWrapAllETH() public { vm.pauseGasMetering(); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); @@ -112,6 +162,56 @@ contract WrapperScriptsTest is Test { assertEq(address(wallet).balance, 10 ether); } + function testUnwrapWETHUpTo() public { + vm.pauseGasMetering(); + QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); + + deal(WETH, address(wallet), 10 ether); + deal(address(wallet), 7 ether); + + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + wallet, + wrapperScript, + abi.encodeWithSelector(WrapperActions.unwrapWETHUpTo.selector, WETH, 10 ether), + ScriptType.ScriptSource + ); + bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op); + + assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); + assertEq(address(wallet).balance, 7 ether); + + vm.resumeGasMetering(); + wallet.executeQuarkOperation(op, signature); + + assertEq(IERC20(WETH).balanceOf(address(wallet)), 7 ether); + assertEq(address(wallet).balance, 10 ether); + } + + function testUnwrapWETHUpToDoesNotUnwrapIfNotNeeded() public { + vm.pauseGasMetering(); + QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); + + deal(WETH, address(wallet), 10 ether); + deal(address(wallet), 10 ether); + + QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata( + wallet, + wrapperScript, + abi.encodeWithSelector(WrapperActions.unwrapWETHUpTo.selector, WETH, 10 ether), + ScriptType.ScriptSource + ); + bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op); + + assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); + assertEq(address(wallet).balance, 10 ether); + + vm.resumeGasMetering(); + wallet.executeQuarkOperation(op, signature); + + assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether); + assertEq(address(wallet).balance, 10 ether); + } + function testUnwrapAllWETH() public { vm.pauseGasMetering(); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); diff --git a/test/builder/QuarkBuilderTransfer.t.sol b/test/builder/QuarkBuilderTransfer.t.sol index 4bc2a01..bb4c839 100644 --- a/test/builder/QuarkBuilderTransfer.t.sol +++ b/test/builder/QuarkBuilderTransfer.t.sol @@ -1066,8 +1066,8 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { morphoVaultPositions: emptyMorphoVaultPositions_() }); - // Transfer 1.5ETH to 0xceecee on chain 1 - // Should able to have auto unwrapping 0.5 WETH to ETH to cover the amount + // Transfer 1.5 ETH to 0xceecee on chain 1 + // Should unwrap up to 1.5 WETH to ETH to cover the amount (0.5 WETH will actually be unwrapped) QuarkBuilder.BuilderResult memory result = builder.transfer( transferEth_(1, 1.5e18, address(0xceecee), BLOCK_TIMESTAMP), chainAccountsList, paymentUsd_() ); @@ -1089,13 +1089,14 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { callContracts[0] = wrapperActionsAddress; callContracts[1] = transferActionsAddress; bytes[] memory callDatas = new bytes[](2); - callDatas[0] = - abi.encodeWithSelector(WrapperActions.unwrapAllWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + callDatas[0] = abi.encodeWithSelector( + WrapperActions.unwrapWETHUpTo.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1.5e18 + ); callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 1.5e18); assertEq( result.quarkOperations[0].scriptCalldata, abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), - "calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapAllWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]);" + "calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapWETHUpTo(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1.5e18), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]);" ); assertEq( result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days" @@ -1172,8 +1173,8 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { morphoVaultPositions: emptyMorphoVaultPositions_() }); - // Transfer 1.5ETH to 0xceecee on chain 1 - // Should able to have auto unwrapping 0.5 WETH to ETH to cover the amount + // Transfer 1.5 ETH to 0xceecee on chain 1 + // Should unwrap up to 1.5 WETH to ETH to cover the amount (0.5 WETH will actually be unwrapped) QuarkBuilder.BuilderResult memory result = builder.transfer( transferEth_(1, 1.5e18, address(0xceecee), BLOCK_TIMESTAMP), chainAccountsList, paymentUsdc_(maxCosts) ); @@ -1198,8 +1199,9 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { callContracts[0] = wrapperActionsAddress; callContracts[1] = transferActionsAddress; bytes[] memory callDatas = new bytes[](2); - callDatas[0] = - abi.encodeWithSelector(WrapperActions.unwrapAllWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + callDatas[0] = abi.encodeWithSelector( + WrapperActions.unwrapWETHUpTo.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1.5e18 + ); callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 1.5e18); assertEq( result.quarkOperations[0].scriptCalldata, @@ -1209,7 +1211,7 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), 1e5 ), - "calldata is Paycall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapAllWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]), 1e5);" + "calldata is Paycall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapWETHUpTo(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1.5e18), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]), 1e5);" ); assertEq( result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days" @@ -1286,8 +1288,8 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { morphoVaultPositions: emptyMorphoVaultPositions_() }); - // Transfer max ETH to 0xceecee on chain 1 - // Should able to have auto unwrapping 0.5 WETH to ETH to cover the amount + // Transfer max (2) ETH to 0xceecee on chain 1 + // Should unwrap up to 2 WETH to ETH to cover the amount (1 WETH will actually be unwrapped) QuarkBuilder.BuilderResult memory result = builder.transfer( transferEth_(1, type(uint256).max, address(0xceecee), BLOCK_TIMESTAMP), chainAccountsList, @@ -1314,8 +1316,9 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { callContracts[0] = wrapperActionsAddress; callContracts[1] = transferActionsAddress; bytes[] memory callDatas = new bytes[](2); - callDatas[0] = - abi.encodeWithSelector(WrapperActions.unwrapAllWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + callDatas[0] = abi.encodeWithSelector( + WrapperActions.unwrapWETHUpTo.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 2e18 + ); callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 2e18); assertEq( result.quarkOperations[0].scriptCalldata, @@ -1325,7 +1328,7 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas), 1e5 ), - "calldata is Quotecall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapAllWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), TransferActions.transferNativeToken(address(0xceecee), 2e18)]), 1e5);" + "calldata is Quotecall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapWETHUpTo(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 2e18), TransferActions.transferNativeToken(address(0xceecee), 2e18)]), 1e5);" ); assertEq( result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days"