Trade Tokens

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

/// @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