Skip to content

Commit

Permalink
Use high risk policy preset when creating account
Browse files Browse the repository at this point in the history
  • Loading branch information
hbriese committed Nov 9, 2023
1 parent d538954 commit 7226147
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 107 deletions.
33 changes: 12 additions & 21 deletions app/src/app/(drawer)/accounts/create.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useRouter } from 'expo-router';
import { gql } from '@api/generated';
import { useApproverAddress } from '@network/useApprover';
import { useForm } from 'react-hook-form';
import { View } from 'react-native';
import { useMutation } from 'urql';
Expand All @@ -15,6 +14,8 @@ import { Address } from 'lib';
import { Text } from 'react-native-paper';
import { AccountNameFormField } from '~/components/fields/AccountNameFormField';
import { createStyles } from '@theme/styles';
import { usePolicyPresets } from '~/lib/policy/presets';
import { asPolicyInput } from '~/lib/policy/draft';

const Create = gql(/* GraphQL */ `
mutation CreateAccountScreen_Create($input: CreateAccountInput!) {
Expand All @@ -35,8 +36,8 @@ export interface CreateAccountScreenProps {

function CreateAccountScreen({ onCreate }: CreateAccountScreenProps) {
const router = useRouter();
const approver = useApproverAddress();
const create = useMutation(Create)[1];
const presets = usePolicyPresets(undefined);

const { control, handleSubmit } = useForm<Inputs>({
defaultValues: { label: '' },
Expand All @@ -60,26 +61,16 @@ function CreateAccountScreen({ onCreate }: CreateAccountScreenProps) {
style={styles.button}
control={control}
onPress={handleSubmit(async ({ label }) => {
try {
const account = (
await create({
input: {
label,
policies: [{ name: 'High risk', approvers: [approver] }],
},
})
).data?.createAccount;
const r = await create({ input: { label, policies: [asPolicyInput(presets.high)] } });

if (onCreate) {
onCreate(account!.address);
} else {
router.push({
pathname: `/(drawer)/[account]/(home)/`,
params: { account: account!.address },
});
}
} catch (error) {
showError('Failed to create account', { event: { error } });
const account = r.data?.createAccount.address;
if (!account)
return showError('Failed to create account', { event: { error: r.error } });

if (onCreate) {
onCreate(account);
} else {
router.push({ pathname: `/(drawer)/[account]/(home)/`, params: { account } });
}
})}
>
Expand Down
8 changes: 4 additions & 4 deletions app/src/components/policy/ActionsSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AddIcon, CustomActionIcon, materialIcon } from '@theme/icons';
import { CustomActionIcon, materialIcon } from '@theme/icons';
import { createStyles } from '@theme/styles';
import { Divider, Switch } from 'react-native-paper';
import Collapsible from 'react-native-collapsible';
Expand All @@ -8,7 +8,7 @@ import { ListItemHorizontalTrailing } from '~/components/list/ListItemHorizontal
import { ListItemTrailingText } from '~/components/list/ListItemTrailingText';
import { useToggle } from '~/hooks/useToggle';
import { PolicyDraftAction, usePolicyDraftState } from '~/lib/policy/draft';
import { getActionPresets } from '~/lib/policy/presets';
import { ACTION_PRESETS } from '~/lib/policy/presets';

export const INTERNAL_ACTION_LABEL_PREFIX = '__app__: ';

Expand All @@ -21,10 +21,10 @@ export interface ActionsSettingsProps {
}

export function ActionsSettings(props: ActionsSettingsProps) {
const [{ account, actions }, update] = usePolicyDraftState();
const [{ actions }, update] = usePolicyDraftState();
const [expanded, toggleExpanded] = useToggle(props.initiallyExpanded);

const actionPresets = Object.values(getActionPresets(account));
const actionPresets = Object.values(ACTION_PRESETS);

const defaultAllow = actions.find(isDefaultAllowAction)?.allow;
const allowedExplicitActions = actions.filter((a) => a.allow && !isDefaultAllowAction(a)).length;
Expand Down
6 changes: 3 additions & 3 deletions app/src/components/policy/PolicySuggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { useState } from 'react';
import { View } from 'react-native';
import { Chip, Text } from 'react-native-paper';
import { POLICY_DRAFT_ATOM } from '~/lib/policy/draft';
import { getPolicyPresets } from '~/lib/policy/presets';
import { usePolicyPresets } from '~/lib/policy/presets';

const Account = gql(/* GraphQL */ `
fragment PolicySuggestions_Account on Account {
id
...getPolicyTemplate_Account
...getPolicyPresets_Account
}
`);

Expand All @@ -20,7 +20,7 @@ export interface PolicySuggestionsProps {

export function PolicySuggestions(props: PolicySuggestionsProps) {
const account = useFragment(Account, props.account);
const presets = getPolicyPresets(account);
const presets = usePolicyPresets(account);

const policy = useAtomValue(POLICY_DRAFT_ATOM);
const setDraft = useSetAtom(POLICY_DRAFT_ATOM);
Expand Down
14 changes: 4 additions & 10 deletions app/src/hooks/useHydratePolicyDraft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { useHydrateAtoms } from 'jotai/utils';
import { ZERO_ADDR } from 'lib';
import { useEffect, useMemo } from 'react';
import { POLICY_DRAFT_ATOM, PolicyDraft, policyAsDraft } from '~/lib/policy/draft';
import { getPolicyPresets } from '~/lib/policy/presets';
import { usePolicyPresets } from '~/lib/policy/presets';

const Account = gql(/* GraphQL */ `
fragment useHydratePolicyDraft_Account on Account {
id
address
...getPolicyTemplate_Account
...getPolicyPresets_Account
}
`);

Expand Down Expand Up @@ -39,6 +39,7 @@ export interface UseHydratePolicyDraftParams {
export function useHydratePolicyDraft(params: UseHydratePolicyDraftParams) {
const account = useFragment(Account, params.account);
const policy = useFragment(Policy, params.policy);
const presets = usePolicyPresets(account);

const init = useMemo(
(): PolicyDraft => ({
Expand All @@ -48,14 +49,7 @@ export function useHydratePolicyDraft(params: UseHydratePolicyDraftParams) {
...((params.view === 'state' && policy?.state && policyAsDraft(policy.state)) ||
(policy?.draft && policyAsDraft(policy.draft)) ||
(policy?.state && policyAsDraft(policy.state)) ||
(account
? getPolicyPresets(account).high
: {
approvers: new Set(),
threshold: 0,
actions: [],
transfers: { defaultAllow: false, limits: {} },
})),
presets.low),
}),
[account, policy, params.view],
);
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/policy/draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function policyAsDraft(
};
}

export function asPolicyInput(p: PolicyDraft): PolicyInput {
export function asPolicyInput(p: Omit<PolicyDraft, 'account'>): PolicyInput {
return {
key: p.key,
name: p.name,
Expand Down
159 changes: 91 additions & 68 deletions app/src/lib/policy/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,32 @@ import _ from 'lodash';
import { FC } from 'react';
import { getAbiItem, getFunctionSelector } from 'viem';
import { SYNCSWAP_ROUTER } from '~/util/swap/syncswap/contracts';
import { useApproverAddress } from '@network/useApprover';

export const getActionPresets = (account: Address) =>
({
all: {
icon: materialIcon('circle'),
label: 'Anything else',
functions: [{}],
},
transferNfts: {
icon: imageFromSource(require('assets/ENS.svg')),
label: 'Transfer NFTs',
functions: [
getAbiItem({ abi: ERC721_ABI, name: 'safeTransferFrom', args: ['0x', '0x', 0n] }),
getAbiItem({ abi: ERC721_ABI, name: 'safeTransferFrom', args: ['0x', '0x', 0n, '0x'] }),
getAbiItem({ abi: ERC721_ABI, name: 'transferFrom' }),
getAbiItem({ abi: ERC721_ABI, name: 'approve' }),
getAbiItem({ abi: ERC721_ABI, name: 'setApprovalForAll' }),
].map((f) => ({ selector: asSelector(getFunctionSelector(f)) })),
},
manageAccount: {
icon: AccountIcon,
label: 'Manage account',
functions: [
type ActionDefinition = Omit<PolicyDraftAction, 'allow'> & { icon?: FC<IconProps> };

export const ACTION_PRESETS = {
all: {
icon: materialIcon('circle'),
label: 'Anything else',
functions: [{}],
},
transferNfts: {
icon: imageFromSource(require('assets/ENS.svg')),
label: 'Transfer NFTs',
functions: [
getAbiItem({ abi: ERC721_ABI, name: 'safeTransferFrom', args: ['0x', '0x', 0n] }),
getAbiItem({ abi: ERC721_ABI, name: 'safeTransferFrom', args: ['0x', '0x', 0n, '0x'] }),
getAbiItem({ abi: ERC721_ABI, name: 'transferFrom' }),
getAbiItem({ abi: ERC721_ABI, name: 'approve' }),
getAbiItem({ abi: ERC721_ABI, name: 'setApprovalForAll' }),
].map((f) => ({ selector: asSelector(getFunctionSelector(f)) })),
},
manageAccount: {
icon: AccountIcon,
label: 'Manage account',
functions: (account: Address) =>
[
getAbiItem({ abi: ACCOUNT_ABI, name: 'addPolicy' }),
getAbiItem({ abi: ACCOUNT_ABI, name: 'removePolicy' }),
getAbiItem({ abi: ACCOUNT_ABI, name: 'upgradeTo' }),
Expand All @@ -44,39 +47,45 @@ export const getActionPresets = (account: Address) =>
contract: account,
selector: asSelector(getFunctionSelector(f)),
})),
},
syncswapSwap: {
icon: SwapIcon,
label: 'Swap (SyncSwap)',
functions: [
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'swap' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'swapWithPermit' }),
].map((f) => ({
contract: SYNCSWAP_ROUTER.address,
selector: asSelector(getFunctionSelector(f)),
})),
},
syncswapLiquidity: {
icon: materialCommunityIcon('water'),
label: 'Manage liquidity (SyncSwap)',
functions: [
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidity' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidity2' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidityWithPermit' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidityWithPermit2' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquidity' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquiditySingle' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquiditySingleWithPermit' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquidityWithPermit' }),
].map((f) => ({
contract: SYNCSWAP_ROUTER.address,
selector: asSelector(getFunctionSelector(f)),
})),
},
}) satisfies Record<string, Omit<PolicyDraftAction, 'allow'> & { icon?: FC<IconProps> }>;
},
syncswapSwap: {
icon: SwapIcon,
label: 'Swap (SyncSwap)',
functions: [
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'swap' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'swapWithPermit' }),
].map((f) => ({
contract: SYNCSWAP_ROUTER.address,
selector: asSelector(getFunctionSelector(f)),
})),
},
syncswapLiquidity: {
icon: materialCommunityIcon('water'),
label: 'Manage liquidity (SyncSwap)',
functions: [
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidity' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidity2' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidityWithPermit' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidityWithPermit2' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquidity' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquiditySingle' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquiditySingleWithPermit' }),
getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquidityWithPermit' }),
].map((f) => ({
contract: SYNCSWAP_ROUTER.address,
selector: asSelector(getFunctionSelector(f)),
})),
},
} satisfies Record<
string,
| ActionDefinition
| (Omit<ActionDefinition, 'functions'> & {
functions: (...params: any[]) => ActionDefinition['functions'];
})
>;

const Account = gql(/* GraphQL */ `
fragment getPolicyTemplate_Account on Account {
fragment getPolicyPresets_Account on Account {
id
address
approvers {
Expand All @@ -86,44 +95,58 @@ const Account = gql(/* GraphQL */ `
}
`);

export function getPolicyPresets(accountFragment: FragmentType<typeof Account>) {
export function usePolicyPresets(accountFragment: FragmentType<typeof Account> | null | undefined) {
const account = getFragment(Account, accountFragment);
const actions = getActionPresets(account.address);
const approver = useApproverAddress();

const approvers = new Set(account.approvers.map((a) => a.address));
const approvers = new Set([approver, ...(account?.approvers.map((a) => a.address) ?? [])]);

return {
low: {
name: 'Low risk',
approvers,
threshold: 1,
actions: [
{ ...actions.syncswapSwap, allow: true },
{ ...actions.all, allow: false },
],
account && {
...ACTION_PRESETS.manageAccount,
functions: ACTION_PRESETS.manageAccount.functions(account.address),
allow: false,
},
{ ...ACTION_PRESETS.syncswapSwap, allow: true },
{ ...ACTION_PRESETS.all, allow: false },
].filter(Boolean),
transfers: { defaultAllow: false, limits: {} }, // TODO: allow transfers up to $x
},
medium: {
name: 'Medium risk',
approvers,
threshold: Math.max(approvers.size > 3 ? 3 : 2, approvers.size),
actions: [
{ ...actions.syncswapSwap, allow: true },
{ ...actions.syncswapLiquidity, allow: true },
{ ...actions.all, allow: true },
],
account && {
...ACTION_PRESETS.manageAccount,
functions: ACTION_PRESETS.manageAccount.functions(account.address),
allow: false,
},
{ ...ACTION_PRESETS.syncswapSwap, allow: true },
{ ...ACTION_PRESETS.syncswapLiquidity, allow: true },
{ ...ACTION_PRESETS.all, allow: true },
].filter(Boolean),
transfers: { defaultAllow: false, limits: {} }, // TODO: allow transfers up to $y
},
high: {
name: 'High risk',
approvers,
threshold: _.clamp(approvers.size - 2, 1, 5),
actions: [
{ ...actions.manageAccount, allow: true },
{ ...actions.syncswapSwap, allow: true },
{ ...actions.syncswapLiquidity, allow: true },
{ ...actions.all, allow: true },
],
account && {
...ACTION_PRESETS.manageAccount,
functions: ACTION_PRESETS.manageAccount.functions(account.address),
allow: true,
},
{ ...ACTION_PRESETS.syncswapSwap, allow: true },
{ ...ACTION_PRESETS.syncswapLiquidity, allow: true },
{ ...ACTION_PRESETS.all, allow: true },
].filter(Boolean),
transfers: { defaultAllow: true, limits: {} },
},
} satisfies Record<string, Omit<PolicyDraft, 'account' | 'key'>>;
Expand Down

0 comments on commit 7226147

Please sign in to comment.