Skip to content
Merged

Dev #228

Show file tree
Hide file tree
Changes from all commits
Commits
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 .env.sepolia.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
ETHERSCAN_API_KEY=...
RPC_URL="https://eth-sepolia.g.alchemy.com/v2/xyz"
...
4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ jobs:
RPC_URL: ${{ secrets.RPC_URL }}
DEPLOYER_PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
LP_PRIVATE_KEY: ${{ secrets.LP_PRIVATE_KEY }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}

- name: "Lint"
run: pnpm lint
Expand All @@ -56,7 +55,6 @@ jobs:
RPC_URL: ${{ secrets.RPC_URL }}
DEPLOYER_PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
LP_PRIVATE_KEY: ${{ secrets.LP_PRIVATE_KEY }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}

- name: "Add test summary"
Expand Down Expand Up @@ -99,7 +97,6 @@ jobs:
RPC_URL: ${{ secrets.RPC_URL }}
DEPLOYER_PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
LP_PRIVATE_KEY: ${{ secrets.LP_PRIVATE_KEY }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}

publish-abis:
name: "Publish Contract ABIs"
Expand Down Expand Up @@ -136,7 +133,6 @@ jobs:
RPC_URL: ${{ secrets.RPC_URL }}
DEPLOYER_PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
LP_PRIVATE_KEY: ${{ secrets.LP_PRIVATE_KEY }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}

- name: "Extract and package ABIs + Bytecode"
run: |
Expand Down
29 changes: 15 additions & 14 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 29 states declarations but allowed no more than 15
Initializable,
Ownable2StepUpgradeable,
ReentrancyGuardTransient,
Expand Down Expand Up @@ -86,9 +86,6 @@
/// @notice Minibatch size for fulfill deposit and redeem processing
uint8 public minibatchSize;

/// @notice Number of vault leaves folded into the commitment per StateCommitment upkeep step
uint8 public commitmentMinibatchSize;

/// @notice Upkeep phase
LiquidityUpkeepPhase public currentPhase;

Expand All @@ -111,9 +108,6 @@
/// @notice Live buffer amount [assets]
uint256 public bufferAmount;

/// @notice Buffer snapshot captured at epoch start and used as deterministic proof input anchor [assets]
uint256 public initialEpochBufferAmount;

/// @notice Pending protocol fees [assets]
uint256 public pendingProtocolFees;

Expand All @@ -125,10 +119,8 @@
/// @notice Cached vaults hash from last full commitment build
bytes32 private _cachedVaultsHash;

/// @notice Running vault leaf fold accumulator during StateCommitment phase
bytes32 private _partialVaultsHash;
/// @notice Number of vault leaves already folded this epoch
uint16 private _commitmentBatchIndex;
/// @notice Buffer snapshot captured at epoch start and used as deterministic proof input anchor [assets]
uint256 public initialEpochBufferAmount;

Comment thread
matteoettam09 marked this conversation as resolved.
/// @notice Epoch protocol fees to accrue when transitioning to ProcessVaultOperations.
uint256 private _pendingEpochProtocolFees;
Expand All @@ -152,6 +144,18 @@
/// @notice Current epoch state
EpochState internal _currentEpoch;

/// @notice Address of the upgrade timelock that must authorise all implementation upgrades
address public upgradeTimelock;

/// @notice Number of vault leaves folded into the commitment per StateCommitment upkeep step
uint8 public commitmentMinibatchSize;

/// @notice Running vault leaf fold accumulator during StateCommitment phase
bytes32 private _partialVaultsHash;

/// @notice Number of vault leaves already folded this epoch
uint16 private _commitmentBatchIndex;

/* -------------------------------------------------------------------------- */
/* MODIFIERS */
/* -------------------------------------------------------------------------- */
Expand Down Expand Up @@ -564,7 +568,7 @@
}

/// @notice Folds the next batch of vault leaves into the running accumulator.
function _processCommitmentMinibatch() internal {

Check warning on line 571 in contracts/LiquidityOrchestrator.sol

View workflow job for this annotation

GitHub Actions / Build, Lint and Test

Function body contains 58 lines but allowed no more than 50 lines
uint16 vaultCount = uint16(_currentEpoch.vaultsEpoch.length);
uint256 maxFulfillBatchSize = config.maxFulfillBatchSize();

Expand Down Expand Up @@ -714,7 +718,7 @@
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 721 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);
Expand Down Expand Up @@ -748,7 +752,7 @@
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 755 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);
Expand Down Expand Up @@ -936,9 +940,6 @@
}
}

/// @notice Address of the upgrade timelock that must authorise all implementation upgrades
address public upgradeTimelock;

/// @notice Sets the upgrade timelock address.
/// @dev If no timelock is set yet, only the owner may call this. Once a timelock is active,
/// only the timelock itself may replace it, preventing the owner from bypassing the delay.
Expand Down Expand Up @@ -967,5 +968,5 @@
}

/// @dev Storage gap to allow for future upgrades
uint256[50] private __gap;
uint256[47] private __gap;
}
8 changes: 4 additions & 4 deletions contracts/OrionConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
* @author Orion Finance
* @custom:security-contact security@orionfinance.ai
*/
contract OrionConfig is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, IOrionConfig {

Check warning on line 33 in contracts/OrionConfig.sol

View workflow job for this annotation

GitHub Actions / Build, Lint and Test

Contract has 26 states declarations but allowed no more than 15
using EnumerableSet for EnumerableSet.AddressSet;

/// @notice Guardian address for emergency pausing
Expand Down Expand Up @@ -89,6 +89,9 @@
/// @notice Timestamp when new protocol fee rates become effective
uint256 public newProtocolFeeRatesTimestamp;

/// @notice Address of the upgrade timelock that must authorise all implementation upgrades
address public upgradeTimelock;

modifier onlyFactories() {
if (msg.sender != transparentVaultFactory) revert ErrorsLib.NotAuthorized();
_;
Expand Down Expand Up @@ -536,9 +539,6 @@
return tokenDecimals[token];
}

/// @notice Address of the upgrade timelock that must authorise all implementation upgrades
address public upgradeTimelock;

/// @notice Sets the upgrade timelock address.
/// @dev If no timelock is set yet, only the owner may call this. Once a timelock is active,
/// only the timelock itself may replace it, preventing the owner from bypassing the delay.
Expand Down Expand Up @@ -567,5 +567,5 @@
}

/// @dev Storage gap to allow for future upgrades
uint256[50] private __gap;
uint256[49] private __gap;
}
8 changes: 4 additions & 4 deletions contracts/factories/TransparentVaultFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
/// @notice UpgradeableBeacon for transparent vaults
UpgradeableBeacon public vaultBeacon;

/// @notice Address of the upgrade timelock that must authorise all implementation upgrades
address public upgradeTimelock;

/// @notice Constructor that disables initializers for the implementation contract
/// @custom:oz-upgrades-unsafe-allow constructor
// solhint-disable-next-line use-natspec
Expand Down Expand Up @@ -73,7 +76,7 @@
if (!config.isSystemIdle()) revert ErrorsLib.SystemNotIdle();

// Encode the initialization call
bytes memory initData = abi.encodeWithSignature(

Check warning on line 79 in contracts/factories/TransparentVaultFactory.sol

View workflow job for this annotation

GitHub Actions / Build, Lint and Test

GC: String exceeds 32 bytes
"initialize(address,address,address,string,string,uint8,uint16,uint16,address)",
manager,
strategist,
Expand Down Expand Up @@ -113,9 +116,6 @@
emit EventsLib.VaultBeaconUpdated(newVaultBeacon);
}

/// @notice Address of the upgrade timelock that must authorise all implementation upgrades
address public upgradeTimelock;

/// @notice Sets the upgrade timelock address.
/// @dev If no timelock is set yet, only the owner may call this. Once a timelock is active,
/// only the timelock itself may replace it, preventing the owner from bypassing the delay.
Expand Down Expand Up @@ -144,5 +144,5 @@
}

/// @dev Storage gap to allow for future upgrades
uint256[50] private __gap;
uint256[49] private __gap;
}
26 changes: 3 additions & 23 deletions contracts/interfaces/IOrionVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pragma solidity ^0.8.34;

import "@openzeppelin/contracts/interfaces/IERC4626.sol";
import "./IOrionConfig.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

/// @title IOrionVault
/// @notice Interface for Orion vaults
Expand Down Expand Up @@ -53,17 +52,6 @@ interface IOrionVault is IERC4626 {
/// @param sharesBurned The number of shares burned for the user.
event Redeem(address indexed user, uint256 indexed redeemAmount, uint256 indexed sharesBurned);

/// @notice Transfer to the redeemer failed (e.g. USDC denylist); funds are held until claimed.
/// @param user The address of the recipient whose transfer failed.
/// @param amount The underlying amount that could not be transferred.
/// @param shares The number of shares that could not be redeemed.
event RedemptionFailed(address indexed user, uint256 indexed amount, uint256 indexed shares);

/// @notice A previously failed redemption transfer has been claimed by the user.
/// @param user The address of the claimer.
/// @param amount The underlying amount claimed.
event RedemptionClaimed(address indexed user, uint256 indexed amount);

/// @notice Fees have been accrued.
/// @param managementFee The amount of management fees accrued.
/// @param performanceFee The amount of performance fees accrued.
Expand Down Expand Up @@ -124,13 +112,13 @@ interface IOrionVault is IERC4626 {
/// @return The currently active fee model
function activeFeeModel() external view returns (FeeModel memory);

/// --------- CONFIG FUNCTIONS ---------
// --------- CONFIG FUNCTIONS ---------

/// @notice Override intent to 100% underlying asset for decommissioning
/// @dev Can only be called by the OrionConfig contract
function overrideIntentForDecommissioning() external;

/// --------- LP FUNCTIONS ---------
// --------- LP FUNCTIONS ---------

/// @notice Submit an asynchronous deposit request.
/// @dev No share tokens are minted immediately. The specified amount of underlying tokens
Expand Down Expand Up @@ -158,10 +146,6 @@ interface IOrionVault is IERC4626 {
/// @param shares The amount of share tokens to recover.
function cancelRedeemRequest(uint256 shares) external;

/// @notice Claim underlying funds from a previously failed redemption transfer.
/// @dev Called by the user after the transfer blocker (e.g. denylist) has been resolved.
function claimUnderlying() external;

// --------- MANAGER AND STRATEGIST FUNCTIONS ---------

/// @notice Update the strategist address
Expand Down Expand Up @@ -189,7 +173,7 @@ interface IOrionVault is IERC4626 {
/// to ensure the deposit access control is capable of performing its duties.
function setDepositAccessControl(address newDepositAccessControl) external;

/// --------- LIQUIDITY ORCHESTRATOR FUNCTIONS ---------
// --------- LIQUIDITY ORCHESTRATOR FUNCTIONS ---------

/// @notice Get total pending deposit amount across all users
/// @param fulfillBatchSize The maximum number of requests to process per fulfill call
Expand Down Expand Up @@ -221,10 +205,6 @@ interface IOrionVault is IERC4626 {
uint256 fulfillBatchSize
) external view returns (address[] memory users, uint256[] memory shares);

/// @notice Total underlying assets owed to users whose redemption transfer failed
/// @return total Sum of all pending underlying claims across all users
function totalPendingUnderlyingClaims() external view returns (uint256 total);

/// @notice Process all pending deposit requests and mint shares to depositors
/// @param depositTotalAssets The total assets associated with the deposit requests
function fulfillDeposit(uint256 depositTotalAssets) external;
Expand Down
70 changes: 66 additions & 4 deletions contracts/price/ERC4626PriceAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,16 @@
address vaultUnderlying = vault.asset();

uint8 vaultAssetDecimals = IERC20Metadata(vaultAsset).decimals();
uint256 precisionAmount = 10 ** (PRICE_DECIMALS + vaultAssetDecimals);
uint256 totalAssets = vault.totalAssets();
uint256 totalSupply = vault.totalSupply();

// Floor rounding here, previewMint uses ceil in execution,
// buffer to deal with negligible truncation and rounding errors.
uint256 vaultUnderlyingAssetAmount = vault.convertToAssets(precisionAmount);
if (totalSupply == 0) {
return (0, PRICE_DECIMALS + CONFIG.getTokenDecimals(vaultUnderlying));
}

uint8 effectiveShareDecimals = _effectiveShareDecimals(totalAssets, totalSupply, vaultAssetDecimals);
uint256 precisionAmount = 10 ** (PRICE_DECIMALS + effectiveShareDecimals);
uint256 vaultUnderlyingAssetAmount = Math.mulDiv(totalAssets, precisionAmount, totalSupply);

if (vaultUnderlying == address(UNDERLYING_ASSET)) {
return (vaultUnderlyingAssetAmount, PRICE_DECIMALS + UNDERLYING_ASSET_DECIMALS);
Expand All @@ -78,4 +83,61 @@

return (vaultPrice, PRICE_DECIMALS + CONFIG.getTokenDecimals(vaultUnderlying));
}

/// @notice Resolves share scale for pricing when reported vault decimals understate per-share value.
/// @dev Most vaults return `vaultAssetDecimals` after a single mulDiv. High-supply / low-asset
/// vaults (decimal offset) need a larger scale: `vaultDecimals + digitCount(totalSupply / totalAssets)`.
/// @param totalAssets Vault total assets in underlying wei.
/// @param totalSupply Vault total supply in share wei.
/// @param vaultAssetDecimals IERC20Metadata(vault).decimals().
/// @return effectiveShareDecimals Exponent used in 10**effectiveShareDecimals share units.
function _effectiveShareDecimals(
uint256 totalAssets,
uint256 totalSupply,
uint8 vaultAssetDecimals
) private pure returns (uint8 effectiveShareDecimals) {
uint256 shareUnit = 10 ** uint256(vaultAssetDecimals);
uint256 perShare = Math.mulDiv(totalAssets, shareUnit, totalSupply);

if (perShare > 1 || totalAssets == 0) {
return vaultAssetDecimals;
}

if (perShare == 1) {
// Distinguish true ~1 wei/share from a truncated larger value.
uint256 probePerShare = Math.mulDiv(totalAssets, shareUnit * 10, totalSupply);
if (probePerShare <= 10) {

Check warning on line 109 in contracts/price/ERC4626PriceAdapter.sol

View workflow job for this annotation

GitHub Actions / Build, Lint and Test

GC: Non strict inequality found. Try converting to a strict one
return vaultAssetDecimals;
}
}

uint256 supplyToAssetsRatio = Math.mulDiv(totalSupply, 1, totalAssets);
uint8 extraDecimals = _decimalDigits(supplyToAssetsRatio);
uint256 adjusted = uint256(vaultAssetDecimals) + uint256(extraDecimals);

if (adjusted > 38) {
return 38;
}

return uint8(adjusted);
}
Comment thread
matteoettam09 marked this conversation as resolved.

/// @notice Returns the number of decimal digits in `value` (minimum 1 when `value` > 0).
/// @param value The integer to measure.
/// @return digits The count of decimal digits in `value`.
function _decimalDigits(uint256 value) private pure returns (uint8) {
if (value == 0) {
return 0;
}

uint8 digits = 1;
while (value > 9) {
value /= 10;
unchecked {
++digits;
}
}

return digits;
}
}
8 changes: 4 additions & 4 deletions contracts/price/PriceAdapterRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ contract PriceAdapterRegistry is Initializable, IPriceAdapterRegistry, Ownable2S
/// @notice Mapping of asset addresses to their corresponding price adapters
mapping(address => IPriceAdapter) public adapterOf;

/// @notice Address of the upgrade timelock that must authorise all implementation upgrades
address public upgradeTimelock;

modifier onlyConfig() {
if (msg.sender != configAddress) revert ErrorsLib.NotAuthorized();
_;
Expand Down Expand Up @@ -83,9 +86,6 @@ contract PriceAdapterRegistry is Initializable, IPriceAdapterRegistry, Ownable2S
return normalizedPrice;
}

/// @notice Address of the upgrade timelock that must authorise all implementation upgrades
address public upgradeTimelock;

/// @notice Sets the upgrade timelock address.
/// @dev If no timelock is set yet, only the owner may call this. Once a timelock is active,
/// only the timelock itself may replace it, preventing the owner from bypassing the delay.
Expand Down Expand Up @@ -114,5 +114,5 @@ contract PriceAdapterRegistry is Initializable, IPriceAdapterRegistry, Ownable2S
}

/// @dev Storage gap to allow for future upgrades
uint256[50] private __gap;
uint256[49] private __gap;
}
44 changes: 44 additions & 0 deletions contracts/test/TestFixedRatioERC4626.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.34;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

/// @notice ERC-4626 vault with fixed totalAssets/totalSupply for ratio-based price adapter tests.
contract TestFixedRatioERC4626 is ERC4626 {
using Math for uint256;
uint8 private immutable _shareDecimals;
uint256 private immutable _fixedTotalAssets;
uint256 private immutable _fixedTotalSupply;

constructor(
ERC20 underlyingAsset_,
string memory name_,
string memory symbol_,
uint8 shareDecimals_,
uint256 fixedTotalAssets_,
uint256 fixedTotalSupply_
) ERC20(name_, symbol_) ERC4626(underlyingAsset_) {
_shareDecimals = shareDecimals_;
_fixedTotalAssets = fixedTotalAssets_;
_fixedTotalSupply = fixedTotalSupply_;
}

function decimals() public view override returns (uint8) {
return _shareDecimals;
}

function totalAssets() public view override returns (uint256) {
return _fixedTotalAssets;
}

function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
return _fixedTotalSupply;
}

function convertToAssets(uint256 shares) public view override returns (uint256) {
return shares.mulDiv(_fixedTotalAssets, _fixedTotalSupply);
}
Comment thread
matteoettam09 marked this conversation as resolved.
}
Loading
Loading