Skip to content

Commit

Permalink
Use wrap up to scripts in QuarkBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
kevincheng96 committed Nov 19, 2024
1 parent 108c525 commit 703f5f5
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 50 deletions.
15 changes: 15 additions & 0 deletions src/builder/BridgeRoutes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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"));
}
}
33 changes: 27 additions & 6 deletions src/builder/QuarkBuilderBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -229,6 +229,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,
Expand All @@ -240,7 +249,8 @@ contract QuarkBuilderBase {
chainId: actionIntent.chainId,
account: actionIntent.actor,
blockTimestamp: actionIntent.blockTimestamp,
useQuotecall: actionIntent.useQuotecall
useQuotecall: actionIntent.useQuotecall,
useWrapUpTo: useWrapUpTo
});
}
}
Expand Down Expand Up @@ -407,25 +417,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,
Expand Down
46 changes: 31 additions & 15 deletions src/builder/TokenWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/builder/actions/Actions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ library Actions {
uint256 amount;
uint256 chainId;
address sender;
bool useWrapUpTo;
uint256 blockTimestamp;
}

Expand Down Expand Up @@ -1470,7 +1471,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
Expand Down
51 changes: 25 additions & 26 deletions test/builder/BridgingLogic.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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_()
Expand All @@ -69,6 +70,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(
Expand Down Expand Up @@ -101,8 +106,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
Expand All @@ -122,28 +127,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"
Expand All @@ -168,8 +166,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,
Expand Down
12 changes: 10 additions & 2 deletions test/builder/mocks/AcrossFFI.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@ 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 */
uint256, /* srcChain */
uint256, /* dstChain */
uint256 /* amount */
) external pure override returns (uint256 gasFee, uint256 variableFeePct) {
return (1e6, 0.01e18);
return (MockAcrossFFIConstants.GAS_FEE, MockAcrossFFIConstants.VARIABLE_FEE_PCT);
}
}

0 comments on commit 703f5f5

Please sign in to comment.