Skip to main content

Corn Banner

🟢 BTCN Zap – Swap & Bridge

1. What this Zap does​

The SwapFacilitySwapAndBridgeZap contracts (one for wBTC↔︎BTCN, one for cbBTC↔︎BTCN) wrap two on‑chain actions into one user transaction:

  1. Swap the collateral token (wBTC or cbBTC) 1‑for‑1 into freshly‑minted BTCN through the underlying Swap Facility.
  2. Bridge the BTCN to another chain (e.g. Corn) via LayerZero v2 OFT in the same transaction.

The result is a friction‑less “Asset on Mainnet in -> BTCN on Corn out” flow that any interoperability aggregator can surface as a single route.

info

There are no additional fees other than:

  • The Swap Facility fee (currently 0 bps)
  • LayerZero messaging fee

2. Contract addresses (Ethereum Mainnet)​

RolewBTC PathcbBTC Path
Swap Facility0xBf5eB70b93d5895C839B8BeB3C27dc36f6B56fea0x554335b8C994E47e6dbfDC08Fa8aca0510e66BA1
Zap0x1C2c9eFa3693572d008FB55253f7DEAAa7F3E6b10x8516F1BBdB7744F76E742b8380f9D3796b9a0010
Collateral TokenwBTC: 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599cbBTC: 0xcBb7a6B3Ff08D6CbE404DC7cCA23A6a179833Bf5

3. Prerequisites​

  1. SDKs / ABIs
    • @layerzerolabs/lz‑v2‑utilities (or raw TypeScript helpers)
    • ABI for the Zap (identical for wBTC & cbBTC)
    • ERC‑20 ABI for approvals
  2. Signer connected to Ethereum Mainnet.
  3. Destination chain Endpoint ID (Corn = 30331).
  4. RPC that can return gas price in real time (used for fee estimation).

4. High‑level TX sequence​


Zap Transaction Sequence

5. Building the call​

5.1 Approve collateral​

await collateralToken
.connect(signer)
.approve(ZAP_ADDRESS, amountIn);

5.2 Prepare SendParam​

FieldHow to buildExample value
dstEidLayerZero Endpoint ID of the destination chain30331 (Corn)
tobytes32 of the final recipientaddressToBytes32(0xRecipient)
amountLDAmount of BTCN expected (in 18 decimals)parseUnits("1.0", 18)
minAmountLDMinimum BTCN (sharedDecimals) the user accepts after the swap. Include slippage buffer.parseUnits("1.0", sharedDecimals)
extraOptionsHex‑encoded options. At minimum add a receive‑gas option: Options.newOptions().addExecutorLzReceiveOption(200_000).toHex()0x01…
composeMsgAlways 0x (Zap prohibits compose)0x
oftCmdAlways 0x0x

Shared vs native decimals
BTCN has 18 native decimals but sharedDecimals for OFT are 8.
• When you estimate fees you pass amounts in sharedDecimals.
• When you show the UI or build min‑out in native decimals use 18.

5.3 Estimate LayerZero fee​

const oft = new Contract(BTCN_OFT_ADDRESS, IOFT_ABI, provider);
const [nativeFee] = await oft.estimateSendFee(
false, // payInLzToken = false
sendParam, // as built above
false // useZro = false
);

Set MessagingFee struct:

const lzFee: MessagingFee = {
nativeFee,
lzTokenFee: 0n // ZRO payment disabled
};

5.4 Call the Zap​

const tx = await zap.swapExactCollateralForDebtAndLZSend(
amountIn, // collateralIn (wBTC / cbBTC)
sendParam,
lzFee,
signer.address, // refundAddress
Math.floor(Date.now()/1000) + 1800, // 30‑min deadline
{ value: nativeFee }
);
await tx.wait();

Return values: [debtOut, fee] (both in native 18‑dec BTCN).

info

The following transaction is an example of a successful executions of this operation on-chain.

6. Optional – Native drop (gas refuel) on the destination​

To auto‑fund the recipient with destination gas you can append one more option:

const extraOptions = Options
.newOptions()
.addExecutorLzReceiveOption(200_000)
.addExecutorNativeDropOption(parseEther("0.01")) // 0.01 native on Corn
.toHex();

Pass the larger nativeFee (estimate again after changing options!).

7. Reference TypeScript helper (minimal)​

export async function bridgeWithZap({
signer, // ethers.Signer
zapAddress, // depends on collateral
collateralAddress,
amountIn,
dstEid,
recipient,
slippageBps = 0 // Default to 0 as there are no fees on the SwapFacility
}: Args) {
const zap = new Contract(zapAddress, ZapAbi, signer);
const collateral = new Contract(collateralAddress, ERC20Abi, signer);

// 1) Allowance
const allowance = await collateral.allowance(await signer.getAddress(), zapAddress);
if (allowance < amountIn) {
await (await collateral.approve(zapAddress, amountIn)).wait();
}

// 2) Calculate amounts for LayerZero
const sharedDecimals: number = await zap.debtToken().then(async (d: string) => {
const oft = new Contract(d, IOFT_ABI, signer);
return Number(await oft.sharedDecimals());
});

// Expected BTCN amount (1:1 swap, 18 decimals)
const expectedBTCNAmount = amountIn * (10_000n - BigInt(slippageBps)) / 10_000n;
// For LayerZero fee estimation (shared decimals - 8)
const minAmountLDShared = amountIn * 10n ** BigInt(sharedDecimals) / 10n ** 18n * (10_000n - BigInt(slippageBps)) / 10_000n;

// 3) SendParam for fee estimation
const sendParam = {
dstEid,
to: addressToBytes32(recipient),
amountLD: expectedBTCNAmount,
minAmountLD: minAmountLDShared,
extraOptions: Options.newOptions().addExecutorLzReceiveOption(200_000).toHex(),
composeMsg: "0x",
oftCmd: "0x"
};

// 4) Estimate LayerZero fee
const oft = new Contract(await zap.debtToken(), IOFT_ABI, signer);
const [nativeFee] = await oft.estimateSendFee(false, sendParam, false);
const lzFee = { nativeFee, lzTokenFee: 0n };

// 5) Update sendParam for the actual Zap call (minAmountLD in 18 decimals)
sendParam.minAmountLD = expectedBTCNAmount;

// 6) Zap!
return zap.swapExactCollateralForDebtAndLZSend(
amountIn,
sendParam,
lzFee,
await signer.getAddress(),
Math.floor(Date.now()/1000) + 1800,
{ value: nativeFee }
);
}

8. Troubleshooting & Gotchas​

SymptomLikely causeFix
InvalidFee() revertmsg.value < lzFee.nativeFeePass the exact nativeFee from estimateSendFee (and re‑estimate if you changed extraOptions).
No compose / No oftCmd revertcomposeMsg or oftCmd not emptyKeep them 0x.
INVALID_ASSET_ON_NETWORK (helper error)Using wrong collateral token for current chainCheck collateralAddress & correct Zap.
Fee spikes when adding native dropYou must re‑run estimateSendFee after changing options.

Learn more about LayerZero's Options here.

9. Security considerations​

  • Contract is paused if LayerZero endpoints mis‑behave. Monitor Paused() events.
  • Do not send more collateral than swapFacility.maxIn() – front‑end should call and limit.