Skip to content

Commit 42a2c1c

Browse files
committed
Use unwrap up to for WETH
1 parent 8ec798c commit 42a2c1c

6 files changed

Lines changed: 144 additions & 22 deletions

File tree

src/WrapperScripts.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ contract WrapperActions {
1010
IWETH(weth).deposit{value: amount}();
1111
}
1212

13+
function wrapETHUpTo(address weth, uint256 targetAmount) external payable {
14+
uint256 currentBalance = IERC20(weth).balanceOf(address(this));
15+
if (currentBalance < targetAmount) {
16+
IWETH(weth).deposit{value: targetAmount - currentBalance}();
17+
}
18+
}
19+
1320
function wrapAllETH(address weth) external payable {
1421
uint256 ethBalance = address(this).balance;
1522
if (ethBalance > 0) {
@@ -21,6 +28,13 @@ contract WrapperActions {
2128
IWETH(weth).withdraw(amount);
2229
}
2330

31+
function unwrapWETHUpTo(address weth, uint256 targetAmount) external {
32+
uint256 currentBalance = address(this).balance;
33+
if (currentBalance < targetAmount) {
34+
IWETH(weth).withdraw(targetAmount - currentBalance);
35+
}
36+
}
37+
2438
function unwrapAllWETH(address weth) external payable {
2539
uint256 wethBalance = IERC20(weth).balanceOf(address(this));
2640
if (wethBalance > 0) {

src/builder/QuarkBuilderBase.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,8 +432,8 @@ contract QuarkBuilderBase {
432432
Actions.WrapOrUnwrapAsset({
433433
chainAccountsList: chainAccountsList,
434434
assetSymbol: counterpartSymbol,
435-
// This is just to indicate we plan to wrap all
436-
amount: type(uint256).max,
435+
// Note: The wrapper logic should only "wrap all" or "wrap up to" the amount needed
436+
amount: amountNeeded,
437437
chainId: chainId,
438438
sender: account,
439439
blockTimestamp: blockTimestamp

src/builder/TokenWrapper.sol

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,17 @@ library TokenWrapper {
108108
return Strings.stringEqIgnoreCase(tokenSymbol, getKnownWrapperTokenPair(chainId, tokenSymbol).wrappedSymbol);
109109
}
110110

111-
function encodeActionToWrapOrUnwrap(uint256 chainId, string memory tokenSymbol)
111+
/// Note: We "wrap/unwrap all" for every asset except for ETH/WETH. For ETH/WETH, we will "wrap all ETH" but
112+
/// "unwrap up to X WETH". This is an intentional choice to prefer WETH over ETH since it is much more
113+
/// usable across protocols.
114+
function encodeActionToWrapOrUnwrap(uint256 chainId, string memory tokenSymbol, uint256 amount)
112115
internal
113116
pure
114117
returns (bytes memory)
115118
{
116119
KnownWrapperTokenPair memory pair = getKnownWrapperTokenPair(chainId, tokenSymbol);
117120
if (isWrappedToken(chainId, tokenSymbol)) {
118-
return encodeActionToUnwrapToken(chainId, tokenSymbol);
121+
return encodeActionToUnwrapToken(chainId, tokenSymbol, amount);
119122
} else {
120123
return encodeActionToWrapToken(chainId, tokenSymbol, pair.underlyingToken);
121124
}
@@ -140,14 +143,14 @@ library TokenWrapper {
140143
revert NotWrappable();
141144
}
142145

143-
function encodeActionToUnwrapToken(uint256 chainId, string memory tokenSymbol)
146+
function encodeActionToUnwrapToken(uint256 chainId, string memory tokenSymbol, uint256 amount)
144147
internal
145148
pure
146149
returns (bytes memory)
147150
{
148151
if (Strings.stringEqIgnoreCase(tokenSymbol, "WETH")) {
149152
return abi.encodeWithSelector(
150-
WrapperActions.unwrapAllWETH.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper
153+
WrapperActions.unwrapWETHUpTo.selector, getKnownWrapperTokenPair(chainId, tokenSymbol).wrapper, amount
151154
);
152155
} else if (Strings.stringEqIgnoreCase(tokenSymbol, "wstETH")) {
153156
return abi.encodeWithSelector(

src/builder/actions/Actions.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1490,7 +1490,9 @@ library Actions {
14901490
nonce: accountSecret.nonceSecret,
14911491
isReplayable: false,
14921492
scriptAddress: CodeJarHelper.getCodeAddress(type(WrapperActions).creationCode),
1493-
scriptCalldata: TokenWrapper.encodeActionToWrapOrUnwrap(wrapOrUnwrap.chainId, wrapOrUnwrap.assetSymbol),
1493+
scriptCalldata: TokenWrapper.encodeActionToWrapOrUnwrap(
1494+
wrapOrUnwrap.chainId, wrapOrUnwrap.assetSymbol, wrapOrUnwrap.amount
1495+
),
14941496
scriptSources: scriptSources,
14951497
expiry: wrapOrUnwrap.blockTimestamp + STANDARD_EXPIRY_BUFFER
14961498
});

test/WrapperScripts.t.sol

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,56 @@ contract WrapperScriptsTest is Test {
6565
assertEq(address(wallet).balance, 0 ether);
6666
}
6767

68+
function testWrapETHUpTo() public {
69+
vm.pauseGasMetering();
70+
QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0)));
71+
72+
deal(address(wallet), 10 ether);
73+
deal(WETH, address(wallet), 7 ether);
74+
75+
QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata(
76+
wallet,
77+
wrapperScript,
78+
abi.encodeWithSelector(WrapperActions.wrapETHUpTo.selector, WETH, 10 ether),
79+
ScriptType.ScriptSource
80+
);
81+
bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op);
82+
83+
assertEq(IERC20(WETH).balanceOf(address(wallet)), 7 ether);
84+
assertEq(address(wallet).balance, 10 ether);
85+
86+
vm.resumeGasMetering();
87+
wallet.executeQuarkOperation(op, signature);
88+
89+
assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether);
90+
assertEq(address(wallet).balance, 7 ether);
91+
}
92+
93+
function testWrapETHUpToDoesNotWrapIfNotNeeded() public {
94+
vm.pauseGasMetering();
95+
QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0)));
96+
97+
deal(address(wallet), 10 ether);
98+
deal(WETH, address(wallet), 10 ether);
99+
100+
QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata(
101+
wallet,
102+
wrapperScript,
103+
abi.encodeWithSelector(WrapperActions.wrapETHUpTo.selector, WETH, 10 ether),
104+
ScriptType.ScriptSource
105+
);
106+
bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op);
107+
108+
assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether);
109+
assertEq(address(wallet).balance, 10 ether);
110+
111+
vm.resumeGasMetering();
112+
wallet.executeQuarkOperation(op, signature);
113+
114+
assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether);
115+
assertEq(address(wallet).balance, 10 ether);
116+
}
117+
68118
function testWrapAllETH() public {
69119
vm.pauseGasMetering();
70120
QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0)));
@@ -112,6 +162,56 @@ contract WrapperScriptsTest is Test {
112162
assertEq(address(wallet).balance, 10 ether);
113163
}
114164

165+
function testUnwrapWETHUpTo() public {
166+
vm.pauseGasMetering();
167+
QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0)));
168+
169+
deal(WETH, address(wallet), 10 ether);
170+
deal(address(wallet), 7 ether);
171+
172+
QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata(
173+
wallet,
174+
wrapperScript,
175+
abi.encodeWithSelector(WrapperActions.unwrapWETHUpTo.selector, WETH, 10 ether),
176+
ScriptType.ScriptSource
177+
);
178+
bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op);
179+
180+
assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether);
181+
assertEq(address(wallet).balance, 7 ether);
182+
183+
vm.resumeGasMetering();
184+
wallet.executeQuarkOperation(op, signature);
185+
186+
assertEq(IERC20(WETH).balanceOf(address(wallet)), 7 ether);
187+
assertEq(address(wallet).balance, 10 ether);
188+
}
189+
190+
function testUnwrapWETHUpToDoesNotUnwrapIfNotNeeded() public {
191+
vm.pauseGasMetering();
192+
QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0)));
193+
194+
deal(WETH, address(wallet), 10 ether);
195+
deal(address(wallet), 10 ether);
196+
197+
QuarkWallet.QuarkOperation memory op = new QuarkOperationHelper().newBasicOpWithCalldata(
198+
wallet,
199+
wrapperScript,
200+
abi.encodeWithSelector(WrapperActions.unwrapWETHUpTo.selector, WETH, 10 ether),
201+
ScriptType.ScriptSource
202+
);
203+
bytes memory signature = new SignatureHelper().signOp(alicePrivateKey, wallet, op);
204+
205+
assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether);
206+
assertEq(address(wallet).balance, 10 ether);
207+
208+
vm.resumeGasMetering();
209+
wallet.executeQuarkOperation(op, signature);
210+
211+
assertEq(IERC20(WETH).balanceOf(address(wallet)), 10 ether);
212+
assertEq(address(wallet).balance, 10 ether);
213+
}
214+
115215
function testUnwrapAllWETH() public {
116216
vm.pauseGasMetering();
117217
QuarkWallet wallet = QuarkWallet(factory.create(alice, address(0)));

test/builder/QuarkBuilderTransfer.t.sol

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,8 +1066,8 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest {
10661066
morphoVaultPositions: emptyMorphoVaultPositions_()
10671067
});
10681068

1069-
// Transfer 1.5ETH to 0xceecee on chain 1
1070-
// Should able to have auto unwrapping 0.5 WETH to ETH to cover the amount
1069+
// Transfer 1.5 ETH to 0xceecee on chain 1
1070+
// Should unwrap up to 1.5 WETH to ETH to cover the amount (0.5 WETH will actually be unwrapped)
10711071
QuarkBuilder.BuilderResult memory result = builder.transfer(
10721072
transferEth_(1, 1.5e18, address(0xceecee), BLOCK_TIMESTAMP), chainAccountsList, paymentUsd_()
10731073
);
@@ -1089,13 +1089,14 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest {
10891089
callContracts[0] = wrapperActionsAddress;
10901090
callContracts[1] = transferActionsAddress;
10911091
bytes[] memory callDatas = new bytes[](2);
1092-
callDatas[0] =
1093-
abi.encodeWithSelector(WrapperActions.unwrapAllWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
1092+
callDatas[0] = abi.encodeWithSelector(
1093+
WrapperActions.unwrapWETHUpTo.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1.5e18
1094+
);
10941095
callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 1.5e18);
10951096
assertEq(
10961097
result.quarkOperations[0].scriptCalldata,
10971098
abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas),
1098-
"calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapAllWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]);"
1099+
"calldata is Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapWETHUpTo(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1.5e18), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]);"
10991100
);
11001101
assertEq(
11011102
result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days"
@@ -1172,8 +1173,8 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest {
11721173
morphoVaultPositions: emptyMorphoVaultPositions_()
11731174
});
11741175

1175-
// Transfer 1.5ETH to 0xceecee on chain 1
1176-
// Should able to have auto unwrapping 0.5 WETH to ETH to cover the amount
1176+
// Transfer 1.5 ETH to 0xceecee on chain 1
1177+
// Should unwrap up to 1.5 WETH to ETH to cover the amount (0.5 WETH will actually be unwrapped)
11771178
QuarkBuilder.BuilderResult memory result = builder.transfer(
11781179
transferEth_(1, 1.5e18, address(0xceecee), BLOCK_TIMESTAMP), chainAccountsList, paymentUsdc_(maxCosts)
11791180
);
@@ -1198,8 +1199,9 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest {
11981199
callContracts[0] = wrapperActionsAddress;
11991200
callContracts[1] = transferActionsAddress;
12001201
bytes[] memory callDatas = new bytes[](2);
1201-
callDatas[0] =
1202-
abi.encodeWithSelector(WrapperActions.unwrapAllWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
1202+
callDatas[0] = abi.encodeWithSelector(
1203+
WrapperActions.unwrapWETHUpTo.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1.5e18
1204+
);
12031205
callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 1.5e18);
12041206
assertEq(
12051207
result.quarkOperations[0].scriptCalldata,
@@ -1209,7 +1211,7 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest {
12091211
abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas),
12101212
1e5
12111213
),
1212-
"calldata is Paycall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapAllWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]), 1e5);"
1214+
"calldata is Paycall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapWETHUpTo(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1.5e18), TransferActions.transferNativeToken(address(0xceecee), 1.5e18)]), 1e5);"
12131215
);
12141216
assertEq(
12151217
result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days"
@@ -1286,8 +1288,8 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest {
12861288
morphoVaultPositions: emptyMorphoVaultPositions_()
12871289
});
12881290

1289-
// Transfer max ETH to 0xceecee on chain 1
1290-
// Should able to have auto unwrapping 0.5 WETH to ETH to cover the amount
1291+
// Transfer max (2) ETH to 0xceecee on chain 1
1292+
// Should unwrap up to 2 WETH to ETH to cover the amount (1 WETH will actually be unwrapped)
12911293
QuarkBuilder.BuilderResult memory result = builder.transfer(
12921294
transferEth_(1, type(uint256).max, address(0xceecee), BLOCK_TIMESTAMP),
12931295
chainAccountsList,
@@ -1314,8 +1316,9 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest {
13141316
callContracts[0] = wrapperActionsAddress;
13151317
callContracts[1] = transferActionsAddress;
13161318
bytes[] memory callDatas = new bytes[](2);
1317-
callDatas[0] =
1318-
abi.encodeWithSelector(WrapperActions.unwrapAllWETH.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
1319+
callDatas[0] = abi.encodeWithSelector(
1320+
WrapperActions.unwrapWETHUpTo.selector, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 2e18
1321+
);
13191322
callDatas[1] = abi.encodeWithSelector(TransferActions.transferNativeToken.selector, address(0xceecee), 2e18);
13201323
assertEq(
13211324
result.quarkOperations[0].scriptCalldata,
@@ -1325,7 +1328,7 @@ contract QuarkBuilderTransferTest is Test, QuarkBuilderTest {
13251328
abi.encodeWithSelector(Multicall.run.selector, callContracts, callDatas),
13261329
1e5
13271330
),
1328-
"calldata is Quotecall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapAllWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), TransferActions.transferNativeToken(address(0xceecee), 2e18)]), 1e5);"
1331+
"calldata is Quotecall.run(Multicall.run([wrapperActionsAddress, transferActionsAddress], [WrapperActions.unwrapWETHUpTo(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 2e18), TransferActions.transferNativeToken(address(0xceecee), 2e18)]), 1e5);"
13291332
);
13301333
assertEq(
13311334
result.quarkOperations[0].expiry, BLOCK_TIMESTAMP + 7 days, "expiry is current blockTimestamp + 7 days"

0 commit comments

Comments
 (0)