Skip to content

Commit

Permalink
Centrifuge App: Operational Security for Pools (#1313)
Browse files Browse the repository at this point in the history
  • Loading branch information
onnovisser authored May 31, 2023
1 parent c7dbac4 commit 0a7ce8f
Show file tree
Hide file tree
Showing 97 changed files with 3,408 additions and 1,434 deletions.
64 changes: 63 additions & 1 deletion .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ It's also recommended to run Prettier automatically in your editor, e.g. using [
### Frontend

1. Create pools for initial data
2. Create proxy for POD authentication

### Faucet

Expand All @@ -38,6 +37,69 @@ It's also recommended to run Prettier automatically in your editor, e.g. using [
d. Fund both the proxy and the controlling account
e. In each pool give the pure proxy whitelisting permission - this can only be done by the pool admin

### Asset Originator POD Access

When setting up an Asset Originator for a pool, the account on the POD needs to be manually created

1. Create AO on the Access tab of the Issuers Pool page
2. Copy the address of the newly created AO proxy
3. Get a jw3t auth token. Needs to be signed as Eve on behalf of Eve with proxy type `PodAdmin`. Example token: `ewogImFsZ29yaXRobSI6ICJzcjI1NTE5IiwKICJ0b2tlbl90eXBlIjogIkpXM1QiLAogImFkZHJlc3NfdHlwZSI6ICJzczU4Igp9.ewogImFkZHJlc3MiOiAiNUhHaldBZUZEZkZDV1BzakZRZFZWMk1zdnoyWHRNa3R2Z29jRVpjQ2o2OGtVTWF3IiwKICJpc3N1ZWRfYXQiOiAiMTY4MTk5Mzc4MCIsCiAiZXhwaXJlc19hdCI6ICIxOTk3MzUzNzgwIiwKICJvbl9iZWhhbGZfb2YiOiAiNUhHaldBZUZEZkZDV1BzakZRZFZWMk1zdnoyWHRNa3R2Z29jRVpjQ2o2OGtVTWF3IiwKICJub3RfYmVmb3JlIjogIjE2ODE5OTM3ODAiLAogInByb3h5X3R5cGUiOiAiUG9kQWRtaW4iCn0.-BJ7Y6WurKYwesCMfkTrudsH5ZVseMviVNdZ0kFZmEnAtAYvdxqxN56aVwRR5QvEjK8Of4TVtY_-oPK4hP7Dhg`

A token can be created with the code below:

```js
const keyring = new Keyring({ type: 'sr25519' })
const EveKeyRing = keyring.addFromUri('//Eve')

const centrifuge = new Centrifuge({
polkadotWsUrl: 'wss://fullnode-relay.development.cntrfg.com',
centrifugeWsUrl: 'wss://fullnode.development.cntrfg.com',
signingAddress: AliceKeyRing,
})

const token = await centrifuge.auth.generateJw3t(EveKeyRing, undefined, {
onBehalfOf: EveKeyRing.address,
proxyType: 'PodAdmin',
expiresAt: String(Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365 * 10), // 10 years
})
```

`PodAdmin` is a special proxy type that only exists on the POD and not on-chain.

4. Call `https://pod.development.cntrfg.com/v2/accounts/generate` (More details about the request here: https://app.swaggerhub.com/apis/centrifuge.io/cent-node/2.1.0#/Accounts/generate_account_v2) with the token in the Authorization header. Example below:

```bash
curl --request POST \
--url https://pod.development.cntrfg.com/v2/accounts/generate \
--header 'Authorization: Bearer ewogImFsZ29yaXRobSI6ICJzcjI1NTE5IiwKICJ0b2tlbl90eXBlIjogIkpXM1QiLAogImFkZHJlc3NfdHlwZSI6ICJzczU4Igp9.ewogImFkZHJlc3MiOiAiNUhHaldBZUZEZkZDV1BzakZRZFZWMk1zdnoyWHRNa3R2Z29jRVpjQ2o2OGtVTWF3IiwKICJpc3N1ZWRfYXQiOiAiMTY4MTIwNjk4NCIsCiAiZXhwaXJlc19hdCI6ICIxNjgzNzk4OTg0IiwKICJvbl9iZWhhbGZfb2YiOiAiNUhHaldBZUZEZkZDV1BzakZRZFZWMk1zdnoyWHRNa3R2Z29jRVpjQ2o2OGtVTWF3IiwKICJub3RfYmVmb3JlIjogIjE2ODEyMDY5ODQiLAogInByb3h5X3R5cGUiOiAiUG9kQWRtaW4iCn0.oLovvmVzXJRz-eY1V0wHFNdF6HnVa1unx684xEoMhgBOdCyV8I4yZvUjMx4qLK1vj9Oeh42dAmJ5_vAti9D4jQ' \
--header 'Content-Type: application/json' \
--data '{
"account": {
"identity": "0x3fe43572af486a48cf27e038fd42a2657cd8495c5f4f1a5553833135eb75b316",
"precommit_enabled": true,
"webhook_url": "https://centrifuge.io"
}
}'
```

`identity` is the hex formatted address for the account you want to create.
The response will look something like:

```json
{
"identity": "0x3fe43572af486a48cf27e038fd42a2657cd8495c5f4f1a5553833135eb75b316",
"webhook_url": "https://centrifuge.io",
"precommit_enabled": true,
"document_signing_public_key": "0x85d46bae1577ead77f00931fc63618e2587486d8c95dc7fc8637a63fde0668ed",
"p2p_public_signing_key": "0xafed109165d041b83f2a42a8863a28277e0fa35e900e9544d0c46e2e2772b488",
"pod_operator_account_id": "0x1cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c"
}
```

5. Copy `document_signing_public_key`, `p2p_public_signing_key` and `pod_operator_account_id` of the returned result and paste those in the AO section on the Access tab
6. Add hot wallets to the AO and submit the form
7. If successful, the hot wallets should now be able to authenticate with the POD and be able to create assets.

## Notes

To add other repositories to this monorepo while preserving the Git history, we can use the following steps: https://medium.com/@filipenevola/how-to-migrate-to-mono-repository-without-losing-any-git-history-7a4d80aa7de2
Expand Down
2 changes: 1 addition & 1 deletion centrifuge-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@ethersproject/units": "^5.6.0",
"@makerdao/multicall": "^0.12.0",
"@polkadot/extension-dapp": "^0.44.1",
"@polkadot/react-identicon": "^2.8.2",
"@polkadot/react-identicon": "^3.1.4",
"@stablelib/blake2b": "^1.0.1",
"@styled-system/css": "^5.1.5",
"@styled-system/should-forward-prop": "^5.1.5",
Expand Down
11 changes: 5 additions & 6 deletions centrifuge-app/src/components/BuyDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import { useBalances, useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import { Button, Dialog, Shelf, Stack, Text } from '@centrifuge/fabric'
import BN from 'bn.js'
import * as React from 'react'
import { Dec } from '../utils/Decimal'
import { useAddress } from '../utils/useAddress'
import { useBalance } from '../utils/useBalance'
import { useCentNFT } from '../utils/useNFTs'
import { ButtonGroup } from './ButtonGroup'

Expand All @@ -19,7 +18,7 @@ const TRANSFER_FEE_ESTIMATE = 0.1

export const BuyDialog: React.FC<Props> = ({ open, onClose, collectionId, nftId }) => {
const address = useAddress('substrate')
const balance = useBalance()
const balances = useBalances(address)
const nft = useCentNFT(collectionId, nftId)

const isConnected = !!address
Expand Down Expand Up @@ -55,14 +54,14 @@ export const BuyDialog: React.FC<Props> = ({ open, onClose, collectionId, nftId
}

const priceDec = Dec(nft?.sellPrice ?? 0).div('1e18')
const balanceDec = Dec(balance ?? 0)
const balanceDec = balances?.native.balance.toDecimal() ?? Dec(0)

const balanceLow = balanceDec.lt(priceDec.add(Dec(TRANSFER_FEE_ESTIMATE)))

const disabled = balanceLow || !nft

function getMessage() {
if (balance == null) return
if (balances == null) return
if (balanceDec.lt(priceDec)) return 'Insufficient funds to purchase this NFT'
if (balanceLow) return 'Insufficient funds to pay for transaction costs'
}
Expand All @@ -82,7 +81,7 @@ export const BuyDialog: React.FC<Props> = ({ open, onClose, collectionId, nftId
<Text variant="heading1" fontWeight={400}>
{nft?.sellPrice && `${formatPrice(priceDec.toNumber())} AIR`}
</Text>
{balance != null && <Text variant="label2">{formatPrice(balance)} AIR balance</Text>}
{balances != null && <Text variant="label2">{formatPrice(balanceDec.toNumber())} AIR balance</Text>}
</Stack>
</Shelf>
</Stack>
Expand Down
12 changes: 10 additions & 2 deletions centrifuge-app/src/components/DebugFlags/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export type Key =
| 'editPoolConfig'
| 'poolReporting'
| 'editPoolVisibility'
| 'showAdvancedAccounts'
| 'editAdminConfig'

export const flagsConfig: Record<Key, DebugFlagConfig> = {
address: {
Expand All @@ -52,12 +54,14 @@ export const flagsConfig: Record<Key, DebugFlagConfig> = {
editPoolConfig: {
type: 'checkbox',
default: false,
alwaysShow: true,
},
editAdminConfig: {
type: 'checkbox',
default: false,
},
poolReporting: {
type: 'checkbox',
default: false,
alwaysShow: true,
},
persistDebugFlags: {
type: 'checkbox',
Expand All @@ -71,6 +75,10 @@ export const flagsConfig: Record<Key, DebugFlagConfig> = {
editPoolVisibility: {
type: 'checkbox',
default: false,
},
showAdvancedAccounts: {
type: 'checkbox',
default: false,
alwaysShow: true,
},
}
17 changes: 11 additions & 6 deletions centrifuge-app/src/components/Dialogs/CreateCollectionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CollectionMetadataInput } from '@centrifuge/centrifuge-js/dist/modules/
import {
ConnectionGuard,
useAsyncCallback,
useBalances,
useCentrifuge,
useCentrifugeTransaction,
useWallet,
Expand All @@ -22,8 +23,9 @@ import * as React from 'react'
import { Redirect } from 'react-router'
import { lastValueFrom } from 'rxjs'
import { collectionMetadataSchema } from '../../schemas'
import { Dec } from '../../utils/Decimal'
import { getFileDataURI } from '../../utils/getFileDataURI'
import { useBalance } from '../../utils/useBalance'
import { useAddress } from '../../utils/useAddress'
import { ButtonGroup } from '../ButtonGroup'

// TODO: replace with better fee estimate
Expand All @@ -38,7 +40,8 @@ export const CreateCollectionDialog: React.FC<{ open: boolean; onClose: () => vo
const [description, setDescription] = React.useState<string>('')
const [logo, setLogo] = React.useState<File | null>(null)
const cent = useCentrifuge()
const balance = useBalance()
const address = useAddress('substrate')
const balances = useBalances(address)
const [redirect, setRedirect] = React.useState<string>('')
const [confirmOpen, setConfirmOpen] = React.useState(false)
const [termsAccepted, setTermsAccepted] = React.useState(false)
Expand Down Expand Up @@ -70,15 +73,16 @@ export const CreateCollectionDialog: React.FC<{ open: boolean; onClose: () => vo
const collectionId = await cent.nfts.getAvailableCollectionId()

let fileDataUri
let imageMetadataHash
if (logo) {
fileDataUri = await getFileDataURI(logo)
imageMetadataHash = await lastValueFrom(cent.metadata.pinFile(fileDataUri))
}

const imageMetadataHash = await lastValueFrom(cent.metadata.pinFile(fileDataUri))
const metadataValues: CollectionMetadataInput = {
name: nameValue,
description: descriptionValue,
image: imageMetadataHash.ipfsHash,
image: imageMetadataHash?.ipfsHash,
}

doTransaction([collectionId, substrate.selectedAccount!.address, metadataValues])
Expand Down Expand Up @@ -106,7 +110,8 @@ export const CreateCollectionDialog: React.FC<{ open: boolean; onClose: () => vo
onClose()
}

const balanceLow = !balance || balance < CREATE_FEE_ESTIMATE
const balanceDec = balances?.native.balance.toDecimal() ?? Dec(0)
const balanceLow = balanceDec.lt(CREATE_FEE_ESTIMATE)
const isTxPending = metadataIsUploading || transactionIsPending

const fieldDisabled = !isConnected || balanceLow || isTxPending
Expand Down Expand Up @@ -156,7 +161,7 @@ export const CreateCollectionDialog: React.FC<{ open: boolean; onClose: () => vo
<Shelf justifyContent="space-between">
{balanceLow && (
<Text variant="label1" color="criticalForeground">
Your balance is too low ({(balance || 0).toFixed(2)} AIR)
Your balance is too low ({(balanceDec || 0).toFixed(2)} AIR)
</Text>
)}
<ButtonGroup ml="auto">
Expand Down
13 changes: 8 additions & 5 deletions centrifuge-app/src/components/Dialogs/RemoveListingDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useCentrifuge, useCentrifugeTransaction, useWallet } from '@centrifuge/centrifuge-react'
import { useBalances, useCentrifuge, useCentrifugeTransaction, useWallet } from '@centrifuge/centrifuge-react'
import { Button, Dialog, Shelf, Stack, Text } from '@centrifuge/fabric'
import * as React from 'react'
import { useBalance } from '../../utils/useBalance'
import { Dec } from '../../utils/Decimal'
import { useAddress } from '../../utils/useAddress'
import { useCentNFT } from '../../utils/useNFTs'
import { ButtonGroup } from '../ButtonGroup'

Expand All @@ -16,7 +17,8 @@ const TRANSFER_FEE_ESTIMATE = 0.1

export const RemoveListingDialog: React.FC<Props> = ({ open, onClose, collectionId, nftId }) => {
const { substrate } = useWallet()
const balance = useBalance()
const address = useAddress('substrate')
const balances = useBalances(address)
const centrifuge = useCentrifuge()
const nft = useCentNFT(collectionId, nftId)

Expand Down Expand Up @@ -52,7 +54,8 @@ export const RemoveListingDialog: React.FC<Props> = ({ open, onClose, collection
onClose()
}

const balanceLow = !balance || balance < TRANSFER_FEE_ESTIMATE
const balanceDec = balances?.native.balance.toDecimal() ?? Dec(0)
const balanceLow = balanceDec.lt(TRANSFER_FEE_ESTIMATE)

const disabled = balanceLow

Expand All @@ -73,7 +76,7 @@ export const RemoveListingDialog: React.FC<Props> = ({ open, onClose, collection
<Shelf justifyContent="space-between">
{balanceLow && (
<Text variant="label1" color="criticalForeground">
Your balance is too low ({(balance || 0).toFixed(2)} AIR)
Your balance is too low ({(balanceDec || 0).toFixed(2)} AIR)
</Text>
)}
<ButtonGroup ml="auto">
Expand Down
13 changes: 8 additions & 5 deletions centrifuge-app/src/components/Dialogs/SellDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useCentrifugeTransaction, useWallet } from '@centrifuge/centrifuge-react'
import { useBalances, useCentrifugeTransaction, useWallet } from '@centrifuge/centrifuge-react'
import { Button, CurrencyInput, Dialog, Shelf, Stack, Text } from '@centrifuge/fabric'
import BN from 'bn.js'
import * as React from 'react'
import { useBalance } from '../../utils/useBalance'
import { Dec } from '../../utils/Decimal'
import { useAddress } from '../../utils/useAddress'
import { ButtonGroup } from '../ButtonGroup'

const e18 = new BN(10).pow(new BN(18))
Expand All @@ -20,7 +21,8 @@ export const SellDialog: React.FC<Props> = ({ open, onClose, collectionId, nftId
const [price, setPrice] = React.useState<number | ''>()
const [touched, setTouched] = React.useState(false)
const { substrate } = useWallet()
const balance = useBalance()
const address = useAddress('substrate')
const balances = useBalances(address)

const isConnected = !!substrate.selectedAccount?.address

Expand Down Expand Up @@ -66,7 +68,8 @@ export const SellDialog: React.FC<Props> = ({ open, onClose, collectionId, nftId

const error = getError()

const balanceLow = !balance || balance < TRANSFER_FEE_ESTIMATE
const balanceDec = balances?.native.balance.toDecimal() ?? Dec(0)
const balanceLow = balanceDec.lt(TRANSFER_FEE_ESTIMATE)

const disabled = !!error || balanceLow

Expand All @@ -89,7 +92,7 @@ export const SellDialog: React.FC<Props> = ({ open, onClose, collectionId, nftId
<Shelf justifyContent="space-between">
{balanceLow && (
<Text variant="label1" color="criticalForeground">
Your balance is too low ({(balance || 0).toFixed(2)} AIR)
Your balance is too low ({(balanceDec || 0).toFixed(2)} AIR)
</Text>
)}
<ButtonGroup ml="auto">
Expand Down
66 changes: 66 additions & 0 deletions centrifuge-app/src/components/Dialogs/ShareMultisigDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Multisig } from '@centrifuge/centrifuge-js'
import { Button, Dialog, IconCopy, IconSend, Shelf, Stack, Text, TextInput } from '@centrifuge/fabric'
import { copyToClipboard } from '../../utils/copyToClipboard'
import { ButtonGroup } from '../ButtonGroup'

export type ShareMultisigDialogProps = {
multisig: Multisig
hash: string
callData?: string
open: boolean
onClose: () => void
}

export function ShareMultisigDialog({ multisig, hash, callData, open, onClose }: ShareMultisigDialogProps) {
const shareUrl = getMultisigShareUrl({ multisig, hash, callData })

const shareData: ShareData = {
title: 'Approve Transaction',
text: 'Approve a multisig transaction on the Centrifuge App',
url: shareUrl,
}

return (
<Dialog isOpen={open} onClose={onClose} title="Share link with other multisig signers">
<Stack gap={2}>
<Text variant="body2">Share the link below with the other multisig signers to finalize the transaction</Text>
<Shelf gap={1}>
<TextInput
style={{
cursor: 'copy',
}}
onClick={(e: any) => {
copyToClipboard(shareUrl)
e.target.select()
}}
value={shareUrl}
readOnly
/>
<ButtonGroup>
<Button variant="tertiary" icon={IconCopy} onClick={() => copyToClipboard(shareUrl)} />
{navigator.share && (
<Button variant="tertiary" icon={IconSend} onClick={() => navigator.share(shareData)} />
)}
</ButtonGroup>
</Shelf>
</Stack>
</Dialog>
)
}

export function getMultisigShareUrl({
multisig,
hash,
callData,
}: Pick<ShareMultisigDialogProps, 'multisig' | 'hash' | 'callData'>) {
const params = new URLSearchParams({
hash,
signers: multisig.signers.join(','),
threshold: multisig.threshold.toString(),
data: callData || '',
})
const url = new URL(`/multisig-approval`, window.location.origin)
url.search = params as any
const shareUrl = url.toString()
return shareUrl
}
Loading

0 comments on commit 0a7ce8f

Please sign in to comment.