Skip to content

Commit

Permalink
Add address checks when withdrawing to help users not reduce privacy
Browse files Browse the repository at this point in the history
  • Loading branch information
mds1 committed Mar 19, 2021
1 parent b99e9a8 commit e5c48da
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 12 deletions.
42 changes: 34 additions & 8 deletions frontend/src/components/AccountReceiveTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- Modal to show when warning user of bad privacy hygiene -->
<q-dialog v-model="showPrivacyModal">
<account-receive-table-warning
@acknowledged="showPrivacyModal = false"
@acknowledged="executeWithdraw"
:addressDescription="privacyModalAddressDescription"
class="q-pa-lg"
/>
Expand Down Expand Up @@ -100,7 +100,7 @@
<div>Enter address to withdraw funds to</div>
<base-input
v-model="destinationAddress"
@click="withdraw(props.row)"
@click="initializeWithdraw(props.row)"
appendButtonLabel="Withdraw"
:appendButtonDisable="isWithdrawInProgress"
:appendButtonLoading="isWithdrawInProgress"
Expand Down Expand Up @@ -137,11 +137,11 @@ import useAlerts from 'src/utils/alerts';
import UmbraRelayRecipient from 'src/contracts/umbra-relay-recipient.json';
import AccountReceiveTableWarning from 'components/AccountReceiveTableWarning.vue';
import { SupportedChainIds } from 'components/models';
import { lookupOrFormatAddresses, toAddress } from 'src/utils/address';
import { lookupOrFormatAddresses, toAddress, isAddressSafe } from 'src/utils/address';
import BaseButton from './BaseButton.vue';
function useReceivedFundsTable(announcements: UserAnnouncement[]) {
const { tokens, signer, provider, umbra, spendingKeyPair, domainService } = useWalletStore();
const { tokens, userAddress, signer, provider, umbra, spendingKeyPair, domainService } = useWalletStore();
const { txNotify, notifyUser } = useAlerts();
const paginationConfig = { rowsPerPage: 25 };
const expanded = ref<string[]>([]); // for managing expansion rows
Expand All @@ -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<UserAnnouncement>();
const mainTableColumns = [
{
align: 'left',
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -317,6 +341,7 @@ function useReceivedFundsTable(announcements: UserAnnouncement[]) {
});
} finally {
isWithdrawInProgress.value = false;
activeAnnouncement.value = undefined;
}
}
Expand All @@ -337,7 +362,8 @@ function useReceivedFundsTable(announcements: UserAnnouncement[]) {
formatAmount,
formattedAnnouncements,
destinationAddress,
withdraw,
initializeWithdraw,
executeWithdraw,
};
}
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/components/AccountReceiveTableWarning.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
</q-card-section>

<q-card-section>
It looks like you are withdrawing to {{ addressDescription }}.
<span class="text-bold">This is not recommended</span> 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 }}. <span class="text-bold">This is not recommended</span> unless you
know what you are doing, as this may reduce or entirely remove the privacy guarantees provided by Umbra.
</q-card-section>

<q-card-section>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/css/app.sass
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ p, div
text-align: center

.form
max-width: 500px
max-width: 510px
margin: 0 auto

.horizontal-center
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/utils/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

0 comments on commit e5c48da

Please sign in to comment.