diff --git a/centrifuge-app/src/components/OnboardingAuthProvider.tsx b/centrifuge-app/src/components/OnboardingAuthProvider.tsx index 92fd8a8a1b..c287133d85 100644 --- a/centrifuge-app/src/components/OnboardingAuthProvider.tsx +++ b/centrifuge-app/src/components/OnboardingAuthProvider.tsx @@ -237,6 +237,8 @@ Issued At: ${new Date().toISOString()}` safeAddress: address, messageHash, evmChainId, + network: 'evmOnSafe', + nonce, }) } else { throw new Error('Invalid signature') diff --git a/onboarding-api/src/controllers/auth/authenticateWallet.ts b/onboarding-api/src/controllers/auth/authenticateWallet.ts index ccb77ad9a8..79bd3811f1 100644 --- a/onboarding-api/src/controllers/auth/authenticateWallet.ts +++ b/onboarding-api/src/controllers/auth/authenticateWallet.ts @@ -1,9 +1,6 @@ -import { InfuraProvider } from '@ethersproject/providers' import { isAddress } from '@polkadot/util-crypto' -import { BigNumber, ethers } from 'ethers' import { Request, Response } from 'express' import * as jwt from 'jsonwebtoken' -import fetch from 'node-fetch' import { InferType, number, object, string, StringSchema } from 'yup' import { SupportedNetworks } from '../../database' import { reportHttpError } from '../../utils/httpError' @@ -48,6 +45,8 @@ const verifySafeInput = object({ }, }), evmChainId: number().required(), + network: string().oneOf(['evmOnSafe']).required() as StringSchema<'evmOnSafe'>, + nonce: string().required(), }) export const authenticateWalletController = async ( @@ -55,17 +54,9 @@ export const authenticateWalletController = async ( res: Response ) => { try { - let payload - - if ('safeAddress' in req.body) { - await validateInput(req.body, verifySafeInput) + await validateInput(req.body, req.body.network === 'evmOnSafe' ? verifySafeInput : verifyWalletInput) - const provider = new InfuraProvider(req.body.evmChainId, process.env.INFURA_KEY) - payload = await verifySafeWallet(provider, req.body.safeAddress, req.body.messageHash, req.body.evmChainId) - } else { - await validateInput(req.body, verifyWalletInput) - payload = await new NetworkSwitch(req.body.network).verifyWallet(req, res) - } + const payload = await new NetworkSwitch(req.body.network).verifyWallet(req, res) const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '8h', @@ -77,62 +68,3 @@ export const authenticateWalletController = async ( return res.status(error.code).send({ error: error.message, e }) } } - -const verifySafeWallet = async (provider: any, safeAddress: string, messageHash: string, evmChainId: number) => { - try { - const MAGIC_VALUE_BYTES = '0x20c13b0b' - - const safeContract = new ethers.Contract( - safeAddress, - [ - 'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)', - 'function getMessageHash(bytes memory message) public view returns (bytes32)', - 'function getThreshold() public view returns (uint256)', - ], - provider - ) - - const safeMessageHash = await safeContract.getMessageHash(messageHash) - - const safeMessage = await fetchSafeMessage(safeMessageHash, evmChainId) - - if (!safeMessage) { - throw new Error('Unable to fetch SafeMessage') - } - - const threshold = BigNumber.from(await safeContract.getThreshold()).toNumber() - - if (!threshold || threshold > safeMessage.confirmations.length) { - throw new Error('Threshold has not been met') - } - - const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature) - - if (response === MAGIC_VALUE_BYTES) { - return { - address: safeAddress, - chainId: evmChainId, - network: 'evm', - } - } - - throw new Error('Invalid signature') - } catch { - throw new Error('Something went wrong') - } -} - -const TX_SERVICE_URLS: Record = { - '1': 'https://safe-transaction-mainnet.safe.global/api', - '5': 'https://safe-transaction-goerli.staging.5afe.dev/api', -} - -const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => { - const TX_SERVICE_URL = TX_SERVICE_URLS[chainId.toString()] - - const response = await fetch(`${TX_SERVICE_URL}/v1/messages/${safeMessageHash}/`, { - headers: { 'Content-Type': 'application/json' }, - }) - - return response.json() -} diff --git a/onboarding-api/src/utils/networks/evm.ts b/onboarding-api/src/utils/networks/evm.ts index 7463fc7b60..72c027f914 100644 --- a/onboarding-api/src/utils/networks/evm.ts +++ b/onboarding-api/src/utils/networks/evm.ts @@ -1,7 +1,9 @@ import { isAddress } from '@ethersproject/address' import { Contract } from '@ethersproject/contracts' import { InfuraProvider, JsonRpcProvider, Provider } from '@ethersproject/providers' +import { BigNumber, ethers } from 'ethers' import { Request, Response } from 'express' +import fetch from 'node-fetch' import { SiweMessage } from 'siwe' import { InferType } from 'yup' import { signAndSendDocumentsInput } from '../../controllers/emails/signAndSendDocuments' @@ -66,3 +68,72 @@ export async function verifyEvmWallet(req: Request, res: Response): Promise { + const { safeAddress, messageHash, evmChainId, nonce } = req.body + const MAGIC_VALUE_BYTES = '0x20c13b0b' + + if (!isAddress(safeAddress)) { + throw new HttpError(400, 'Invalid address') + } + + const cookieNonce = req.signedCookies[`onboarding-auth-${safeAddress.toLowerCase()}`] + + if (!cookieNonce || cookieNonce !== nonce) { + throw new HttpError(400, 'Invalid nonce') + } + + const provider = new InfuraProvider(req.body.evmChainId, process.env.INFURA_KEY) + const safeContract = new ethers.Contract( + safeAddress, + [ + 'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)', + 'function getMessageHash(bytes memory message) public view returns (bytes32)', + 'function getThreshold() public view returns (uint256)', + ], + provider + ) + + const safeMessageHash = await safeContract.getMessageHash(messageHash) + + const safeMessage = await fetchSafeMessage(safeMessageHash, evmChainId) + + if (!safeMessage) { + throw new HttpError(400, 'Unable to fetch SafeMessage') + } + + const threshold = BigNumber.from(await safeContract.getThreshold()).toNumber() + + if (!threshold || threshold > safeMessage.confirmations.length) { + throw new HttpError(400, 'Threshold has not been met') + } + + const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature) + + if (response === MAGIC_VALUE_BYTES) { + res.clearCookie(`onboarding-auth-${safeAddress.toLowerCase()}`) + + return { + address: safeAddress, + chainId: evmChainId, + network: 'evm', + } + } + + throw new HttpError(400, 'Invalid signature') +} + +const TX_SERVICE_URLS: Record = { + '1': 'https://safe-transaction-mainnet.safe.global/api', + '5': 'https://safe-transaction-goerli.staging.5afe.dev/api', +} + +const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => { + const TX_SERVICE_URL = TX_SERVICE_URLS[chainId.toString()] + + const response = await fetch(`${TX_SERVICE_URL}/v1/messages/${safeMessageHash}/`, { + headers: { 'Content-Type': 'application/json' }, + }) + + return response.json() +} diff --git a/onboarding-api/src/utils/networks/networkSwitch.ts b/onboarding-api/src/utils/networks/networkSwitch.ts index 19fcf6e146..334d9f2362 100644 --- a/onboarding-api/src/utils/networks/networkSwitch.ts +++ b/onboarding-api/src/utils/networks/networkSwitch.ts @@ -9,18 +9,20 @@ import { validateSubstrateRemark, verifySubstrateWallet, } from './centrifuge' -import { validateEvmRemark, verifyEvmWallet } from './evm' +import { validateEvmRemark, verifyEvmWallet, verifySafeWallet } from './evm' import { addTinlakeInvestorToMemberList, getTinlakePoolById } from './tinlake' export class NetworkSwitch { - network: SupportedNetworks - constructor(network: SupportedNetworks = 'substrate') { + network: SupportedNetworks | 'evmOnSafe' + constructor(network: SupportedNetworks | 'evmOnSafe' = 'substrate') { this.network = network } verifyWallet = (req: Request, res: Response) => { if (this.network === 'substrate') { return verifySubstrateWallet(req, res) + } else if (this.network === 'evmOnSafe') { + return verifySafeWallet(req, res) } else if (this.network === 'evm' || this.network === 'evmOnSubstrate') { return verifyEvmWallet(req, res) }