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