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 ( + +
+

+ You are attempting to deposit funds to the same address{' '} + + {shortenAddress(destinationAddress)} + {' '} + on {networkName}. +

+

+ This is an uncommon action because your smart contract wallet is only + deployed on the currently connected chain. +

+
+ +

+ + I confirm that I have full control over the entered destination + address on {networkName} and understand that proceeding without + control may result in an{' '} + irrecoverable loss of funds. + + } + checked={checkboxChecked} + onChange={setCheckboxChecked} + /> +

+ +

+ If not sure, please reach out to us on our{' '} + + support channel + {' '} + for assistance. +

+
+ ) +} 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} /> + +