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 tokenoutputToken
is the token you wanna buy
sell a token:
inputToken
is the token you wanna selloutputToken
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 addressoutputToken
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 supportsnativeToQuoteSwap
):inputToken
is zero address, representing the gas tokenoutputToken
is the token you wanna buyunder 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 selloutputToken
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
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
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)
Please do not use these legacy methods, they will be removed soon.
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