Trade Tokens

Get A Quote

To get a quote , we call the quoteExactInput function:

/// @notice Parameters for quoting the output amount for a given input
struct QuoteExactInputParams {
    /// @notice The address of the input token (use address(0) for native asset)
    address inputToken;
    /// @notice The address of the output token (use address(0) for native asset)
    address outputToken;
    /// @notice The amount of input token to swap (in input token decimals)
    uint256 inputAmount;
}

/// @notice Quote the output amount for a given input
/// @param params The quote parameters
/// @return outputAmount The quoted output amount
/// @dev refer to the swapExactInput method for the scenarios
function quoteExactInput(QuoteExactInputParams calldata params) external returns (uint256 outputAmount);

Note: the quoteExactInput method is not a view function, but we donโ€™t need to send a transaction to get the quote (an eth_call or the simulation in viem will do the work) .

Here are the possible scenarios:

  • When the quote token is the native gas token (BNB or ETH):

    • buy a token:

      • inputToken is zero address, representing the gas token

      • outputToken is the token you wanna buy

    • sell a token:

      • inputToken is the token you wanna sell

      • outputToken is zero address, representing the gas token

  • When the quote token is not the native gas token (taking USD1 as an example) :

    • buy a token with USD1:

      • inputToken is USD1 address

      • outputToken is the token you wanna buy

    • buy with native gas token (only when the tokenโ€™s nativeToQuoteSwap is enabled , check below to inspect if a tokenโ€™s quote token supports nativeToQuoteSwap ):

      • inputToken is zero address, representing the gas token

      • outputToken is the token you wanna buy

      • under the hood, we will help you swap BNB for for USD1 on PancakeSwap as an intermediate step in the contract.

    • sell a token:

      • inputToken is the token you wanna sell

      • outputToken is USD1

    • Sell directly from token to BNB is also supported, but we will not support this in our UI.

Swap

To swap we call the swapExactInput method:

/// @notice Parameters for swapping exact input amount for output token
struct ExactInputParams {
    /// @notice The address of the input token (use address(0) for native asset)
    address inputToken;
    /// @notice The address of the output token (use address(0) for native asset)
    address outputToken;
    /// @notice The amount of input token to swap (in input token decimals)
    uint256 inputAmount;
    /// @notice The minimum amount of output token to receive
    uint256 minOutputAmount;
    /// @notice Optional permit data for the input token (can be empty)
    bytes permitData;
}

/// @notice Swap exact input amount for output token
/// @param params The swap parameters
/// @return outputAmount The amount of output token received
/// @dev Here are some possible scenarios:
///   If the token's reserve is BNB or ETH (i.e: the quote token is the native gas token):
///      - BUY: input token is address(0), output token is the token address
///      - SELL: input token is the token address, output token is address(0)
///   If the token's reserve is another ERC20 token (eg. USD*, i.e, the quote token is an ERC20 token):
///      - BUY with USD*: input token is the USD* address, output token is the token address
///      - SELL for USD*: input token is the token address, output token is the USD* address
///      - BUY with BNB or ETH: input token is address(0), output token is the token address.
///        (Note: this requires an internal swap to convert BNB/ETH to USD*, nativeToQuoteSwap must be anabled for this quote token)
/// Note: Currently, this method only supports trading tokens that are still in the bonding curve state.
///       However, in the future, we may also support trading tokens that are already in DEX state.
function swapExactInput(ExactInputParams calldata params) external payable returns (uint256 outputAmount);

This is quite straightforward after getting a quote.

Events

TokenBought

Emitted when a user buys tokens through the Portal.

event TokenBought(
    uint256 ts,
    address token,
    address buyer,
    uint256 amount,
    uint256 eth,
    uint256 fee,
    uint256 postPrice
);

Parameters:

  • ts: Timestamp of the trade.

  • token: Address of the token bought.

  • buyer: Address of the buyer.

  • amount: Amount of tokens bought.

  • eth: Amount of ETH (or quote token) spent.

  • fee: Amount of ETH (or quote token) spent as a fee.

  • postPrice: Price of the token after this trade.

When emitted: Whenever a user successfully buys tokens via the Portal.

TokenSold

Emitted when a user sells tokens through the Portal.

event TokenSold(
    uint256 ts,
    address token,
    address seller,
    uint256 amount,
    uint256 eth,
    uint256 fee,
    uint256 postPrice
);

Parameters:

  • ts: Timestamp of the trade.

  • token: Address of the token sold.

  • seller: Address of the seller.

  • amount: Amount of tokens sold.

  • eth: Amount of ETH (or quote token) received.

  • fee: Amount of ETH (or quote token) deducted as a fee.

  • postPrice: Price of the token after this trade.

When emitted: Whenever a user successfully sells tokens via the Portal.

Legacy Methods (Deprecated)

Buy

Preview:

    /// @notice Preview the amount of tokens to buy with ETH
    /// @param token  The address of the token to buy
    /// @param eth  The amount of ETH to spend
    /// @return amount  The amount of tokens to buy
    function previewBuy(address token, uint256 eth) external view returns (uint256 amount);

Buy tokens:

    /// @notice Buy token with ETH
    /// @param token  The address of the token to buy
    /// @param recipient  The address to send the token to
    /// @param minAmount  The minimum amount of tokens to buy
    function buy(address token, address recipient, uint256 minAmount) external payable returns (uint256 amount);

Whenever someone buys some amount of a token, a TokenBought event would emit from our Portal contract:

    /// @notice emitted when a token is bought
    ///
    /// @param ts The timestamp of the event
    /// @param token  The address of the token
    /// @param buyer  The address of the buyer
    /// @param amount  The amount of tokens bought
    /// @param eth  The amount of ETH spent
    /// @param fee The amount of ETH spent on fee
    /// @param postPrice The price of the token after this trade
    event TokenBought(
        uint256 ts, address token, address buyer, uint256 amount, uint256 eth, uint256 fee, uint256 postPrice
    );

Sell

preview:


    /// @notice preview the amount of ETH to receive for selling tokens
    /// @param token  The address of the token to sell
    /// @param amount  The amount of tokens to sell
    /// @return eth  The amount of ETH to receive
    function previewSell(address token, uint256 amount) external view returns (uint256 eth);

sell:

    /// @param token  The address of the token to sell
    /// @param amount The amount of tokens to sell
    /// @param minEth The minimum amount of ETH to receive
    function sell(address token, uint256 amount, uint256 minEth) external returns (uint256 eth);

event:

    /// @notice emitted when a token is sold
    ///
    /// @param ts The timestamp of the event
    /// @param token  The address of the token
    /// @param seller  The address of the seller
    /// @param amount  The amount of tokens sold
    /// @param eth  The amount of ETH received
    /// @param fee  The amount of ETH deducted as a fee
    /// @param postPrice The price of the token after this trade
    event TokenSold(
        uint256 ts, address token, address seller, uint256 amount, uint256 eth, uint256 fee, uint256 postPrice
    );

Permit On Sell

In How to find the reserve/price/fdv from the circulating supply of a token? , we introduce the getTokenV2 method of our contract, which returns the information of the token:

/// @notice Get token state
/// @param token  The address of the token
/// @return state  The state of the token
function getTokenV2(address token) external view returns (TokenStateV2 memory state);


/// @dev Token version
/// Which token implementation is used
enum TokenVersion {
    TOKEN_LEGACY_MINT_NO_PERMIT,
    TOKEN_LEGACY_MINT_NO_PERMIT_DUPLICATE, // for historical reasons, both 0 and 1 are the same: TOKEN_LEGACY_MINT_NO_PERMIT
    TOKEN_V2_PERMIT,
    TOKEN_GOPLUS
}
/// @notice the status of a token
/// The token has 4 statuses:
//    - Tradable: The token can be traded(buy/sell)
//    - InDuel: (obsolete) The token is in a battle, it can only be bought but not sold.
//    - Killed: (obsolete) The token is killed, it can not be traded anymore. Can only be redeemed for another token.
//    - DEX: The token has been added to the DEX
enum TokenStatus {
    Invalid, // The token does not exist
    Tradable,
    InDuel, // obsolete
    Killed, // obsolete
    DEX
}

/// @notice the state of a token (with dex related fields)
struct TokenStateV2 {
    TokenStatus status; // the status of the token
    uint256 reserve; // the reserve of the token
    uint256 circulatingSupply; // the circulatingSupply of the token
    uint256 price; // the price of the token
    TokenVersion tokenVersion; // the version of the token implementation this token is using
    uint256 r; // the r of the curve of the token
    uint256 dexSupplyThresh; // the cirtulating supply threshold for adding the token to the DEX
}

The TokenVersion determines that if we need approve/permit before selling the token.

If the TokenVersion is TOKEN_LEGACY_MINT_NO_PERMIT or TOKEN_LEGACY_MINT_NO_PERMIT_DUPLICATE , you don't need to approve our contract to spend your token, as this is implemented by burning your token.

If the TokenVersion is TOKEN_V2_PERMIT or higher, you need to approve/permit our contract to spend your token. The sell call allows extra data appended to its calldata, and these extra data are treated as the call to the permit method of the selling token, making it possible to permit and sell in one transaction. Here is an example in typescript to permit on sell:

// you can get the abi from bscscan. 
import portalABI from '../../abi/portal.json'; 
import tokenABI from '../../abi/tokenv2.json';
import { bsc } from 'viem/chains'
import { createPublicClient, createWalletClient, encodeAbiParameters, encodeFunctionData, formatEther, getContract, hashDomain, http, parseEther, parseSignature, signatureToCompactSignature } from 'viem';


const test_account = privateKeyToAccount("your private key here");



async function main() {

    // for coin with coin.version > 1, 
    // we should append "permit" data after the calldata for selling 


    // public client 
    const pubClient = createPublicClient({
        chain: bsc,
        transport: http('https://rpc.ankr.com/bsc/'),
        batch: {
            multicall: {
                batchSize: 1024 * 200,
            },
        },
    });

    // wallet client
    const walletClient = createWalletClient({
        chain: bsc,
        transport: http('https://rpc.ankr.com/bsc/'),
        account: test_account,
    });


    const portal = getContract({
        address: "0xe2cE6ab80874Fa9Fa2aAE65D277Dd6B8e65C9De0",
        abi: portalABI.abi,
        client: {
            public: pubClient,
            wallet: walletClient
        }
    });


    // The token that uses the new TokenV2 implementation 
    const token = "token address here";

    const tokenInst = getContract({
        address: token,
        abi: tokenABI.abi,
        client: {
            public: pubClient,
            wallet: walletClient
        }
    });

    const balance = (await tokenInst.read.balanceOf([test_account.address])) as bigint;


    // 
    // Let's sell all our token 
    // 


    // 
    // step1: construct the permit data
    // 

    // 1.1 fetch nonce & name from the token contract

    const nonce = (await tokenInst.read.nonces([test_account.address])) as bigint;
    const name = (await tokenInst.read.name()) as string; // you may cache the name to reduce rpc calls 
    const deadline = BigInt(Date.now() + 10 * 60 * 1000); // 10 minutes ttl


    // 1.2 sign the permit data , get the signature
    const sig = await test_account.signTypedData({
        domain: {
            name,
            version: "1",
            chainId: bsc.id,
            verifyingContract: token
        },
        types: {
            Permit: [
                { name: "owner", type: "address" },
                { name: "spender", type: "address" },
                { name: "value", type: "uint256" },
                { name: "nonce", type: "uint256" },
                { name: "deadline", type: "uint256" }
            ]
        },
        primaryType: "Permit",
        message: {
            owner: test_account.address,
            spender: portal.address,
            value: balance,
            nonce,
            deadline,
        }
    });


    console.log(`permit sig: ${sig}`);

    // we want r,s,v separately, let's parse the signature 
    const { r, s, v } = parseSignature(sig) as { r: `0x${string}`, s: `0x${string}`, v: bigint, yParity: number };

    console.log(`\tr: ${r}`);
    console.log(`\ts: ${s}`);
    console.log(`\tv: ${v}`);


    // 3. construct the piggback data to be appended to the calldata
    const piggyback = encodeAbiParameters([
        { name: "owner", type: "address" },
        { name: "spender", type: "address" },
        { name: "value", type: "uint256" },
        { name: "deadline", type: "uint256" },
        { name: "v", type: "uint8" },
        { name: "r", type: "bytes32" },
        { name: "s", type: "bytes32" }
    ],
        [test_account.address, portal.address, balance, deadline, Number(v), r, s]);


    console.log(`piggyback data: ${piggyback}`);



    // 
    // step2: construct the sell calldata 
    // 

    const selldata = encodeFunctionData({
        abi: portalABI.abi,
        functionName: "sell",
        args: [
            token,
            balance,
            0, // min receive eth amount 
        ]
    });


    // 
    // Step3: append the permit data to the sell calldata & send the tx 
    // 

    const data = selldata + piggyback.slice(2); // remove the 0x prefix in piggyback data

    {
        const hash = await walletClient.sendTransaction({
            to: portal.address,
            data: data as `0x${string}`,
        });


        console.log(`sell hash: ${hash}`);

        // wait for the transaction to be mined
        await pubClient.waitForTransactionReceipt({
            hash,
            confirmations: 2,
        });
    }

}


main().catch(console.error).then(() => process.exit(0));

Last updated