Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
179385c
Fix MetaVault's redeem assets calculation does not account for sub-va…
tsudmi Mar 30, 2026
e659b6c
Fix Penalty donation calls in NodesManager waste gas outside pendingP…
tsudmi Mar 31, 2026
564006d
Fix dust loss in NodesManager.updateOperatorState() penalty calculati…
tsudmi Mar 31, 2026
7ff7e73
Fix penalty rounding tests and add audit comments
tsudmi Apr 1, 2026
42674cb
Fix GnoErc20Vault emitting Transfer on direct redeem
tsudmi Apr 1, 2026
fdcbe55
Fix ERC-20 vaults missing Transfer event on transferOsTokenPositionTo…
tsudmi Apr 1, 2026
f9d7156
Fix user assets are transferred unnecessarily when the swap converts …
tsudmi Apr 1, 2026
173fe69
Fix stale pendingPenaltyAssets in NodesManager._deposit() by adding n…
tsudmi Apr 1, 2026
03df924
Fix surplus ETH stuck in NodesManager on withdrawValidators
tsudmi Apr 1, 2026
7258b0e
Skip redundant computation in getExitQueueMissingAssets when queuedSh…
tsudmi Apr 1, 2026
263429d
Remove incorrect availableAssets adjustment in getExitQueueMissingAssets
tsudmi Apr 1, 2026
8523498
Fix token transfer ordering in GnoMetaVault.__GnoMetaVault_init()
tsudmi Apr 1, 2026
0e9a484
Reduce redundant memory operations in OsTokenRedeemer and SubVaultsRe…
tsudmi Apr 1, 2026
b8395a7
Cache metaVault SLOAD before access check in enterSubVaultsExitQueue
tsudmi Apr 1, 2026
1a2ed7a
Remove misleading SLOAD comments from non-SLOAD operations
tsudmi Apr 1, 2026
5e8d7ab
Add vault harvest freshness check in NodesManager.claimExitedAssets
tsudmi Apr 2, 2026
be518bc
Skip harvest check for uncollateralized meta vaults
tsudmi Apr 2, 2026
5c7e0d0
Align leftShares threshold in SubVaultsRegistry with subvault exit logic
tsudmi Apr 2, 2026
127faa2
Respect sub-vault capacities in BalancedCurator deposit distribution
tsudmi Apr 2, 2026
27261c3
Revert on empty processExitQueue in OsTokenRedeemer
tsudmi Apr 3, 2026
ed9a9a9
Pin commit hashes for github actions
tsudmi Apr 3, 2026
f7573fb
Update snapshots
tsudmi Apr 3, 2026
1c61cdf
Simpify BalancedCurator
tsudmi Apr 3, 2026
2e0f3ba
Fix test assertion
tsudmi Apr 3, 2026
a0de746
Fix fork test
tsudmi Apr 3, 2026
1ad5e9c
fix snapshots
tsudmi Apr 3, 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
8 changes: 4 additions & 4 deletions .github/workflows/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
submodules: recursive

- name: Install foundry
uses: foundry-rs/foundry-toolchain@v1
uses: foundry-rs/foundry-toolchain@8789b3e21e6c11b2697f5eb56eddae542f746c10
with:
version: stable

Expand All @@ -43,7 +43,7 @@ jobs:
TEST_USE_FORK_VAULTS: false

- name: Check coverage is updated
uses: actions/github-script@v5
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
Expand All @@ -61,7 +61,7 @@ jobs:

- name: Comment on PR
id: comment
uses: actions/github-script@v5
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:

steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
uses: foundry-rs/foundry-toolchain@8789b3e21e6c11b2697f5eb56eddae542f746c10
with:
version: stable

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/slither.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
uses: foundry-rs/foundry-toolchain@8789b3e21e6c11b2697f5eb56eddae542f746c10
with:
version: stable

- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
with:
python-version: '3.10'

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-fork.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
uses: foundry-rs/foundry-toolchain@8789b3e21e6c11b2697f5eb56eddae542f746c10
with:
version: stable

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
uses: foundry-rs/foundry-toolchain@8789b3e21e6c11b2697f5eb56eddae542f746c10
with:
version: stable

Expand Down
73 changes: 55 additions & 18 deletions contracts/curators/BalancedCurator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.22;

import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {ISubVaultsCurator} from "../interfaces/ISubVaultsCurator.sol";
import {IVaultState} from "../interfaces/IVaultState.sol";
import {Errors} from "../libraries/Errors.sol";

/**
Expand All @@ -15,7 +16,7 @@ contract BalancedCurator is ISubVaultsCurator {
/// @inheritdoc ISubVaultsCurator
function getDeposits(uint256 assetsToDeposit, address[] calldata subVaults, address ejectingVault)
external
pure
view
override
returns (Deposit[] memory deposits)
{
Expand All @@ -24,42 +25,78 @@ contract BalancedCurator is ISubVaultsCurator {
}

uint256 subVaultsCount = subVaults.length;
// the deposits should not be made to the vault that is being ejected
uint256 depositSubVaultsCount = ejectingVault != address(0) ? subVaultsCount - 1 : subVaultsCount;
if (depositSubVaultsCount == 0) {
revert Errors.EmptySubVaults();
}
uint256 amountPerVault = assetsToDeposit / depositSubVaultsCount;
uint256 dust = assetsToDeposit % depositSubVaultsCount;

// distribute assets evenly across sub-vaults
deposits = new Deposit[](subVaultsCount);
bool ejectingVaultFound = false;

// fetch remaining capacities and validate vaults
uint256[] memory capacities = new uint256[](subVaultsCount);
uint256 depositSubVaultsCount;
for (uint256 i = 0; i < subVaultsCount;) {
address subVault = subVaults[i];
if (subVault == address(0)) {
revert Errors.ZeroAddress();
} else if (subVault == ejectingVault) {
}
deposits[i].vault = subVault;
if (subVault == ejectingVault) {
if (ejectingVaultFound) {
// only one vault can be ejected at a time
revert Errors.RepeatedEjectingVault();
}
deposits[i] = Deposit({vault: subVault, assets: 0});
ejectingVaultFound = true;
} else if (dust > 0) {
deposits[i] = Deposit({vault: subVault, assets: amountPerVault + dust});
dust = 0; // only one vault can receive dust
} else {
deposits[i] = Deposit({vault: subVault, assets: amountPerVault});
uint256 capacity = IVaultState(subVault).capacity();
uint256 totalAssets = IVaultState(subVault).totalAssets();
if (capacity > totalAssets) {
capacities[i] = capacity - totalAssets;
depositSubVaultsCount += 1;
}
}
unchecked {
// cannot realistically overflow
++i;
}
}
if (ejectingVault != address(0) && !ejectingVaultFound) {
revert Errors.EjectingVaultNotFound();
}

// distribute assets evenly across sub-vaults, respecting capacities
while (assetsToDeposit > 0) {
if (depositSubVaultsCount == 0) {
revert Errors.EmptySubVaults();
}
uint256 amountPerVault =
assetsToDeposit > depositSubVaultsCount ? assetsToDeposit / depositSubVaultsCount : assetsToDeposit;

depositSubVaultsCount = 0;
for (uint256 i = 0; i < subVaultsCount;) {
uint256 subVaultCapacity = capacities[i];

if (subVaultCapacity == 0) {
unchecked {
++i;
}
continue;
}

uint256 depositAmount = Math.min(Math.min(subVaultCapacity, amountPerVault), assetsToDeposit);

deposits[i].assets += depositAmount;
assetsToDeposit -= depositAmount;
if (assetsToDeposit == 0) {
return deposits;
}

subVaultCapacity -= depositAmount;
capacities[i] = subVaultCapacity;

if (subVaultCapacity > 0) {
depositSubVaultsCount += 1;
}

unchecked {
++i;
}
}
}
}

/// @inheritdoc ISubVaultsCurator
Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/ISubVaultsCurator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface ISubVaultsCurator {
*/
function getDeposits(uint256 assetsToDeposit, address[] calldata subVaults, address ejectingVault)
external
pure
view
returns (Deposit[] memory deposits);

/**
Expand Down
1 change: 0 additions & 1 deletion contracts/keeper/KeeperRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ abstract contract KeeperRewards is KeeperOracles, IKeeperRewards {
revert Errors.InvalidProof();
}

// SLOAD to memory
Reward storage lastReward = rewards[msg.sender];
// check whether Vault's nonce is smaller that the current, otherwise it's already harvested
if (lastReward.nonce >= currentNonce) return (0, 0, false);
Expand Down
1 change: 0 additions & 1 deletion contracts/libraries/OsTokenUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ library OsTokenUtils {
IOsTokenVaultController osTokenVaultController,
RedemptionData memory data
) external view returns (uint256 receivedAssets) {
// SLOAD to memory
IOsTokenConfig.Config memory config = osTokenConfig.getConfig(address(this));

// calculate received assets
Expand Down
40 changes: 29 additions & 11 deletions contracts/nodes/NodesManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ abstract contract NodesManager is
uint256 penaltyAssetsDelta = params.cumPenaltyAssets - operatorState.cumPenaltyAssets;
uint256 totalPenaltyAssets = penaltyAssetsDelta + pendingPenaltyAssets[operator];
if (totalPenaltyAssets > 0) {
totalPenaltyShares = IVaultState(vault).convertToShares(totalPenaltyAssets);
totalPenaltyShares = IVaultState(vault).convertToShares(totalPenaltyAssets + 1);
}

// apply penalty to balance, storing excess as pending if insufficient
Expand Down Expand Up @@ -318,8 +318,19 @@ abstract contract NodesManager is
}

/// @inheritdoc INodesManager
function withdrawValidators(bytes calldata validators) external payable override onlyWithdrawalsManager {
function withdrawValidators(bytes calldata validators)
external
payable
override
nonReentrant
onlyWithdrawalsManager
{
uint256 balanceBefore = address(this).balance - msg.value;
IVaultValidators(vault).withdrawValidators{value: msg.value}(validators, bytes(""));
uint256 surplus = address(this).balance - balanceBefore;
if (surplus > 0) {
_transferAssets(msg.sender, surplus);
}
emit ValidatorWithdrawalSubmitted(msg.sender);
}

Expand Down Expand Up @@ -360,6 +371,9 @@ abstract contract NodesManager is
address operator = _exitPositions[positionTicket];
if (operator == address(0)) revert Errors.InvalidTicket();

// check whether the vault is harvested
if (_keeper.isHarvestRequired(vault)) revert Errors.NotHarvested();

// check whether the operator has synced the latest state
uint128 currentNonce = stateData.currentNonce;
if (operatorNonces[operator][OperatorNonceType.LastStateUpdate] != currentNonce) {
Expand Down Expand Up @@ -413,10 +427,7 @@ abstract contract NodesManager is
pendingPenaltyAssets[operator] = 0;
exitedAssets -= pendingPenalty;
}
}

// donate deducted penalty back to the vault
if (penaltyDeducted > 0) {
// donate deducted penalty back to the vault
_donateAssets(penaltyDeducted);
}

Expand All @@ -436,6 +447,14 @@ abstract contract NodesManager is
function _deposit(uint256 assets) internal returns (uint256 addedShares) {
if (assets < minDepositAssets) revert Errors.InvalidAssets();

// check whether the operator has synced the latest state
if (
operatorStates[msg.sender].totalAssets > 0
&& operatorNonces[msg.sender][OperatorNonceType.LastStateUpdate] != stateData.currentNonce
) {
revert Errors.NotHarvested();
}

// deposit assets to the vault
uint256 depositShares = _depositToVault(assets);

Expand All @@ -453,11 +472,10 @@ abstract contract NodesManager is
penaltyAssets = IVaultState(vault).convertToAssets(penaltyShares);
pendingPenaltyAssets[msg.sender] = pendingPenalty - penaltyAssets;
}
}

if (penaltyShares > 0) {
// donate penalty shares to the vault
IVaultState(vault).donateShares(penaltyShares);
if (penaltyShares > 0) {
// donate penalty shares to the vault
IVaultState(vault).donateShares(penaltyShares);
}
}

// update operator's shares balance
Expand Down
Loading
Loading