🟢 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:
- Swap the collateral token (
wBTC
orcbBTC
) 1‑for‑1 into freshly‑mintedBTCN
through the underlying Swap Facility. - 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.
There are no additional fees other than:
- The Swap Facility fee (currently 0 bps)
- LayerZero messaging fee
2. Contract addresses (Ethereum Mainnet)​
Role | wBTC Path | cbBTC Path |
---|---|---|
Swap Facility | 0xBf5eB70b93d5895C839B8BeB3C27dc36f6B56fea | 0x554335b8C994E47e6dbfDC08Fa8aca0510e66BA1 |
Zap | 0x1C2c9eFa3693572d008FB55253f7DEAAa7F3E6b1 | 0x8516F1BBdB7744F76E742b8380f9D3796b9a0010 |
Collateral Token | wBTC: 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 | cbBTC: 0xcBb7a6B3Ff08D6CbE404DC7cCA23A6a179833Bf5 |
3. Prerequisites​
- SDKs / ABIs
•Â@layerzerolabs/lz‑v2‑utilities
(or raw TypeScript helpers)
• ABI for the Zap (identical for wBTC & cbBTC)
• ERC‑20 ABI for approvals - Signer connected to Ethereum Mainnet.
- Destination chain Endpoint ID (Corn =Â
30331
). - RPC that can return gas price in real time (used for fee estimation).
4. High‑level TX sequence​
5. Building the call​
5.1 Approve collateral​
await collateralToken
.connect(signer)
.approve(ZAP_ADDRESS, amountIn);
5.2Â Prepare SendParam
​
Field | How to build | Example value |
---|---|---|
dstEid | LayerZero Endpoint ID of the destination chain | 30331 (Corn) |
to | bytes32 of the final recipient | addressToBytes32(0xRecipient) |
amountLD | Amount of BTCN expected (in 18 decimals) | parseUnits("1.0", 18) |
minAmountLD | Minimum BTCN (sharedDecimals) the user accepts after the swap. Include slippage buffer. | parseUnits("1.0", sharedDecimals) |
extraOptions | Hex‑encoded options. At minimum add a receive‑gas option: Options.newOptions().addExecutorLzReceiveOption(200_000).toHex() | 0x01… |
composeMsg | Always 0x (Zap prohibits compose) | 0x |
oftCmd | Always 0x | 0x |
Shared vs native decimals
BTCN
has18
native decimals but sharedDecimals for OFT are8
.
• When you estimate fees you pass amounts in sharedDecimals.
• When you show the UI or build min‑out in native decimals use18
.
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).
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​
Symptom | Likely cause | Fix |
---|---|---|
InvalidFee() revert | msg.value < lzFee.nativeFee | Pass the exact nativeFee from estimateSendFee (and re‑estimate if you changed extraOptions ). |
No compose / No oftCmd revert | composeMsg or oftCmd not empty | Keep them 0x . |
INVALID_ASSET_ON_NETWORK (helper error) | Using wrong collateral token for current chain | Check collateralAddress & correct Zap. |
Fee spikes when adding native drop | You 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.