Skip to content

Conversation

@imef-femi
Copy link
Contributor

Summary:
Introduces a custom forward-allocating bump heap allocator with memory recycling capabilities, increasing MAX_KAMINO_POSITIONS from 8 to 16.

Key Changes:

  • New allocator.rs: Forward-allocating bump allocator with heap_pos()/heap_restore() for memory recycling between oracle price loads
  • Increased position limit: MAX_KAMINO_POSITIONS bumped from 8 → 16
  • Updated liquidation instructions: liquidate, liquidate_start, liquidate_end, and handle_bankruptcy now use heap-efficient health calculations
  • New test k18: Validates liquidation works correctly with 16 total positions (15 Kamino deposits + 1 borrow)

Why:
Previously, accounts with many Kamino positions risked becoming unliquidatable due to CU/heap memory exhaustion. The custom allocator recycles heap memory (~3KB per position) so 16 positions fit within 32KB without requiring requestHeapFrame.

@Henry-E
Copy link
Contributor

Henry-E commented Dec 12, 2025

We didn't really discuss this ahead of time. Not entirely sure this is the right solution here. It's quite a heavy change to make when we can support 14 out of 16 positions already.

@Henry-E
Copy link
Contributor

Henry-E commented Dec 12, 2025

There's a more comprehensive set of realistic tests of combinations of positions and mints and other things that @IliaZyrin identified that we need to test and get a realistic view of first before doing something like this imo.

## Why Backward Allocation Breaks with Extended Heap
If we simply compile program with increased HEAP_LENGTH without changing direction
and or not using `requestHeapFrame` (for functions that fit in 32 KiB), we get this situation:
Copy link
Contributor

Choose a reason for hiding this comment

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

this is interesting I didn't know the forward allocation was incompatible with requestheapframe, is there a demo or something to show this?

Copy link
Contributor Author

@imef-femi imef-femi Dec 14, 2025

Choose a reason for hiding this comment

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

I've updated the doc to explain this better... but a similar implementation can be seen here:
Squads allocator.rs

let oracle_ais = &remaining_ais[oracle_ai_idx..end_idx];

// Create oracle adapter (heap allocation happens here)
let price_adapter_result = OraclePriceFeedAdapter::try_from_bank(&bank, oracle_ais, &clock);
Copy link
Contributor

Choose a reason for hiding this comment

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

did we experiment at all with how much heap the oracle call here actually uses in the kamino (like did we confirm it's 3kish bytes?)

maybe there is a way to cut back the heap usage?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the ~3k ish bytes is just an over-exaggeration (worst case)... after testing with logs, the usage usually doesn't exceed ~128 bytes (and thats for the switchboard oracles)

Copy link
Contributor

Choose a reason for hiding this comment

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

yeh it's fine if you're documenting worst case, just want to confirm the size at worst for our napkin math of memory usage.

If you can print the heap pointer before/after should be able to precisely doc the worst case.

@imef-femi
Copy link
Contributor Author

There's a more comprehensive set of realistic tests of combinations of positions and mints and other things that @IliaZyrin identified that we need to test and get a realistic view of first before doing something like this imo.

wasn't aware we had that solution already @Henry-E ... @jgur-psyops mentioned it a while back and i just thought i'd give it a try

@Henry-E
Copy link
Contributor

Henry-E commented Dec 14, 2025

It's not a solution as such. It's more like there's a lot more factors to consider than just memory issues when scaling to >8 integration assets.

Things that would be a better use of your time than this

  • strip out anything that doesn't use bankrun and replace it with bankrun in the current typescript testing framework. The setup takes 40 - 60 seconds each time partly because of mocks program txes and then also some other tests that still use non-bank run testing. It would be much appreciated if you could just strip out and replace all that with bankrun transactions.
  • Once that's done and everything in the testing framework is on bankrun, that's when we can sit down and discuss what a proper comprehensive testing framework for scaling past 8 assets looks like. As mentioned earlier there's a few different aspects which need to also be tested which aren't being covered, e.g. different mints per bank. And also none of the integrations have tests with switchboard or the new liquidation approach. This is something we should sit down and discuss a plan for though.

@jgur-psyops
Copy link
Contributor

Great implementation overall, will give it another look after I land. Once we confirm that 16 positions works with the custom heap reset (it seems to), let's see if we can cut memory down to get the same pass condition without resetting the heap pointer (the answer might be no, but I suspect it's possible).

don't spent too much time on it tho, if it's working with the custom heap then 1-2 days at most to try getting it working without.

let checkpoint = heap_pos();
{
let temp_vec: Vec<u8> = vec![1, 2, 3]; // Heap allocation
let sum: u64 = temp_vec.iter().sum(); // Copy to stack!
Copy link
Contributor

Choose a reason for hiding this comment

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

This is confusing comment, because we're not copying anything here, but rather putting a new value (sum) on stack.

// Note: in flashloans, risk_engine is None, and we skip the cache price update.
maybe_price = risk_engine.and_then(|e| e.get_unbiased_price_for_bank(&bank_pk).ok());
// Fetch unbiased price for cache update
let bank = bank_loader.load()?;
Copy link
Contributor

Choose a reason for hiding this comment

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

So the logic is now exactly the same as in the else below. I suggest we just move it out the scope to not duplicate.

{
let heap_after_oracle = heap_pos();
let heap_used = heap_after_oracle.saturating_sub(heap_checkpoint);
msg!(
Copy link
Contributor

Choose a reason for hiding this comment

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

We should remove this before merging. Or, which is probably better, change it to debug!().

remaining_ais: &'info [AccountInfo<'info>],
health_cache: &mut Option<&mut HealthCache>,
) -> MarginfiResult {
let (equity_assets, equity_liabs) = get_health_components(
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing check for flashloan here? Also, should we just put it directly inside get_health_components if it's only used by the callers of this function?

fi

if [ "$cluster" = "mainnet" ]; then
features="--features mainnet-beta"
Copy link
Contributor

Choose a reason for hiding this comment

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

Not using custom-heap in mainnet?


// Only deposit into the first MAX_KAMINO_POSITIONS banks
for (let i = 0; i < MAX_KAMINO_POSITIONS; i += depositsPerTx) {
// NOTE: We only deposit into 8 banks for this test suite since we don't use a LUT.
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's just rename the const above to be KAMINO_POSITIONS and use it everywhere (with the value of 8, as before). Otherwise it creates confusion since MAX_KAMINO_POSITIONS is not even used.

});

// Add compute budget
// Add compute budget
Copy link
Contributor

Choose a reason for hiding this comment

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

formatting

const MAX_KAMINO_DEPOSITS = 8; // Maximum Kamino positions per account
const NUM_KAMINO_BANKS_FOR_TESTING = 9; // Create 9 banks to test liquidator limit

const NUM_KAMINO_BANKS_FOR_TESTING = 17;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really need 17 here? 15 is not enough?

Please update/delete the outdated comments throughout this file. E.g. they mention 9 banks and non-actual expectations.

import { CONF_INTERVAL_MULTIPLE, ORACLE_CONF_INTERVAL } from "./utils/types";

/** Maximum Kamino positions per account - now 16 with custom allocator */
const MAX_KAMINO_POSITIONS = 16;
Copy link
Contributor

@IliaZyrin IliaZyrin Jan 8, 2026

Choose a reason for hiding this comment

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

  1. This is unused. Let's delete it.
  2. I don't think we need a separate const for Kamino, given that the maximum Kamino positions is now equal to the maximum positions in general. But up to you, if you want to keep it for documentation purposes.

/// with up to 16 positions.
///
/// Returns (account_health, assets, liabilities) if the account is liquidatable.
pub fn check_pre_liquidation_condition_and_get_account_health<'info>(
Copy link
Contributor

Choose a reason for hiding this comment

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

General comment about all these new functions. Should we remove their non-heap-optimized versions now? Or is there a reason for both variants to stay?

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.

4 participants