Skip to content

Commit

Permalink
Merge pull request #48 from consenlabs/feature/forward_orders_to_exte…
Browse files Browse the repository at this point in the history
…rnal_service

Forwarding unsigned orders if SIGNING_URL is set by market maker
  • Loading branch information
BenjaminLu authored Oct 14, 2022
2 parents d91593d + 36a8bff commit a7176f4
Show file tree
Hide file tree
Showing 13 changed files with 164 additions and 54 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ See [docs](https://docs.token.im/tokenlon-mmsk/)

## Setup

Require Node.JS v10 as runtime.
Require Node.JS v12 as runtime.

Program setup,
- Create a wallet as order signer, and save it as keystore or private key
Expand All @@ -20,8 +20,15 @@ Program setup,
- PROVIDER_URL, point to ethereum node, like your infura endpoint
- WALLET_ADDRESS, as your signer wallet address
- WALLET_PRIVATE_KEY, private key of above wallet, or use WALLET_KEYSTORE
- WALLET_TYPE, a market maker's wallet smart contract.
- types.WalletType.MMP_VERSION_4 (compatible with PMM protocol, see [example contract](https://gist.github.com/NIC619/a3db1a743175bf592f2db983f17680dd#file-mmpv4-sol-L1236))
- types.WalletType.MMP_VERSION_5
- types.WalletType.ERC1271
- types.WalletType.EOA
- SIGNING_URL, If you wanna sign orders in your own service instead of the mmsk,
please set the SIGNING_URL to your service endpoint. the mmsk would post every unsigned RFQ orders to your service. Remember to set the WALLET_ADDRESS as well.
- HTTP_SERVER_ENDPOINT, your backend http server
- CHAIN_ID, 1 for mainnet, 42 for testnet(kovan)
- CHAIN_ID, 1 for mainnet, 5 for testnet(Goerli)
- Testing with `node app/check.js`
- Register contract address & signer address & MMSK server url to Tokenlon team

Expand Down
21 changes: 14 additions & 7 deletions app/mmConfig.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
const types = require('../lib/signer/types')

module.exports = {
//
// Tokenlon server address
EXCHANGE_URL: process.env.EXCHANGE_URL,
PROVIDER_URL: process.env.PROVIDER_URL,

//
// Wallet
// Signing
/**
* If you wanna sign orders in your own service instead of the mmsk,
* please set the SIGNING_URL to your service endpoint.
* the mmsk would post every unsigned orders to your service.
* Remember to set the WALLET_ADDRESS as well.
*/
SIGNING_URL: process.env.SIGNING_URL,
WALLET_ADDRESS: process.env.WALLET_ADDRESS,
WALLET_TYPE: types.WalletType.ERC1271,
WALLET_TYPE: types.WalletType.MMP_VERSION_4,
USE_KEYSTORE: false,
WALLET_KEYSTORE: {},
/**
* If you set the SIGNING_URL and WALLET_ADDRESS, it's unnecessary to set the WALLET_PRIVATE_KEY.
* It would forward evey unsigned order to SIGNING_URL instead of signing orders with WALLET_PRIVATE_KEY
*/
WALLET_PRIVATE_KEY: process.env.WALLET_PRIVATE_KEY,

// AMM
AMMWRAPPER_CONTRACT_ADDRESS: process.env.AMMWRAPPER_CONTRACT_ADDRESS,

//
// MM backend config
HTTP_SERVER_ENDPOINT: process.env.HTTP_SERVER_ENDPOINT,
// ZERORPC_SERVER_ENDPOINT: process.env.ZERORPC_SERVER_ENDPOINT,

//
// Server config
CHAIN_ID: process.env.CHAIN_ID || 42,
CHAIN_ID: process.env.CHAIN_ID || 5,
MMSK_SERVER_PORT: process.env.MMSK_SERVER_PORT || 80,
SENTRY_DSN: '',
NODE_ENV: 'PRODUCTION',
Expand Down
2 changes: 1 addition & 1 deletion src/check/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const checkMMSK = async (config: ConfigForStart) => {
quoter = new QuoteDispatcher(config.HTTP_SERVER_ENDPOINT, QuoterProtocol.HTTP)
}
const wallet = getWallet()
await startUpdater(quoter, wallet)
await startUpdater(quoter, wallet.address)

for (let i = 0; i < arr.length; i += 1) {
const item = arr[i]
Expand Down
15 changes: 8 additions & 7 deletions src/handler/newOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,13 @@ function getOrderAndFeeFactor(query: QueryInterface, rate, tokenList, tokenConfi
}

const _getBaseTokenByAddress = (baseTokenAddr, tokenList) => {
return tokenList.find(
(token) => token.contractAddress.toLowerCase() === baseTokenAddr
)
return tokenList.find((token) => token.contractAddress.toLowerCase() === baseTokenAddr)
}

const getBaseTokenByAddress = memoize(_getBaseTokenByAddress)

export const newOrder = async (ctx) => {
const { quoter, signer, chainID, walletType } = ctx
const { quoter, signer, chainID, walletType, signingUrl } = ctx
const req: QueryInterface = {
protocol: Protocol.PMMV5, // by default is v2 protocol
...ctx.query, // overwrite from request
Expand All @@ -213,9 +211,8 @@ export const newOrder = async (ctx) => {
const tokenConfigs = updaterStack.tokenConfigsFromImtokenUpdater.cacheResult
const tokenList = getSupportedTokens()

const { rate, minAmount, maxAmount, quoteId } = rateBody
const { rate, minAmount, maxAmount, quoteId, salt } = rateBody
const order = getOrderAndFeeFactor(query, rate, tokenList, tokenConfigs, config)

const resp: Response = {
rate,
minAmount,
Expand Down Expand Up @@ -261,7 +258,11 @@ export const newOrder = async (ctx) => {
userAddr.toLowerCase(),
chainID,
config.addressBookV5.RFQ,
walletType
walletType,
{
signingUrl,
salt,
}
)
break
default:
Expand Down
3 changes: 2 additions & 1 deletion src/quoting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const removeQuoteIdPrefix = (quoteId: string): string => {
}

export const constructQuoteResponse = (indicativePrice: IndicativePriceApiResult, side: SIDE) => {
const { minAmount, maxAmount, message, makerAddress } = indicativePrice
const { minAmount, maxAmount, message, makerAddress, salt } = indicativePrice
if (indicativePrice.exchangeable === false || !indicativePrice.price) {
throw new BackendError(
message || `Can't support this trade: ${JSON.stringify(indicativePrice)}`
Expand All @@ -27,6 +27,7 @@ export const constructQuoteResponse = (indicativePrice: IndicativePriceApiResult

const rate = side === 'BUY' ? 1 / indicativePrice.price : indicativePrice.price
return {
salt,
minAmount,
maxAmount,
rate: toBN((+rate).toFixed(DISPLAY_PRECEISION)).toNumber(),
Expand Down
1 change: 1 addition & 0 deletions src/request/marketMaker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface IndicativePriceApiResult {
price: number
makerAddress?: string
message?: string
salt?: string
}

export interface PriceApiParams extends IndicativePriceApiParams {
Expand Down
20 changes: 17 additions & 3 deletions src/signer/pmmv5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,24 @@ const EIP712_ORDER_SCHEMA = {
// - fee factor from salt
// - user address from fee recipient

export const generateSaltWithFeeFactor = (feeFactor: number) => {
const feeHex = utils.hexZeroPad('0x' + feeFactor.toString(16), 2)
export const generateSaltWithFeeFactor = (feeFactor: number, prefixSalt?: string) => {
// append 001e = 30 (fee factor to salt)
return new BigNumber(generatePseudoRandomSalt().toString(16).slice(0, -4) + feeHex.slice(2), 16)
const feeHex = utils.hexZeroPad('0x' + feeFactor.toString(16), 2)
if (prefixSalt) {
if (!(prefixSalt.toString().length === 32 || prefixSalt.toString().length === 34)) {
throw new Error('Invalid salt from market maker')
}
if (prefixSalt.toString().startsWith('0x')) {
prefixSalt = prefixSalt.toString().slice(2)
}
const postfixSalt = `${generatePseudoRandomSalt()
.toString(16)
.slice(0, 32)
.slice(0, -4)}${feeHex.slice(2)}`
return new BigNumber(`${prefixSalt}${postfixSalt}`, 16)
} else {
return new BigNumber(generatePseudoRandomSalt().toString(16).slice(0, -4) + feeHex.slice(2), 16)
}
}

// Signature:
Expand Down
42 changes: 35 additions & 7 deletions src/signer/rfqv1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getOrderHash, getOrderSignDigest } from './orderHash'
import { RFQOrder, WalletType } from './types'
import * as ethUtils from 'ethereumjs-util'
import { SignatureType } from './types'
import axios from 'axios'

// spec of RFQV1
// - taker address point to userAddr
Expand Down Expand Up @@ -36,7 +37,7 @@ export async function signByMMPSigner(
wallet: Wallet,
walletType: WalletType
): Promise<string> {
if (walletType === WalletType.MMP_VERSOIN_4) {
if (walletType === WalletType.MMP_VERSION_4) {
// For V4 Maket Maker Proxy (MMP)
// Signature:
// +------|---------|---------|---------|---------|---------+
Expand Down Expand Up @@ -80,28 +81,55 @@ export async function signByMMPSigner(
}
}

export const forwardUnsignedOrder = async (signingUrl: string, orderInfo: any): Promise<string> => {
const resp = await axios.post(signingUrl, orderInfo)
const body = resp.data
if (body.signature) {
return body.signature
} else {
throw new Error('Invalid signature')
}
}

export const buildSignedOrder = async (
signer: Wallet,
order,
userAddr: string,
chainId: number,
rfqAddr: string,
walletType: WalletType
walletType: WalletType,
options?: {
signingUrl?: string
salt?: string
}
): Promise<any> => {
// inject fee factor to salt
const feeFactor = order.feeFactor
order.takerAddress = userAddr.toLowerCase()
order.salt = generateSaltWithFeeFactor(feeFactor)
const salt = options ? options.salt : undefined
const signingUrl = options ? options.signingUrl : undefined
order.salt = generateSaltWithFeeFactor(feeFactor, salt)

const rfqOrer = toRFQOrder(order)
const orderHash = getOrderHash(rfqOrer)
console.log(`orderHash: ${orderHash}`)
const orderSignDigest = getOrderSignDigest(rfqOrer, chainId, rfqAddr)
console.log(`orderSignDigest: ${orderSignDigest}`)
const makerWalletSignature =
signer.address.toLowerCase() == order.makerAddress.toLowerCase()
? await signByEOA(orderSignDigest, signer)
: await signByMMPSigner(orderSignDigest, userAddr, feeFactor, signer, walletType)
let makerWalletSignature
if (!signingUrl) {
makerWalletSignature =
signer.address.toLowerCase() == order.makerAddress.toLowerCase()
? await signByEOA(orderSignDigest, signer)
: await signByMMPSigner(orderSignDigest, userAddr, feeFactor, signer, walletType)
} else {
makerWalletSignature = await forwardUnsignedOrder(signingUrl, {
rfqOrer: rfqOrer,
userAddr: userAddr,
signer: signer.address,
chainId: chainId,
rfqAddr: rfqAddr,
})
}

const signedOrder = {
...order,
Expand Down
4 changes: 2 additions & 2 deletions src/signer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export enum SignatureType {
}

export enum WalletType {
EOA = 0,
MMP_VERSOIN_4 = 1, // https://gist.github.com/NIC619/a3db1a743175bf592f2db983f17680dd#file-mmpv4-sol-L1236
MMP_VERSION_4 = 1, // https://gist.github.com/NIC619/a3db1a743175bf592f2db983f17680dd#file-mmpv4-sol-L1236
MMP_VERSION_5 = 2, // https://github.com/consenlabs/tokenlon-contracts/blob/e2edf7581b69bc8a40e61ff7fc1cd29674ae4887/contracts/MarketMakerProxy.sol#L19
ERC1271 = 3, // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.6.0/contracts/utils/cryptography/SignatureChecker.sol#L36
EOA = 4, // less security for market makers
}
37 changes: 23 additions & 14 deletions src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { VERSION } from './handler/version'
// FIXME: construct wallet(signer), quoter and worker separately
// FIXME: better retry implementation
const beforeStart = async (config: ConfigForStart, triedTimes?: number) => {
const wallet = getWallet()
// const wallet = getWallet()
triedTimes = triedTimes || 0
try {
let quoter: Quoter
Expand All @@ -37,7 +37,7 @@ const beforeStart = async (config: ConfigForStart, triedTimes?: number) => {
} else {
quoter = new QuoteDispatcher(config.HTTP_SERVER_ENDPOINT, QuoterProtocol.HTTP)
}
await startUpdater(quoter, wallet)
await startUpdater(quoter, config.WALLET_ADDRESS)
return quoter
} catch (e) {
triedTimes += 1
Expand Down Expand Up @@ -69,21 +69,25 @@ export const startMMSK = async (config: ConfigForStart) => {
const app = new Koa()
const router = new Router()
const MMSK_SERVER_PORT = config.MMSK_SERVER_PORT || 80

let wallet
setConfig(config)
try {
const wallet = getWallet()
if (wallet.address.toLowerCase() != config.WALLET_ADDRESS.toLowerCase()) {
throw `wallet's address${wallet.address} and ${
config.USE_KEYSTORE ? 'keystore' : 'privateKey'
}(${config.WALLET_ADDRESS}) not matched`
console.log(config.SIGNING_URL)
if (!config.SIGNING_URL) {
wallet = getWallet()
if (!wallet) {
throw new Error(`Please set either WALLET_PRIVATE_KEY or SIGNING_URL`)
}
if (wallet.address.toLowerCase() != config.WALLET_ADDRESS.toLowerCase()) {
throw `wallet's address${wallet.address} and ${
config.USE_KEYSTORE ? 'keystore' : 'privateKey'
}(${config.WALLET_ADDRESS}) not matched`
}
}

console.log({
version: VERSION,
signerAddress: wallet.address,
mmpAddress: config.WALLET_ADDRESS,
mmpType: config.WALLET_TYPE || WalletType.MMP_VERSOIN_4,
signerAddress: config.WALLET_ADDRESS,
mmpType: config.WALLET_TYPE || WalletType.MMP_VERSION_4,
chainId: config.CHAIN_ID,
exchangeUrl: config.EXCHANGE_URL,
})
Expand All @@ -108,8 +112,13 @@ export const startMMSK = async (config: ConfigForStart) => {

app.context.chainID = config.CHAIN_ID || 5
app.context.quoter = quoter
app.context.signer = wallet
app.context.walletType = config.WALLET_TYPE || WalletType.MMP_VERSOIN_4
if (wallet) {
app.context.signer = wallet
}
if (config.SIGNING_URL) {
app.context.signingUrl = config.SIGNING_URL
}
app.context.walletType = config.WALLET_TYPE || WalletType.MMP_VERSION_4

app
.use(async (ctx, next) => {
Expand Down
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface Wallet {
export interface ConfigForStart {
EXCHANGE_URL: string
PROVIDER_URL: string
SIGNING_URL: string

WALLET_ADDRESS: string
WALLET_TYPE: WalletType
Expand Down Expand Up @@ -95,6 +96,7 @@ export interface QueryInterface {
uniqId?: number | string
userAddr?: string
protocol?: Protocol
salt?: string
}

export enum Protocol {
Expand Down
11 changes: 7 additions & 4 deletions src/worker/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getMarketMakerConfig, getTokenList, getTokenConfigsForMM } from '../request/imToken'
import { Quoter } from '../request/marketMaker'
import Updater from './updater'
import { Wallet } from '../types'
import { utils } from 'ethers'

const updaterStack = {
markerMakerConfigUpdater: null as Updater,
Expand All @@ -10,11 +10,14 @@ const updaterStack = {
pairsFromMMUpdater: null as Updater,
}

const startUpdater = async (quoter: Quoter, wallet: Wallet) => {
const startUpdater = async (quoter: Quoter, walletAddress: string) => {
if (!utils.isAddress(utils.getAddress(walletAddress))) {
throw new Error('WALLET_ADDRESS is not valid')
}
updaterStack.markerMakerConfigUpdater = new Updater({
name: 'markerMakerConfig',
updater() {
return getMarketMakerConfig(wallet.address)
return getMarketMakerConfig(walletAddress)
},
})

Expand All @@ -28,7 +31,7 @@ const startUpdater = async (quoter: Quoter, wallet: Wallet) => {
updaterStack.tokenConfigsFromImtokenUpdater = new Updater({
name: 'tokenConfigsFromImtoken',
updater() {
return getTokenConfigsForMM(wallet.address)
return getTokenConfigsForMM(walletAddress)
},
})

Expand Down
Loading

0 comments on commit a7176f4

Please sign in to comment.