Skip to content

Commit a94d0e3

Browse files
authored
PolymerCCTP add support for more chains [PolymerCCTPFacet v2.0.0] (#1630)
* Update PolymerCCTPFacet version to 1.0.1 and add support for Monad (chainId 143) and Bsc (chainId 56) in bridging logic. * update * added ReentracyGuard * added audit * updates * added new demo cases * updates * deleted check for mintRecipient * updated tests * Refactor Solana address handling in demo scripts and update wallet address usage. Removed hardcoded addresses and implemented dynamic derivation from private keys. Updated relevant scripts to use a single source of truth for the dev wallet address. * Update PolymerCCTPFacet version to 2.0.0 * deleted audit * added staging diamond * added check * added audit
1 parent f958713 commit a94d0e3

File tree

13 files changed

+218
-72
lines changed

13 files changed

+218
-72
lines changed

.cursor/rules/200-typescript.mdc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ globs:
4040

4141
- When referring to "demo script", this means scripts in `script/demoScripts/`. They follow similar structural patterns (e.g., `main()` function, `setupEnvironment()`, helpers from `demoScriptHelpers`). See `demoLidoWrapper.ts`, `demoUnit.ts`, `demoEco.ts` as reference examples.
4242

43+
## Solana addresses and dev wallet address ([CONV:SOLANA-DEV-WALLET])
44+
45+
- **Solana addresses (base58, bytes32)**: Derive dynamically from the signer’s private key. Do **not** hardcode Solana addresses or store them in config (e.g. `config/global.json`).
46+
- **Source**: `script/demoScripts/utils/demoScriptHelpers.ts`
47+
- **Base58**: `deriveSolanaAddress(privateKey)` — use for logging, Solana APIs, or ATA computation.
48+
- **Bytes32**: `solanaAddressToBytes32(deriveSolanaAddress(privateKey))` — use for contract/struct fields that expect bytes32 (e.g. non-EVM receiver, mintRecipient).
49+
- **Private key**: `getPrivateKeyForEnvironment(EnvironmentEnum.staging)` (or `EnvironmentEnum.production`) from `demoScriptHelpers`. Example: `demoAcrossV4.ts`, `demoPolymerCCTP.ts`, `demoNEARIntents.ts`.
50+
- **Dev wallet (EVM)**: Use a single source of truth from config.
51+
- Use **only** `DEV_WALLET_ADDRESS` from `script/demoScripts/utils/demoScriptHelpers.ts` (it reads `config/global.json` → `devWallet`). Do not use `globalConfig.devWallet` elsewhere.
52+
- Do not add or use separate constants for the same EVM dev wallet (e.g. no `ADDRESS_DEV_WALLET_V5`-style aliases).
53+
4354
## CLI and Logging
4455

4556
- CLI: use `citty`; logging via `consola`; validate env via `getEnvVar()`; exit 0/1 appropriately.

audit/auditLog.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,13 @@
538538
"auditorGitHandle": "sujithsomraaj",
539539
"auditReportPath": "./audit/reports/2026.02.05_LiFiIntentEscrowFacet(v1.0.1,v1.1.1).pdf",
540540
"auditCommitHash": "260f8e8088c88049d4d5de532613b78af188f9a3"
541+
},
542+
"audit20260216": {
543+
"auditCompletedOn": "16.02.2026",
544+
"auditedBy": "Sujith Somraaj (individual security researcher)",
545+
"auditorGitHandle": "sujithsomraaj",
546+
"auditReportPath": "./audit/reports/2026.02.16_PolymerCCTPFacet(v2.0.0).pdf",
547+
"auditCommitHash": "7f8a0efe29162fede3f8c647f35044a66459aadb"
541548
}
542549
},
543550
"auditedContracts": {
@@ -817,7 +824,8 @@
817824
"1.0.0": ["audit20250508"]
818825
},
819826
"PolymerCCTPFacet": {
820-
"1.0.0": ["audit20251201"]
827+
"1.0.0": ["audit20251201"],
828+
"2.0.0": ["audit20260216"]
821829
},
822830
"Receiver": {
823831
"2.0.3": ["audit20250109_3"],
68.4 KB
Binary file not shown.

deployments/arbitrum.diamond.staging.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,18 @@
212212
"0xd35fC069dcBD50780c706750A41AfC418f25f5D6": {
213213
"Name": "WhitelistRecoveryFacet",
214214
"Version": "1.0.2"
215+
},
216+
"0xa8deB34CDD4915D3D5E37e622fB2585bC2c1a7d6": {
217+
"Name": "PolymerCCTPFacet",
218+
"Version": "2.0.0"
219+
},
220+
"0xC31f575546A12Bc9dEf107CE19a242cd351BaDe2": {
221+
"Name": "AcrossV4SwapFacet",
222+
"Version": "1.0.0"
223+
},
224+
"0xBdE21104479e1373a20F487D3f6ffDED62574FA7": {
225+
"Name": "PolymerCCTPFacet",
226+
"Version": "2.0.0"
215227
}
216228
},
217229
"Periphery": {

deployments/arbitrum.staging.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"Patcher": "0x18069208cA7c2D55aa0073E047dD45587B26F6D4",
6363
"WhitelistManagerFacet": "0x6ab927f0766eE43418F508Fbad5Bd70D610fB281",
6464
"RelayDepositoryFacet": "0x004E291b9244C811B0BE00cA2C179d54FAA5073D",
65-
"PolymerCCTPFacet": "0xc547F046174A880d08A251aF10FA659c339d30Ff",
65+
"PolymerCCTPFacet": "0xBdE21104479e1373a20F487D3f6ffDED62574FA7",
6666
"LiFiIntentEscrowFacet": "0x06A8fA5B326F663507FfF045B90351a9DCF3f9B1",
6767
"NEARIntentsFacet": "0x2c748Eda11e9717Bd487c8CF6e52862351909Ad0",
6868
"CelerCircleBridgeV2Facet": "0xfeac1be55bCccc46D88743F9dBCCF60f66562357",

script/demoScripts/demoAcrossV4.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ import {
99
AcrossFacetV4__factory,
1010
} from '../../typechain'
1111
import type { LibSwap } from '../../typechain/AcrossFacetV4'
12+
import { EnvironmentEnum } from '../common/types'
1213

1314
import {
14-
ADDRESS_DEV_WALLET_SOLANA_BYTES32,
15-
ADDRESS_DEV_WALLET_V4,
1615
ADDRESS_UNISWAP_ARB,
1716
ADDRESS_UNISWAP_OPT,
1817
ADDRESS_USDC_ARB,
@@ -23,8 +22,10 @@ import {
2322
ADDRESS_WETH_ARB,
2423
ADDRESS_WETH_OPT,
2524
DEFAULT_DEST_PAYLOAD_ABI,
25+
deriveSolanaAddress,
2626
DEV_WALLET_ADDRESS,
2727
ensureBalanceAndAllowanceToDiamond,
28+
getPrivateKeyForEnvironment,
2829
getProvider,
2930
getUniswapDataERC20toExactERC20,
3031
getUniswapDataERC20toExactETH,
@@ -34,6 +35,7 @@ import {
3435
ITransactionTypeEnum,
3536
leftPadAddressToBytes32,
3637
sendTransaction,
38+
solanaAddressToBytes32,
3739
} from './utils/demoScriptHelpers'
3840

3941
// SUCCESSFUL TRANSACTIONS PRODUCED BY THIS SCRIPT ---------------------------------------------------------------------------------------------------
@@ -370,11 +372,6 @@ const WITH_EXCLUSIVE_RELAYER = false
370372
const EXCLUSIVE_RELAYER = '0x07ae8551be970cb1cca11dd7a11f47ae82e70e67' // biggest across relayer
371373
const SRC_CHAIN = 'optimism'
372374
const DIAMOND_ADDRESS_SRC = deploymentsOPT.LiFiDiamond
373-
const RECEIVER_ADDRESS_DST = isSolana
374-
? ADDRESS_DEV_WALLET_SOLANA_BYTES32
375-
: WITH_DEST_CALL
376-
? deploymentsARB.ReceiverAcrossV3
377-
: ADDRESS_DEV_WALLET_V4
378375
const EXPLORER_BASE_URL = 'https://optimistic.etherscan.io/tx/'
379376

380377
// ############################################################################################################
@@ -385,6 +382,15 @@ async function main() {
385382
const walletAddress = await wallet.getAddress()
386383
consola.info('you are using this wallet address: ', walletAddress)
387384

385+
// Receiver on destination: derive Solana from signer key when bridging to Solana, else use config/deployment
386+
const privateKey = getPrivateKeyForEnvironment(EnvironmentEnum.staging)
387+
const solanaReceiverBase58 = isSolana ? deriveSolanaAddress(privateKey) : ''
388+
const receiverAddressDst = isSolana
389+
? solanaAddressToBytes32(solanaReceiverBase58)
390+
: WITH_DEST_CALL
391+
? deploymentsARB.ReceiverAcrossV3
392+
: DEV_WALLET_ADDRESS
393+
388394
// Helper function to format amount with decimals
389395
const formatAmount = (amount: string, isNative: boolean): string => {
390396
if (isNative) {
@@ -416,9 +422,7 @@ async function main() {
416422
consola.info(`🎯 Sending Asset: ${sendingAssetId}`)
417423
consola.info(`📦 Receiving Asset: ${receivingAssetId}`)
418424
consola.info(
419-
`👤 Receiver: ${
420-
isSolana ? 'S5ARSDD3ddZqqqqqb2EUE2h2F1XQHBk7bErRW1WPGe4' : walletAddress
421-
}`
425+
`👤 Receiver: ${isSolana ? solanaReceiverBase58 : walletAddress}`
422426
)
423427
consola.info(`🔄 Transaction Type: ${ITransactionTypeEnum[TRANSACTION_TYPE]}`)
424428
consola.info('')
@@ -454,7 +458,7 @@ async function main() {
454458
const bridgeDataReceiver = isSolana
455459
? NON_EVM_ADDRESS // Use NON_EVM_ADDRESS for Solana
456460
: WITH_DEST_CALL
457-
? RECEIVER_ADDRESS_DST
461+
? receiverAddressDst
458462
: walletAddress
459463

460464
const bridgeData: ILiFi.BridgeDataStruct = {
@@ -615,7 +619,7 @@ async function main() {
615619
fromChainId,
616620
toChainId,
617621
fromAmount,
618-
RECEIVER_ADDRESS_DST, // must be a contract address when a message is provided
622+
receiverAddressDst, // must be a contract address when a message is provided
619623
payload
620624
)
621625

@@ -644,10 +648,10 @@ async function main() {
644648
// prepare AcrossV4Data - note the differences from V3
645649
const acrossV4Data: AcrossFacetV4.AcrossV4DataStruct = {
646650
receiverAddress: isSolana
647-
? ADDRESS_DEV_WALLET_SOLANA_BYTES32 // Use pre-converted Solana bytes32 address
651+
? receiverAddressDst // Derived from signer private key (Solana bytes32)
648652
: leftPadAddressToBytes32(
649653
// For other chains, convert to bytes32
650-
WITH_DEST_CALL ? RECEIVER_ADDRESS_DST : walletAddress
654+
WITH_DEST_CALL ? receiverAddressDst : walletAddress
651655
), // bytes32
652656
refundAddress: leftPadAddressToBytes32(walletAddress),
653657
sendingAssetId: leftPadAddressToBytes32(sendingAssetId),

script/demoScripts/demoEco.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import { randomBytes } from 'crypto'
1414

15-
import { getAssociatedTokenAddress } from '@solana/spl-token'
15+
import { getAssociatedTokenAddressSync } from '@solana/spl-token'
1616
import { Keypair, PublicKey } from '@solana/web3.js'
1717
import { defineCommand, runMain } from 'citty'
1818
import { config } from 'dotenv'
@@ -150,7 +150,7 @@ async function computeSolanaATA(
150150
const mintPublicKey = new PublicKey(tokenMint)
151151

152152
// Compute the Associated Token Account
153-
const ata = await getAssociatedTokenAddress(mintPublicKey, ownerPublicKey)
153+
const ata = getAssociatedTokenAddressSync(mintPublicKey, ownerPublicKey)
154154

155155
// Convert to bytes32 (32 bytes) - take the first 32 bytes of the public key
156156
const ataBytes = ata.toBytes()

script/demoScripts/demoPolymerCCTP.ts

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,32 @@ import {
1414
zeroAddress,
1515
} from 'viem'
1616

17+
import deploymentsARB from '../../deployments/arbitrum.staging.json'
18+
import deploymentsOPT from '../../deployments/optimism.staging.json'
1719
import {
1820
ERC20__factory,
1921
PolymerCCTPFacet__factory,
2022
type ILiFi,
2123
type PolymerCCTPFacet,
2224
} from '../../typechain'
25+
import { EnvironmentEnum } from '../common/types'
2326

2427
import {
25-
ADDRESS_DEV_WALLET_SOLANA_BYTES32,
2628
ADDRESS_USDC_ARB,
2729
ADDRESS_USDC_OPT,
2830
ADDRESS_USDC_SOL,
2931
DEV_WALLET_ADDRESS,
3032
NON_EVM_ADDRESS,
33+
computeSolanaATABytes32,
3134
createContractObject,
35+
deriveSolanaAddress,
3236
ensureAllowance,
3337
ensureBalance,
3438
executeTransaction,
39+
getPrivateKeyForEnvironment,
3540
logBridgeDataStruct,
3641
setupEnvironment,
42+
solanaAddressToBytes32,
3743
} from './utils/demoScriptHelpers'
3844

3945
config()
@@ -53,44 +59,42 @@ config()
5359
// RECEIVE: https://arbiscan.io/tx/0xfb6ac6f8dd9369cff32ffc2c6a166a4252f310ab1253d53fa13960dbee530824
5460
// ---------------------------------------------------------------------------------------------------------------------------------------------------
5561

62+
// ADDITIONAL DEMO TXS
63+
// ARB.USDC > OPT.USDC (fast path):
64+
// SEND: https://arbiscan.io/tx/0x8bece438c05af716fa727a228d5b4f4a0bdc9ab1fe63c820f4864f5def8bea2f
65+
// RECEIVE: https://optimistic.etherscan.io/tx/0x546f37d42364f9613ad702f6340c55935b3de8f7e933b7301a02fcd30ed4709c
66+
// EXPLORER: https://lifi-explorer.polymer.zone/transfer/testnet/0x8bece438c05af716fa727a228d5b4f4a0bdc9ab1fe63c820f4864f5def8bea2f
67+
// ARB.USDC > SOLANA.USDC (fast path):
68+
// SEND: https://arbiscan.io/tx/0x17ae338fce5766f004e51cfe9897c41da7ed56e3ffd40ca8a4d0b6a722f8bc70
69+
// RECEIVE: https://explorer.solana.com/address/AhLkXqoSJR7hYcLcJAdjpCFG6HJUTDBRpvR2uBMfnq63
70+
// EXPLORER: https://lifi-explorer.polymer.zone/transfer/testnet/0x17ae338fce5766f004e51cfe9897c41da7ed56e3ffd40ca8a4d0b6a722f8bc70
71+
5672
// ########################################## CONFIGURE SCRIPT HERE ##########################################
57-
const BRIDGE_TO_SOLANA = false
73+
const BRIDGE_TO_SOLANA = true
5874
const SEND_TX = false // Set to false to dry-run without sending transaction
59-
const USE_FAST_MODE = false // Set to true for fast route (1000), false for standard route (2000)
75+
const USE_FAST_MODE = true // Set to true for fast route (1000), false for standard route (2000)
6076

6177
// Polymer API configuration
62-
// const POLYMER_API_URL = 'https://lifi.devnet.polymer.zone' // testnet API URL
63-
const POLYMER_API_URL = 'https://lifi.shadownet.polymer.zone' // mainnet API URL
78+
const POLYMER_API_URL = 'https://lifi.testnet.polymer.zone' // testnet API URL
6479

6580
// Source chain: 'arbitrum' or 'optimism'
66-
const SRC_CHAIN = 'optimism' as 'arbitrum' | 'optimism'
67-
68-
// in order to test with our staging diamond, the polymer team needs to update their off-chain logic
69-
// to monitor our addresses. So far we tested with their deployments.
70-
// const DIAMOND_ADDRESS_SRC =
71-
// SRC_CHAIN === 'arbitrum'
72-
// ? deploymentsARB.LiFiDiamond
73-
// : deploymentsOPT.LiFiDiamond
74-
75-
// these are test deployments by Polymer team
76-
const LIFI_DIAMOND_ADDRESS_ARB = '0xD99A49304227d3fE2c27A1F12Ef66A95b95837b6'
77-
const LIFI_DIAMOND_ADDRESS_OPT = '0x36d7A6e0B2FE968a9558C5AaF5713aC2DAc0DbFc'
81+
const SRC_CHAIN = 'arbitrum' as 'arbitrum' | 'optimism'
82+
7883
const DIAMOND_ADDRESS_SRC = getAddress(
79-
SRC_CHAIN === 'arbitrum' ? LIFI_DIAMOND_ADDRESS_ARB : LIFI_DIAMOND_ADDRESS_OPT
84+
SRC_CHAIN === 'arbitrum'
85+
? deploymentsARB.LiFiDiamond
86+
: deploymentsOPT.LiFiDiamond
8087
)
8188

82-
const LIFI_CHAIN_ID_SOLANA = 1151111081099710
89+
const LIFI_AND_POLYMER_CHAIN_ID_SOLANA = 1151111081099710
8390
const LIFI_CHAIN_ID_ARBITRUM = 42161
8491
const LIFI_CHAIN_ID_OPTIMISM = 10
8592

86-
// Polymer API chain IDs (different from LiFi chain IDs)
87-
// Note: even though SOLANA's custom chain id in LifiData.sol is 1151111081099710,
88-
// polymer's chain id for solana is 2, so we need to pass in 2 for the polymer endpoint
89-
const POLYMER_CHAIN_ID_SOLANA = 2
90-
9193
const DST_CHAIN_ID_EVM =
9294
SRC_CHAIN === 'arbitrum' ? LIFI_CHAIN_ID_OPTIMISM : LIFI_CHAIN_ID_ARBITRUM
93-
const DST_CHAIN_ID = BRIDGE_TO_SOLANA ? LIFI_CHAIN_ID_SOLANA : DST_CHAIN_ID_EVM
95+
const DST_CHAIN_ID = BRIDGE_TO_SOLANA
96+
? LIFI_AND_POLYMER_CHAIN_ID_SOLANA
97+
: DST_CHAIN_ID_EVM
9498

9599
const sendingAssetId = getAddress(
96100
SRC_CHAIN === 'arbitrum' ? ADDRESS_USDC_ARB : ADDRESS_USDC_OPT
@@ -100,7 +104,6 @@ const fromAmount = parseUnits('1', 6) // 1 USDC (6 decimals)
100104
const receiverAddress = getAddress(
101105
BRIDGE_TO_SOLANA ? NON_EVM_ADDRESS : DEV_WALLET_ADDRESS
102106
)
103-
const solanaReceiverBytes32 = ADDRESS_DEV_WALLET_SOLANA_BYTES32
104107

105108
const EXPLORER_BASE_URL =
106109
SRC_CHAIN === 'arbitrum'
@@ -314,7 +317,7 @@ async function main() {
314317

315318
// Polymer API uses its own chain ID mapping (Solana = 2, not LiFi's 1151111081099710)
316319
const destinationChainIdPolymer = BRIDGE_TO_SOLANA
317-
? POLYMER_CHAIN_ID_SOLANA
320+
? LIFI_AND_POLYMER_CHAIN_ID_SOLANA
318321
: DST_CHAIN_ID
319322

320323
// Get quote from Polymer API
@@ -370,13 +373,32 @@ async function main() {
370373
consola.info('')
371374
logBridgeDataStruct(bridgeData)
372375

376+
// For Solana: derive wallet from same key as EVM signer, then compute USDC ATA (CCTP v2 requires mintRecipient = ATA)
377+
let solanaUserWalletBytes32: `0x${string}` = toHex(0, { size: 32 })
378+
let solanaReceiverATABytes32: `0x${string}` = toHex(0, { size: 32 })
379+
if (BRIDGE_TO_SOLANA) {
380+
const privateKey = getPrivateKeyForEnvironment(EnvironmentEnum.staging)
381+
const solanaBase58 = deriveSolanaAddress(privateKey)
382+
solanaUserWalletBytes32 = solanaAddressToBytes32(solanaBase58)
383+
consola.info(` Solana user wallet (bytes32): ${solanaUserWalletBytes32}`)
384+
solanaReceiverATABytes32 = await computeSolanaATABytes32(
385+
solanaBase58,
386+
ADDRESS_USDC_SOL
387+
)
388+
consola.info(
389+
` Solana USDC ATA (solanaReceiverATA): ${solanaReceiverATABytes32}`
390+
)
391+
consola.info(` Solana user wallet (base58): ${solanaBase58}`)
392+
}
393+
373394
// Prepare PolymerCCTP data using fees extracted from API response
374395
const polymerData: PolymerCCTPFacet.PolymerCCTPDataStruct = {
375396
polymerTokenFee: polymerTokenFee.toString(),
376397
maxCCTPFee: maxCCTPFee.toString(),
377398
nonEVMReceiver: BRIDGE_TO_SOLANA
378-
? toHex(solanaReceiverBytes32)
399+
? solanaUserWalletBytes32
379400
: toHex(0, { size: 32 }),
401+
solanaReceiverATA: solanaReceiverATABytes32,
380402
minFinalityThreshold,
381403
}
382404
consola.info('\n📋 POLYMER CCTP DATA PREPARED:')
@@ -386,7 +408,12 @@ async function main() {
386408
maxCCTPFee === 0n ? '0 = no limit' : 'from API'
387409
})`
388410
)
389-
consola.info(` nonEVMReceiver: ${polymerData.nonEVMReceiver}`)
411+
consola.info(
412+
` nonEVMReceiver (Solana user wallet): ${polymerData.nonEVMReceiver}`
413+
)
414+
consola.info(
415+
` solanaReceiverATA (USDC ATA): ${polymerData.solanaReceiverATA}`
416+
)
390417
consola.info(
391418
` minFinalityThreshold: ${polymerData.minFinalityThreshold} (from API, ${
392419
minFinalityThreshold === 1000 ? 'fast path' : 'standard path'
@@ -401,6 +428,10 @@ async function main() {
401428
walletClient
402429
)
403430

431+
consola.info(`Wallet: ${walletAddress}`)
432+
consola.info(
433+
`Wallet balance: ${await tokenContract.read.balanceOf([walletAddress])}`
434+
)
404435
// Contract transfers minAmount, so user must approve minAmount (fromAmount)
405436
await ensureBalance(tokenContract, walletAddress, fromAmount, publicClient)
406437
await ensureAllowance(

0 commit comments

Comments
 (0)