Skip to main content
返回应用

EIP-712 订单签名

Conviction 的所有订单都通过 EIP-712 typed data 签署。普通用户无需关心,因为 UI 会自动处理这一过程。本页是面向以程序方式直接挂单的机器人·做市商的参考资料。

#域(EIP-712 Domain Separator)

{
  name: 'Conviction CTF Exchange', // 必须 byte-for-byte 完全一致
  version: '1',
  chainId: 56,                           // BSC mainnet
  verifyingContract: CTF_EXCHANGE_ADDRESS, // 因环境而异
}
域 name 一个字符都不能错

即便有 typo,ECDSA 本身也会通过,因此在客户端时看起来正常。但由于与合约一侧的域不同,会在成交时 reverts。

verifyingContract 是 BSC 主网的 CTFExchange 地址。请从服务端响应或环境变量中获取 —— 不要硬编码到客户端。

#Order 结构

字段类型含义
saltuint256随机标识符(每次重新签名都新生成)
makeraddress用户地址。通过 EIP-7702 委托,EOA 即 AA,因此 maker == signer
signeraddress用户的 EOA —— 与 maker 相同的地址。
takeraddress0x0000...(公开挂单)
tokenIduint256结果的 position ID(= outcome 的 position_id
makerAmountuint256maker 付出的资产的 wei 数量
takerAmountuint256maker 想要收到的资产的 wei 数量
expirationuint256订单过期 unix 秒。0 = 无过期
nonceuint256每次用 CTFExchange.nonces(maker) 重新获取
feeRateBpsuint256手续费上限(basis points)
sideuint8BUY = 0SELL = 1
signatureTypeuint8EOA = 0(用户订单)

#EIP-712 type 定义

const types = {
  Order: [
    { name: 'salt',          type: 'uint256' },
    { name: 'maker',         type: 'address' },
    { name: 'signer',        type: 'address' },
    { name: 'taker',         type: 'address' },
    { name: 'tokenId',       type: 'uint256' },
    { name: 'makerAmount',   type: 'uint256' },
    { name: 'takerAmount',   type: 'uint256' },
    { name: 'expiration',    type: 'uint256' },
    { name: 'nonce',         type: 'uint256' },
    { name: 'feeRateBps',    type: 'uint256' },
    { name: 'side',          type: 'uint8'   },
    { name: 'signatureType', type: 'uint8'   },
  ],
};

#单个订单签名示例(viem)

import { parseUnits } from 'viem';
 
const SHARES = parseUnits('100', 18);
 
const order = {
  salt: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
  maker: eoaAddress,                 // EIP-7702: EOA 即 AA
  signer: eoaAddress,                // 相同地址
  taker: '0x0000000000000000000000000000000000000000',
  tokenId: BigInt(outcome.position_id),
  // BUY 100 shares at 0.5 USDT each
  makerAmount: parseUnits('50', 18),       // 100 × 0.5 = 50 USDT
  takerAmount: SHARES,                     // 100 shares
  expiration: BigInt(Math.floor(Date.now() / 1000) + 3600),
  nonce: await ctfExchange.read.nonces([eoaAddress]),
  feeRateBps: BigInt(market.platformFee),
  side: 0n,                                 // BUY
  signatureType: 0,                         // EOA
};
 
const signature = await walletClient.signTypedData({
  account: eoaAddress,
  domain,
  types: { Order: types.Order },
  primaryType: 'Order',
  message: order,
});
 
// POST /orders
await fetch('/api/orders', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    marketId: market.id,
    outcomeId: outcome.id,
    side: 'buy',
    type: 'limit',
    price: 0.5,
    shares: 100,
    userId: user.id,
    salt: '0x' + order.salt.toString(16),
    signature,
    signerAddress: order.signer,
    makerAddress: order.maker,
    expiration: order.expiration.toString(),
    signatureType: 0,
    tokenId: order.tokenId.toString(),
    makerAmount: order.makerAmount.toString(),
    takerAmount: order.takerAmount.toString(),
    feeRateBps: market.platformFee,
  }),
});

#makerAmount / takerAmount 计算规则

所有金额都是 18-decimal wei 整数。

SideMaker 付出Maker 收到makerAmounttakerAmount
BUYUSDT结果代币shares × price(USDT wei)shares(token wei)
SELL结果代币USDTshares(token wei)shares × price(USDT wei)
  • price 是 0..1 的 float,但在 wei-space 中以 price × 1e18 用于乘法。
  • 所有乘法都用 bigint 算术 —— 禁止 JS Number(1 USDT = 10^18,超出安全整数范围)。

#常见错误

以下四点最常见
  1. makersigner 填入不同地址。 在 EIP-7702 模型中,两者都必须是用户的 EOA 地址,保持一致。
  2. 缓存 nonce。 每次都用 CTFExchange.nonces(maker) 重新获取。一旦调用 incrementNonce(),此前 nonce 的所有未成交订单都会 reverts。
  3. 把 price 当作 percent 或 bps 发送。 载荷中的 price 是 0..1 的 float。
  4. 把 makerAmount / takerAmount 当作人类可读单位发送。 始终是 18-dec wei 的 decimal string。

#签名在成交时校验

  • 后端不校验签名 —— 它原样保存已签名的载荷,并在成交时提交到链上。
  • 错误的签名在成交时会让链上 ECDSA 复原返回另一个地址而 reverts → 该订单被标记为失败。
  • 因此签名步骤必须准确 —— 请避免上述四个错误。

#下一步

本页面是否有帮助?