Skip to content

Conversation

@mikalsande
Copy link
Collaborator

@mikalsande mikalsande commented Nov 18, 2025

We have deployed Vault + DeFi wrapper to Hoodi: #2

Commit we deployed from: https://github.com/ChorusONe/vaults-wrapper/blob/9b3512886386f5991832db51a085633bf640f099/README.md#deployment

To make sure things will be testable and correctly versioned we start building our Looped Staking strategy with Morpho lending from commit 9b35128

This PR supersedes #4

@mikalsande mikalsande self-assigned this Nov 18, 2025
@mikalsande mikalsande changed the base branch from main to develop November 18, 2025 09:05
@mikalsande mikalsande changed the base branch from develop to deployed_to_hoodi_branch November 18, 2025 09:07
@mikalsande mikalsande changed the title Hoodi morpho strategy Write deposit workflow for morpho looped staking strategy + tests Nov 18, 2025
@mikalsande
Copy link
Collaborator Author

mikalsande commented Nov 18, 2025

Got the test env setup working, so now we can run integration tests locally. All the original tests run green, our Morpho tests fails, but at least now we have a functioning local environment we can use for integration testing.

To run Morpho tests.

bash testenv-anvil.sh
...

You can now run integration tests with: make test-integration
RPC_URL=http://localhost:9123 CORE_LOCATOR_ADDRESS=0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB make test-integration

Run Morpho tests:
FOUNDRY_PROFILE=test CORE_LOCATOR_ADDRESS=0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB \
forge test --match-contract MorphoLoopStrategyTest -vvvvv --fork-url http://localhost:9123

Will sleep for "forever"

Then we can run integration tests in another terminal.

FOUNDRY_PROFILE=test CORE_LOCATOR_ADDRESS=0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB \
forge test --match-contract MorphoLoopStrategyTest -vvvvv --fork-url http://localhost:9123
...
    ├─ [0] VM::deployCode("src/strategy/MorphoLoopStrategy.sol:MorphoLoopStrategy", 0xf48df2ad15c7a42c2e1bf11fa57c88240dce8c4c783c813d8baaa535d3208948000000000000000000000000c7183455a4c133ae270771860664b6b7ec320bb10000000000000000000000003519d1eb9b4c239ee946c0de2b15803027c95669000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b00000000000000000000000036c02da8a0983159322a80ffe9f24b1acff8b570000000000000000000000000000000000000000000000000000000000000111100000000000000000000000000000000000000000000000000000000000022220000000000000000000000000000000000000000000000000bef55718ad600000000000000000000000000000000000000000000000000000000000000007530)
    │   ├─ [2677836] → new MorphoLoopStrategy@0xa0Cb889707d426A7A386870A03bc70d1b0697598
    │   │   ├─ [2530] StvStETHPool::fallback() [staticcall]
    │   │   │   ├─ [2019] StvStETHPool::WSTETH() [delegatecall]
    │   │   │   │   └─ ← [Return] WstETH: [0x36C02dA8a0983159322a80FFE9F24b1acfF8B570]
    │   │   │   └─ ← [Return] WstETH: [0x36C02dA8a0983159322a80FFE9F24b1acfF8B570]
    │   │   ├─ [2662] StvStETHPool::fallback() [staticcall]
    │   │   │   ├─ [2151] StvStETHPool::STETH() [delegatecall]
    │   │   │   │   └─ ← [Return] Lido: [0xf2A08B9C303496f7FF99Ce2d4A6b6efb65E0e752]
    │   │   │   └─ ← [Return] Lido: [0xf2A08B9C303496f7FF99Ce2d4A6b6efb65E0e752]
    │   │   ├─ emit Initialized(version: 18446744073709551615 [1.844e19])
    │   │   ├─ emit FeaturePaused(featureId: 0x43bfa4d224389b1d46b861e73ea11cdb97b1781b56bee2ffd44f313f312fb273, account: MorphoLoopStrategyTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
    │   │   └─ ← [Return] 13094 bytes of code
    │   └─ ← [Return] MorphoLoopStrategy: [0xa0Cb889707d426A7A386870A03bc70d1b0697598]
    ├─ [0] VM::prank(0x0000000000000000000000000000000000001004)
    │   └─ ← [Return]
    ├─ [1271] MorphoLoopStrategy::initialize(0x0000000000000000000000000000000000001004)
    │   └─ ← [Revert] InvalidInitialization()
    └─ ← [Revert] InvalidInitialization()

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 24.08ms (0.00ns CPU time)

Ran 1 test suite in 266.04ms (24.08ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/integration/morpho-loop.test.sol:MorphoLoopStrategyTest
[FAIL: InvalidInitialization()] setUp() (gas: 0)

Encountered a total of 1 failing tests, 0 tests succeeded

@mikalsande
Copy link
Collaborator Author

mikalsande commented Nov 19, 2025

Tests using a Morpho Mock works now.

bash testenv-anvil.sh

FOUNDRY_PROFILE=test \
CORE_LOCATOR_ADDRESS=0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB \
forge test --match-contract MorphoLoopStrategyTest -vvvvv --fork-url http://localhost:9123

Next is to use the feedback from Morpho for how to set up Morpho properly for integration tests so that we use the same level of realism in our integration tests as Morpho uses them selves in their own repo.

@mikalsande
Copy link
Collaborator Author

Got all but 1 test to run ✅ in CI. The Integrations On Hoodi Core / Integration Tests On Hoodi Core (push) uses Fork testing from Hoodi, so there is probably some state there that our tests are not in sync with, we can take a look at that later. For now we are happy that the local integration test runs ok.

Comment on lines 41 to 45
address public immutable MARKET_LOAN_TOKEN;
address public immutable MARKET_COLLATERAL_TOKEN;
address public immutable MARKET_ORACLE;
address public immutable MARKET_IRM;
uint256 public immutable MARKET_LLTV;
Copy link
Member

Choose a reason for hiding this comment

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

This could be simplified as a MarketParams. We'd need to drop the immutable, but it's easy to check if the args are mutable, they'll need a way to be replaced.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As I understood it we want to use immutable where we can to save gas for storage and structs cannot be immutable for some reason. Happy to change this this to store the struct, gas optimization is not something we need to really think about yet.

_pauseFeature(SUPPLY_FEATURE);
}

function initialize(address _admin) external initializer {
Copy link
Member

Choose a reason for hiding this comment

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

Note: this is to be used as an upgradeable contract I suppose, so it's called by a proxy atomically with the constructor or with some logic so this is never left to be called in different tx than constructor.

_checkFeatureNotPaused(SUPPLY_FEATURE);

LoopSupplyParams memory params = abi.decode(_params, (LoopSupplyParams));
if (params.targetLeverageBp < 10000) revert InvalidLeverage();
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't make sense to have targetLeverageBp 10_000, in that case should it be params.targetLeverageBp <= 10000?!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree, this was just a placeholder. We can set it some other minimum level, maybe 1.5x is a reasonable minimum?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actually, I don't know what is a good minimum. It must be > 1x, but it must also be high enough that there is a tangible benefit to using leverage. That requires some financial analysis that I don't really know how to do.

console.log("Additional minting capacity from borrow (stETH):", additionalMintingCapacitySteth);

// The actual wstETH we can mint is the minimum of what we want and what's available
uint256 additionalWstethFromBorrow = additionalMintingCapacitySteth < targetBorrowAmount
Copy link
Member

Choose a reason for hiding this comment

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

Here we are comparing wstEth with Eth, right? Maybe add a _token as a suffix so we know what we are comparing. In Rust we could create a type per token to ensure only dealing with the same tokens in operations.

console.log("Leverage loop completed");

// Clear callback context
delete _callbackContext;
Copy link
Member

Choose a reason for hiding this comment

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

Why is this done? It doesn't free storage, is it a limit in the stack?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It deletes the callback context so to make sure that no further callbacks can come from the same source.

actualLeverageBp = (position.collateral * 10000) / initialWsteth;
}

emit LoopExecuted(msg.sender, initialWsteth, position.collateral, position.borrowShares, actualLeverageBp);
Copy link
Member

Choose a reason for hiding this comment

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

Great event. We can maybe add some constraints in the end to verify things worked as expected

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea 👍

@mikalsande mikalsande force-pushed the hoodi_morpho_strategy branch from ad30def to a463869 Compare December 8, 2025 12:59
@mikalsande mikalsande force-pushed the hoodi_morpho_strategy branch from 05931d5 to d6d7b6e Compare December 9, 2025 10:00
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