分享一个简单的DEX项目代码及文档
Dex.top项目源码及文档分享
// DEx.top - Instant Trading on Chain
//
// Author: DEx.top Team
pragma solidity 0.4.21;
pragma experimental "v0.5.0";
interface Token {
function transfer(address to, uint256 value) external returns (bool success);
function transferFrom(address from, address to, uint256 value) external returns (bool success);
}
contract Dex2 {
//------------------------------ Struct Definitions: ---------------------------------------------
struct TokenInfo {
string symbol; // e.g., "ETH", "ADX"
address tokenAddr; // ERC20 token address
uint64 scaleFactor; // <original token amount> = <scaleFactor> x <DEx amountE8> / 1e8
uint minDeposit; // mininum deposit (original token amount) allowed for this token
}
struct TraderInfo {
address withdrawAddr;
uint8 feeRebatePercent; // range: [0, 100]
}
struct TokenAccount {
uint64 balanceE8; // available amount for trading
uint64 pendingWithdrawE8; // the amount to be transferred out from this contract to the trader
}
struct Order {
uint32 pairId; // <cashId>(16) <stockId>(16)
uint8 action; // 0 means BUY; 1 means SELL
uint8 ioc; // 0 means a regular order; 1 means an immediate-or-cancel (IOC) order
uint64 priceE8;
uint64 amountE8;
uint64 expireTimeSec;
}
struct Deposit {
address traderAddr;
uint16 tokenCode;
uint64 pendingAmountE8; // amount to be confirmed for trading purpose
}
struct DealInfo {
uint16 stockCode; // stock token code
uint16 cashCode; // cash token code
uint64 stockDealAmountE8;
uint64 cashDealAmountE8;
}
struct ExeStatus {
uint64 logicTimeSec; // logic timestamp for checking order expiration
uint64 lastOperationIndex; // index of the last executed operation
}
//----------------- Constants: -------------------------------------------------------------------
uint constant MAX_UINT256 = 2**256 - 1;
uint16 constant MAX_FEE_RATE_E4 = 60; // upper limit of fee rate is 0.6% (60 / 1e4)
// <original ETH amount in Wei> = <DEx amountE8> * <ETH_SCALE_FACTOR> / 1e8
uint64 constant ETH_SCALE_FACTOR = 10**18;
uint8 constant ACTIVE = 0;
uint8 constant CLOSED = 2;
bytes32 constant HASHTYPES =
keccak256('string title', 'address market_address', 'uint64 nonce', 'uint64 expire_time_sec',
'uint64 amount_e8', 'uint64 price_e8', 'uint8 immediate_or_cancel', 'uint8 action',
'uint16 cash_token_code', 'uint16 stock_token_code');
//----------------- States that cannot be changed once set: --------------------------------------
address public admin; // admin address, and it cannot be changed
mapping (uint16 => TokenInfo) public tokens; // mapping of token code to token information
//----------------- Other states: ----------------------------------------------------------------
uint8 public marketStatus; // market status: 0 - Active; 1 - Suspended; 2 - Closed
uint16 public makerFeeRateE4; // maker fee rate (* 10**4)
uint16 public takerFeeRateE4; // taker fee rate (* 10**4)
uint16 public withdrawFeeRateE4; // withdraw fee rate (* 10**4)
uint64 public lastDepositIndex; // index of the last deposit operation
ExeStatus public exeStatus; // status of operation execution
mapping (address => TraderInfo) public traders; // mapping of trade address to trader information
mapping (uint176 => TokenAccount) public accounts; // mapping of trader token key to its account information
mapping (uint224 => Order) public orders; // mapping of order key to order information
mapping (uint64 => Deposit) public deposits; // mapping of deposit index to deposit information
//------------------------------ Dex2 Events: ----------------------------------------------------
event DeployMarketEvent();
event ChangeMarketStatusEvent(uint8 status);
event SetTokenInfoEvent(uint16 tokenCode, string symbol, address tokenAddr, uint64 scaleFactor, uint minDeposit);
event SetWithdrawAddrEvent(address trader, address withdrawAddr);
event DepositEvent(address trader, uint16 tokenCode, string symbol, uint64 amountE8, uint64 depositIndex);
event WithdrawEvent(address trader, uint16 tokenCode, string symbol, uint64 amountE8, uint64 lastOpIndex);
event TransferFeeEvent(uint16 tokenCode, uint64 amountE8, address toAddr);
// `balanceE8` is the total balance after this deposit confirmation
event ConfirmDepositEvent(address trader, uint16 tokenCode, uint64 balanceE8);
// `amountE8` is the post-fee initiated withdraw amount
// `pendingWithdrawE8` is the total pending withdraw amount after this withdraw initiation
event InitiateWithdrawEvent(address trader, uint16 tokenCode, uint64 amountE8, uint64 pendingWithdrawE8);
event MatchOrdersEvent(address trader1, uint64 nonce1, address trader2, uint64 nonce2);
event HardCancelOrderEvent(address trader, uint64 nonce);
event SetFeeRatesEvent(uint16 makerFeeRateE4, uint16 takerFeeRateE4, uint16 withdrawFeeRateE4);
event SetFeeRebatePercentEvent(address trader, uint8 feeRebatePercent);
//------------------------------ Contract Initialization: ----------------------------------------
function Dex2(address admin_) public {
admin = admin_;
setTokenInfo(0 /*tokenCode*/, "ETH", 0 /*tokenAddr*/, ETH_SCALE_FACTOR, 0 /*minDeposit*/);
emit DeployMarketEvent();
}
//------------------------------ External Functions: ---------------------------------------------
function() external {
revert();
}
// Change the market status of DEX.
function changeMarketStatus(uint8 status_) external {
if (msg.sender != admin) revert();
if (marketStatus == CLOSED) revert(); // closed is forever
marketStatus = status_;
emit ChangeMarketStatusEvent(status_);
}
// Each trader can specify a withdraw address (but cannot change it later). Once a trader's
// withdraw address is set, following withdrawals of this trader will go to the withdraw address
// instead of the trader's address.
function setWithdrawAddr(address withdrawAddr) external {
if (withdrawAddr == 0) revert();
if (traders[msg.sender].withdrawAddr != 0) revert(); // cannot change withdrawAddr once set
traders[msg.sender].withdrawAddr = withdrawAddr;
emit SetWithdrawAddrEvent(msg.sender, withdrawAddr);
}
// Deposit ETH from msg.sender for the given trader.
function depositEth(address traderAddr) external payable {
if (marketStatus != ACTIVE) revert();
if (traderAddr == 0) revert();
if (msg.value < tokens[0].minDeposit) revert();
if (msg.data.length != 4 + 32) revert(); // length condition of param count
uint64 pendingAmountE8 = uint64(msg.value / (ETH_SCALE_FACTOR / 10**8)); // msg.value is in Wei
if (pendingAmountE8 == 0) revert();
uint64 depositIndex = ++lastDepositIndex;
setDeposits(depositIndex, traderAddr, 0, pendingAmountE8);
emit DepositEvent(traderAddr, 0, "ETH", pendingAmountE8, depositIndex);
}
// Deposit token (other than ETH) from msg.sender for a specified trader.
//
// After the deposit has been confirmed enough times on the blockchain, it will be added to the
// trader's token account for trading.
function depositToken(address traderAddr, uint16 tokenCode, uint originalAmount) external {
if (marketStatus != ACTIVE) revert();
if (traderAddr == 0) revert();
if (tokenCode == 0) revert(); // this function does not handle ETH
if (msg.data.length != 4 + 32 + 32 + 32) revert(); // length condition of param count
TokenInfo memory tokenInfo = tokens[tokenCode];
if (originalAmount < tokenInfo.minDeposit) revert();
if (tokenInfo.scaleFactor == 0) revert(); // unsupported token
// Need to make approval by calling Token(address).approve() in advance for ERC-20 Tokens.
if (!Token(tokenInfo.tokenAddr).transferFrom(msg.sender, this, originalAmount)) revert();
if (originalAmount > MAX_UINT256 / 10**8) revert(); // avoid overflow
uint amountE8 = originalAmount * 10**8 / uint(tokenInfo.scaleFactor);
if (amountE8 >= 2**64 || amountE8 == 0) revert();
uint64 depositIndex = ++lastDepositIndex;
setDeposits(depositIndex, traderAddr, tokenCode, uint64(amountE8));
emit DepositEvent(traderAddr, tokenCode, tokens[tokenCode].symbol, uint64(amountE8), depositIndex);
}
// Withdraw ETH from the contract.
function withdrawEth(address traderAddr) external {
if (traderAddr == 0) revert();
if (msg.data.length != 4 + 32) revert(); // length condition of param count
uint176 accountKey = uint176(traderAddr);
uint amountE8 = accounts[accountKey].pendingWithdrawE8;
if (amountE8 == 0) return;
// Write back to storage before making the transfer.
accounts[accountKey].pendingWithdrawE8 = 0;
uint truncatedWei = amountE8 * (ETH_SCALE_FACTOR / 10**8);
address withdrawAddr = traders[traderAddr].withdrawAddr;
if (withdrawAddr == 0) withdrawAddr = traderAddr;
withdrawAddr.transfer(truncatedWei);
emit WithdrawEvent(traderAddr, 0, "ETH", uint64(amountE8), exeStatus.lastOperationIndex);
}
// Withdraw token (other than ETH) from the contract.
function withdrawToken(address traderAddr, uint16 tokenCode) external {
if (traderAddr == 0) revert();
if (tokenCode == 0) revert(); // this function does not handle ETH
if (msg.data.length != 4 + 32 + 32) revert(); // length condition of param count
TokenInfo memory tokenInfo = tokens[tokenCode];
if (tokenInfo.scaleFactor == 0) revert(); // unsupported token
uint176 accountKey = uint176(tokenCode) << 160 | uint176(traderAddr);
uint amountE8 = accounts[accountKey].pendingWithdrawE8;
if (amountE8 == 0) return;
// Write back to storage before making the transfer.
accounts[accountKey].pendingWithdrawE8 = 0;
uint truncatedAmount = amountE8 * uint(tokenInfo.scaleFactor) / 10**8;
address withdrawAddr = traders[traderAddr].withdrawAddr;
if (withdrawAddr == 0) withdrawAddr = traderAddr;
if (!Token(tokenInfo.tokenAddr).transfer(withdrawAddr, truncatedAmount)) revert();
emit WithdrawEvent(traderAddr, tokenCode, tokens[tokenCode].symbol, uint64(amountE8),
exeStatus.lastOperationIndex);
}
// Transfer the collected fee out of the contract.
function transferFee(uint16 tokenCode, uint64 amountE8, address toAddr) external {
if (msg.sender != admin) revert();
if (toAddr == 0) revert();
if (msg.data.length != 4 + 32 + 32 + 32) revert();
TokenAccount memory feeAccount = accounts[uint176(tokenCode) << 160];
uint64 withdrawE8 = feeAccount.pendingWithdrawE8;
if (amountE8 < withdrawE8) {
withdrawE8 = amountE8;
}
feeAccount.pendingWithdrawE8 -= withdrawE8;
accounts[uint176(tokenCode) << 160] = feeAccount;
TokenInfo memory tokenInfo = tokens[tokenCode];
uint originalAmount = uint(withdrawE8) * uint(tokenInfo.scaleFactor) / 10**8;
if (tokenCode == 0) { // ETH
toAddr.transfer(originalAmount);
} else {
if (!Token(tokenInfo.tokenAddr).transfer(toAddr, originalAmount)) revert();
}
emit TransferFeeEvent(tokenCode, withdrawE8, toAddr);
}
// Replay the trading sequence from the off-chain ledger exactly onto the on-chain ledger.
function exeSequence(uint header, uint[] body) external {
if (msg.sender != admin) revert();
uint64 nextOperationIndex = uint64(header);
if (nextOperationIndex != exeStatus.lastOperationIndex + 1) revert(); // check sequence index
uint64 newLogicTimeSec = uint64(header >> 64);
if (newLogicTimeSec < exeStatus.logicTimeSec) revert();
for (uint i = 0; i < body.length; nextOperationIndex++) {
uint bits = body[i];
uint opcode = bits & 0xFFFF;
bits >>= 16;
if ((opcode >> 8) != 0xDE) revert(); // check the magic number
// ConfirmDeposit: <depositIndex>(64)
if (opcode == 0xDE01) {
confirmDeposit(uint64(bits));
i += 1;
continue;
}
// InitiateWithdraw: <amountE8>(64) <tokenCode>(16) <traderAddr>(160)
if (opcode == 0xDE02) {
initiateWithdraw(uint176(bits), uint64(bits >> 176));
i += 1;
continue;
}
//-------- The rest operation types are allowed only when the market is active ---------
if (marketStatus != ACTIVE) revert();
// MatchOrders
if (opcode == 0xDE03) {
uint8 v1 = uint8(bits);
bits >>= 8; // bits is now the key of the maker order
Order memory makerOrder;
if (v1 == 0) { // order already in storage
if (i + 1 >= body.length) revert(); // at least 1 body element left
makerOrder = orders[uint224(bits)];
i += 1;
} else {
if (orders[uint224(bits)].pairId != 0) revert(); // order must not be already in storage
if (i + 4 >= body.length) revert(); // at least 4 body elements left
makerOrder = parseNewOrder(uint224(bits) /*makerOrderKey*/, v1, body, i);
i += 4;
}
uint8 v2 = uint8(body[i]);
uint224 takerOrderKey = uint224(body[i] >> 8);
Order memory takerOrder;
if (v2 == 0) { // order already in storage
takerOrder = orders[takerOrderKey];
i += 1;
} else {
if (orders[takerOrderKey].pairId != 0) revert(); // order must not be already in storage
if (i + 3 >= body.length) revert(); // at least 3 body elements left
takerOrder = parseNewOrder(takerOrderKey, v2, body, i);
i += 4;
}
matchOrder(uint224(bits) /*makerOrderKey*/, makerOrder, takerOrderKey, takerOrder);
continue;
}
// HardCancelOrder: <nonce>(64) <traderAddr>(160)
if (opcode == 0xDE04) {
hardCancelOrder(uint224(bits) /*orderKey*/);
i += 1;
continue;
}
// SetFeeRates: <withdrawFeeRateE4>(16) <takerFeeRateE4>(16) <makerFeeRateE4>(16)
if (opcode == 0xDE05) {
setFeeRates(uint16(bits), uint16(bits >> 16), uint16(bits >> 32));
i += 1;
continue;
}
// SetFeeRebatePercent: <rebatePercent>(8) <traderAddr>(160)
if (opcode == 0xDE06) {
setFeeRebatePercent(address(bits) /*traderAddr*/, uint8(bits >> 160) /*rebatePercent*/);
i += 1;
continue;
}
} // for loop
setExeStatus(newLogicTimeSec, nextOperationIndex - 1);
} // function exeSequence
//------------------------------ Public Functions: -----------------------------------------------
// Set information of a token.
function setTokenInfo(uint16 tokenCode, string symbol, address tokenAddr, uint64 scaleFactor,
uint minDeposit) public {
if (msg.sender != admin) revert();
if (marketStatus != ACTIVE) revert();
if (scaleFactor == 0) revert();
TokenInfo memory info = tokens[tokenCode];
if (info.scaleFactor != 0) { // this token already exists
// For an existing token only the minDeposit field can be updated.
tokens[tokenCode].minDeposit = minDeposit;
emit SetTokenInfoEvent(tokenCode, info.symbol, info.tokenAddr, info.scaleFactor, minDeposit);
return;
}
tokens[tokenCode].symbol = symbol;
tokens[tokenCode].tokenAddr = tokenAddr;
tokens[tokenCode].scaleFactor = scaleFactor;
tokens[tokenCode].minDeposit = minDeposit;
emit SetTokenInfoEvent(tokenCode, symbol, tokenAddr, scaleFactor, minDeposit);
}
//------------------------------ Private Functions: ----------------------------------------------
function setDeposits(uint64 depositIndex, address traderAddr, uint16 tokenCode, uint64 amountE8) private {
deposits[depositIndex].traderAddr = traderAddr;
deposits[depositIndex].tokenCode = tokenCode;
deposits[depositIndex].pendingAmountE8 = amountE8;
}
function setExeStatus(uint64 logicTimeSec, uint64 lastOperationIndex) private {
exeStatus.logicTimeSec = logicTimeSec;
exeStatus.lastOperationIndex = lastOperationIndex;
}
function confirmDeposit(uint64 depositIndex) private {
Deposit memory deposit = deposits[depositIndex];
uint176 accountKey = (uint176(deposit.tokenCode) << 160) | uint176(deposit.traderAddr);
TokenAccount memory account = accounts[accountKey];
// Check that pending amount is non-zero and no overflow would happen.
if (account.balanceE8 + deposit.pendingAmountE8 <= account.balanceE8) revert();
account.balanceE8 += deposit.pendingAmountE8;
deposits[depositIndex].pendingAmountE8 = 0;
accounts[accountKey].balanceE8 += deposit.pendingAmountE8;
emit ConfirmDepositEvent(deposit.traderAddr, deposit.tokenCode, account.balanceE8);
}
function initiateWithdraw(uint176 tokenAccountKey, uint64 amountE8) private {
uint64 balanceE8 = accounts[tokenAccountKey].balanceE8;
uint64 pendingWithdrawE8 = accounts[tokenAccountKey].pendingWithdrawE8;
if (balanceE8 < amountE8 || amountE8 == 0) revert();
balanceE8 -= amountE8;
uint64 feeE8 = calcFeeE8(amountE8, withdrawFeeRateE4, address(tokenAccountKey));
amountE8 -= feeE8;
if (pendingWithdrawE8 + amountE8 < amountE8) revert(); // check overflow
pendingWithdrawE8 += amountE8;
accounts[tokenAccountKey].balanceE8 = balanceE8;
accounts[tokenAccountKey].pendingWithdrawE8 = pendingWithdrawE8;
// Note that the fee account has a dummy trader address of 0.
if (accounts[tokenAccountKey & (0xffff << 160)].pendingWithdrawE8 + feeE8 >= feeE8) { // no overflow
accounts[tokenAccountKey & (0xffff << 160)].pendingWithdrawE8 += feeE8;
}
emit InitiateWithdrawEvent(address(tokenAccountKey), uint16(tokenAccountKey >> 160) /*tokenCode*/,
amountE8, pendingWithdrawE8);
}
function getDealInfo(uint32 pairId, uint64 priceE8, uint64 amount1E8, uint64 amount2E8)
private pure returns (DealInfo deal) {
deal.stockCode = uint16(pairId);
deal.cashCode = uint16(pairId >> 16);
if (deal.stockCode == deal.cashCode) revert(); // we disallow homogeneous trading
deal.stockDealAmountE8 = amount1E8 < amount2E8 ? amount1E8 : amount2E8;
uint cashDealAmountE8 = uint(priceE8) * uint(deal.stockDealAmountE8) / 10**8;
if (cashDealAmountE8 >= 2**64) revert();
deal.cashDealAmountE8 = uint64(cashDealAmountE8);
}
function calcFeeE8(uint64 amountE8, uint feeRateE4, address traderAddr)
private view returns (uint64) {
uint feeE8 = uint(amountE8) * feeRateE4 / 10000;
feeE8 -= feeE8 * uint(traders[traderAddr].feeRebatePercent) / 100;
return uint64(feeE8);
}
function settleAccounts(DealInfo deal, address traderAddr, uint feeRateE4, bool isBuyer) private {
uint16 giveTokenCode = isBuyer ? deal.cashCode : deal.stockCode;
uint16 getTokenCode = isBuyer ? deal.stockCode : deal.cashCode;
uint64 giveAmountE8 = isBuyer ? deal.cashDealAmountE8 : deal.stockDealAmountE8;
uint64 getAmountE8 = isBuyer ? deal.stockDealAmountE8 : deal.cashDealAmountE8;
uint176 giveAccountKey = uint176(giveTokenCode) << 160 | uint176(traderAddr);
uint176 getAccountKey = uint176(getTokenCode) << 160 | uint176(traderAddr);
uint64 feeE8 = calcFeeE8(getAmountE8, feeRateE4, traderAddr);
getAmountE8 -= feeE8;
// Check overflow.
if (accounts[giveAccountKey].balanceE8 < giveAmountE8) revert();
if (accounts[getAccountKey].balanceE8 + getAmountE8 < getAmountE8) revert();
// Write storage.
accounts[giveAccountKey].balanceE8 -= giveAmountE8;
accounts[getAccountKey].balanceE8 += getAmountE8;
if (accounts[uint176(getTokenCode) << 160].pendingWithdrawE8 + feeE8 >= feeE8) { // no overflow
accounts[uint176(getTokenCode) << 160].pendingWithdrawE8 += feeE8;
}
}
function setOrders(uint224 orderKey, uint32 pairId, uint8 action, uint8 ioc,
uint64 priceE8, uint64 amountE8, uint64 expireTimeSec) private {
orders[orderKey].pairId = pairId;
orders[orderKey].action = action;
orders[orderKey].ioc = ioc;
orders[orderKey].priceE8 = priceE8;
orders[orderKey].amountE8 = amountE8;
orders[orderKey].expireTimeSec = expireTimeSec;
}
function matchOrder(uint224 makerOrderKey, Order makerOrder,
uint224 takerOrderKey, Order takerOrder) private {
// Check trading conditions.
if (marketStatus != ACTIVE) revert();
if (makerOrderKey == takerOrderKey) revert(); // the two orders must not have the same key
if (makerOrder.pairId != takerOrder.pairId) revert();
if (makerOrder.action == takerOrder.action) revert();
if (makerOrder.priceE8 == 0 || takerOrder.priceE8 == 0) revert();
if (makerOrder.action == 0 && makerOrder.priceE8 < takerOrder.priceE8) revert();
if (takerOrder.action == 0 && takerOrder.priceE8 < makerOrder.priceE8) revert();
if (makerOrder.amountE8 == 0 || takerOrder.amountE8 == 0) revert();
if (makerOrder.expireTimeSec <= exeStatus.logicTimeSec) revert();
if (takerOrder.expireTimeSec <= exeStatus.logicTimeSec) revert();
DealInfo memory deal = getDealInfo(
makerOrder.pairId, makerOrder.priceE8, makerOrder.amountE8, takerOrder.amountE8);
// Update accounts.
settleAccounts(deal, address(makerOrderKey), makerFeeRateE4, (makerOrder.action == 0));
settleAccounts(deal, address(takerOrderKey), takerFeeRateE4, (takerOrder.action == 0));
// Update orders.
if (makerOrder.ioc == 1) { // IOC order
makerOrder.amountE8 = 0;
} else {
makerOrder.amountE8 -= deal.stockDealAmountE8;
}
if (takerOrder.ioc == 1) { // IOC order
takerOrder.amountE8 = 0;
} else {
takerOrder.amountE8 -= deal.stockDealAmountE8;
}
// Write orders back to storage.
setOrders(makerOrderKey, makerOrder.pairId, makerOrder.action, makerOrder.ioc,
makerOrder.priceE8, makerOrder.amountE8, makerOrder.expireTimeSec);
setOrders(takerOrderKey, takerOrder.pairId, takerOrder.action, takerOrder.ioc,
takerOrder.priceE8, takerOrder.amountE8, takerOrder.expireTimeSec);
emit MatchOrdersEvent(address(makerOrderKey), uint64(makerOrderKey >> 160) /*nonce*/,
address(takerOrderKey), uint64(takerOrderKey >> 160) /*nonce*/);
}
function hardCancelOrder(uint224 orderKey) private {
orders[orderKey].pairId = 0xFFFFFFFF;
orders[orderKey].amountE8 = 0;
emit HardCancelOrderEvent(address(orderKey) /*traderAddr*/, uint64(orderKey >> 160) /*nonce*/);
}
function setFeeRates(uint16 makerE4, uint16 takerE4, uint16 withdrawE4) private {
if (makerE4 > MAX_FEE_RATE_E4) revert();
if (takerE4 > MAX_FEE_RATE_E4) revert();
if (withdrawE4 > MAX_FEE_RATE_E4) revert();
makerFeeRateE4 = makerE4;
takerFeeRateE4 = takerE4;
withdrawFeeRateE4 = withdrawE4;
emit SetFeeRatesEvent(makerE4, takerE4, withdrawE4);
}
function setFeeRebatePercent(address traderAddr, uint8 feeRebatePercent) private {
if (feeRebatePercent > 100) revert();
traders[traderAddr].feeRebatePercent = feeRebatePercent;
emit SetFeeRebatePercentEvent(traderAddr, feeRebatePercent);
}
function parseNewOrder(uint224 orderKey, uint8 v, uint[] body, uint i) private view returns (Order) {
// bits: <expireTimeSec>(64) <amountE8>(64) <priceE8>(64) <ioc>(8) <action>(8) <pairId>(32)
uint240 bits = uint240(body[i + 1]);
uint64 nonce = uint64(orderKey >> 160);
address traderAddr = address(orderKey);
if (traderAddr == 0) revert(); // check zero addr early since `ecrecover` returns 0 on error
// verify the signature of the trader
bytes32 hash1 = keccak256("\x19Ethereum Signed Message:\n70DEx2 Order: ", address(this), nonce, bits);
if (traderAddr != ecrecover(hash1, v, bytes32(body[i + 2]), bytes32(body[i + 3]))) {
bytes32 hashValues = keccak256("DEx2 Order", address(this), nonce, bits);
bytes32 hash2 = keccak256(HASHTYPES, hashValues);
if (traderAddr != ecrecover(hash2, v, bytes32(body[i + 2]), bytes32(body[i + 3]))) revert();
}
Order memory order;
order.pairId = uint32(bits); bits >>= 32;
order.action = uint8(bits); bits >>= 8;
order.ioc = uint8(bits); bits >>= 8;
order.priceE8 = uint64(bits); bits >>= 64;
order.amountE8 = uint64(bits); bits >>= 64;
order.expireTimeSec = uint64(bits);
return order;
}
} // contract
DEx Smart Contract Spec
Bits format in this doc: The rightmost bit is the LSB, the leftmost bit is the MSB. For example, a byte <1001>(4) <0011>(4) has value 147.
Market States
State that can not be changed:
States that can be changed directly by an external function:
-
marketStatus: uint8
- 0: Active
- 1: Suspended
- A suspended market temporarily stops accepting deposits and executing operations. Withdrawing is Ok.
- 2: Closed
- A closed status cannot be changed even by the admin. Only withdrawing, setWithdrawAddr, transferFee and operation ConfirmDeposit and InitiateWithdraw are allowed in a closed market.
-
tokens: map[tokenCode(16)] -> TokenInfo
- tokenCode is a uint16
- TokenInfo is a struct
struct TokenInfo {
bytes[4] symbol // e.g. "ETH ", "ADX " (there is a trailing whitespace 0x20).
Address tokenAddr // the address of the ERC20 token contract.
uint64 scaleFactor // <original amount> = <Dex amountE8> x scaleFactor / 1e8
uint minDeposit // minimum deposit (original token amount) allowed per token
}
- Once a token added, its info cannot be changed.
2. The token code of ETH is 0 and the scale factor is 1e18. 3. Token code [0, 99] are reserved for cash tokens. [100, 999] are reserved for tokens on Ethereum block chain. 4. For an ERC20 token, the scaleFactor of the token must be set as scaleFactor = 10 ** decimal. For example, demical=3 => scaleFactor=1000, decimal=0 => scaleFactor=1.
States that can only be changed via executing the operation sequence:
Account States (Trader Related States)
-
traders: map[traderAddr] -> TraderInfo(256)
- traderAddr is an address
- TraderInfo is a struct
struct TraderInfo {
address withdrawAddr
uint8 feeRebatePercent // between 0~100
}
-
accounts: map[tokenAccountKey(176)] -> TokenAccount(256)
- tokenAccountKey is a uint176
<tokenCode>(16) <traderAddr>(160) - TokenAccount is a struct
struct TokenAccount {
uint64 balanceE8 // available amount for trading
uint64 pendingWithdrawE8
}
-
Note
- The total fee collected in a token is kept as the pendingWithdrawE8 of that token of traderAddr 0, i.e.
accounts[tokenCode << 160].pendingWithdrawE8 - We use address 0 but not a real address to avoid handling the case where the fee address is one of the trader address. Besides, we do not have to set the fee address in advance.
-
orders: map[orderKey(224)] -> Order
- orderKey is a uint224
<orderNonce>(64) <traderAddr>(160) - Order is a struct
struct Order {
uint32 pairId // <cashId>(16) <stockId>(16)
uint8 action
uint8 ioc
uint64 priceE8
uint64 amountE8
uint64 expireTimeSec
}
-
Note
- action: 0 means BUY, 1 means SELL.
- ioc: 0 means not IOC (Immediate-Or-Cancel), 1 means IOC.
If an order has been hard cancelled, update its amountE8 to 0. -
deposits: map[depositIndex(64)] -> Deposit
- depositIndex is a uint64
- Deposit is a struct
struct Deposit {
address traderAddr
uint16 tokenCode
uint64 pendingAmountE8 // the amount to be confirmed for trading
}
Operation Sequence
An operation sequence is represented as an array of uint256 with the following format:
<OpSeq>: <header>(256) {[operation],[operation],...}
Format of header:
<header>: <newLogicTimeSec>(64) <beginIndex>(64)
External function:
function exeSequence(uint header, uint[] body)
Operations
Each operation can be one of the following. As a convention, the lowest 16 bits of the first uint256 of an operation is always the opcode.
-
ConfirmDeposit
- move a pending deposit to balance
- opcode: 0xDE 0x01
- body length (in uint256): 1
<depositIndex>(64) <opcode>(16) -
InitiateWithdraw
-
MatchOrders
-
execute a pair of matching orders. -
opcode: 0xDE 0x03 -
body length (in uint256): 2 ~ 8 -
<nonce1>(64) <traderAddr1>(160) <v1>(8) <opcode>(16)
- The following 3 uint256s exists if and only if v1 is NOT 0. (v1 = 0 means that this order is already in the storage.)
<expireTimeSec1>(64) <amount1E8>(64) <price1E8>(64) <ioc1>(8) <action1>(8) <pairId1>(32)
<r1>(256)
<s1>(256)
-
<nonce2>(64) <traderAddr2>(160) <v2>(8)
- The following 3 uint256s exists if and only if v2 is NOT 0. (v2 = 0 means that this order is already in the storage.)
<expireTimeSec2>(64) <amount2E8>(64) <price2E8>(64) <ioc2>(8) <action2>(8) <pairId2>(32)
<r2>(256)
<s2>(256)
-
Note
- The first order is the maker, the second order is the taker.
- When v1/v2 is not zero, it is either 27 or 28.
- Format of
<pairId>: <cashId>(16) <stockId>(16) - stockId is the lower u16, cashId is the higher u16
- AmountE8 is referring to stock amount
Signing Scheme 1 (Friendly to API usage)
- The bytes to be hashed (using keccak256) for signing are the concatenation of the following (uints are in big-endian order):
- Prefix “\x19Ethereum Signed Message:\n70”.
- String "DEx2 Order: " (Note the trailing whitespace)
- The market address.
- This is for replay attack protection when we deploy a new market.
<nonce>(64) <expireTimeSec>(64) <amountE8>(64) <priceE8>(64) <ioc>(8) <action>(8) <pairId>(32) Signing Scheme 2 (Friendly to UI supporting eth_signTypedData)
-
The fields to be sent to eth_signTypedData: string title = "DEx2 Order"
address market_address = <market address>
uint64 nonce
uint64 expire_time_sec
uint64 amount_e8
uint64 price_e8
uint8 immediate_or_cancel
uint8 action
uint16 cash_token_symbol = `<high 16 bits of pairId>`
uint16 stock_token_symbol = `<low 16 bits of pairId>`
-
The bytes to be hashed (using keccak256) for signing are the concatenation of the following: keccak256(<TYPES_NAMES_HASH>, <DATA_HASH>)
-
<TYPES_NAMES_HASH> is u256 0x8382001f45a579e41d75c5535cff758120963de17b4af5e92d5191b9a5f69f1f
- It is the result of:
keccak256(
"string title", "address market_address", "uint64 nonce",
"uint64 expire_time_sec", "uint64 amount_e8", "uint64 price_e8",
"uint8 immediate_or_cancel", "uint8 action",
"uint16 cash_token_symbol", "uint16 stock_token_symbol")
-
<DATA_HASH> is the keccak256 result of the following: "DEx2 Order"
<market address>(160)
<nonce>(64)
<expireTimeSec>(64) <amountE8>(64) <priceE8>(64) <ioc>(8) <action>(8) <pairId>(32)
-
HardCancelOrder
-
mark an order as cancelled in the storage, no matter whether it is currently in the storage or not. -
opcode: 0xDE 0x04 -
body length (in uint256): 1 -
<nonce>(64) <traderAddr>(160) <opcode>(16) -
Note
- No signature required. The admin keeps the right to cancel any order.
-
SetFeeRates
- set the fee rates.
- opcode: 0xDE 0x05
- body length (in uint256): 1
<withdrawFeeRateE4>(16) <takerFeeRateE4>(16) <makerFeeRateE4>(16) <opcode>(16) -
SetFeeRebatePercent
- set the rebate fee percentage.
- opcode: 0xDE 0x06
- body length (in uint256): 1
<feeRebatePercent>(8) <traderAddr>(160) <opcode>(16)
Free External Methods
- Free external methods can be called by either the admin or the traders, at anytime, in any order.
Deposit
-
depositEth(address traderAddr) external payable -
depositToken(address traderAddr, uint16 tokenCode, uint originalAmount) external -
Note
- confirmDeposit (called by exeSequence) comes after depositEth/depositToken
Withdraw
-
withdrawEth(address traderAddr) external -
withdrawToken(address traderAddr, uint16 tokenCode) external -
Note
- initiateWithdraw (called by exeSequence) comes before withdrawEth/withdrawToken
|