Skip to content

Commit

Permalink
Use unwrap up to for WETH
Browse files Browse the repository at this point in the history
  • Loading branch information
kevincheng96 committed Nov 21, 2024
1 parent 8ec798c commit 1700ab3
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 22 deletions.
14 changes: 14 additions & 0 deletions src/WrapperScripts.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,27 @@ 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}();
}
}

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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/builder/QuarkBuilderBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions src/builder/TokenWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion src/builder/actions/Actions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down
100 changes: 100 additions & 0 deletions test/WrapperScripts.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down Expand Up @@ -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)));
Expand Down
33 changes: 18 additions & 15 deletions test/builder/QuarkBuilderTransfer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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_()
);
Expand All @@ -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"
Expand Down Expand Up @@ -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)
);
Expand All @@ -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,
Expand All @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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"
Expand Down

0 comments on commit 1700ab3

Please sign in to comment.