Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions packages/chain-adapters/src/tron/TRON_FEE_ESTIMATION_ISSUES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# TRON Fee Estimation Issues & Findings

## Critical Issue: Inaccurate Fee Estimation for TRC20 Tokens

### Current Implementation Problems

**File:** `packages/chain-adapters/src/tron/TronChainAdapter.ts:361-384`

The `getFeeData()` method returns **FIXED fees of 0.268 TRX** for ALL transactions:

```typescript
async getFeeData(_input: GetFeeDataInput<KnownChainIds.TronMainnet>) {
const { fast, average, slow, estimatedBandwidth } = await this.providers.http.getPriorityFees()
// getPriorityFees() returns FIXED 268,000 SUN (0.268 TRX)
// Ignores _input completely - doesn't check TRC20 vs TRX!
}
```

**File:** `packages/unchained-client/src/tron/api.ts:247-276`

```typescript
async getPriorityFees() {
const estimatedBytes = 268 // FIXED value
const baseFee = String(estimatedBytes * bandwidthPrice)
// Returns same fee for TRX and TRC20!
}
```

### Real-World Costs

| Transaction Type | getFeeData Returns | Actual Cost | Error Margin |
|-----------------|-------------------|-------------|--------------|
| TRX transfer | 0.268 TRX | 0.268 TRX | ✅ Correct |
| TRC20 transfer (no memo) | 0.268 TRX | **6.4-13 TRX** | ❌ 24-48x underestimate |
| TRC20 transfer (with memo) | 0.268 TRX | **8-15 TRX** | ❌ 30-56x underestimate |

### Impact on Users

1. **UI Shows Misleading Fees**
- User sees "~$0.05 fee" in UI
- Reality: ~$1.50-$3.00 fee
- Transaction broadcasts and fails on-chain
- User loses ~3-4 TRX in partial execution

2. **Failed On-Chain Transactions**
- Example: `dcd71c73fb3de9d79d6d3ff78fb3da7a5b9b8fd1c3e72e0c7bf1badff9332a51`
- Result: `OUT_OF_ENERGY`
- Used 32,128 energy, paid 3.56 TRX, then failed
- Account started with 0.25 TRX, needed 7-8 TRX

3. **Thorchain Swaps Fail**
- Memo adds 1 TRX fee (`getMemoFee` network parameter)
- User doesn't see this in fee preview
- Gets `BANDWITH_ERROR` (misleading - actually insufficient TRX for energy)

## Cost Breakdown for TRC20 Transfers

### Network Parameters (2025)
```json
{
"getEnergyFee": 100, // 100 SUN per energy unit
"getTransactionFee": 1000, // 1,000 SUN per bandwidth byte
"getMemoFee": 1000000, // 1 TRX if raw_data.data present
"getFreeNetLimit": 600 // Daily free bandwidth
}
```

### TRC20 USDT Transfer Costs

**Without Memo:**
- Energy: 64,000-130,000 units × 100 SUN = **6.4-13 TRX**
- Bandwidth: 345 bytes × 1,000 SUN = **0.345 TRX**
- **Total: 6.7-13.3 TRX**

**With Memo (Thorchain):**
- Energy: 64,000-130,000 units × 100 SUN = **6.4-13 TRX**
- Bandwidth: 405 bytes × 1,000 SUN = **0.405 TRX**
- Memo fee: **1 TRX** (fixed network parameter)
- **Total: 7.8-14.4 TRX**

*Energy cost varies based on recipient:*
- Has USDT balance: ~64k energy (~6.4 TRX)
- Empty USDT balance: ~130k energy (~13 TRX)

## TODO: Required Improvements

### 1. Fix getFeeData() to Estimate Real Costs

**Unchained-client already has the methods!**

File: `packages/unchained-client/src/tron/api.ts`
- ✅ `estimateTRC20TransferFee()` - Estimates energy for TRC20 (lines 217-245)
- ✅ `estimateFees()` - Estimates bandwidth for TRX (lines 203-215)
- ✅ `getChainPrices()` - Gets live energy/bandwidth prices (lines 188-201)

**What needs to be done:**

```typescript
async getFeeData(input: GetFeeDataInput<KnownChainIds.TronMainnet>) {
const { to, value, chainSpecific: { contractAddress, memo } = {} } = input

let energyFee = 0
let bandwidthFee = 0

if (contractAddress) {
// TRC20: Estimate energy
const feeEstimate = await this.providers.http.estimateTRC20TransferFee({
contractAddress,
from: to, // placeholder
to,
amount: value,
})
energyFee = Number(feeEstimate)
}

// Build transaction to get accurate bandwidth
const tronWeb = new TronWeb({ fullHost: this.rpcUrl })
let tx = contractAddress
? await this.buildTRC20Tx(...)
: await tronWeb.transactionBuilder.sendTrx(to, value, to)

if (memo) {
tx = await tronWeb.transactionBuilder.addUpdateData(tx, memo, 'utf8')
}

// Calculate bandwidth
const txBytes = tx.raw_data_hex.length / 2
const { bandwidthPrice } = await this.getChainPrices()
bandwidthFee = txBytes * bandwidthPrice

// Add memo fee
const memoFee = memo ? 1_000_000 : 0

const totalFee = energyFee + bandwidthFee + memoFee

return {
fast: { txFee: String(totalFee), chainSpecific: { bandwidth: String(txBytes) } },
average: { txFee: String(totalFee), chainSpecific: { bandwidth: String(txBytes) } },
slow: { txFee: String(totalFee), chainSpecific: { bandwidth: String(txBytes) } },
}
}
```

### 2. Prevent Insufficient Balance Broadcasts

Before broadcasting, check:
```typescript
const accountBalance = await this.getBalance(from)
const estimatedFee = await this.getFeeData(...)

if (accountBalance < estimatedFee.fast.txFee) {
throw new Error(
`Insufficient TRX balance. Need ${estimatedFee.fast.txFee} SUN, have ${accountBalance} SUN`
)
}
```

### 3. Better Error Messages

Current: `"Account resource insufficient error"` (cryptic)

Should be:
- `"Insufficient TRX for TRC20 transfer. Need ~8 TRX for energy costs, have 0.25 TRX"`
- `"Need 10-15 TRX for TRC20 swap with memo (energy + bandwidth + memo fee)"`

### 4. UI Fee Display Improvements

Show breakdown:
```
Estimated Fees:
Energy: 6.4 TRX
Bandwidth: 0.4 TRX
Memo: 1 TRX
Total: ~7.8 TRX
```

## Evidence

### Failed Transactions (Insufficient Balance)
- `dcd71c73fb3de9d79d6d3ff78fb3da7a5b9b8fd1c3e72e0c7bf1badff9332a51`
- Account: 0.25 TRX
- Paid 3.56 TRX in fees before failing
- Result: `OUT_OF_ENERGY`

- `e7ffaf590ea20e715e1956438aa507c2916870afb95b54cdc054527ccd9246ab`
- Paid 3.81 TRX before failing
- Result: `OUT_OF_ENERGY`

### Successful Transactions (Sufficient Balance)
- `5AAD9FD5501B860C1C38FB362D6D92212DEB328CC10BD24C18A5CD90CDD75320`
- Fee: 7.8 TRX
- Energy: 64,285 units
- Bandwidth: Covered by free daily
- Result: `SUCCESS`

## References

- TRON Resource Model: https://developers.tron.network/docs/resource-model
- TronWeb estimateEnergy: https://tronweb.network/docu/docs/API%20List/transactionBuilder/estimateEnergy/
- SwapKit TRON implementation: https://github.com/swapkit/SwapKit/tree/develop/packages/toolboxes/src/tron
- Network Parameters: `getMemoFee: 1000000`, `getEnergyFee: 100`, `getTransactionFee: 1000`

## Priority

**HIGH** - Users are losing TRX on failed transactions due to inaccurate fee estimates.
36 changes: 28 additions & 8 deletions packages/chain-adapters/src/tron/TronChainAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,17 +176,23 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.TronMainnet> {
input: BuildSendApiTxInput<KnownChainIds.TronMainnet>,
): Promise<TronSignTx> {
try {
const { from, accountNumber, to, value, chainSpecific: { contractAddress } = {} } = input
const {
from,
accountNumber,
to,
value,
chainSpecific: { contractAddress, memo } = {},
} = input

// Create TronWeb instance once and reuse
const tronWeb = new TronWeb({
fullHost: this.rpcUrl,
})

let txData

if (contractAddress) {
// Use TronWeb to build TRC20 transfer transaction
const tronWeb = new TronWeb({
fullHost: this.rpcUrl,
})

// Build the TRC20 transfer transaction without signing/broadcasting
// Build TRC20 transfer transaction
const parameter = [
{ type: 'address', value: to },
{ type: 'uint256', value },
Expand All @@ -195,7 +201,7 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.TronMainnet> {
const functionSelector = 'transfer(address,uint256)'

const options = {
feeLimit: 100_000_000, // 100 TRX
feeLimit: 100_000_000, // 100 TRX standard limit
callValue: 0,
}

Expand Down Expand Up @@ -227,6 +233,11 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.TronMainnet> {
txData = await response.json()
}

// Add memo if provided
if (memo) {
txData = await tronWeb.transactionBuilder.addUpdateData(txData, memo, 'utf8')
}

if (!txData.raw_data_hex) {
throw new Error('Failed to create transaction')
}
Expand Down Expand Up @@ -344,10 +355,19 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.TronMainnet> {
}
}

// TODO: CRITICAL - Fix fee estimation for TRC20 tokens
// Current implementation returns FIXED 0.268 TRX for all transactions
// Reality: TRC20 transfers cost 6-15 TRX (energy + bandwidth + memo)
// This causes UI to show wrong fees and transactions to fail on-chain
// See TRON_FEE_ESTIMATION_ISSUES.md for detailed analysis and fix
async getFeeData(
_input: GetFeeDataInput<KnownChainIds.TronMainnet>,
): Promise<FeeDataEstimate<KnownChainIds.TronMainnet>> {
try {
// TODO: Use _input.chainSpecific.contractAddress to detect TRC20
// TODO: Call estimateTRC20TransferFee() for TRC20 tokens
// TODO: Build actual transaction with memo to get accurate bandwidth
// TODO: Add 1 TRX memo fee if _input.chainSpecific.memo present
const { fast, average, slow, estimatedBandwidth } =
await this.providers.http.getPriorityFees()

Expand Down
1 change: 1 addition & 0 deletions packages/chain-adapters/src/tron/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type FeeData = {

export type BuildTxInput = {
contractAddress?: string
memo?: string
}

export interface TronUnsignedTx {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ export const thorchainSwapper: Swapper = {
executeUtxoTransaction: (txToSign, { signAndBroadcastTransaction }) => {
return signAndBroadcastTransaction(txToSign)
},
executeTronTransaction: (txToSign, { signAndBroadcastTransaction }) => {
return signAndBroadcastTransaction(txToSign)
},
}
3 changes: 3 additions & 0 deletions packages/swapper/src/swappers/ThorchainSwapper/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
cosmossdk,
evm,
getInboundAddressDataForChain,
tron,
utxo,
} from '../../thorchain-utils'
import type { CosmosSdkFeeData, SwapperApi } from '../../types'
Expand All @@ -24,6 +25,8 @@ export const thorchainApi: SwapperApi = {
getEvmTransactionFees: input => evm.getEvmTransactionFees(input, swapperName),
getUnsignedUtxoTransaction: input => utxo.getUnsignedUtxoTransaction(input, swapperName),
getUtxoTransactionFees: input => utxo.getUtxoTransactionFees(input, swapperName),
getUnsignedTronTransaction: input => tron.getUnsignedTronTransaction(input, swapperName),
getTronTransactionFees: input => tron.getTronTransactionFees(input, swapperName),
getUnsignedCosmosSdkTransaction: async ({
tradeQuote,
stepIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"ETH.GUSD-0X056FD409E1D7A124BD7017459DFEA2F387B6D5CD": "eip155:1/erc20:0x056fd409e1d7a124bd7017459dfea2f387b6d5cd",
"ETH.LINK-0X514910771AF9CA656AF840DFF83E8264ECF986CA": "eip155:1/erc20:0x514910771af9ca656af840dff83e8264ecf986ca",
"ETH.LUSD-0X5F98805A4E8BE255A32880FDEC7F6728C6568BA0": "eip155:1/erc20:0x5f98805a4e8be255a32880fdec7f6728c6568ba0",
"ETH.RAZE-0X5EAA69B29F99C84FE5DE8200340B4E9B4AB38EAC": "eip155:1/erc20:0x5eaa69b29f99c84fe5de8200340b4e9b4ab38eac",
"ETH.SNX-0XC011A73EE8576FB46F5E1C5751CA3B9FE0AF2A6F": "eip155:1/erc20:0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f",
"ETH.TGT-0X108A850856DB3F85D0269A2693D896B394C80325": "eip155:1/erc20:0x108a850856db3f85d0269a2693d896b394c80325",
"ETH.THOR-0XA5F2211B9B8170F694421F2046281775E8468044": "eip155:1/erc20:0xa5f2211b9b8170f694421f2046281775e8468044",
Expand All @@ -41,5 +40,7 @@
"LTC.LTC": "bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2",
"THOR.RUJI": "cosmos:thorchain-1/slip44:ruji",
"THOR.TCY": "cosmos:thorchain-1/slip44:tcy",
"TRON.TRX": "tron:0x2b6653dc/slip44:195",
"TRON.USDT-TR7NHQJEKQXGTCI8Q8ZY4PL8OTSZGJLJ6T": "tron:0x2b6653dc/trc20:tr7nhqjekqxgtci8q8zy4pl8otszgjlj6t",
"THOR.RUNE": "cosmos:thorchain-1/slip44:931"
}
36 changes: 32 additions & 4 deletions packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,12 +441,40 @@ export const getL1RateOrQuote = async <T extends ThorTradeRateOrQuote>(
)
}
case CHAIN_NAMESPACE.Tron: {
return Err(
makeSwapErrorRight({
message: 'Tron is not supported',
code: TradeQuoteError.UnsupportedTradePair,
const maybeRoutes = await Promise.allSettled(
perRouteValues.map((route): Promise<T> => {
const memo = getMemo(route)

// For rate quotes (no wallet), we can't calculate fees
// Actual fees will be calculated in getTronTransactionFees when executing
const networkFeeCryptoBaseUnit = undefined

return Promise.resolve(
makeThorTradeRateOrQuote<ThorUtxoOrCosmosTradeRateOrQuote>({
route,
allowanceContract: '0x0', // not applicable to TRON
memo,
feeData: {
networkFeeCryptoBaseUnit,
protocolFees: getProtocolFees(route.quote),
},
}),
)
}),
)

const routes = maybeRoutes.filter(isFulfilled).map(maybeRoute => maybeRoute.value)

if (!routes.length)
return Err(
makeSwapErrorRight({
message: 'Unable to create any routes',
code: TradeQuoteError.UnsupportedTradePair,
cause: maybeRoutes.filter(isRejected).map(maybeRoute => maybeRoute.reason),
}),
)

return Ok(routes)
}
case CHAIN_NAMESPACE.Sui: {
return Err(
Expand Down
1 change: 1 addition & 0 deletions packages/swapper/src/thorchain-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export * from './getPoolDetails'

export * as cosmossdk from './cosmossdk'
export * as evm from './evm'
export * as tron from './tron'
export * as utxo from './utxo'

export const getChainIdBySwapper = (swapperName: SwapperName) => {
Expand Down
Loading