diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/CustomDestinationAddressConfirmationDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/CustomDestinationAddressConfirmationDialog.tsx
new file mode 100644
index 0000000000..8e687f0818
--- /dev/null
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/CustomDestinationAddressConfirmationDialog.tsx
@@ -0,0 +1,85 @@
+import { useState } from 'react'
+
+import { Dialog, UseDialogProps } from '../common/Dialog'
+import { ExternalLink } from '../common/ExternalLink'
+import { useNetworks } from '../../hooks/useNetworks'
+import { getExplorerUrl, getNetworkName } from '../../util/networks'
+import { shortenAddress } from '../../util/CommonUtils'
+import { useArbQueryParams } from '../../hooks/useArbQueryParams'
+import { Checkbox } from '../common/Checkbox'
+
+export function CustomDestinationAddressConfirmationDialog(
+ props: UseDialogProps
+) {
+ const [{ destinationAddress = '' }] = useArbQueryParams()
+
+ const [networks] = useNetworks()
+
+ const networkName = getNetworkName(networks.destinationChain.id)
+
+ const [checkboxChecked, setCheckboxChecked] = useState(false)
+
+ function closeWithReset(confirmed: boolean) {
+ props.onClose(confirmed)
+ setCheckboxChecked(false)
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx
index 5710d4ba57..cf2b0f1c9d 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx
@@ -19,6 +19,7 @@ import { useArbQueryParams } from '../../hooks/useArbQueryParams'
import { useDialog } from '../common/Dialog'
import { TokenApprovalDialog } from './TokenApprovalDialog'
import { WithdrawalConfirmationDialog } from './WithdrawalConfirmationDialog'
+import { CustomDestinationAddressConfirmationDialog } from './CustomDestinationAddressConfirmationDialog'
import { TransferPanelSummary } from './TransferPanelSummary'
import { useAppContextActions } from '../App/AppContext'
import { trackEvent } from '../../util/AnalyticsUtils'
@@ -173,6 +174,11 @@ export function TransferPanel() {
openUSDCDepositConfirmationDialog
] = useDialog()
+ const [
+ customDestinationAddressConfirmationDialogProps,
+ openCustomDestinationAddressConfirmationDialog
+ ] = useDialog()
+
const isCustomDestinationTransfer = !!latestDestinationAddress.current
const {
@@ -218,6 +224,13 @@ export function TransferPanel() {
return isDepositMode && isUnbridgedToken
}, [isDepositMode, selectedToken])
+ const areSenderAndCustomDestinationAddressesEqual = useMemo(() => {
+ return (
+ destinationAddress?.trim()?.toLowerCase() ===
+ walletAddress?.trim().toLowerCase()
+ )
+ }, [destinationAddress, walletAddress])
+
async function depositToken() {
if (!selectedToken) {
throw new Error('Invalid app state: no selected token')
@@ -335,6 +348,12 @@ export function TransferPanel() {
setShowSmartContractWalletTooltip(true)
}, 3000)
+ const confirmCustomDestinationAddressForSCWallets = async () => {
+ const waitForInput = openCustomDestinationAddressConfirmationDialog()
+ const [confirmed] = await waitForInput()
+ return confirmed
+ }
+
const transferCctp = async () => {
if (!selectedToken) {
return
@@ -371,6 +390,17 @@ export function TransferPanel() {
if (!withdrawalConfirmation) return
}
+ // confirm if the user is certain about the custom destination address, especially if it matches the connected SCW address.
+ // this ensures that user funds do not end up in the destination chain’s address that matches their source-chain wallet address, which they may not control.
+ if (
+ isSmartContractWallet &&
+ isDepositMode &&
+ areSenderAndCustomDestinationAddressesEqual
+ ) {
+ const confirmation = await confirmCustomDestinationAddressForSCWallets()
+ if (!confirmation) return false
+ }
+
const cctpTransferStarter = new CctpTransferStarter({
sourceChainProvider,
destinationChainProvider
@@ -560,12 +590,11 @@ export function TransferPanel() {
destinationChainErc20Address
})
- const { isNativeCurrencyTransfer, isWithdrawal } =
- getBridgeTransferProperties({
- sourceChainId,
- sourceChainErc20Address,
- destinationChainId
- })
+ const { isWithdrawal } = getBridgeTransferProperties({
+ sourceChainId,
+ sourceChainErc20Address,
+ destinationChainId
+ })
if (isWithdrawal && selectedToken && !sourceChainErc20Address) {
/*
@@ -588,6 +617,17 @@ export function TransferPanel() {
const destinationAddress = latestDestinationAddress.current
+ // confirm if the user is certain about the custom destination address, especially if it matches the connected SCW address.
+ // this ensures that user funds do not end up in the destination chain’s address that matches their source-chain wallet address, which they may not control.
+ if (
+ isSmartContractWallet &&
+ isDepositMode &&
+ areSenderAndCustomDestinationAddressesEqual
+ ) {
+ const confirmation = await confirmCustomDestinationAddressForSCWallets()
+ if (!confirmation) return false
+ }
+
const isCustomNativeTokenAmount2 =
nativeCurrency.isCustom &&
isBatchTransferSupported &&
@@ -971,6 +1011,10 @@ export function TransferPanel() {
amount={amount}
/>
+