Skip to content

Conversation

peer2f00l
Copy link
Collaborator

@peer2f00l peer2f00l commented Mar 17, 2025

Adds some compile-time guarantees to interest and yield accumulations. It's not incredibly obvious how the code works, but I think it's actually quite simple once the intent is understood.

Basic idea:

Before certain operations (adding/removing principal to borrow positions, adding/removing supply to supply positions), we need to recalculate the interest/yield (since the rate of accumulation will change after the addition/removal).

One way to ensure this happens is to just call the accumulation function at the top of the addition/removal functions. The problem with this approach is twofold:

  1. Operations that involve more than just a single addition/removal of the asset may end up calling that function multiple times within the same receipt unnecessarily. We guarantee that >1 calls to that function per position per receipt is unnecessary because those functions calculate up to, but not including the current time chunk.
  2. It is possible to forget to call that function, e.g. should new operations be introduced or if the code undergoes a major overhaul. Tests should hopefully catch this, but it is not guaranteed.

The way presented in this PR is to require any change of those balances (borrow principal or supply deposit) to also take ownership of an argument of a type that can only be created by calling the corresponding accumulation function. We call this special ZST a "Proof" type. (There might be a more formal name for this pattern that is escaping me at present.)

@peer2f00l peer2f00l requested a review from royalf00l March 17, 2025 09:44
Copy link

github-actions bot commented Mar 17, 2025

Staging contract is deployed to gh-55.templar-in-training.testnet account


Gas Report

harvest_yield

Iterations Gas
0 3.3 Tgas
10 4.0 Tgas
20 4.4 Tgas

Copy link
Collaborator

@petarvujovic98 petarvujovic98 left a comment

Choose a reason for hiding this comment

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

Another way, although a bit more involved, would be to use the type state pattern. In this case we could have a generic type which enables us to hide certain functions/methods under the InterestAccumulated type constraint.
If it is something you'd want to try out, I can do a similar implementation to this PR sometime this week

@peer2f00l
Copy link
Collaborator Author

@petarvujovic98 Let's give it a shot.

@royalf00l
Copy link
Collaborator

Adds some compile-time guarantees to interest and yield accumulations. It's not incredibly obvious how the code works, but I think it's actually quite simple once the intent is understood.

Basic idea:

Before certain operations (adding/removing principal to borrow positions, adding/removing supply to supply positions), we need to recalculate the interest/yield (since the rate of accumulation will change after the addition/removal).

One way to ensure this happens is to just call the accumulation function at the top of the addition/removal functions. The problem with this approach is twofold:

  1. Operations that involve more than just a single addition/removal of the asset may end up calling that function multiple times within the same receipt unnecessarily. We guarantee that >1 calls to that function per position per receipt is unnecessary because those functions calculate up to, but not including the current time chunk.
  2. It is possible to forget to call that function, e.g. should new operations be introduced or if the code undergoes a major overhaul. Tests should hopefully catch this, but it is not guaranteed.

The way presented in this PR is to require any change of those balances (borrow principal or supply deposit) to also take ownership of an argument of a type that can only be created by calling the corresponding accumulation function. We call this special ZST a "Proof" type. (There might be a more formal name for this pattern that is escaping me at present.)

Just clarifying that interest rates, and thus interest/yield amounts, will only change based on stablecoin utilization (aka demand for specific stablecoin lending market). Addition or removal of collateral only changes the collateral ratio, not yield for borrower or lender. Changes to the stablecoin borrowed/repaid will (marginally) affect the interest rate of the market and thus the total interest on the loan.

I don't have a strong opinion on the details of how this calculation is implemented.

@peer2f00l
Copy link
Collaborator Author

@royalf00l Good clarification.

The way the contract currently calculates yield/interest is as follows:

  • The contract keeps track of the global state changes (e.g. total deposited, total borrowed, yield distributions) in a list of snapshots.
  • When a position needs to calculate yield/interest, it can do so by iterating through the snapshots for a given period and calculating e.g. usage ratio & interest rate for a borrow position during a particular timespan.
  • In order to accurately calculate the yield/interest at any point in time, the system must know how much deposit/principal the position had at that point in time.
  • That could be accomplished by also keeping track of a position's history. Instead, I opted to force recalculations at every point that the deposit/principal of the position changed.

Maybe it would be better to rethink that last point and track position history as well?

@royalf00l
Copy link
Collaborator

  • In order to accurately calculate the yield/interest at any point in time, the system must know how much deposit/principal the position had at that point in time.
  • That could be accomplished by also keeping track of a position's history. Instead, I opted to force recalculations at every point that the deposit/principal of the position changed.

That's fine for now, even if it's not optimally efficient. Ideally, the yield recalculations are only done when changes are made to the stablecoin position, not the collateral. I'm fine noting this for now, saving as a low priority issue (optimization) to come back to later.

@peer2f00l
Copy link
Collaborator Author

Currently no calculations are triggered for collateral balance changes. Only for supply deposit and borrow principal. This issue is more of a matter of safety than optimization, to be honest. It has the upside of a small amount of optimization, but that is not the reason I proposed this change, it's just a happy side-effect.

@peer2f00l peer2f00l changed the title Proof-based invariants Proof type invariants Mar 21, 2025
@petarvujovic98
Copy link
Collaborator

@petarvujovic98 Let's give it a shot.

My initial idea for the typestate pattern would overcomplicate things substantially, mainly in ways where we would have the generic types spread into too much of the codebase to only save us from using a simple marker (proof) type like you implemented here.
So it seems more reasonable to simply go ahead with this approach.

@peer2f00l peer2f00l marked this pull request as ready for review March 24, 2025 07:21
@peer2f00l peer2f00l merged commit 8c9716c into dev Mar 24, 2025
5 checks passed
@peer2f00l peer2f00l deleted the feat/token-based-invariants branch March 24, 2025 09:37
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