diff --git a/README.md b/README.md index 36552da..16b9c4b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Program setup, - Modify the options in `app/mmConfig.js`, including, - EXCHANGE_URL, point to tokenlon exchange server - PROVIDER_URL, point to ethereum node, like your infura endpoint + - PERMIT_TYPE, approve tokens to `RFQv2` contract directly or approve tokens to `AllowanceTarget` contract. - 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. @@ -26,10 +27,10 @@ Program setup, - 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. An example request is shown below: + please set the SIGNING_URL to your service endpoint. the mmsk would post every unsigned PMM/RFQV1/RFQV2 orders to your service. Remember to set the WALLET_ADDRESS as well. An example RFQV1 request is shown below: ``` { - rfqOrer: { + rfqOrder: { takerAddr: '0x87fca7135c1c54876a62dc4922da3ce45f38debf', makerAddr: '0x86B9F429C3Ef44c599EB560Eb531A0E3f2E36f64', takerAssetAddr: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', @@ -55,7 +56,7 @@ Program setup, - HTTP_SERVER_ENDPOINT, your backend http quoting server - 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 +- Register contract address, signer address and MMSK server url to Tokenlon team ## Version Release diff --git a/app/mmConfig.js b/app/mmConfig.js index 35df245..454e27d 100644 --- a/app/mmConfig.js +++ b/app/mmConfig.js @@ -4,6 +4,7 @@ module.exports = { // Tokenlon server address EXCHANGE_URL: process.env.EXCHANGE_URL, PROVIDER_URL: process.env.PROVIDER_URL, + PERMIT_TYPE: types.PermitType.APPROVE_RFQV2, // Signing /** diff --git a/package.json b/package.json index 3405b57..2e96bdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@consenlabs/tokenlon-mmsk", - "version": "5.2.10", + "version": "5.3.0", "description": "Tokenlon market maker server kit", "author": "imToken PTE. LTD.", "license": "MIT", @@ -26,7 +26,7 @@ "check": "node ./app/check.js", "clean": "rm -rf ./lib", "start": "node ./app/start.js", - "test": "chainId=1 npx hardhat --network hardhat test", + "test": "chainId=5 npx hardhat --network hardhat test", "watch": "tsc -w" }, "devDependencies": { diff --git a/src/handler/newOrder.ts b/src/handler/newOrder.ts index 5c82aad..5f1ba3e 100644 --- a/src/handler/newOrder.ts +++ b/src/handler/newOrder.ts @@ -8,6 +8,7 @@ import { addQuoteIdPrefix, constructQuoteResponse, preprocessQuote } from '../qu import { assetDataUtils } from '0x-v2-order-utils' import { buildSignedOrder as buildRFQV1SignedOrder } from '../signer/rfqv1' +import { buildSignedOrder as buildRFQV2SignedOrder } from '../signer/rfqv2' import { buildSignedOrder } from '../signer/pmmv5' import { buildSignedOrder as buildAMMV1Order } from '../signer/ammv1' import { buildSignedOrder as buildAMMV2Order } from '../signer/ammv2' @@ -158,11 +159,10 @@ function getOrderAndFeeFactor(query: QueryInterface, rate, tokenList, tokenConfi config.wethContractAddress ) // ETH -> WETH - const takerAssetAddress = getWethAddrIfIsEth( - takerToken.contractAddress, - config.wethContractAddress - ) - + let takerAssetAddress = getWethAddrIfIsEth(takerToken.contractAddress, config.wethContractAddress) + if (Protocol.RFQV2 === query.protocol) { + takerAssetAddress = takerToken.contractAddress + } return { makerAddress: config.mmProxyContractAddress.toLowerCase(), makerAssetAmount, @@ -192,7 +192,7 @@ const _getBaseTokenByAddress = (baseTokenAddr, tokenList) => { const getBaseTokenByAddress = memoize(_getBaseTokenByAddress) export const newOrder = async (ctx) => { - const { quoter, signer, chainID, walletType, signingUrl } = ctx + const { quoter, signer, chainID, walletType, signingUrl, permitType } = ctx const req: QueryInterface = { protocol: Protocol.PMMV5, // by default is v2 protocol ...ctx.query, // overwrite from request @@ -270,6 +270,21 @@ export const newOrder = async (ctx) => { } ) break + case Protocol.RFQV2: + resp.order = await buildRFQV2SignedOrder( + signer, + order, + userAddr.toLowerCase(), + chainID, + config.addressBookV5.RFQV2, + walletType, + permitType, + { + signingUrl, + salt, + } + ) + break default: console.log(`unknown protocol ${protocol}`) throw new Error('Unrecognized protocol: ' + protocol) diff --git a/src/handler/version.ts b/src/handler/version.ts index 2699552..d299a81 100644 --- a/src/handler/version.ts +++ b/src/handler/version.ts @@ -1,4 +1,4 @@ -export const VERSION = '5.2.10' +export const VERSION = '5.3.0' export const version = (ctx) => { ctx.body = { diff --git a/src/signer/orderHash.ts b/src/signer/orderHash.ts index 177dcc3..909a58a 100644 --- a/src/signer/orderHash.ts +++ b/src/signer/orderHash.ts @@ -1,5 +1,5 @@ import { utils } from 'ethers' -import { RFQOrder } from './types' +import { Offer, RFQOrder } from './types' const EIP712_DOMAIN_NAME = 'Tokenlon' const EIP712_DOMAIN_VERSION = 'v5' @@ -18,6 +18,20 @@ const RFQ_ORDER_SCHEMA = { ], } +const RFQ_V2_ORDER_SCHEMA = { + Offer: [ + { name: 'taker', type: 'address' }, + { name: 'maker', type: 'address' }, + { name: 'takerToken', type: 'address' }, + { name: 'takerTokenAmount', type: 'uint256' }, + { name: 'makerToken', type: 'address' }, + { name: 'makerTokenAmount', type: 'uint256' }, + { name: 'feeFactor', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, + { name: 'salt', type: 'uint256' }, + ], +} + export function getOrderSignDigest(order: RFQOrder, chainId: number, address: string): string { const domain = { name: EIP712_DOMAIN_NAME, @@ -36,6 +50,17 @@ export function getOrderSignDigest(order: RFQOrder, chainId: number, address: st return utils._TypedDataEncoder.hash(domain, RFQ_ORDER_SCHEMA, value) } +export function getOfferSignDigest(order: Offer, chainId: number, address: string): string { + const domain = { + name: EIP712_DOMAIN_NAME, + version: EIP712_DOMAIN_VERSION, + chainId: chainId, + verifyingContract: address, + } + + return utils._TypedDataEncoder.hash(domain, RFQ_V2_ORDER_SCHEMA, order) +} + export function getOrderHash(order: RFQOrder): string { // The data to sign const value = { @@ -47,3 +72,7 @@ export function getOrderHash(order: RFQOrder): string { return utils._TypedDataEncoder.hashStruct('Order', RFQ_ORDER_SCHEMA, value) } + +export function getOfferHash(order: Offer): string { + return utils._TypedDataEncoder.hashStruct('Offer', RFQ_V2_ORDER_SCHEMA, order) +} diff --git a/src/signer/rfqv1.ts b/src/signer/rfqv1.ts index de2d084..39d13da 100644 --- a/src/signer/rfqv1.ts +++ b/src/signer/rfqv1.ts @@ -149,11 +149,11 @@ export const buildSignedOrder = async ( const signingUrl = options ? options.signingUrl : undefined order.salt = generateSaltWithFeeFactor(feeFactor, salt) - const rfqOrer = toRFQOrder(order) + const rfqOrder = toRFQOrder(order) - const orderHash = getOrderHash(rfqOrer) + const orderHash = getOrderHash(rfqOrder) console.log(`orderHash: ${orderHash}`) - const orderSignDigest = getOrderSignDigest(rfqOrer, chainId, rfqAddr) + const orderSignDigest = getOrderSignDigest(rfqOrder, chainId, rfqAddr) console.log(`orderSignDigest: ${orderSignDigest}`) let makerWalletSignature if (!signingUrl) { @@ -161,9 +161,9 @@ export const buildSignedOrder = async ( makerWalletSignature = await signRFQOrder( chainId, rfqAddr, - rfqOrer, + rfqOrder, signer, - rfqOrer.feeFactor, + rfqOrder.feeFactor, SignatureType.EIP712 ) } else if (walletType === WalletType.ERC1271_EIP712) { @@ -171,9 +171,9 @@ export const buildSignedOrder = async ( makerWalletSignature = await signRFQOrder( chainId, rfqAddr, - rfqOrer, + rfqOrder, signer, - rfqOrer.feeFactor, + rfqOrder.feeFactor, SignatureType.WalletBytes32 ) } else { @@ -188,7 +188,7 @@ export const buildSignedOrder = async ( } } else { makerWalletSignature = await forwardUnsignedOrder(signingUrl, { - rfqOrer: rfqOrer, + rfqOrder: rfqOrder, userAddr: userAddr, signer: signer.address, chainId: chainId, diff --git a/src/signer/rfqv2.ts b/src/signer/rfqv2.ts new file mode 100644 index 0000000..fbd0b5e --- /dev/null +++ b/src/signer/rfqv2.ts @@ -0,0 +1,207 @@ +import { utils, Wallet } from 'ethers' +import { orderBNToString } from '../utils' +import { getOfferHash, getOfferSignDigest } from './orderHash' +import { Offer, PermitType, WalletType, SignatureType } from './types' +import * as ethUtils from 'ethereumjs-util' +import axios from 'axios' +import { generatePseudoRandomSalt } from '0x-v2-order-utils' +import { signWithUserAndFee } from './pmmv5' + +// spec of RFQV2 +// - taker address point to userAddr +// - fee factor from salt +// - SignatureType EthSign for EOA address +// - SignatureType Wallet for contract address + +// Signature: +// +------|---------|---------|-------------------|---------+ +// | R | S | V | reserved 32 bytes | type(3) | +// +------|---------|---------|-------------------|---------+ +export async function signByEOA(orderSignDigest: string, wallet: Wallet): Promise { + // signature: R+S+V + const hashArray = utils.arrayify(orderSignDigest) + let signature = await wallet.signMessage(hashArray) + const signatureBuffer = Buffer.concat([ + ethUtils.toBuffer(signature), + ethUtils.toBuffer('0x' + '00'.repeat(32)), + ethUtils.toBuffer(SignatureType.EthSign), + ]) + signature = '0x' + signatureBuffer.toString('hex') + return signature +} + +export async function signByMMPSigner( + orderSignDigest: string, + userAddr: string, + feeFactor: number, + wallet: Wallet, + walletType: WalletType +): Promise { + if (walletType === WalletType.MMP_VERSION_4) { + // For V4 Maket Maker Proxy (MMP) + // Signature: + // +------|---------|---------|---------|---------|---------+ + // | V | R | S |userAddr |feeFactor| type(6) | + // +------|---------|---------|---------|---------|---------+ + let signature = await signWithUserAndFee(wallet, orderSignDigest, userAddr, feeFactor) + const signatureBuffer = Buffer.concat([ + ethUtils.toBuffer(signature), + ethUtils.toBuffer(SignatureType.Wallet), + ]) + signature = '0x' + signatureBuffer.toString('hex') + return signature + } else if (walletType === WalletType.ERC1271_EIP712_EIP191) { + // | 32 byte | 32 byte |1 byte| 1 bytes | + // +---------|---------|------|---------+ + // | R | S | V | type(5) | + // +---------|---------|------|---------+ + let signature = await wallet.signMessage(utils.arrayify(orderSignDigest)) + const signatureBuffer = Buffer.concat([ + ethUtils.toBuffer(signature), + ethUtils.toBuffer(SignatureType.WalletBytes32), + ]) + signature = '0x' + signatureBuffer.toString('hex') + return signature + } else { + throw new Error('Unsupported wallet contract') + } +} + +export const forwardUnsignedOrder = async (signingUrl: string, orderInfo: any): Promise => { + 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 signOffer = async ( + chainId: number, + rfqAddr: string, + order: Offer, + maker: Wallet, + signatureType = SignatureType.EIP712 +): Promise => { + const domain = { + name: 'Tokenlon', + version: 'v5', + chainId: chainId, + verifyingContract: rfqAddr, + } + + // The named list of all type definitions + const types = { + Offer: [ + { name: 'taker', type: 'address' }, + { name: 'maker', type: 'address' }, + { name: 'takerToken', type: 'address' }, + { name: 'takerTokenAmount', type: 'uint256' }, + { name: 'makerToken', type: 'address' }, + { name: 'makerTokenAmount', type: 'uint256' }, + { name: 'feeFactor', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, + { name: 'salt', type: 'uint256' }, + ], + } + const signatureTypedData = await maker._signTypedData(domain, types, order) + const signature = Buffer.concat([ + ethUtils.toBuffer(signatureTypedData), + ethUtils.toBuffer(signatureType), + ]) + const eip712sig = '0x' + signature.toString('hex') + + return eip712sig +} + +export const buildSignedOrder = async ( + signer: Wallet, + order: any, + userAddr: string, + chainId: number, + rfqAddr: string, + walletType: WalletType, + permitType: PermitType, + options?: { + signingUrl?: string + salt?: string + } +): Promise => { + // inject fee factor to salt + const feeFactor = order.feeFactor + order.takerAddress = userAddr.toLowerCase() + const salt = options ? options.salt : undefined + order.salt = salt ? salt : generatePseudoRandomSalt() + + const signingUrl = options ? options.signingUrl : undefined + const rfqOrder = toOffer(order) + console.log(`rfqOrder`) + console.log(rfqOrder) + const orderHash = getOfferHash(rfqOrder) + console.log(`orderHash: ${orderHash}`) + const orderSignDigest = getOfferSignDigest(rfqOrder, chainId, rfqAddr) + console.log(`chainId: ${chainId}`) + console.log(`rfqAddr: ${rfqAddr}`) + console.log(`orderSignDigest: ${orderSignDigest}`) + let makerWalletSignature + if (!signingUrl) { + if (signer.address.toLowerCase() == order.makerAddress.toLowerCase()) { + makerWalletSignature = await signOffer( + chainId, + rfqAddr, + rfqOrder, + signer, + SignatureType.EIP712 + ) + } else if (walletType === WalletType.ERC1271_EIP712) { + // standard ERC1271 + ERC712 signature + makerWalletSignature = await signOffer( + chainId, + rfqAddr, + rfqOrder, + signer, + SignatureType.WalletBytes32 + ) + } else { + // non-standard wallet contract signature checks + makerWalletSignature = await signByMMPSigner( + orderSignDigest, + userAddr, + feeFactor, + signer, + walletType + ) + } + } else { + makerWalletSignature = await forwardUnsignedOrder(signingUrl, { + rfqOrder: rfqOrder, + userAddr: userAddr, + signer: signer.address, + chainId: chainId, + rfqAddr: rfqAddr, + }) + } + + const signedOrder = { + ...order, + payload: Buffer.from(JSON.stringify({ makerTokenPermit: permitType })).toString('base64'), + makerWalletSignature, + } + + return orderBNToString(signedOrder) +} + +export function toOffer(order): Offer { + return { + taker: order.takerAddress, + maker: order.makerAddress, + takerToken: order.takerAssetAddress, + takerTokenAmount: order.takerAssetAmount.toString(), + makerToken: order.makerAssetAddress, + makerTokenAmount: order.makerAssetAmount.toString(), + feeFactor: order.feeFactor.toString(), + expiry: order.expirationTimeSeconds.toString(), + salt: order.salt.toString(), + } +} diff --git a/src/signer/types.ts b/src/signer/types.ts index 7f0bc86..85f12ca 100644 --- a/src/signer/types.ts +++ b/src/signer/types.ts @@ -33,6 +33,23 @@ export interface RFQOrder { feeFactor: number } +export interface Offer { + taker: string + maker: string + takerToken: string + takerTokenAmount: BigNumber + makerToken: string + makerTokenAmount: BigNumber + feeFactor: number + expiry: number + salt: BigNumber | string +} + +export interface RFQV2Order { + offer: Offer + recipient: string +} + export enum SignatureType { Illegal = 0, // 0x00, default value Invalid = 1, // 0x01 @@ -51,3 +68,8 @@ export enum WalletType { EOA = 4, // less security for market makers ERC1271_EIP712 = 5, } + +export enum PermitType { + ALLOWANCE_TARGET = '0x00', + APPROVE_RFQV2 = '0x01', +} diff --git a/src/start.ts b/src/start.ts index 5c7699f..caedb2e 100644 --- a/src/start.ts +++ b/src/start.ts @@ -22,7 +22,7 @@ import { startUpdater } from './worker' import { QuoteDispatcher, QuoterProtocol } from './request/marketMaker' import tracker from './utils/tracker' import { Quoter } from './request/marketMaker/types' -import { WalletType } from './signer/types' +import { PermitType, WalletType } from './signer/types' import { VERSION } from './handler/version' // FIXME: construct wallet(signer), quoter and worker separately @@ -119,6 +119,7 @@ export const startMMSK = async (config: ConfigForStart) => { app.context.signingUrl = config.SIGNING_URL } app.context.walletType = config.WALLET_TYPE || WalletType.MMP_VERSION_4 + app.context.permitType = config.PERMIT_TYPE || PermitType.ALLOWANCE_TARGET app .use(async (ctx, next) => { diff --git a/src/types/index.ts b/src/types/index.ts index 53cb985..7292a22 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,6 +35,7 @@ export interface ConfigForStart { EXCHANGE_URL: string PROVIDER_URL: string SIGNING_URL: string + PERMIT_TYPE: string WALLET_ADDRESS: string WALLET_TYPE: WalletType @@ -104,4 +105,5 @@ export enum Protocol { AMMV2 = 'AMMV2', PMMV5 = 'PMMV5', RFQV1 = 'RFQV1', + RFQV2 = 'RFQV2', } diff --git a/test/new_order.spec.ts b/test/new_order.spec.ts index 23906e9..973f2d7 100644 --- a/test/new_order.spec.ts +++ b/test/new_order.spec.ts @@ -6,13 +6,14 @@ import { NULL_ADDRESS } from '../src/constants' import { Protocol } from '../src/types' import { buildSignedOrder, toRFQOrder } from '../src/signer/rfqv1' import { SignatureType, WalletType } from '../src/signer/types' -import { getOrderSignDigest } from '../src/signer/orderHash' +import { getOrderSignDigest, getOfferSignDigest } from '../src/signer/orderHash' import { BigNumber } from '../src/utils' import * as ethUtils from 'ethereumjs-util' -import { Signer as TokenlonSigner, AllowanceTarget, USDT, ABI, WETH } from '@tokenlon/sdk' +import { AllowanceTarget, USDT, ABI, WETH, ZERO } from '@tokenlon/sdk' import * as crypto from 'crypto' import { expect } from 'chai' import { generateSaltWithFeeFactor } from '../src/signer/pmmv5' +import { toOffer } from '../src/signer/rfqv2' const usdtHolders = { 1: '0x15abb66bA754F05cBC0165A64A11cDed1543dE48', 5: '0x031BBFB9379c4e6E3F42fb93a9f09C060c7fA037', @@ -46,10 +47,11 @@ describe('NewOrder', function () { orderExpirationSeconds: 600, feeFactor: 30, addressBookV5: { - Tokenlon: '0xF1eC89551112da48C3b43B5a167AF0b2a7Cc2614', + Tokenlon: '0x085966eE3E32A0Da16467569512535D38626B547', PMM: '0x7bd7d025D4231aAD1233967b527FFd7416410257', AMMWrapper: '0xCF011536f10e85e376E70905EED4CA9eA8Cded34', - RFQ: '0xfD6C2d2499b1331101726A8AC68CCc9Da3fAB54F', + RFQ: '0x117CAf73eB142eDC431E707DC33D4dfeF7c5BAd0', + RFQV2: '0xaE5FDd548E5B107C54E5c0D36952fB8a089f10C7', }, } const mockTokenConfigsFromImtokenUpdater = new Updater({ @@ -364,6 +366,366 @@ describe('NewOrder', function () { expect(signedOrderResp.order.salt.length > 0).is.true expect(Number(signedOrderResp.order.expirationTimeSeconds) > 0).is.true }) + it('should sign rfqv2 order for MMPv4', async () => { + const ethersNetwork = await ethers.provider.getNetwork() + const chainId = ethersNetwork.chainId + const usdtHolder = await ethers.provider.getSigner(usdtHolders[chainId]) + const usdt = await ethers.getContractAt(ABI.IERC20, USDT[chainId]) + const [deployer, ethHolder] = await ethers.getSigners() + const privateKey = crypto.randomBytes(32) + const user = new ethers.Wallet(privateKey, ethers.provider) + const userAddr = user.address.toLowerCase() + await ethHolder.sendTransaction({ + to: userAddr, + value: ethers.utils.parseEther('10'), + }) + const mmpSigner = Wallet.createRandom() + console.log(`mmpSigner: ${mmpSigner.address}`) + const mmproxy: Contract = await ( + await ethers.getContractFactory('MarketMakerProxy', deployer) + ).deploy(mmpSigner.address) + await usdt.connect(usdtHolder).transfer(mmproxy.address, ethers.utils.parseUnits('1000', 6)) + await mmproxy.connect(deployer).setAllowance([USDT[chainId]], AllowanceTarget[chainId]) + const mmproxyUsdtBalance = await usdt.balanceOf(mmproxy.address) + const mmproxyUsdtAllowance = await usdt.allowance(mmproxy.address, AllowanceTarget[chainId]) + console.log(`mmproxyUsdtBalance: ${ethers.utils.formatUnits(mmproxyUsdtBalance, 6)}`) + console.log(`mmproxyUsdtAllowance: ${ethers.utils.formatUnits(mmproxyUsdtAllowance, 6)}`) + console.log(`mmproxy: ${mmproxy.address}`) + expect(mmproxy.address).is.not.null + const mockMarkerMakerConfigUpdater = new Updater({ + name: 'mockMarkerMakerConfigUpdater', + updater() { + return Promise.resolve({}) + }, + }) + const cacheResult = { + mmId: 1, + mmProxyContractAddress: mmproxy.address.toLowerCase(), // sign for v4 MMP contract + tokenlonExchangeContractAddress: '0xd489f1684cf5e78d933e254bd7ac8a9a6a70d491', + exchangeContractAddress: '0x30589010550762d2f0d06f650d8e8b6ade6dbf4b', + userProxyContractAddress: '0x25657705a6be20511687d483f2fccfb2d92f6033', + wethContractAddress: WETH[chainId].toLowerCase(), + orderExpirationSeconds: 600, + feeFactor: 30, + addressBookV5: { + Tokenlon: '0x085966eE3E32A0Da16467569512535D38626B547', + PMM: '0x7bd7d025D4231aAD1233967b527FFd7416410257', + AMMWrapper: '0xCF011536f10e85e376E70905EED4CA9eA8Cded34', + RFQ: '0x117CAf73eB142eDC431E707DC33D4dfeF7c5BAd0', + RFQV2: '0xaE5FDd548E5B107C54E5c0D36952fB8a089f10C7', + }, + } + mockMarkerMakerConfigUpdater.cacheResult = cacheResult + updaterStack['markerMakerConfigUpdater'] = mockMarkerMakerConfigUpdater + const signedOrderResp = await newOrder({ + walletType: WalletType.MMP_VERSION_4, + signer: mmpSigner, + chainID: chainId, + quoter: { + getPrice: () => { + return Promise.resolve({ + result: true, + exchangeable: true, + minAmount: 0, + maxAmount: 1000, + price: 1, + quoteId: 'echo-testing-8888', + }) + }, + }, + query: { + base: 'ETH', + quote: 'USDT', + side: 'SELL', + amount: 0.1, + uniqId: 'testing-1111', + userAddr: userAddr, + protocol: Protocol.RFQV2, + }, + }) + expect(signedOrderResp).is.not.null + // verify data object + const order = signedOrderResp.order + console.log(order) + expect(order).is.not.null + expect(order.protocol).eq(Protocol.RFQV2) + expect(order.quoteId).eq('1--echo-testing-8888') + expect(order.makerAddress).eq(mmproxy.address.toLowerCase()) + expect(order.makerAssetAmount).eq('100000') + expect(order.makerAssetAddress).eq('0xdac17f958d2ee523a2206206994597c13d831ec7') + expect(order.makerAssetData).eq( + '0xf47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7' + ) + expect(order.takerAddress).eq(userAddr) + expect(order.takerAssetAmount).eq('100000000000000000') + expect(order.takerAssetAddress).eq(ZERO[chainId].toLowerCase()) + expect(order.takerAssetData).eq( + `0xf47261b0000000000000000000000000${ZERO[chainId].toLowerCase().slice(2)}` + ) + expect(order.senderAddress).eq('0xd489f1684cf5e78d933e254bd7ac8a9a6a70d491') + expect(order.feeRecipientAddress).eq('0xb9e29984fe50602e7a619662ebed4f90d93824c7') + expect(order.exchangeAddress).eq('0x30589010550762d2f0d06f650d8e8b6ade6dbf4b') + // The following fields are to be compatible `Order` struct. + expect(order.makerFee).eq('0') + expect(order.takerFee).eq('0') + // verify signature type + const sigBytes = utils.arrayify(signedOrderResp.order.makerWalletSignature) + expect(sigBytes.length).eq(88) + expect(sigBytes[87]).eq(SignatureType.Wallet) + // verify random values + expect(signedOrderResp.order.salt.length > 0).is.true + expect(Number(signedOrderResp.order.expirationTimeSeconds) > 0).is.true + const rfqAddr = updaterStack['markerMakerConfigUpdater'].cacheResult.addressBookV5.RFQV2 + const orderHash = getOfferSignDigest(toOffer(signedOrderResp.order), chainId, rfqAddr) + const message = ethUtils.bufferToHex( + Buffer.concat([ + ethUtils.toBuffer(orderHash), + ethUtils.toBuffer(userAddr.toLowerCase()), + ethUtils.toBuffer(order.feeFactor > 255 ? order.feeFactor : [0, order.feeFactor]), + ]) + ) + const v = utils.hexlify(sigBytes.slice(0, 1)) + const r = utils.hexlify(sigBytes.slice(1, 33)) + const s = utils.hexlify(sigBytes.slice(33, 65)) + const recovered = utils.verifyMessage(utils.arrayify(message), { + v: parseInt(v), + r: r, + s: s, + }) + expect(recovered.toLowerCase()).eq(mmpSigner.address.toLowerCase()) + }).timeout(360000) + + it('should sign rfqv2 order for a standard ERC1271 MMP contract', async () => { + const ethersNetwork = await ethers.provider.getNetwork() + const chainId = ethersNetwork.chainId + const usdtHolder = await ethers.provider.getSigner(usdtHolders[chainId]) + const usdt = await ethers.getContractAt(ABI.IERC20, USDT[chainId]) + const [deployer, ethHolder] = await ethers.getSigners() + const privateKey = crypto.randomBytes(32) + const user = new ethers.Wallet(privateKey, ethers.provider) + const userAddr = user.address.toLowerCase() + await ethHolder.sendTransaction({ + to: userAddr, + value: ethers.utils.parseEther('10'), + }) + const mmpSigner = Wallet.createRandom() + console.log(`mmpSigner: ${mmpSigner.address}`) + const mmproxy: Contract = await ( + await ethers.getContractFactory('MarketMakerProxy', deployer) + ).deploy(mmpSigner.address) + await usdt.connect(usdtHolder).transfer(mmproxy.address, ethers.utils.parseUnits('1000', 6)) + await mmproxy.connect(deployer).setAllowance([USDT[chainId]], AllowanceTarget[chainId]) + const mmproxyUsdtBalance = await usdt.balanceOf(mmproxy.address) + const mmproxyUsdtAllowance = await usdt.allowance(mmproxy.address, AllowanceTarget[chainId]) + console.log(`mmproxyUsdtBalance: ${ethers.utils.formatUnits(mmproxyUsdtBalance, 6)}`) + console.log(`mmproxyUsdtAllowance: ${ethers.utils.formatUnits(mmproxyUsdtAllowance, 6)}`) + console.log(`mmproxy: ${mmproxy.address}`) + expect(mmproxy.address).is.not.null + const mockMarkerMakerConfigUpdater = new Updater({ + name: 'mockMarkerMakerConfigUpdater', + updater() { + return Promise.resolve({}) + }, + }) + const cacheResult = { + mmId: 1, + mmProxyContractAddress: mmproxy.address.toLowerCase(), // sign for v4 MMP contract + tokenlonExchangeContractAddress: '0xd489f1684cf5e78d933e254bd7ac8a9a6a70d491', + exchangeContractAddress: '0x30589010550762d2f0d06f650d8e8b6ade6dbf4b', + userProxyContractAddress: '0x25657705a6be20511687d483f2fccfb2d92f6033', + wethContractAddress: WETH[chainId].toLowerCase(), + orderExpirationSeconds: 600, + feeFactor: 30, + addressBookV5: { + Tokenlon: '0x085966eE3E32A0Da16467569512535D38626B547', + PMM: '0x7bd7d025D4231aAD1233967b527FFd7416410257', + AMMWrapper: '0xCF011536f10e85e376E70905EED4CA9eA8Cded34', + RFQ: '0x117CAf73eB142eDC431E707DC33D4dfeF7c5BAd0', + RFQV2: '0xaE5FDd548E5B107C54E5c0D36952fB8a089f10C7', + }, + } + mockMarkerMakerConfigUpdater.cacheResult = cacheResult + updaterStack['markerMakerConfigUpdater'] = mockMarkerMakerConfigUpdater + const signedOrderResp = await newOrder({ + walletType: WalletType.ERC1271_EIP712_EIP191, + signer: mmpSigner, + chainID: chainId, + quoter: { + getPrice: () => { + return Promise.resolve({ + result: true, + exchangeable: true, + minAmount: 0, + maxAmount: 1000, + price: 1, + quoteId: 'echo-testing-8888', + }) + }, + }, + query: { + base: 'ETH', + quote: 'USDT', + side: 'SELL', + amount: 0.1, + uniqId: 'testing-1111', + userAddr: userAddr, + protocol: Protocol.RFQV2, + }, + }) + expect(signedOrderResp).is.not.null + // verify data object + const order = signedOrderResp.order + console.log(order) + expect(order).is.not.null + expect(order.protocol).eq(Protocol.RFQV2) + expect(order.quoteId).eq('1--echo-testing-8888') + expect(order.makerAddress).eq(mmproxy.address.toLowerCase()) + expect(order.makerAssetAmount).eq('100000') + expect(order.makerAssetAddress).eq('0xdac17f958d2ee523a2206206994597c13d831ec7') + expect(order.makerAssetData).eq( + '0xf47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7' + ) + expect(order.takerAddress).eq(userAddr) + expect(order.takerAssetAmount).eq('100000000000000000') + expect(order.takerAssetAddress).eq(ZERO[chainId].toLowerCase()) + expect(order.takerAssetData).eq( + `0xf47261b0000000000000000000000000${ZERO[chainId].toLowerCase().slice(2)}` + ) + expect(order.senderAddress).eq('0xd489f1684cf5e78d933e254bd7ac8a9a6a70d491') + expect(order.feeRecipientAddress).eq('0xb9e29984fe50602e7a619662ebed4f90d93824c7') + expect(order.exchangeAddress).eq('0x30589010550762d2f0d06f650d8e8b6ade6dbf4b') + // The following fields are to be compatible `Order` struct. + expect(order.makerFee).eq('0') + expect(order.takerFee).eq('0') + // verify signature type + const sigBytes = utils.arrayify(signedOrderResp.order.makerWalletSignature) + expect(sigBytes.length).eq(66) + expect(sigBytes[65]).eq(SignatureType.WalletBytes32) + // verify random values + expect(signedOrderResp.order.salt.length > 0).is.true + expect(Number(signedOrderResp.order.expirationTimeSeconds) > 0).is.true + const rfqAddr = updaterStack['markerMakerConfigUpdater'].cacheResult.addressBookV5.RFQV2 + const orderSignDigest = getOfferSignDigest(toOffer(signedOrderResp.order), chainId, rfqAddr) + const r = utils.hexlify(sigBytes.slice(0, 32)) + const s = utils.hexlify(sigBytes.slice(32, 64)) + const v = utils.hexlify(sigBytes.slice(64, 65)) + console.log(`r: ${r}`) + console.log(`s: ${s}`) + console.log(`v: ${v}`) + const recovered = utils.verifyMessage(utils.arrayify(orderSignDigest), { + v: parseInt(v), + r: r, + s: s, + }) + expect(recovered.toLowerCase()).eq(mmpSigner.address.toLowerCase()) + console.log(`recovered.toLowerCase(): ${recovered.toLowerCase()}`) + console.log(`mmpSigner.address.toLowerCase(): ${mmpSigner.address.toLowerCase()}`) + }).timeout(360000) + it('should sign rfqv2 order by EOA', async function () { + const userAddr = Wallet.createRandom().address.toLowerCase() + const signedOrderResp = await newOrder({ + walletType: WalletType.EOA, + signer: signer, + chainID: chainId, + quoter: { + getPrice: () => { + return Promise.resolve({ + result: true, + exchangeable: true, + minAmount: 0, + maxAmount: 1000, + price: 1, + quoteId: 'echo-testing-8888', + }) + }, + }, + query: { + base: 'ETH', + quote: 'USDT', + side: 'SELL', + amount: 0.1, + uniqId: 'testing-1111', + userAddr: userAddr, + protocol: Protocol.RFQV2, + }, + }) + expect(signedOrderResp).is.not.null + // verify data object + const order = signedOrderResp.order + console.log(order) + expect(order).is.not.null + expect(order.protocol).eq(Protocol.RFQV2) + expect(order.quoteId).eq('1--echo-testing-8888') + expect(order.makerAddress).eq(signer.address.toLowerCase()) + expect(order.makerAssetAmount).eq('100000') + expect(order.makerAssetAddress).eq('0xdac17f958d2ee523a2206206994597c13d831ec7') + expect( + order.makerAssetData, + '0xf47261b0000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7' + ) + expect(order.takerAddress).eq(userAddr) + expect(order.takerAssetAmount).eq('100000000000000000') + expect(order.takerAssetAddress).eq(ZERO[chainId].toLowerCase()) + expect( + order.takerAssetData, + `0xf47261b0000000000000000000000000${ZERO[chainId].toLowerCase().slice(2)}` + ) + expect(order.senderAddress).eq('0xd489f1684cf5e78d933e254bd7ac8a9a6a70d491') + expect(order.feeRecipientAddress).eq('0xb9e29984fe50602e7a619662ebed4f90d93824c7') + expect(order.exchangeAddress).eq('0x30589010550762d2f0d06f650d8e8b6ade6dbf4b') + // The following fields are to be compatible `Order` struct. + expect(order.makerFee).eq('0') + expect(order.takerFee).eq('0') + // verify signature type + const sigBytes = utils.arrayify(signedOrderResp.order.makerWalletSignature) + expect(sigBytes.length).eq(66) + expect(sigBytes[65]).eq(SignatureType.EIP712) + // verify signature + const rfqAddr = updaterStack['markerMakerConfigUpdater'].cacheResult.addressBookV5.RFQV2 + const signedOrder = toOffer(signedOrderResp.order) + const domain = { + name: 'Tokenlon', + version: 'v5', + chainId: chainId, + verifyingContract: rfqAddr, + } + // The named list of all type definitions + const types = { + Offer: [ + { name: 'taker', type: 'address' }, + { name: 'maker', type: 'address' }, + { name: 'takerToken', type: 'address' }, + { name: 'takerTokenAmount', type: 'uint256' }, + { name: 'makerToken', type: 'address' }, + { name: 'makerTokenAmount', type: 'uint256' }, + { name: 'feeFactor', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, + { name: 'salt', type: 'uint256' }, + ], + } + // The data to sign + const value = { + taker: signedOrder.taker, + maker: signedOrder.maker, + takerToken: signedOrder.takerToken, + takerTokenAmount: signedOrder.takerTokenAmount.toString(), + makerToken: signedOrder.makerToken, + makerTokenAmount: signedOrder.makerTokenAmount.toString(), + feeFactor: signedOrder.feeFactor.toString(), + expiry: signedOrder.expiry.toString(), + salt: signedOrder.salt.toString(), + } + const recovered = ethers.utils.verifyTypedData( + domain, + types, + value, + signedOrderResp.order.makerWalletSignature.slice(0, -2) + ) + expect(recovered.toLowerCase()).eq(signer.address.toLowerCase()) + // verify random values + expect(signedOrderResp.order.salt.length > 0).is.true + expect(Number(signedOrderResp.order.expirationTimeSeconds) > 0).is.true + }) it('should sign rfqv1 order for MMPv4', async () => { const ethersNetwork = await ethers.provider.getNetwork() const chainId = ethersNetwork.chainId @@ -406,10 +768,11 @@ describe('NewOrder', function () { orderExpirationSeconds: 600, feeFactor: 30, addressBookV5: { - Tokenlon: '0xF1eC89551112da48C3b43B5a167AF0b2a7Cc2614', + Tokenlon: '0x085966eE3E32A0Da16467569512535D38626B547', PMM: '0x7bd7d025D4231aAD1233967b527FFd7416410257', AMMWrapper: '0xCF011536f10e85e376E70905EED4CA9eA8Cded34', - RFQ: '0xfD6C2d2499b1331101726A8AC68CCc9Da3fAB54F', + RFQ: '0x117CAf73eB142eDC431E707DC33D4dfeF7c5BAd0', + RFQV2: '0xaE5FDd548E5B107C54E5c0D36952fB8a089f10C7', }, } mockMarkerMakerConfigUpdater.cacheResult = cacheResult @@ -417,7 +780,7 @@ describe('NewOrder', function () { const signedOrderResp = await newOrder({ walletType: WalletType.MMP_VERSION_4, signer: mmpSigner, - chainID: 1, + chainID: chainId, quoter: { getPrice: () => { return Promise.resolve({ @@ -473,7 +836,7 @@ describe('NewOrder', function () { expect(signedOrderResp.order.salt.length > 0).is.true expect(Number(signedOrderResp.order.expirationTimeSeconds) > 0).is.true const rfqAddr = updaterStack['markerMakerConfigUpdater'].cacheResult.addressBookV5.RFQ - const orderHash = getOrderSignDigest(toRFQOrder(signedOrderResp.order), 1, rfqAddr) + const orderHash = getOrderSignDigest(toRFQOrder(signedOrderResp.order), chainId, rfqAddr) const message = ethUtils.bufferToHex( Buffer.concat([ ethUtils.toBuffer(orderHash), @@ -489,30 +852,9 @@ describe('NewOrder', function () { r: r, s: s, }) + console.log(`recovered: ${recovered}`) + console.log(`mmpSigner.address: ${mmpSigner.address}`) expect(recovered.toLowerCase()).eq(mmpSigner.address.toLowerCase()) - - const tokenlonSigner = new TokenlonSigner(user) - const signResult = await tokenlonSigner.signOrder(order, { - receiverAddress: user.address, - }) - console.log(`signResult`) - console.log(signResult) - const userUsdtBalanceBefore = await usdt.balanceOf(user.address) - const txRequest = await tokenlonSigner.getRawTransactionFromOrder(signResult, { - receiverAddress: user.address, - }) - console.log(txRequest) - const tx = await tokenlonSigner.sendTransaction(txRequest) - const receipt = await tx.wait() - console.log(receipt) - const userUsdtBalanceAfter = await usdt.balanceOf(user.address) - console.log( - `user got ${ethers.utils.formatUnits( - userUsdtBalanceAfter.sub(userUsdtBalanceBefore), - 6 - )} usdt` - ) - expect(Number(userUsdtBalanceAfter.sub(userUsdtBalanceBefore))).gt(0) }).timeout(360000) it('should sign rfqv1 order for a standard ERC1271 MMP contract', async () => { const ethersNetwork = await ethers.provider.getNetwork() @@ -556,10 +898,11 @@ describe('NewOrder', function () { orderExpirationSeconds: 600, feeFactor: 30, addressBookV5: { - Tokenlon: '0xF1eC89551112da48C3b43B5a167AF0b2a7Cc2614', + Tokenlon: '0x085966eE3E32A0Da16467569512535D38626B547', PMM: '0x7bd7d025D4231aAD1233967b527FFd7416410257', AMMWrapper: '0xCF011536f10e85e376E70905EED4CA9eA8Cded34', - RFQ: '0xfD6C2d2499b1331101726A8AC68CCc9Da3fAB54F', + RFQ: '0x117CAf73eB142eDC431E707DC33D4dfeF7c5BAd0', + RFQV2: '0xaE5FDd548E5B107C54E5c0D36952fB8a089f10C7', }, } mockMarkerMakerConfigUpdater.cacheResult = cacheResult @@ -567,7 +910,7 @@ describe('NewOrder', function () { const signedOrderResp = await newOrder({ walletType: WalletType.ERC1271_EIP712_EIP191, signer: mmpSigner, - chainID: 1, + chainID: chainId, quoter: { getPrice: () => { return Promise.resolve({ @@ -623,7 +966,7 @@ describe('NewOrder', function () { expect(signedOrderResp.order.salt.length > 0).is.true expect(Number(signedOrderResp.order.expirationTimeSeconds) > 0).is.true const rfqAddr = updaterStack['markerMakerConfigUpdater'].cacheResult.addressBookV5.RFQ - const orderSignDigest = getOrderSignDigest(toRFQOrder(signedOrderResp.order), 1, rfqAddr) + const orderSignDigest = getOrderSignDigest(toRFQOrder(signedOrderResp.order), chainId, rfqAddr) const r = utils.hexlify(sigBytes.slice(0, 32)) const s = utils.hexlify(sigBytes.slice(32, 64)) const v = utils.hexlify(sigBytes.slice(64, 65)) @@ -644,7 +987,7 @@ describe('NewOrder', function () { const signedOrderResp = await newOrder({ walletType: WalletType.EOA, signer: signer, - chainID: 1, + chainID: chainId, quoter: { getPrice: () => { return Promise.resolve({ @@ -833,7 +1176,7 @@ describe('NewOrder', function () { }) it('Should forward unsigned orders to signing service', async () => { // const url = `http://localhost:3000` - const rfqAddr = '0xfD6C2d2499b1331101726A8AC68CCc9Da3fAB54F' + const rfqAddr = '0x117CAf73eB142eDC431E707DC33D4dfeF7c5BAd0' const order = { takerAddress: '0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69', makerAddress: '0x86B9F429C3Ef44c599EB560Eb531A0E3f2E36f64',