diff --git a/src/WrapperScripts.sol b/src/WrapperScripts.sol index 66ddf83..73b4a9b 100644 --- a/src/WrapperScripts.sol +++ b/src/WrapperScripts.sol @@ -17,6 +17,13 @@ contract WrapperActions { } } + function wrapAllETH(address weth) external payable { + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) { + IWETH(weth).deposit{value: ethBalance}(); + } + } + function unwrapWETH(address weth, uint256 amount) external { IWETH(weth).withdraw(amount); } @@ -28,12 +35,34 @@ contract WrapperActions { } } + function unwrapAllWETH(address weth) external payable { + uint256 wethBalance = IERC20(weth).balanceOf(address(this)); + if (wethBalance > 0) { + IWETH(weth).withdraw(wethBalance); + } + } + function wrapLidoStETH(address wstETH, address stETH, uint256 amount) external { IERC20(stETH).approve(wstETH, amount); 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 8ba3cb8..346a288 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..be28f98 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"; @@ -37,7 +37,7 @@ contract QuarkBuilderBase { /* ===== Constants ===== */ - string constant VERSION = "0.2.0"; + string constant VERSION = "0.3.0"; /* ===== Custom Errors ===== */ @@ -232,13 +232,20 @@ 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, we need to assume there is no supplemental balance that arrives on the destination chain. + if (Across.isNonDeterministicBridgeAction(assetsBridged, assetSymbolOut)) { + supplementalBalance = 0; + } + checkAndInsertWrapOrUnwrapAction({ actions: actions, quarkOperations: quarkOperations, chainAccountsList: chainAccountsList, payment: payment, assetSymbol: assetSymbolOut, - amount: actionIntent.amountOuts[i], + amountNeeded: actionIntent.amountOuts[i], supplementalBalance: supplementalBalance, chainId: actionIntent.chainId, account: actionIntent.actor, @@ -405,7 +412,7 @@ contract QuarkBuilderBase { Accounts.ChainAccounts[] memory chainAccountsList, PaymentInfo.Payment memory payment, string memory assetSymbol, - uint256 amount, + uint256 amountNeeded, uint256 supplementalBalance, uint256 chainId, address account, @@ -415,8 +422,8 @@ contract QuarkBuilderBase { // 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 (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 @@ -425,8 +432,8 @@ contract QuarkBuilderBase { Actions.WrapOrUnwrapAsset({ chainAccountsList: chainAccountsList, assetSymbol: counterpartSymbol, - // NOTE: Wrap/unwrap the amount needed to cover the amount - amount: amount - assetBalanceOnChain, + // 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 31798b4..f066cae 100644 --- a/src/builder/TokenWrapper.sol +++ b/src/builder/TokenWrapper.sol @@ -108,6 +108,9 @@ library TokenWrapper { return Strings.stringEqIgnoreCase(tokenSymbol, getKnownWrapperTokenPair(chainId, tokenSymbol).wrappedSymbol); } + /// 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 @@ -117,25 +120,24 @@ library TokenWrapper { if (isWrappedToken(chainId, tokenSymbol)) { return encodeActionToUnwrapToken(chainId, tokenSymbol, amount); } else { - return encodeActionToWrapToken(chainId, tokenSymbol, pair.underlyingToken, amount); + return encodeActionToWrapToken(chainId, tokenSymbol, pair.underlyingToken); } } - function encodeActionToWrapToken(uint256 chainId, string memory tokenSymbol, address tokenAddress, uint256 amount) + function encodeActionToWrapToken(uint256 chainId, string memory tokenSymbol, address tokenAddress) internal pure returns (bytes memory) { if (Strings.stringEqIgnoreCase(tokenSymbol, "ETH")) { return abi.encodeWithSelector( - WrapperActions.wrapETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount + 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(); @@ -148,11 +150,11 @@ library TokenWrapper { { if (Strings.stringEqIgnoreCase(tokenSymbol, "WETH")) { return abi.encodeWithSelector( - WrapperActions.unwrapWETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount + WrapperActions.unwrapWETHUpTo.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount ); } 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/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..1b889a9 100644 --- a/test/WrapperScripts.t.sol +++ b/test/WrapperScripts.t.sol @@ -115,6 +115,31 @@ contract WrapperScriptsTest is Test { assertEq(address(wallet).balance, 10 ether); } + function testWrapAllETH() 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.wrapAllETH.selector, WETH), + 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)), 17 ether); + assertEq(address(wallet).balance, 0 ether); + } + function testUnwrapWETH() public { vm.pauseGasMetering(); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); @@ -187,6 +212,31 @@ contract WrapperScriptsTest is Test { assertEq(address(wallet).balance, 10 ether); } + function testUnwrapAllWETH() 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.unwrapAllWETH.selector, WETH), + 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)), 0 ether); + assertEq(address(wallet).balance, 17 ether); + } + function testWrapStETH() public { vm.pauseGasMetering(); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); @@ -208,12 +258,44 @@ contract WrapperScriptsTest is Test { 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); assertApproxEqAbs(IERC20(wstETH).balanceOf(address(wallet)), 8.74 ether, 0.01 ether); } + function testWrapAllStETH() public { + vm.pauseGasMetering(); + QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); + + // 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.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); + + // 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); + } + function testUnwrapWstETH() public { vm.pauseGasMetering(); QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0))); @@ -230,8 +312,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 2146ac7..92917fc 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.wrapAllETH.selector, TokenWrapper.getKnownWrapperTokenPair(8453, "WETH").wrapper + ); + 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.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" @@ -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/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..bb4c839 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) @@ -1070,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_() ); @@ -1094,13 +1090,13 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { callContracts[1] = transferActionsAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = abi.encodeWithSelector( - WrapperActions.unwrapWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0.5e18 + 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.unwrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0.5e18), 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" @@ -1177,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) ); @@ -1204,7 +1200,7 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest { callContracts[1] = transferActionsAddress; bytes[] memory callDatas = new bytes[](2); callDatas[0] = abi.encodeWithSelector( - WrapperActions.unwrapWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0.5e18 + WrapperActions.unwrapWETHUpTo.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1.5e18 ); callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 1.5e18); assertEq( @@ -1215,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.unwrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0.5e18), 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" @@ -1292,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, @@ -1320,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.unwrapWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18); + callDatas[0] = abi.encodeWithSelector( + WrapperActions.unwrapWETHUpTo.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 2e18 + ); callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 2e18); assertEq( result.quarkOperations[0].scriptCalldata, @@ -1331,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.unwrapWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1e18), 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" @@ -1432,13 +1429,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" 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); } }