Skip to content

feat: migrate to UFix128 + TidalMath; replace scaled UInt128 math; update tests/scripts; use native ops; register TidalMath; remove DeFiActionsMathUtils uses#48

Merged
kgrgpg merged 11 commits intomainfrom
feature/ufix128-upgrade
Oct 17, 2025
Merged

Conversation

@kgrgpg
Copy link
Copy Markdown
Contributor

@kgrgpg kgrgpg commented Oct 7, 2025

Overview

This PR migrates TidalProtocol from scaled UInt128 and DeFiActionsMathUtils to native UFix128 and a new TidalMath helper.

Why

  • Native decimals: precision and clarity with Cadence fixed-point UFix128.
  • Fewer pitfalls: remove implicit 1e24 scaling and conversion bugs.
  • Consistent rounding: centralize UFix64 conversions in TidalMath.
  • Simpler code: use native * and / for arithmetic.

What changed

  • Contracts
    • Add cadence/lib/TidalMath.cdc with constants (one, zero), toUFix128, and UFix64 rounding helpers (toUFix64Round, toUFix64RoundUp, toUFix64RoundDown).
    • Refactor cadence/contracts/TidalProtocol.cdc:
      • Use UFix128 for prices, factors, indices, health, and liquidation math.
      • TokenSnapshot fields renamed: creditIndex, debitIndex.
      • RiskParams stores decimal fractions for collateralFactor, borrowFactor, liquidationBonus (e.g., 0.05).
      • Rewrite scaledBalanceToTrueBalance, trueBalanceToScaledBalance, health, max-withdraw, and liquidation to native decimals.
      • Replace TidalMath.mul/div with native * and / for brevity.
      • Remove unused imports; avoid bitwise operations on UFix128.
  • Scripts
    • position_health.cdc: return type UFix128.
    • funds_req_for_target_health_after_withdraw.cdc: targetHealth: UFix128.
    • funds_avail_above_target_health_after_deposit.cdc: targetHealth: UFix128.
  • Tests
    • cadence/tests/test_helpers.cdc: UFix128 constants/helpers; deploy TidalMath before TidalProtocol.
    • Migrate math/liquidation/insolvency/funds and integration tests to UFix128 semantics.
    • Remove DeFiActionsMathUtils imports; use native ops or TidalMath rounding helpers.
    • Align phase0_pure_math_test.cdc with new TokenSnapshot and RiskParams fields.
  • Config
    • Update flow.json to register TidalMath; remove DeFiActionsMathUtils from emulator deployments.

How it works

  • Use UFix128 end-to-end for protocol math.
  • Use TidalMath only for conversions/rounding where needed.
  • Prefer native * and / for readability and less boilerplate.

Breaking changes

  • Scripts/transactions now accept/return UFix128 for health and ratios.
  • RiskParams.liquidationBonus is a fraction (e.g., 0.05), not 1 + LB.
  • TokenSnapshot uses creditIndex/debitIndex.

Validation

  • Full Cadence test suite passes on this branch.

DeFiActions utilities

  • DeFiActionsUtils and DeFiActionsMathUtils are no longer required by TidalProtocol; TidalMath supersedes them.

Follow-ups

  • Consider exponentiation-by-squaring if integer exponents reappear in interest compounding.

kgrgpg added 2 commits October 7, 2025 13:21
…date tests/scripts; use native ops; register TidalMath; remove DeFiActionsMathUtils uses
@codecov
Copy link
Copy Markdown

codecov Bot commented Oct 7, 2025

Codecov Report

❌ Patch coverage is 79.54545% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cadence/lib/TidalMath.cdc 79.54% 9 Missing ⚠️

📢 Thoughts on this report? Let us know!

@kgrgpg kgrgpg requested review from Kay-Zee and dete October 7, 2025 16:48
…ent, and justify UFix128 usage for recordDeposit/recordWithdrawal and TokenState
@kgrgpg kgrgpg force-pushed the feature/ufix128-upgrade branch from cae7c0d to e3276ff Compare October 13, 2025 17:53
…ain and TidalMath; adopt Type in events; remove DeFiActionsMathUtils from risk math; keep UFix128 health APIs and conversions
… in risk math, adopt Type in events, fix healthComputation (∞ for 0/0 and debt=0), adjust tests/scripts to UFix128 health; all tests passing
Copy link
Copy Markdown
Member

@Kay-Zee Kay-Zee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things, it definitely felt like the mul function is kinda unnecessary, any reason we're keeping it instead of just using *?

Also, we have a constant for 1.0 and 0.0 as UFix128 but we pretty much only use the 1.0 and not really use the 0.0 constant. Any reasoning for why?

}
Test.assert(quote.seizeAmount > 0.0, message: "Expected positive seizeAmount")
Test.assert(quote.newHF > hAfter && quote.newHF < DeFiActionsMathUtils.e24)
Test.assert(quote.newHF > hAfter && quote.newHF < TidalMath.toUFix128(1.0))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like we have a constant for this, no?

Suggested change
Test.assert(quote.newHF > hAfter && quote.newHF < TidalMath.toUFix128(1.0))
Test.assert(quote.newHF > hAfter && quote.newHF < TidalMath.one)

lb: DeFiActionsMathUtils.e24
cf: UFix128(cf),
bf: UFix128(bf),
lb: UFix128(0.05)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did this change to 0.05?

Comment thread flow.json
}
}
} No newline at end of file
"contracts": {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this mostly an indentation issue?

Comment thread cadence/lib/TidalMath.cdc
@@ -0,0 +1,92 @@
access(all) contract TidalMath {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mention in the PR that:

DeFiActionsUtils and DeFiActionsMathUtils are no longer required by TidalProtocol; TidalMath supersedes them.

Can we give a quick explainer why we don't just update DeFiActionsUtils to use ufix128 then continue to use the DeFiActionsUtils?

self.debitInterestIndex = DeFiActionsMathUtils.e24
self.currentCreditRate = UInt128(DeFiActionsMathUtils.e24)
self.currentDebitRate = UInt128(DeFiActionsMathUtils.e24)
self.totalCreditBalance = 0.0 as UFix128
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we not also have this as a constant?

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
@@ -1701,19 +1729,19 @@ access(all) contract TidalProtocol {
if potentialHealth <= targetHealth {
// We will hit the health target before using up all of the withdraw token credit. We can easily
// compute how many units of the token would bring the position down to the target health.
// let availableHealth = healthAfterDeposit == UInt128.max ? UInt128.max : healthAfterDeposit - targetHealth
// let availableEffectiveValue = (effectiveDebtAfterDeposit == 0 || availableHealth == UInt128.max)
// let availableHealth = healthAfterDeposit == UFix128.max ? UFix128.max : healthAfterDeposit - targetHealth
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this suppose to be part of the comments, or is it just commented out code? if so should we just remove?

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
let uintPrice = DeFiActionsMathUtils.toUInt128(self.priceOracle.price(ofToken: type)!)
let uintCollateralFactor = DeFiActionsMathUtils.toUInt128(self.collateralFactor[type]!)
let uintBorrowFactor = DeFiActionsMathUtils.toUInt128(self.borrowFactor[type]!)
let uintAmount = TidalMath.toUFix128(amount)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed that most of these, even though the type is converted to ufix, the naming is still uint, should we adjust them, up to you how you want to name them, buti just feel like uint is misleading now that the type is changed

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
assert(perSecondScaledValue < UInt128.max, message: "Per-second interest rate \(perSecondScaledValue) is too high")
return UInt128(perSecondScaledValue + DeFiActionsMathUtils.e24)
access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 {
let secondsInYearE24 = TidalMath.mul(31_536_000.0 as UFix128, TidalMath.one)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably don't need to bother with this mul

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
}

/// Returns the compounded interest index reflecting the passage of time
/// The result is: newIndex = oldIndex * perSecondRate ^ seconds
access(all) view fun compoundInterestIndex(oldIndex: UInt128, perSecondRate: UInt128, elapsedSeconds: UFix64): UInt128 {
access(all) view fun compoundInterestIndex(oldIndex: UFix128, perSecondRate: UFix128, elapsedSeconds: UFix64): UFix128 {
// For UFix128, we use repeated multiplication per second conservatively for now.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, then should this be an area where we would consider keeping UInt128?

Comment thread cadence/lib/TidalMath.cdc Outdated
access(all) case RoundEven
}

access(all) view fun mul(_ x: UFix128, _ y: UFix128): UFix128 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If mul is just a convenience function for * i do wonder what benefit there is to having it?

…lMath.one/zero, simplify perSecondInterestRate, rename locals for clarity, update tests; all Cadence tests passing
@kgrgpg
Copy link
Copy Markdown
Contributor Author

kgrgpg commented Oct 15, 2025

UFix128 cleanup and math utilities consolidation

What changed

  • Removed TidalMath.mul and replaced with the built-in * across the codebase. Kept TidalMath.div for its division-by-zero precondition.
  • Standardized on TidalMath.one/TidalMath.zero; replaced ad‑hoc TidalMath.toUFix128(1.0) and 0.0 as UFix128. Tests updated accordingly.
  • Simplified perSecondInterestRate: compute yearlyRate / 31_536_000.0 as UFix128 and add TidalMath.one.
  • Kept UFix128 for compoundInterestIndex to match indices/rates and avoid type churn; can optimize exponentiation later if needed.
  • Renamed misleading locals (e.g., uintPrice, uintBorrowFactor) to descriptive names (price, borrowFactor, collateralFactor, depositPrice, withdrawBorrowFactor, etc.).
  • Removed commented‑out code and minor formatting noise.
  • Tightened zero comparisons where types are unsigned: use == TidalMath.zero instead of <= 0.0 as UFix128.

Why

  • The mul wrapper was a no‑op; built‑ins are clearer and idiomatic. Keeping div preserves safety semantics.
  • Using constants improves readability and consistency, and reduces mixed literal typing.
  • Decoupling protocol math from DeFiActions and standardizing on UFix128 simplifies interfaces and testing.

Responses to review points

  • mul convenience: removed; * works for UFix128.
  • one/zero constants: both used; zero is used in liquidation math and clamping.
  • liquidationBonus as 0.05: correct for fractional UFix128.
  • Indices/rates: use TidalMath.one to represent no‑interest; swapped remaining literals to constants.
  • Unsigned checks: changed <= 0.0 to == TidalMath.zero where appropriate.
  • Precondition optional‑chaining: explicit nil/type checks retained for clearer error messages.
  • Commented blocks: removed.
  • Variable names: moved away from uint* labels now that values are UFix128.
  • perSecondInterestRate: removed redundant multiply by one.
  • compoundInterestIndex: kept on UFix128; can revisit for faster exponentiation later.
  • decreaseDebitBalance saturating behavior: still clamp‑to‑zero to avoid negative accounting; can add an assert with small epsilon in a follow‑up if we want stricter guarantees.

Tests

  • All Cadence tests pass locally via ./run_tests.sh on feature/ufix128-upgrade.

@kgrgpg
Copy link
Copy Markdown
Contributor Author

kgrgpg commented Oct 15, 2025

Interest compounding performance update

What changed

  • Added TidalMath.powUFix128(base, expSeconds) implementing exponentiation-by-squaring with integer-second exponent.
  • Updated TidalProtocol.compoundInterestIndex to compute oldIndex * powUFix128(perSecondRate, elapsedSeconds) instead of looping per-second.

Why

  • Keeps the math fully on UFix128 for consistency (rates/indices/health) while eliminating the O(seconds) loop. The new approach is O(log seconds) and avoids type churn to UInt128.
  • Floors elapsed time to whole seconds (as before), which matches Flow’s UFix64 timestamp semantics.

Tests

  • All Cadence tests pass locally with the new implementation via ./run_tests.sh.

…erestIndex to exponentiation-by-squaring; tests passing
Copy link
Copy Markdown
Member

@Kay-Zee Kay-Zee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! just 2 nits

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
TidalMath.div(trueDebt * depositPrice2, depositBorrowFactor2)
effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral +
TidalMath.mul(TidalMath.mul(uintDepositAmount - trueDebt, uintDepositPrice), uintDepositCollateralFactor)
(depositAmountU - trueDebt) * depositPrice2 * depositCollateralFactor2
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not super fond of the 2 in the naming

Comment thread cadence/lib/TidalMath.cdc
let remainder: UFix128 = value - truncatedAs128

if remainder <= 0.0 as UFix128 {
if remainder == 0.0 as UFix128 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might as well use the constant

@@ -0,0 +1,33 @@
### UFix128 cleanup and math utilities consolidation
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not commit these, best for these just to live on the PR, i saw you had added the original PR body to your git ignore, but maybe this folder should be as well

@Kay-Zee
Copy link
Copy Markdown
Member

Kay-Zee commented Oct 15, 2025

Could we also take a look at coverage before merging please

@kgrgpg
Copy link
Copy Markdown
Contributor Author

kgrgpg commented Oct 17, 2025

Addressed latest comments and increased coverage

@kgrgpg kgrgpg merged commit dc59949 into main Oct 17, 2025
2 of 3 checks passed
@kgrgpg kgrgpg deleted the feature/ufix128-upgrade branch October 17, 2025 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants