diff --git a/src/WrapperScripts.sol b/src/WrapperScripts.sol index c4ed4a1..c082fbc 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) { @@ -17,6 +24,13 @@ contract WrapperActions { } } + function unwrapWETHUpTo(address weth, uint256 targetAmount) external { + uint256 currentBalance = address(this).balance; + if (currentBalance < targetAmount) { + IWETH(weth).withdraw(targetAmount - currentBalance); + } + } + function unwrapWETH(address weth, uint256 amount) external { IWETH(weth).withdraw(amount); } 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"