Technical Reference

Token Precision

We support tokens with varying decimal precisions, depending on the blockchain and token standard.

Standard degree of divisibility:

TokenDecimals
ERC2018
ETH18
TRC2018
TRX6
SOL9
SPL6
TON9
JETTON9

20 whole tokens are represented differently depending on the token's decimal precision. For example:

  • ERC20: 20 * 10^18 (20,000,000,000,000,000,000 wei)
  • SOL: 20 * 10^9 (20,000,000,000 lamports)
  • TRX: 20 * 10^6 (20,000,000 sun)

Shared Decimal System

When someone sells 20 ETH for 10 SOL, the difference in precision makes calculating the exact destination amount error-prone. To resolve this, we use a shared system based on 6 decimals, cleaning any excess precision, or "dust", that cannot be represented. Inspired by LayerZero OFT Standard.

Example

Create offer

An advertiser creates an offer selling 1.234567890123456789 ETH = 1,234,567,890,123,456,789 wei (Base) with an exchange rate of 1 ETH to 2 SOL.

OTC Market will:

  1. Get srcDecimalConversionRate:
decimalConversionRate = 10^(localDecimals − sharedDecimals) = 10^(18−6) = 10^12

This means the conversion rate is 10^12, which indicates the smallest unit to sell is 10^-12 in terms of the token's local decimals.

Decimal conversion rate

function _getDecimalConversionRate(
  address _tokenAddress
) internal view virtual returns (uint256 decimalConversionRate) {
  decimalConversionRate = _tokenAddress == address(0)
    ? 10 ** 12 // native (1 for TRON)
    : 10 ** (ERC20(_tokenAddress).decimals() - SHARED_DECIMALS); // fungible token
}
  1. Divide by decimalConversionRate:
1234567890123456789 / 10^12 = 1234567.890123456789 = 1234567

TIP

Remember that solidity performs integer arithmetic. This means when you divide two integers, the result is also an integer with the fractional part discarded. If token decimals is lower than shared decimals, an underflow error is thrown.

This is equivalent to converting from local to shared decimals:

To shared decimals

import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";

function toSD(uint256 _amountLD, uint256 _decimalConversionRate) internal pure returns (uint64 amountSD) {
  amountSD = SafeCast.toUint64(_amountLD / _decimalConversionRate);
}
  1. Multiply by decimalConversionRate:
1234567 * 10^12 = 1234567000000000000

This is equivalent to converting from shared to local decimals:

To local decimals

function toLD(uint64 _amountSD, uint256 _decimalConversionRate) internal pure returns (uint256 amountLD) {
  amountLD = _amountSD * _decimalConversionRate;
}

This process removes the last 12 digits from the original amount, effectively "cleaning" the amount from any "dust" that cannot be represented in a system with 6 decimal places.

Dust removal

function _removeDust(
  uint256 _amountLD,
  address _tokenAddress
) private view returns (uint64 amountSD, uint256 amountLD) {
  uint256 srcDecimalConversionRate = _getDecimalConversionRate(_tokenAddress);

  amountSD = _amountLD.toSD(srcDecimalConversionRate);
  amountLD = amountSD.toLD(srcDecimalConversionRate);
}
  1. Store source amount and exchange rate in shared decimals. Lock cleaned source amount in local decimals in the Escrow.

Accept offer

Let's say buyer wants to accept the offer in whole, source amount = 1.234567 ETH (= 1234567 in shared decimals). They provide the desired source amount in shared decimals.

OTC Market will:

  1. Get dstDecimalConversionRate:
decimalConversionRate = 10^(localDecimals − sharedDecimals) = 10^(9−6) = 10^3
  1. Calculate destination amount to take from buyer in shared decimals:
dstAmountSD = (srcAmountSD * exchangeRateSD) / 10**SHARED_DECIMALS 
= 1234567 * 2000000 / 10^6
= 2469134
  1. Convert destination amount from shared to local decimals
2469134 * 10^3 = 2469134000

This means that the buyer will pay 2,469,134,000 lamports (2.469134 SOL) for 1,234,567,000,000,000,000 wei (1.234567 ETH)

TIP

In reality, arithmetic operations happen in a different order to ensure minimal loss from the rounding.

Destination amount calculation

function _toDstAmount(
    uint64 _srcAmountSD,
    uint64 _exchangeRateSD,
    address _tokenAddress
) internal view virtual returns (AcceptOfferReceipt memory acceptOfferReceipt) {
  uint256 dstDecimalConversionRate = _getDecimalConversionRate(_tokenAddress);

  uint256 dstAmountLD = (uint256(_srcAmountSD) * uint256(_exchangeRateSD) * dstDecimalConversionRate) /
    (10 ** SHARED_DECIMALS);

  ...
}
  1. Transfer destination amount in local decimals from the buyer to the seller destination address.
Previous
Core Settings