From e5c48da5987aaaf78a72c415a054b72c7163e12f Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Fri, 19 Mar 2021 05:37:06 -0700 Subject: [PATCH] Add address checks when withdrawing to help users not reduce privacy --- .../src/components/AccountReceiveTable.vue | 42 ++++++++++++++---- .../components/AccountReceiveTableWarning.vue | 5 +-- frontend/src/css/app.sass | 2 +- frontend/src/utils/address.ts | 43 +++++++++++++++++++ 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/AccountReceiveTable.vue b/frontend/src/components/AccountReceiveTable.vue index 760b3eecb..522cd8acc 100644 --- a/frontend/src/components/AccountReceiveTable.vue +++ b/frontend/src/components/AccountReceiveTable.vue @@ -3,7 +3,7 @@ @@ -100,7 +100,7 @@
Enter address to withdraw funds to
([]); // for managing expansion rows @@ -150,6 +150,7 @@ function useReceivedFundsTable(announcements: UserAnnouncement[]) { const privacyModalAddressDescription = ref('a wallet that may be publicly associated with you'); const destinationAddress = ref(''); const isWithdrawInProgress = ref(false); + const activeAnnouncement = ref(); const mainTableColumns = [ { align: 'left', @@ -238,13 +239,36 @@ function useReceivedFundsTable(announcements: UserAnnouncement[]) { } /** - * @notice Withdraw funds from stealth address + * @notice Initialize the withdraw process * @param announcement Announcement to withdraw */ - async function withdraw(announcement: UserAnnouncement) { + async function initializeWithdraw(announcement: UserAnnouncement) { + // Check if withdrawal destination is safea + activeAnnouncement.value = announcement; + const { safe, reason } = await isAddressSafe( + destinationAddress.value, + userAddress.value as string, + domainService.value as DomainService + ); + + if (safe) { + await executeWithdraw(); + } else { + showPrivacyModal.value = true; + privacyModalAddressDescription.value = reason; + } + } + + /** + * @notice Executes the withdraw process + */ + async function executeWithdraw() { if (!umbra.value) throw new Error('Umbra instance not found'); + if (!activeAnnouncement.value) throw new Error('No announcement is selected for withdraw'); + showPrivacyModal.value = false; - // Get token info and stealth private key, and resolve ENS/CNS names to their address + // Get token info, stealth private key, and destination (acceptor) address + const announcement = activeAnnouncement.value; const token = getTokenInfo(announcement.token); const stealthKeyPair = (spendingKeyPair.value as KeyPair).mulPrivateKey(announcement.randomNumber); const spendingPrivateKey = stealthKeyPair.privateKeyHex as string; @@ -317,6 +341,7 @@ function useReceivedFundsTable(announcements: UserAnnouncement[]) { }); } finally { isWithdrawInProgress.value = false; + activeAnnouncement.value = undefined; } } @@ -337,7 +362,8 @@ function useReceivedFundsTable(announcements: UserAnnouncement[]) { formatAmount, formattedAnnouncements, destinationAddress, - withdraw, + initializeWithdraw, + executeWithdraw, }; } diff --git a/frontend/src/components/AccountReceiveTableWarning.vue b/frontend/src/components/AccountReceiveTableWarning.vue index f7a096261..66b44702c 100644 --- a/frontend/src/components/AccountReceiveTableWarning.vue +++ b/frontend/src/components/AccountReceiveTableWarning.vue @@ -7,9 +7,8 @@ - It looks like you are withdrawing to {{ addressDescription }}. - This is not recommended unless you know what you are doing, as this may reduce or - entirely remove the privacy guarantees provided by Umbra. + You are withdrawing to {{ addressDescription }}. This is not recommended unless you + know what you are doing, as this may reduce or entirely remove the privacy guarantees provided by Umbra. diff --git a/frontend/src/css/app.sass b/frontend/src/css/app.sass index 721f6801e..2c5d8537f 100644 --- a/frontend/src/css/app.sass +++ b/frontend/src/css/app.sass @@ -87,7 +87,7 @@ p, div text-align: center .form - max-width: 500px + max-width: 510px margin: 0 auto .horizontal-center diff --git a/frontend/src/utils/address.ts b/frontend/src/utils/address.ts index cb05fa4cd..f6c2c9e1b 100644 --- a/frontend/src/utils/address.ts +++ b/frontend/src/utils/address.ts @@ -87,3 +87,46 @@ export const lookupOrFormatAddresses = async (addresses: string[], provider: Pro const promises = addresses.map((address) => lookupOrFormatAddress(address, provider)); return Promise.all(promises); }; + +// ================================================== Privacy Checks =================================================== + +// Checks for any potential risks of withdrawing to the provided name or address, returns object containing +// a true/false judgement about risk, and a short description string +export const isAddressSafe = async (name: string, userAddress: string, domainService: DomainService) => { + userAddress = getAddress(userAddress); + + // Check if we're withdrawing to an ENS or CNS name + if (ens.isEnsDomain(name)) return { safe: false, reason: `${name}, which is a publicly viewable ENS name` }; + if (cns.isCnsDomain(name)) return { safe: false, reason: `${name}, which is a publicly viewable CNS name` }; + + // We aren't withdrawing to a domain, so let's get the checksummed address. + const address = getAddress(name); + const provider = domainService.provider as Provider; + + // Check if address resolves to an ENS name + const ensName = await lookupEnsName(address, provider); + if (ensName) return { safe: false, reason: `an address that resolves to the publicly viewable ENS name ${ensName}` }; + + // Check if address owns a CNS name + const cnsName = await lookupCnsName(address, provider); + if (cnsName) return { safe: false, reason: `an address that resolves to the publicly viewable CNS name ${cnsName}` }; + + // Check if address is the wallet user is logged in with + if (address === userAddress) return { safe: false, reason: 'the same address as the connected wallet' }; + + // Check if address owns any POAPs + if (await hasPOAPs(userAddress)) return { safe: false, reason: 'an address that has POAP tokens' }; + + // Check if address has contributed to Gitcoin Grants + // TODO + + return { safe: true, reason: '' }; +}; + +const jsonFetch = (url: string) => fetch(url).then((res) => res.json()); + +// Returns true if the address owns any POAP tokens +const hasPOAPs = async (address: string) => { + const poaps = await jsonFetch(`https://api.poap.xyz/actions/scan/${address}`); + return poaps.length > 0 ? true : false; +};