ido: categories in display/UI byte order, drop hardcoded PLP limits

libriften moved encodeVmTokenCategory/decodeVmTokenCategory (which reverse
the 32 category bytes, display<->VM) into the ido announcement and ORB
IdoParams serialize/deserialize layer. Net wire effects the indexer sees:
- preinit announcement delphiCategory[44..76] and xTokenCategory[126..158]
  are now VM/little-endian (were display order);
- the IdoParams commitment stays VM on-wire, but the derived on-chain
  postlaunch orbPoolParamsCategory push flips display->VM (decode now
  returns display, the unchanged bake re-wraps it in encodeVmTokenCategory).

Keep the three categories (delphiCategory, xTokenCategory,
orbPoolParamsCategory) in display/UI byte order in-memory and over RPC:
reverse at every parse site (announcement + IdoParams NFT commitment) and
restore the .rev() on the output#0 delphi-NFT to_blob() compare. The
build_*_bytecode helpers keep reversing display->VM when baking, so the
output#7 P2SH32 rebuild matches the new on-chain layout.

Also drop the now-unused hardcoded MAX_PLP_SHARE / MAX_PLP_ANNUAL_DISCOUNT
/ MIN_PLP_ANNUAL_DISCOUNT statics and dead checks; the min-PLP limits are
NFT-pinned (minPlpShare / minPlpAfterDiscount) and the discount/max-discount
checks now just guard against negatives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hossein Zoda 2026-07-14 21:44:18 +00:00
parent 4677cc955e
commit c1952142de

View file

@ -70,9 +70,6 @@ const OFFERING_ENTRY_FLAG_XWNT: u8 = 0x20; // exchange with native token (legacy
static PERMANENT_LIQUIDITY_SHARE_DENOMINATOR: LazyLock<Integer> = static PERMANENT_LIQUIDITY_SHARE_DENOMINATOR: LazyLock<Integer> =
LazyLock::new(|| Integer::from(100_000_000i64)); LazyLock::new(|| Integer::from(100_000_000i64));
static MAX_PLP_SHARE: LazyLock<Integer> = LazyLock::new(|| Integer::from(80_000_000i64)); // 80%
static MAX_PLP_ANNUAL_DISCOUNT: LazyLock<Integer> = LazyLock::new(|| Integer::from(10_000_000i64)); // 10%
static MIN_PLP_ANNUAL_DISCOUNT: LazyLock<Integer> = LazyLock::new(|| Integer::from(0i64)); // 0%
// The minimum PLP share and minimum PLP-after-discount are no longer hardcoded: // The minimum PLP share and minimum PLP-after-discount are no longer hardcoded:
// they are pinned per-IdoParams NFT (minPlpShare / minPlpAfterDiscount, numerators // they are pinned per-IdoParams NFT (minPlpShare / minPlpAfterDiscount, numerators
// over PERMANENT_LIQUIDITY_SHARE_DENOMINATOR) and enforced against the offering. // over PERMANENT_LIQUIDITY_SHARE_DENOMINATOR) and enforced against the offering.
@ -155,8 +152,10 @@ static REBUILD_IPFS_PLACEHOLDER_BCMR_FOR_GENESIS_WITH_AUTHGUARD_CONTRACT: LazyLo
/// [117..121] entryExecutionFee (pIntLE, sats) /// [117..121] entryExecutionFee (pIntLE, sats)
/// [121..125] createExecutionFee (pIntLE, sats) /// [121..125] createExecutionFee (pIntLE, sats)
/// [125..127] paramsUseFee (pIntLE, sats/100 — not parsed here) /// [125..127] paramsUseFee (pIntLE, sats/100 — not parsed here)
/// Categories and the nfth are stored in the same byte order the announcement /// Categories are carried on-chain in VM (little-endian) byte order and are
/// uses, so equality checks against announced parameters are direct compares. /// reversed to display/UI order on parse (matching the announcement parse), so
/// equality checks against announced parameters remain direct compares. The
/// nfth is a hash and is kept in its on-chain order.
const IDO_PARAMS_COMMITMENT_SIZE: usize = 127; const IDO_PARAMS_COMMITMENT_SIZE: usize = 127;
/// The only ORB IdoParams NFT commitment version this indexer understands /// The only ORB IdoParams NFT commitment version this indexer understands
@ -188,8 +187,10 @@ fn parse_ido_params_nft_commitment(commitment: &[u8]) -> Result<IdoParamsNftComm
} }
Ok(IdoParamsNftCommitment { Ok(IdoParamsNftCommitment {
version: commitment[0], version: commitment[0],
orbPoolParamsCategory: commitment[1..33].to_vec(), // Categories are stored on-chain in VM (little-endian) byte order; the
delphiCategory: commitment[33..65].to_vec(), // indexer keeps them in display/UI order, so reverse on parse.
orbPoolParamsCategory: commitment[1..33].iter().copied().rev().collect(),
delphiCategory: commitment[33..65].iter().copied().rev().collect(),
platformFeeNfth: commitment[65..97].to_vec(), platformFeeNfth: commitment[65..97].to_vec(),
platformFee: vm_number_to_bigint(&commitment[97..101]), platformFee: vm_number_to_bigint(&commitment[97..101]),
minExpireDuration: vm_number_to_bigint(&commitment[101..105]), minExpireDuration: vm_number_to_bigint(&commitment[101..105]),
@ -1475,7 +1476,8 @@ fn parse_ido_preinit_tx_params(
offering: IdoParametersOffering { offering: IdoParametersOffering {
platformFeeNFTH: data[8..40].to_vec(), platformFeeNFTH: data[8..40].to_vec(),
platformFeeNumerator: vm_number_to_bigint(&data[40..44]), platformFeeNumerator: vm_number_to_bigint(&data[40..44]),
delphiCategory: data[44..76].to_vec(), // On-chain in VM order; kept in display/UI order.
delphiCategory: data[44..76].iter().copied().rev().collect(),
launchConditions: IdoParametersOfferingLaunchConditions { launchConditions: IdoParametersOfferingLaunchConditions {
expiresAt: vm_number_to_bigint(&data[76..82]), expiresAt: vm_number_to_bigint(&data[76..82]),
deployThreshold: vm_number_to_bigint(&data[82..90]), deployThreshold: vm_number_to_bigint(&data[82..90]),
@ -1486,7 +1488,8 @@ fn parse_ido_preinit_tx_params(
discountAnnualRateNumerator: vm_number_to_bigint(&data[102..106]), discountAnnualRateNumerator: vm_number_to_bigint(&data[102..106]),
priceNumerator: vm_number_to_bigint(&data[106..122]), priceNumerator: vm_number_to_bigint(&data[106..122]),
minOffer: vm_number_to_bigint(&data[122..126]), minOffer: vm_number_to_bigint(&data[122..126]),
xTokenCategory: data[126..158].to_vec(), // On-chain in VM order; kept in display/UI order.
xTokenCategory: data[126..158].iter().copied().rev().collect(),
}, },
executionFee: vm_number_to_bigint(&data[158..162]), executionFee: vm_number_to_bigint(&data[158..162]),
}, },
@ -1526,33 +1529,18 @@ fn parse_ido_preinit_tx_params(
} }
} }
if preinit_parameters.permanentLiquidityShareNumerator > *MAX_PLP_SHARE {
invalid_ido_reasons.push(anyhow::anyhow!(
"preinit_parameters.permanentLiquidityShareNumerator > *MAX_PLP_SHARE"
));
is_valid_ido = false;
}
if preinit_parameters if preinit_parameters
.offering .offering
.offer .offer
.discountAnnualRateNumerator .discountAnnualRateNumerator
< *MIN_PLP_ANNUAL_DISCOUNT < 0
{ {
invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.discountAnnualRateNumerator > *MAX_PLP_ANNUAL_DISCOUNT")); invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.discountAnnualRateNumerator < 0"));
is_valid_ido = false;
}
if preinit_parameters
.offering
.offer
.discountAnnualRateNumerator
> *MAX_PLP_ANNUAL_DISCOUNT
{
invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.discountAnnualRateNumerator > *MAX_PLP_ANNUAL_DISCOUNT"));
is_valid_ido = false; is_valid_ido = false;
} }
if preinit_parameters.offering.offer.maxDiscountRateNumerator < 0 { if preinit_parameters.offering.offer.maxDiscountRateNumerator < 0 {
invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.maxDiscountRateNumerator < Integer::from(0)")); invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.maxDiscountRateNumerator < 0"));
is_valid_ido = false; is_valid_ido = false;
} }
if preinit_parameters.offeredTokenAmount <= 0 { if preinit_parameters.offeredTokenAmount <= 0 {
@ -1639,9 +1627,9 @@ fn parse_ido_preinit_tx_params(
match tx.output.first() { match tx.output.first() {
Some(delphiOut) => match delphiOut.token.as_ref() { Some(delphiOut) => match delphiOut.token.as_ref() {
Some(delphiToken) if delphiToken.has_nft() => { Some(delphiToken) if delphiToken.has_nft() => {
let delphiTokenId: Vec<u8> = let delphiTokenCategory: Vec<u8> =
delphiToken.id.to_blob().iter().copied().rev().collect(); delphiToken.id.to_blob().iter().copied().rev().collect();
if delphiTokenId != preinit_parameters.offering.delphiCategory { if delphiTokenCategory != preinit_parameters.offering.delphiCategory {
invalid_ido_reasons.push(anyhow::anyhow!("delphi nft category in first output != offering.delphiCategory")); invalid_ido_reasons.push(anyhow::anyhow!("delphi nft category in first output != offering.delphiCategory"));
is_valid_ido = false; is_valid_ido = false;
} }