Skip to content

refactor: implement phase 0 - extract pure math functions from positionHealth#35

Merged
kgrgpg merged 9 commits intomainfrom
refactor/better-code
Aug 19, 2025
Merged

refactor: implement phase 0 - extract pure math functions from positionHealth#35
kgrgpg merged 9 commits intomainfrom
refactor/better-code

Conversation

@kgrgpg
Copy link
Copy Markdown
Contributor

@kgrgpg kgrgpg commented Aug 4, 2025

Overview

This PR implements Phase 0 of the refactoring plan, extracting pure mathematical functions from the positionHealth method.

Implementation Status

Phase 0 - All tests passing

Changes

Documentation

  • REFACTORING_PLAN.md: Comprehensive plan outlining the functional programming approach
  • REFACTOR_EVALUATION.md: Evaluation criteria for the refactoring

Implementation (Phase 0)

  • Created pure value types (RiskParams, TokenSnapshot, PositionView) to represent immutable data
  • Extracted effectiveCollateral and effectiveDebt as pure functions
  • Refactored positionHealth to use new healthFactor pure function
  • Moved buildPositionView into Pool struct for proper scoping
  • Added parameter names for clarity in function calls

Bug Fixes

  • Fixed InternalBalance struct to use single parameterized initializer (Cadence best practice)
  • Resolved forward reference issues by moving structs before Pool resource
  • Updated all InternalBalance() calls to pass explicit parameters

Benefits

  • Improved testability: Pure functions can be tested in isolation
  • Better modularity: Clear separation between pure computations and stateful operations
  • Enhanced readability: Named parameters and focused functions make the code easier to understand
  • Reusability: Pure functions can be reused in other parts of the codebase

Testing

All 12 tests passing successfully:

  • auto_borrow_behavior_test.cdc
  • funds_available_above_target_health_test.cdc
  • funds_required_for_target_health_test.cdc
  • platform_integration_test.cdc
  • pool_creation_workflow_test.cdc
  • position_lifecycle_happy_test.cdc
  • rebalance_overcollateralised_test.cdc
  • rebalance_undercollateralised_test.cdc
  • reserve_withdrawal_test.cdc
  • token_governance_addition_test.cdc
  • utils_test.cdc
  • zero_debt_withdrawal_test.cdc

The refactored code maintains the same functionality as the original implementation while following Cadence best practices.

@codecov
Copy link
Copy Markdown

codecov Bot commented Aug 4, 2025

Codecov Report

❌ Patch coverage is 95.89041% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cadence/contracts/TidalProtocol.cdc 95.89% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

- Created pure value types (RiskParams, TokenSnapshot, PositionView)
- Extracted effectiveCollateral and effectiveDebt as pure functions
- Refactored positionHealth to use new healthFactor pure function
- Moved buildPositionView into Pool struct for proper scoping
- Added parameter names for clarity in function calls

This refactoring improves code modularity and testability by separating
pure mathematical computations from stateful operations.
@kgrgpg kgrgpg changed the title docs: add comprehensive refactoring plan for TidalProtocol refactor: implement phase 0 - extract pure math functions from positionHealth Aug 7, 2025
…mpatibility

- Remove duplicate init functions (Cadence only allows one init per struct)
- Use single parameterized initializer with explicit parameters
- Update all InternalBalance() calls to pass direction and scaledBalance explicitly
- All 12 tests now passing successfully
Comment thread REFACTORING_PLAN.md
• Mutable pieces that stay in storage
– `Pool.globalLedger[..]` indexes, `InternalPosition.balances`, queues/reserves

## 2. Code (only the slice; other commands stubbed with TODO)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you regenerate cadence code in this file, it shows deprecated cadence syntax, and could cause more confusion if referenced

…add phase0 pure math tests; remove DeFiBlocks dir and REFACTOR_EVALUATION.md
Copy link
Copy Markdown
Contributor

@sisyphusSmiling sisyphusSmiling left a comment

Choose a reason for hiding this comment

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

Looks like this needs to merge latest from main after #33 and update the values to use the UInt128 with 24 decimal places.

Comment thread cadence/contracts/TidalProtocol.cdc Outdated

// PURE HELPERS -------------------------------------------------------------

access(all) fun effectiveCollateral(credit: UInt256, snap: TokenSnapshot): UInt256 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe this and the other pure helpers can be made view. They would also benefit from comments with a brief explanation of what they do and represent in context of the protocol

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
/// Copy-only representation of a position used by pure math
access(all) struct PositionView {
access(all) let balances: {Type: InternalBalance}
access(all) let tokenSnaps: {Type: TokenSnapshot}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

np: snapshots is more intuitive IMO

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
access(all) fun healthFactor(view: PositionView): UInt256 {
var coll: UInt256 = 0
var debt: UInt256 = 0
for t in view.balances.keys {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think being a bit more verbose is helpful for comprehension, maybe using token or type instead of t so a reader doesn't have to revisit the balances type definition.

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
var coll: UInt256 = 0
var debt: UInt256 = 0
for t in view.balances.keys {
let b = view.balances[t]!
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same comment as above - bal would be a bit clearer and consistent with code elsewhere in the contract

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
access(all) struct RiskParams {
access(all) let collateralFactor: UInt256
access(all) let borrowFactor: UInt256
access(all) let liquidationBonus: UInt256
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Have we settled how we will go about liquidation? I thought it was going to be auction style, however this field makes me think it will be a set percentage

Comment thread cadence/contracts/TidalProtocol.cdc Outdated

var effColl: UInt256 = 0
var effDebt: UInt256 = 0
for t in view.balances.keys {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar comments as above. I think these single letter and acronym variables can be difficult to follow.

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
direction: bal.direction,
scaledBalance: bal.scaledBalance
)
let ts = self._borrowUpdatedTokenState(type: t)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ts could be perceived further as TokenState or TokenSnapshot - I think state would be clear and concise enough

Comment thread cadence/contracts/TidalProtocol.cdc Outdated
Comment on lines +1721 to +1725
let bal = position.balances[t]!
balancesCopy[t] = TidalProtocol.InternalBalance(
direction: bal.direction,
scaledBalance: bal.scaledBalance
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure it matters too much since we need to iterate to build snaps, but we could add a method on InternalPosition like fun copyBalances() and copy from there without the need to iteratively reconstruct the balance. Up to you but noting since I've had to use that pattern elsewhere when copying a field from a reference to a composite type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps this is something to add onto later, but I'd hope to see us add quite a few more test cases against these methods since part of the goal of this refactor was to increase test coverage over core calculations

kgrgpg added 3 commits August 18, 2025 15:59
…kenSnaps->snapshots; clearer var names; add InternalPosition.copyBalances; use TidalProtocolUtils.decimals in conversions
…solve conflicts; port pure helpers and PositionView types; keep origin/main positionHealth impl
…eFiActionsMathUtils; all Cadence tests passing
Comment on lines +211 to +221
access(all) fun copyBalances(): {Type: InternalBalance} {
let copy: {Type: InternalBalance} = {}
for tokenType in self.balances.keys {
let balance = self.balances[tokenType]!
copy[tokenType] = TidalProtocol.InternalBalance(
direction: balance.direction,
scaledBalance: balance.scaledBalance
)
}
return copy
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
access(all) fun copyBalances(): {Type: InternalBalance} {
let copy: {Type: InternalBalance} = {}
for tokenType in self.balances.keys {
let balance = self.balances[tokenType]!
copy[tokenType] = TidalProtocol.InternalBalance(
direction: balance.direction,
scaledBalance: balance.scaledBalance
)
}
return copy
}
access(all) fun copyBalances(): {Type: InternalBalance} {
return self.balances
}

risk: TidalProtocol.RiskParams(
cf: DeFiActionsMathUtils.toUInt128(self.collateralFactor[t]!),
bf: DeFiActionsMathUtils.toUInt128(self.borrowFactor[t]!),
lb: DeFiActionsMathUtils.e24 + 50_000_000_000_000_000_000_000
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Will we want to adjust the liquidation bonus at some point in the future? Potentially out of scope for this PR, but flagging in case we want this to be configurable

risk: TidalProtocol.RiskParams(
cf: DeFiActionsMathUtils.toUInt128(self.collateralFactor[type]!),
bf: DeFiActionsMathUtils.toUInt128(self.borrowFactor[type]!),
lb: DeFiActionsMathUtils.e24 + 50_000_000_000_000_000_000_000
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same question I made in buildPositionView regarding ability to configure liquidation bonus value. Seems like the kind of thing we'd want under pool governance.

Comment on lines +644 to +645
cf: DeFiActionsMathUtils.toUInt128(self.collateralFactor[type]!),
bf: DeFiActionsMathUtils.toUInt128(self.borrowFactor[type]!),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I noticed that we never actually use the UFix64 collateral or borrow factors. I think we could refactors those values to be UInt128 to avoid all the conversions whenever they're used. I suppose we could still accept them as UFix64 on addSupportedToken if we think that's less prone to input error, but perform the conversion once on write and just read the stored UInt128 value

@kgrgpg
Copy link
Copy Markdown
Contributor Author

kgrgpg commented Aug 19, 2025

Acknowledged the copyBalances suggestion. Since Cadence dictionaries are values, returning self.balances gives a safe copy. I simplified InternalPosition.copyBalances() to return the balances map directly. Governance-controlled liquidationBonus and storing risk factors as UInt128 (convert once on write) are good ideas; I’ll handle both in follow-up PRs to keep Phase 0 focused.

@kgrgpg kgrgpg merged commit faf2982 into main Aug 19, 2025
2 checks passed
@kgrgpg kgrgpg deleted the refactor/better-code branch August 19, 2025 14:38
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.

3 participants