Skip to content

Fees

Fees are parts of a swap’s input. Plach writes them as a u32 out of 1000000, so 3000 is 0.3% and 1000000 would be 100%.

The creator of a pool sets its pool fees, and Plach takes 20% of each of them as its protocol fee. The protocol fee isn’t added on top: a fee of 1% for an account pays the account 0.8% and Plach 0.2%, and a pool without fees pays no protocol fee either. The protocol’s part of each fee is 20% of its fraction, rounded down.

Each fee is its fraction of the input, rounded down, and each pool of a route charges its own.

A pool’s fee configuration is a list of receivers and their fees:

struct FeeConfiguration {
receivers: Vec<(FeeReceiver, FeeAmount)>,
}
enum FeeReceiver {
/// The fee stays in the pool, or goes to its liquidity providers
Pool,
/// The fee is collected for this account
Account { account_id: AccountId, payout_type: FeePayoutType, claim_mode: ClaimMode },
/// Only in the fees of a swap, never in a fee configuration
Protocol,
}
enum FeeAmount {
Fixed(u32),
/// Goes down from the start fee to the end fee, between the two times
Scheduled { start: (u64, u32), end: (u64, u32), curve: ScheduledFeeCurve },
/// Not supported yet
Dynamic { min: u32, max: u32 },
}

Times are in nanoseconds, like NEAR block timestamps, and the only curve is Linear. A scheduled fee must go down: the start time must be before the end time, and the start fee higher than the end fee. Before the start it’s the start fee, after the end it’s the end fee. For example, a launch pool can start with a high fee that goes down over its first hours.

Limits, checked with the fees at the time of the call when a pool is created and when its fees change:

  • at most 20 receivers,
  • each fee below 1000000, and all of them together at most 900000 (90%),
  • plach.intear.near, the protocol fee receiver, can’t be a receiver, and neither can Protocol,
  • an Account receiver can’t Reinvest.

Who can change a pool’s fees depends on its kind: only the owner of an unlocked private pool, with edit_fees.

A Pool fee goes to the liquidity providers of a public pool, and into the liquidity of private and launch pools, which have no providers.

A swap pays its fees in its input asset. The payout_type of an Account receiver says what the receiver takes:

enum FeePayoutType {
/// The asset the swap paid in, so either asset of the pool
KeepBoth,
/// Always the pool's first asset
ConvertToFirstAsset,
/// Always the pool's second asset
ConvertToSecondAsset,
/// Only for liquidity providers: more shares of the pool
Reinvest,
}

A fee in the other asset is swapped in the same pool, without fees, right after the swap, which moves the pool’s price a little further. In a launch pool, an Account receiver with ConvertToFirstAsset is always paid in the quote asset, sells included.

The protocol fee is 20% of each pool fee, for plach.intear.near. It’s paid in a stable asset if the pool has one, the first asset if both are, and otherwise in the asset the swap paid in. The stable assets are NEAR, USDt (usdt.tether-token.near) and USDC (17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1). Intear may change this list.

The fees of a PoolView are the fees of a swap right now: each receiver’s share after the protocol’s part, and the protocol fee last.

Plach keeps the fees of Account receivers and the protocol fee for each account and asset. The pool registers each receiver for the assets its payout takes when the pool is created or its fees are edited, so receivers don’t have to do anything first. The claim_mode of a receiver says how its fees leave Plach:

enum ClaimMode {
/// The account withdraws them to its inner balance with withdraw_fees
OnlyOwner,
/// Anyone sends them to the account's wallet with claim_fees_anyone
Anyone,
}

OnlyOwner fees are shown by get_pending_fees, and withdraw_fees moves them to the account’s inner balance, so the assets must be registered for it there. Fees that liquidity providers collect are withdrawn the same way.

register_fee_assets registers assets for the caller’s collected fees, with NEAR attached for the storage. Nobody needs it now, since the pool registers its receivers. It was for the referral fees Plach used to charge.

With near CLI, the examples below run as alice.near after a storage deposit and this setup. It registers NEAR and USDt for alice.near in the engine, and deposits 1.12 NEAR and 5 USDt: 1 NEAR and 5 USDt for a pool, 0.02 NEAR for its storage, and 0.1 NEAR to sell. alice.near trades in its own pool to keep the example short. Together they’re one script.

# Fees are withdrawn to the inner balance, so register their assets there too
near contract call-function as-transaction dex.intear.near register_assets \
json-args '{"asset_ids": ["near", "nep141:usdt.tether-token.near"]}' \
prepaid-gas '30.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send
near contract call-function as-transaction dex.intear.near deposit_near \
json-args '{}' \
prepaid-gas '30.0 Tgas' attached-deposit '1.12 NEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send
near contract call-function as-transaction usdt.tether-token.near ft_transfer_call \
json-args '{"receiver_id": "dex.intear.near", "amount": "5000000", "msg": ""}' \
prepaid-gas '50.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send

This creates a private pool of NEAR and USDt with a fee of 1% for alice.near, paid in NEAR, as described in Create a pool:

00 asset 0: near
01 16 00 00 00 "usdt.tether-token.near" asset 1: nep141:usdt.tether-token.near
01 00 00 00 fees: 1 receiver
03 0a 00 00 00 "alice.near" 01 00 Account(alice.near, ConvertToFirstAsset, OnlyOwner)
00 10 27 00 00 Fixed(10000), 1%
00 pool type: PrivateLatest
01 initial liquidity: Some
00 00 00 a1 ed cc ce 1b c2 d3 00 00 00 00 00 00 1000000000000000000000000 (1 NEAR)
40 4b 4c 00 00 00 00 00 00 00 00 00 00 00 00 00 5000000 (5 USDt)
00 creator's fee payout: None
# A private pool of NEAR and USDt with a fee of 1% for alice.near, paid in
# NEAR, and 1 NEAR and 5 USDt of liquidity. The other 0.02 NEAR pays for its storage, and what
# it doesn't use goes to alice.near's wallet.
POOL_ID=$(near --quiet contract call-function as-transaction dex.intear.near dex_call \
json-args '{
"dex_id": "slimedragon.near/xyk",
"method": "create_pool",
"args": "AAEWAAAAdXNkdC50ZXRoZXItdG9rZW4ubmVhcgEAAAADCgAAAGFsaWNlLm5lYXIBAAAQJwAAAAEAAACh7czOG8LTAAAAAAAAQEtMAAAAAAAAAAAAAAAAAAA=",
"attached_assets": {
"near": "1020000000000000000000000",
"nep141:usdt.tether-token.near": "5000000"
}
}' \
prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send | jq -r .)

A swap of 0.1 NEAR in the pool pays alice.near 0.0008 NEAR, and plach.intear.near the other 0.0002 NEAR of the 1% fee:

# Sell 0.1 NEAR in the pool. The pool ID alone is the message of a swap through it.
AMOUNT_OUT=$(near contract call-function as-read-only dex.intear.near simulate_swap_simple \
json-args "$(jq -n --arg pool_id "$POOL_ID" '{
"dex_id": "slimedragon.near/xyk",
"message": $pool_id,
"trader": "alice.near",
"asset_in": "near",
"asset_out": "nep141:usdt.tether-token.near",
"amount": {"ExactIn": "100000000000000000000000"}
}')" \
network-config mainnet now | jq -r '.[1]')
MIN_AMOUNT_OUT=$((AMOUNT_OUT * 99 / 100))
near contract call-function as-transaction dex.intear.near swap_simple \
json-args "$(jq -n --arg pool_id "$POOL_ID" --arg min_amount_out "$MIN_AMOUNT_OUT" '{
"dex_id": "slimedragon.near/xyk",
"message": $pool_id,
"asset_in": "near",
"asset_out": "nep141:usdt.tether-token.near",
"amount": {"ExactIn": "100000000000000000000000"},
"constraint": $min_amount_out
}')" \
prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send

get_pending_fees takes an account and a list of assets, and returns a Borsh HashMap<AssetId, u128> of the fees collected in each, for the assets that are registered. Here NEAR, which is what alice.near is paid in:

near contract call-function as-read-only dex.intear.near dex_view \
json-args '{
"dex_id": "slimedragon.near/xyk",
"method": "get_pending_fees",
"args": "CgAAAGFsaWNlLm5lYXIBAAAAAA=="
}' \
network-config mainnet now

withdraw_fees takes a list of assets, and moves all of the caller’s collected fees in them to their inner balance:

near contract call-function as-transaction dex.intear.near dex_call \
json-args '{
"dex_id": "slimedragon.near/xyk",
"method": "withdraw_fees",
"args": "AQAAAAA=",
"attached_assets": {}
}' \
prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send

An Account receiver with claim_mode: Anyone doesn’t call Plach to get its fees: anyone can send them to its wallet with claim_fees_anyone, which takes the account and a list of assets (ClaimFeesArgs { account_id, asset_ids }). get_claimable_fees shows them, with the same arguments. It’s for accounts that don’t call Intear DEX themselves, like a community’s treasury, or a contract that shares the fees with a token’s holders.

The transfer doesn’t check that the account can receive the asset, like a storage registration on its token contract. Use OnlyOwner for any account that can call withdraw_fees itself.

The Community receivers of launch pools from before are Anyone receivers now, paid in the quote asset.

Apps charge their own fees on the swaps they send with the engine’s referral fees, which work the same on every DEX: out of the swap’s input, before Plach gets it. Pool and protocol fees, and fee discounts, apply to the rest.

Swap messages from before the engine took referral fees listed them after the fee discount. Plach still reads those messages, and ignores their referral fees.

A swap message can carry a signed fee discount, which reduces the pool and protocol fees of that swap. The DEX Aggregator signs them for traders holding or staking TEAR.

struct SignedFeeDiscount {
discount: FeeDiscount,
/// Signature of sha256(borsh(FeeDiscountSignaturePayload)) by the fee discount signer
signature: Vec<u8>,
}
struct FeeDiscount {
routes: Vec<SwapRoute>,
asset_in: AssetId,
asset_out: AssetId,
/// Only this trader can use it, anyone if `None`
trader_id: Option<AccountId>,
amount: SwapRequestAmount,
/// 1000000 waives the pool and protocol fees
fee_discount: u32,
expiration_block_height: u64,
}
/// What the signer signs, so that a discount works only on one chain, engine and DEX
struct FeeDiscountSignaturePayload {
chain_id: String, // "mainnet"
engine_contract_id: AccountId, // dex.intear.near
dex_id: DexId, // deployer "slimedragon.near", id "xyk"
discount: FeeDiscount,
}

The discount is only valid for exactly this swap: the same routes, assets and amount, by trader_id, until the block height. For a swap in the operations of a deposit, trader_id must be the alleged sender and the deposited asset must be asset_in. The signature is Ed25519 (64 bytes), or secp256k1 with the recovery ID (0 or 1, not 27 or 28) as the last byte, by the key that get_fee_discount_signer returns. If that’s None, fee discounts are off.

#!/usr/bin/env bash
# Creates a private NEAR/USDt pool on Plach with a fee of 1% for alice.near,
# swaps in it, and withdraws the fee. alice.near trades in its own pool to keep
# it short. alice.near holds USDt in the wallet and has never used Intear DEX.
set -euo pipefail
near contract call-function as-transaction dex.intear.near storage_deposit \
json-args '{}' \
prepaid-gas '30.0 Tgas' attached-deposit '0.01 NEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send
# Fees are withdrawn to the inner balance, so register their assets there too
near contract call-function as-transaction dex.intear.near register_assets \
json-args '{"asset_ids": ["near", "nep141:usdt.tether-token.near"]}' \
prepaid-gas '30.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send
near contract call-function as-transaction dex.intear.near deposit_near \
json-args '{}' \
prepaid-gas '30.0 Tgas' attached-deposit '1.12 NEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send
near contract call-function as-transaction usdt.tether-token.near ft_transfer_call \
json-args '{"receiver_id": "dex.intear.near", "amount": "5000000", "msg": ""}' \
prepaid-gas '50.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send
# A private pool of NEAR and USDt with a fee of 1% for alice.near, paid in
# NEAR, and 1 NEAR and 5 USDt of liquidity. The other 0.02 NEAR pays for its storage, and what
# it doesn't use goes to alice.near's wallet.
POOL_ID=$(near --quiet contract call-function as-transaction dex.intear.near dex_call \
json-args '{
"dex_id": "slimedragon.near/xyk",
"method": "create_pool",
"args": "AAEWAAAAdXNkdC50ZXRoZXItdG9rZW4ubmVhcgEAAAADCgAAAGFsaWNlLm5lYXIBAAAQJwAAAAEAAACh7czOG8LTAAAAAAAAQEtMAAAAAAAAAAAAAAAAAAA=",
"attached_assets": {
"near": "1020000000000000000000000",
"nep141:usdt.tether-token.near": "5000000"
}
}' \
prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send | jq -r .)
# Sell 0.1 NEAR in the pool. The pool ID alone is the message of a swap through it.
AMOUNT_OUT=$(near contract call-function as-read-only dex.intear.near simulate_swap_simple \
json-args "$(jq -n --arg pool_id "$POOL_ID" '{
"dex_id": "slimedragon.near/xyk",
"message": $pool_id,
"trader": "alice.near",
"asset_in": "near",
"asset_out": "nep141:usdt.tether-token.near",
"amount": {"ExactIn": "100000000000000000000000"}
}')" \
network-config mainnet now | jq -r '.[1]')
MIN_AMOUNT_OUT=$((AMOUNT_OUT * 99 / 100))
near contract call-function as-transaction dex.intear.near swap_simple \
json-args "$(jq -n --arg pool_id "$POOL_ID" --arg min_amount_out "$MIN_AMOUNT_OUT" '{
"dex_id": "slimedragon.near/xyk",
"message": $pool_id,
"asset_in": "near",
"asset_out": "nep141:usdt.tether-token.near",
"amount": {"ExactIn": "100000000000000000000000"},
"constraint": $min_amount_out
}')" \
prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send
near contract call-function as-read-only dex.intear.near dex_view \
json-args '{
"dex_id": "slimedragon.near/xyk",
"method": "get_pending_fees",
"args": "CgAAAGFsaWNlLm5lYXIBAAAAAA=="
}' \
network-config mainnet now
near contract call-function as-transaction dex.intear.near dex_call \
json-args '{
"dex_id": "slimedragon.near/xyk",
"method": "withdraw_fees",
"args": "AQAAAAA=",
"attached_assets": {}
}' \
prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as alice.near network-config mainnet sign-with-keychain send