Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate burned amount for p-chain dynamic fee #880

Merged
merged 9 commits into from
Oct 10, 2024
16 changes: 16 additions & 0 deletions src/fixtures/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const upgradesInfo = {
apricotPhaselTime: '2020-12-05T05:00:00Z',
apricotPhase2Time: '2020-12-05T05:00:00Z',
apricotPhase3Time: '2020-12-05T05:00:00Z',
apricotPhase4Time: '2020-12-05T05:00:00Z',
apricotPhase4MinPChainHeight: 0,
apricotPhase5Time: '2020-12-05T05:00:00Z',
apricotPhasePre6Time: '2020-12-05T05:00:00Z',
apricotPhase6Time: '2020-12-05T05:00:00Z',
apricotPhasePost6Time: '2020-12-05T05:00:00Z',
banffTime: '2020-12-05T05:00:00Z',
cortinaTime: '2020-12-05T05:00:00Z',
cortinaXChainStopVertexID: '11111111111111111111111111111111LpoYY',
durangoTime: '2020-12-05T05:00:00Z',
etnaTime: '2020-12-05T05:00:00Z',
};
3 changes: 2 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export * from './getTransferableInputsByTx';
export * from './getTransferableOutputsByTx';
export * from './getUtxoInfo';
export * from './getBurnedAmountByTx';
export * from './validateBurnedAmount';
export * from './validateBurnedAmount/validateBurnedAmount';
export * from './isEtnaEnabled';
export { unpackWithManager, getManagerForVM, packTx } from './packTx';
8 changes: 8 additions & 0 deletions src/utils/isEtnaEnabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { GetUpgradesInfoResponse } from '../info/model';

export const isEtnaEnabled = (
upgradesInfo: GetUpgradesInfoResponse,
): boolean => {
const { etnaTime } = upgradesInfo;
return new Date(etnaTime) < new Date();
};
136 changes: 0 additions & 136 deletions src/utils/validateBurnedAmount.ts

This file was deleted.

100 changes: 100 additions & 0 deletions src/utils/validateBurnedAmount/validateBurnedAmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { Context } from '../../vms/context/model';
import type { Transaction, UnsignedTx } from '../../vms/common';
import type { EVMTx } from '../../serializable/evm';
import { isImportExportTx as isEvmImportExportTx } from '../../serializable/evm';
import { getBurnedAmountByTx } from '../getBurnedAmountByTx';
import type { AvaxTx } from '../../serializable/avax';
import { validateDynamicBurnedAmount } from './validateDynamicBurnedAmount';
import type { GetUpgradesInfoResponse } from '../../info/model';
import { isEtnaEnabled } from '../isEtnaEnabled';
import { validateStaticBurnedAmount } from './validateStaticBurnedAmount';
import { costCorethTx } from '../costs';
import { calculateFee } from '../../vms/pvm/txs/fee/calculator';

import {
isAddPermissionlessDelegatorTx,
isAddPermissionlessValidatorTx,
isAddSubnetValidatorTx,
isCreateChainTx,
isCreateSubnetTx,
isPvmBaseTx,
isExportTx as isPvmExportTx,
isImportTx as isPvmImportTx,
isRemoveSubnetValidatorTx,
isTransferSubnetOwnershipTx,
} from '../../serializable/pvm';

const _getBurnedAmount = (tx: Transaction, context: Context) => {
const burnedAmounts = getBurnedAmountByTx(tx as AvaxTx | EVMTx);
return burnedAmounts.get(context.avaxAssetID) ?? 0n;
};

// Check supported pvm transactions for Etna
// Todo: add isAvmBaseTx, isAvmExportTx and isAvmImportTx when avm dynmamic fee is implemented
const isEtnaSupported = (tx: Transaction) => {
return (
// isAvmBaseTx(tx) || // not implemented
// isAvmExportTx(tx) || // not implemented
// isAvmImportTx(tx) || // not implemented
isPvmBaseTx(tx) ||
isPvmExportTx(tx) ||
isPvmImportTx(tx) ||
isAddPermissionlessValidatorTx(tx) ||
isAddPermissionlessDelegatorTx(tx) ||
isAddSubnetValidatorTx(tx) ||
isCreateChainTx(tx) ||
isCreateSubnetTx(tx) ||
isRemoveSubnetValidatorTx(tx) ||
isTransferSubnetOwnershipTx(tx)
);
};

/**
* Validate burned amount for avalanche transactions
*
* @param unsignedTx: unsigned transaction
* @param burnedAmount: burned amount in nAVAX
* @param baseFee
** c-chain: fetched from the network and converted into nAvax (https://docs.avax.network/quickstart/transaction-fees#c-chain-fees)
** x/p-chain: pvm dynamic fee caculator, https://github.com/ava-labs/avalanchego/blob/master/vms/platformvm/txs/fee/dynamic_calculator.go
* @param feeTolerance: tolerance percentage range where the burned amount is considered valid. e.g.: with FeeTolerance = 20% -> (expectedFee <= burnedAmount <= expectedFee * 1.2)
* @return {boolean} isValid: : true if the burned amount is valid, false otherwise.
* @return {bigint} txFee: burned amount in nAVAX
*/
export const validateBurnedAmount = ({
unsignedTx,
context,
upgradesInfo,
burnedAmount,
baseFee,
feeTolerance,
}: {
unsignedTx: UnsignedTx;
context: Context;
upgradesInfo?: GetUpgradesInfoResponse;
burnedAmount?: bigint;
baseFee: bigint;
feeTolerance: number;
}): { isValid: boolean; txFee: bigint } => {
const tx = unsignedTx.getTx();
const burned = burnedAmount ?? _getBurnedAmount(tx, context);

if (
isEvmImportExportTx(tx) ||
(upgradesInfo && isEtnaEnabled(upgradesInfo) && isEtnaSupported(tx))
) {
const feeAmount = isEvmImportExportTx(tx)
? baseFee * costCorethTx(unsignedTx)
: calculateFee(tx, context.platformFeeConfig.weights, baseFee);
erictaylor marked this conversation as resolved.
Show resolved Hide resolved
return validateDynamicBurnedAmount({
burnedAmount: burned,
feeAmount,
feeTolerance,
});
}
return validateStaticBurnedAmount({
unsignedTx,
context,
burnedAmount: burned,
});
};
68 changes: 68 additions & 0 deletions src/utils/validateBurnedAmount/validateDynamicBurnedAmount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { validateDynamicBurnedAmount } from './validateDynamicBurnedAmount';

describe('validateDynamicBurnedAmount', () => {
it('throws an expected error if feeTolerance is less than 1', () => {
expect(() =>
validateDynamicBurnedAmount({
burnedAmount: (280750n * 75n) / 100n, // 25% lower,
feeAmount: 280750n,
feeTolerance: 0.5,
}),
).toThrowError('feeTolerance must be [1,100]');
});
it('throws an expected error if feeTolerance is greater than 100', () => {
expect(() =>
validateDynamicBurnedAmount({
burnedAmount: (280750n * 75n) / 100n, // 25% lower,
feeAmount: 280750n,
feeTolerance: 101,
}),
).toThrowError('feeTolerance must be [1,100]');
});

it('returns false if burned amount is over the tolerance range', () => {
const resultHigher = validateDynamicBurnedAmount({
burnedAmount: (280750n * 151n) / 100n, // 51% higher
feeAmount: 280750n,
feeTolerance: 50.9,
});
expect(resultHigher).toStrictEqual({
isValid: false,
txFee: (280750n * 151n) / 100n,
});
});

it('returns false if burned amount is below the tolerance range', () => {
const resultLower = validateDynamicBurnedAmount({
burnedAmount: (280750n * 49n) / 100n, // 51% lower
feeAmount: 280750n,
feeTolerance: 50.9,
});
expect(resultLower).toStrictEqual({
isValid: false,
txFee: (280750n * 49n) / 100n,
});
});
it('returns true if burned amount is within the min tolerance range', () => {
const resultLower = validateDynamicBurnedAmount({
burnedAmount: (280750n * 75n) / 100n, // 25% lower
feeAmount: 280750n,
feeTolerance: 50.9,
});
expect(resultLower).toStrictEqual({
isValid: true,
txFee: (280750n * 75n) / 100n,
});
});
it('returns true if burned amount is within the max tolerance range', () => {
const resultHigher = validateDynamicBurnedAmount({
burnedAmount: (280750n * 125n) / 100n, // 25% higher
feeAmount: 280750n,
feeTolerance: 50.9,
});
expect(resultHigher).toStrictEqual({
isValid: true,
txFee: (280750n * 125n) / 100n,
});
});
});
32 changes: 32 additions & 0 deletions src/utils/validateBurnedAmount/validateDynamicBurnedAmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Validate dynamic burned amount for avalanche c/p transactions
*
* @param burnedAmount: burned amount in nAVAX
* @param feeAmount: fee
* @param feeTolerance: tolerance percentage range where the burned amount is considered valid. e.g.: with FeeTolerance = 20% -> (expectedFee <= burnedAmount <= expectedFee * 1.2)
* @return {boolean} isValid: : true if the burned amount is valid, false otherwise.
* @return {bigint} txFee: burned amount in nAVAX
*/
export const validateDynamicBurnedAmount = ({
burnedAmount,
feeAmount,
feeTolerance,
}: {
burnedAmount: bigint;
feeAmount: bigint;
feeTolerance: number;
}): { isValid: boolean; txFee: bigint } => {
const feeToleranceInt = Math.floor(feeTolerance);

if (feeToleranceInt < 1 || feeToleranceInt > 100) {
throw new Error('feeTolerance must be [1,100]');
}

const min = (feeAmount * (100n - BigInt(feeToleranceInt))) / 100n;
const max = (feeAmount * (100n + BigInt(feeToleranceInt))) / 100n;

return {
isValid: burnedAmount >= min && burnedAmount <= max,
txFee: burnedAmount,
};
};
Loading
Loading