Skip to content
Merged

Dev #162

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0358438
feat: enhance IOrionStrategist interface and implement vault linking …
ojasarora77 Mar 6, 2026
4ac69fe
test: update test as per params
ojasarora77 Mar 6, 2026
66743a5
feat: add mock contracts and tests for strategist linking
ojasarora77 Mar 6, 2026
96b352c
fix: remove unused signer
ojasarora77 Mar 6, 2026
aee6389
feat: Equal Weight Portfolio allocation
ojasarora77 Mar 7, 2026
65682c7
chore: docstring
ojasarora77 Mar 7, 2026
9325c0f
feat: implement ApyStrategistBase contract
ojasarora77 Mar 7, 2026
52307bb
feat: add KBestApyEqualWeighted strategy contract
ojasarora77 Mar 7, 2026
a1ec7e0
feat: add KBestApyWeightedAverage strategy contract
ojasarora77 Mar 7, 2026
6ee729c
test: add comprehensive tests for new strategies including EqualWeigh…
ojasarora77 Mar 7, 2026
a0b25b7
fix: remove dust from fulfillRedeem
matteoettam09 Mar 8, 2026
cefc6be
Merge pull request #161 from OrionFinanceAI/main
matteoettam09 Mar 9, 2026
13fb046
fix: eliminates the uninitialized local variable finding
ojasarora77 Mar 9, 2026
a68c389
chore: natspec
ojasarora77 Mar 9, 2026
70a6230
fix: int truncation
ojasarora77 Mar 9, 2026
aea6f72
chore: Added override to submitIntent()
ojasarora77 Mar 9, 2026
cb352d9
test: exercises the initialize() path directly — no updateStrategist(…
ojasarora77 Mar 9, 2026
1e55c44
fix: apy test strategy
ojasarora77 Mar 9, 2026
2b38ac6
test:_getSharePrice return on 0 decimal
ojasarora77 Mar 9, 2026
683b1c8
chore: ignore inequality
ojasarora77 Mar 9, 2026
3634db3
chore: bump version
ojasarora77 Mar 10, 2026
c01d0e6
feat: enable invariant monitoring for partially rebalanced portfolio
matteoettam09 Mar 10, 2026
c589f7d
feat: USDC-Normalised Pricing
ojasarora77 Mar 13, 2026
d8d7e5a
chor: mock feed for testing
ojasarora77 Mar 13, 2026
62fccc9
test: quoteFeed cross-rate normalisation path using MockChainlinkFeed
ojasarora77 Mar 13, 2026
29cf9ac
chore: add qoute feed params to existing tests
ojasarora77 Mar 13, 2026
6a5120b
chore: slither ignore unused return
ojasarora77 Mar 13, 2026
6da44d0
test: cross-rate: ETH/USDC via ETH/USD ÷ USDC/USD
ojasarora77 Mar 13, 2026
f3b8a5f
chore: refacto
matteoettam09 Mar 14, 2026
072ae01
fix: remove duplicate _epochBufferHistory push on first sellminibatch
matteoettam09 Mar 15, 2026
ecf8fd1
fix: _recordBufferCheckpoint, Commitment refresh can drift from real …
matteoettam09 Mar 17, 2026
e6033d2
chore: bump
matteoettam09 Mar 17, 2026
ee5f85e
chore: override
ojasarora77 Mar 19, 2026
61e2e81
Merge pull request #169 from OrionFinanceAI/invariant
matteoettam09 Mar 19, 2026
bc29bf6
fix: combine Apy strategies
matteoettam09 Mar 21, 2026
f2611e6
chore: PR review
matteoettam09 Mar 21, 2026
b7d07ed
fix: apy strategy, checkpoint at construction
matteoettam09 Mar 21, 2026
81f76c5
test: strategist contracts
matteoettam09 Mar 21, 2026
f856197
chore: deprecate tests now performed in zkVM and in integration testi…
matteoettam09 Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ types
.venv
protocol-costs
contracts/test
test/fixtures

# files
*.env
Expand Down
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

<img src="./assets/Orion_Logo_white_horizontal.png" alt="orion" width="75%">


[![codecov][codecov-badge]][codecov] [![Sourcery][sourcery-badge]][sourcery] [![Github Actions][gha-badge]][gha] [![Hardhat][hardhat-badge]][hardhat]
[![Github Actions][gha-badge]][gha] [![Hardhat][hardhat-badge]][hardhat] [![CodeRabbit][cr-badge]][cr]

[![LinkedIn][linkedin-badge]][linkedin] [![X][x-badge]][x] [![Telegram][telegram-badge]][telegram] [![Discord][discord-badge]][discord]

Expand All @@ -14,11 +13,8 @@
[hardhat]: https://hardhat.org/
[hardhat-badge]: https://img.shields.io/badge/Built%20with-Hardhat-FFDB1C.svg

[codecov]: https://codecov.io/gh/OrionFinanceAI/protocol
[codecov-badge]: https://codecov.io/gh/OrionFinanceAI/protocol/branch/main/graph/badge.svg

[sourcery]: https://sourcery.ai
[sourcery-badge]: https://img.shields.io/badge/Sourcery-enabled-brightgreen
[cr]: https://www.coderabbit.ai/
[cr-badge]: https://img.shields.io/badge/CodeRabbit-Enabled-FF570A?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMiAyMkgyMkwxMiAyWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+

[linkedin]: https://www.linkedin.com/company/orionfinance/
[linkedin-badge]: https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white
Expand Down
92 changes: 63 additions & 29 deletions contracts/LiquidityOrchestrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* - Handling slippage and market execution differences from adapter price estimates via liquidity buffer.
* @custom:security-contact security@orionfinance.ai
*/
contract LiquidityOrchestrator is

Check warning on line 34 in contracts/LiquidityOrchestrator.sol

View workflow job for this annotation

GitHub Actions / Build, Lint and Test

Contract has 25 states declarations but allowed no more than 15
Initializable,
Ownable2StepUpgradeable,
ReentrancyGuardUpgradeable,
Expand Down Expand Up @@ -92,9 +92,6 @@
/// @notice Current minibatch index
uint8 public currentMinibatchIndex;

/// @notice Whether epoch protocol fees have been accrued this epoch
bool private epochFeesAccrued;

/// @notice Target buffer ratio
uint256 public targetBufferRatio;

Expand Down Expand Up @@ -123,6 +120,17 @@
/// @notice Tokens that failed during the current epoch's sell/buy execution (cleared at epoch end)
address[] private _failedEpochTokens;

/// @notice Cached assets hash from last full commitment build
bytes32 private _cachedAssetsHash;
/// @notice Cached vaults hash from last full commitment build
bytes32 private _cachedVaultsHash;

/// @notice Buffer amount after each execution minibatch for market impact tracking.
uint256[] private _epochBufferHistory;

/// @notice Epoch protocol fees to accrue when transitioning to ProcessVaultOperations.
uint256 private _pendingEpochProtocolFees;

/// @notice Struct to hold epoch state data
struct EpochState {
/// @notice Transparent vaults associated to the current epoch
Expand Down Expand Up @@ -355,6 +363,11 @@
return _failedEpochTokens;
}

/// @inheritdoc ILiquidityOrchestrator
function getEpochBufferHistory() external view returns (uint256[] memory) {
return _epochBufferHistory;
}

/* -------------------------------------------------------------------------- */
/* CONFIG FUNCTIONS */
/* -------------------------------------------------------------------------- */
Expand Down Expand Up @@ -444,20 +457,20 @@
if (currentPhase == LiquidityUpkeepPhase.Idle && _shouldTriggerUpkeep()) {
_handleStart();
} else if (currentPhase == LiquidityUpkeepPhase.StateCommitment) {
_currentEpoch.epochStateCommitment = _buildEpochStateCommitment();
(bytes32 commitment, bytes32 assetsHash, bytes32 vaultsHash) = _buildEpochStateCommitmentAndComponents();
_currentEpoch.epochStateCommitment = commitment;
_cachedAssetsHash = assetsHash;
_cachedVaultsHash = vaultsHash;
currentPhase = LiquidityUpkeepPhase.SellingLeg;
emit EventsLib.EpochStateCommitted(epochCounter, _currentEpoch.epochStateCommitment);
} else if (currentPhase == LiquidityUpkeepPhase.SellingLeg) {
StatesStruct memory states = _verifyPerformData(_publicValues, proofBytes, statesBytes);

if (currentMinibatchIndex == 0 && !epochFeesAccrued) {
// Update buffer amount
if (currentMinibatchIndex == 0) {
bufferAmount = states.bufferAmount;
// Accrue protocol fees once per epoch
pendingProtocolFees += states.epochProtocolFees;
emit EventsLib.ProtocolFeesAccrued(states.epochProtocolFees);
epochFeesAccrued = true;
_pendingEpochProtocolFees = states.epochProtocolFees;
}
_recordBufferCheckpoint();

_processMinibatchSell(states.sellLeg);
} else if (currentPhase == LiquidityUpkeepPhase.BuyingLeg) {
Expand Down Expand Up @@ -494,6 +507,8 @@
return;
}

_recordBufferCheckpoint();

currentPhase = LiquidityUpkeepPhase.StateCommitment;

// Snapshot protocol fees at epoch start to ensure consistency throughout the epoch
Expand Down Expand Up @@ -528,23 +543,23 @@
}
}

/// @notice Builds an epoch state commitment from the full epoch state
/// @dev Uses domain separation for cryptographic robustness.
/// @return The epoch state commitment
function _buildEpochStateCommitment() internal view returns (bytes32) {
/// @notice Builds the epoch state commitment and returns assets/vaults hashes for incremental refresh on failure
/// @return commitment The full epoch state commitment
/// @return assetsHash Cached so on sell/buy failure we can recompute only protocolStateHash
/// @return vaultsHash Cached so on sell/buy failure we can recompute only protocolStateHash
function _buildEpochStateCommitmentAndComponents()
internal
view
returns (bytes32 commitment, bytes32 assetsHash, bytes32 vaultsHash)
{
address[] memory assets = config.getAllWhitelistedAssets();
uint256[] memory assetPrices = getAssetPrices(assets);
VaultStateData memory vaultData = _getVaultStateData();

bytes32 protocolStateHash = _buildProtocolStateHash();

bytes32 assetsHash = _aggregateAssetLeaves(assets, assetPrices);

bytes32 vaultsHash = _aggregateVaultLeaves(vaultData);

bytes32 epochStateCommitment = keccak256(abi.encode(protocolStateHash, assetsHash, vaultsHash));

return epochStateCommitment;
assetsHash = _aggregateAssetLeaves(assets, assetPrices);
vaultsHash = _aggregateVaultLeaves(vaultData);
commitment = keccak256(abi.encode(protocolStateHash, assetsHash, vaultsHash));
}

/// @notice Builds the protocol state hash from static epoch parameters
Expand All @@ -556,14 +571,16 @@
_currentEpoch.activeRsFeeCoefficient,
config.maxFulfillBatchSize(),
targetBufferRatio,
bufferAmount,
config.priceAdapterDecimals(),
config.strategistIntentDecimals(),
epochDuration,
config.getAllWhitelistedAssets(),
config.getAllTokenDecimals(),
config.riskFreeRate(),
config.decommissioningAssets()
config.decommissioningAssets(),
_failedEpochTokens,
_epochBufferHistory,
bufferAmount
)
);
return protocolStateHash;
Expand Down Expand Up @@ -700,17 +717,20 @@
address token = sellLeg.sellingTokens[i];
if (token == address(underlyingAsset)) continue;
uint256 amount = sellLeg.sellingAmounts[i];
try this._executeSell(token, amount, sellLeg.sellingEstimatedUnderlyingAmounts[i]) {

Check warning on line 720 in contracts/LiquidityOrchestrator.sol

View workflow job for this annotation

GitHub Actions / Build, Lint and Test

Code contains empty blocks
// successful execution, continue.
} catch {
_failedEpochTokens.push(token);
// Incremental update of the epoch state commitment to avoid re-running the entire epoch execution.
_currentEpoch.epochStateCommitment = keccak256(abi.encode(_currentEpoch.epochStateCommitment, token));
_recordBufferCheckpoint();
_currentEpoch.epochStateCommitment = keccak256(
abi.encode(_buildProtocolStateHash(), _cachedAssetsHash, _cachedVaultsHash)
);
emit EventsLib.EpochStateCommitted(epochCounter, _currentEpoch.epochStateCommitment);
return;
}
}

_recordBufferCheckpoint();
++currentMinibatchIndex;
if (i1 == sellLeg.sellingTokens.length) {
currentMinibatchIndex = 0;
Expand All @@ -733,19 +753,25 @@
address token = buyLeg.buyingTokens[i];
if (token == address(underlyingAsset)) continue;
uint256 amount = buyLeg.buyingAmounts[i];
try this._executeBuy(token, amount, buyLeg.buyingEstimatedUnderlyingAmounts[i]) {

Check warning on line 756 in contracts/LiquidityOrchestrator.sol

View workflow job for this annotation

GitHub Actions / Build, Lint and Test

Code contains empty blocks
// successful execution, continue.
} catch {
_failedEpochTokens.push(token);
// Incremental update of the epoch state commitment to avoid re-running the entire epoch execution.
_currentEpoch.epochStateCommitment = keccak256(abi.encode(_currentEpoch.epochStateCommitment, token));
_recordBufferCheckpoint();
_currentEpoch.epochStateCommitment = keccak256(
abi.encode(_buildProtocolStateHash(), _cachedAssetsHash, _cachedVaultsHash)
);
emit EventsLib.EpochStateCommitted(epochCounter, _currentEpoch.epochStateCommitment);
return;
}
}

_recordBufferCheckpoint();
++currentMinibatchIndex;
if (i1 == buyLeg.buyingTokens.length) {
pendingProtocolFees += _pendingEpochProtocolFees;
emit EventsLib.ProtocolFeesAccrued(_pendingEpochProtocolFees);
_pendingEpochProtocolFees = 0;
currentMinibatchIndex = 0;
currentPhase = LiquidityUpkeepPhase.ProcessVaultOperations;
}
Expand All @@ -761,6 +787,14 @@
}
}

/// @notice Records the current buffer amount as a checkpoint only if it differs from the last entry.
function _recordBufferCheckpoint() internal {
uint256 n = _epochBufferHistory.length;
if (n == 0 || _epochBufferHistory[n - 1] != bufferAmount) {
_epochBufferHistory.push(bufferAmount);
}
}

/// @notice Calculate maximum amount with slippage applied
/// @param estimatedAmount The estimated amount
/// @return The maximum amount with slippage applied
Expand Down Expand Up @@ -837,12 +871,12 @@
i1 = uint16(vaultsEpoch.length);
currentPhase = LiquidityUpkeepPhase.Idle;
currentMinibatchIndex = 0;
epochFeesAccrued = false;
_nextUpdateTime = block.timestamp + epochDuration;
emit EventsLib.EpochEnd(epochCounter);
++epochCounter;

delete _failedEpochTokens;
delete _epochBufferHistory;
}

for (uint16 i = i0; i < i1; ++i) {
Expand Down
4 changes: 4 additions & 0 deletions contracts/interfaces/ILiquidityOrchestrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ interface ILiquidityOrchestrator {
/// @return List of token addresses that failed
function getFailedEpochTokens() external view returns (address[] memory);

/// @notice Returns the buffer amount after each execution minibatch for market impact tracking.
/// @return Buffer amount after each minibatch
function getEpochBufferHistory() external view returns (uint256[] memory);

/// @notice Gets asset prices for the epoch
/// @param assets Array of asset addresses
/// @return assetPrices Array of asset prices
Expand Down
18 changes: 10 additions & 8 deletions contracts/interfaces/IOrionStrategist.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.28;

import "./IOrionTransparentVault.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";

/// @title IOrionStrategist
/// @notice Interface for strategies that compute portfolio intents on-demand
/// @notice Interface for smart contract strategists that compute portfolio intents on-demand.
/// @author Orion Finance
/// @dev Strategists can be smart contracts that implement this interface to provide
/// dynamic portfolio allocation logic in a fully transparent manner.
/// @dev Implementors must support ERC-165 so that vaults can detect and auto-link them on assignment.
/// @custom:security-contact security@orionfinance.ai
interface IOrionStrategist {
/// @notice Submit the current portfolio intent based on market conditions and logic.
/// @param vault The vault to submit the intent to
function submitIntent(IOrionTransparentVault vault) external;
interface IOrionStrategist is IERC165 {
/// @notice Link this strategist to a vault. Can only be called once per deployment.
/// @param vault_ The vault address to link to this strategist.
function setVault(address vault_) external;
Comment thread
ojasarora77 marked this conversation as resolved.

/// @notice Compute the current portfolio intent from on-chain state and submit it to the linked vault.
function submitIntent() external;
}
10 changes: 10 additions & 0 deletions contracts/interfaces/IOrionVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,16 @@ interface IOrionVault is IERC4626 {
/// @dev This returns share amounts, not underlying asset amounts
function pendingRedeem(uint256 fulfillBatchSize) external view returns (uint256);

/// @notice Get the list of pending redeem entries (users and shares) for the next fulfill batch
/// @param fulfillBatchSize The maximum number of requests to consider
/// @return users Addresses with pending redeem requests in batch order
/// @return shares Share amounts per user (same index as users)
/// @dev This function enables per-request convertToAssetsWithPITTotalAssets calculations,
/// ensuring exact rounding behaviour for state transition.
function pendingRedeemBatch(
uint256 fulfillBatchSize
) external view returns (address[] memory users, uint256[] memory shares);

/// @notice Calculate the vault's fee based on total assets using a specific fee model
/// @param totalAssets The total assets under management
/// @param snapshotFeeModel The fee model to use for calculation (typically from epoch snapshot)
Expand Down
3 changes: 3 additions & 0 deletions contracts/libraries/ErrorsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,7 @@ library ErrorsLib {
/// @param proofCommitment The commitment from the zk proof.
/// @param onchainCommitment The commitment from the onchain.
error CommitmentMismatch(bytes32 proofCommitment, bytes32 onchainCommitment);

/// @notice Strategist is already linked to a vault and cannot be re-linked.
error StrategistVaultAlreadyLinked();
}
Loading
Loading