Early rate$2,400 of senior audit time for $500. Early members keep the rate as it climbs.$2,400 of senior audit time for $500See how →
F-2026-0009·input-validation

ParentVault beneficiary erases withdrawals and strands all redeemed assets

Fixedvaultcrosschainepoch-accountinggithub.com/contractlevel/yield-v2
TL;DR

depositFor and withdrawFor accept the ParentVault itself as beneficiary. The resulting claim self-transfers, consuming the entry while the payer receives nothing, and the assets become permanently unreachable.

Severity
LOW
Impact
LOW
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
LOW
02Section · Description

Description

depositFor and withdrawFor reject the zero address as a beneficiary but accept the ParentVault's own address.

With withdrawFor(address(parentVault), shares) the payer's shares are escrowed under the vault's own withdraw entry, which the payer cannot cancel: cancelWithdraw resolves the caller's entry, the vault cannot originate a call, and there is no force-cancel-withdraw. After the epoch settles, claimAssetFor(address(parentVault), epochNonce) consumes the entry, burns the escrowed shares and calls safeTransfer(address(this), amount). The self-transfer leaves the balance unchanged while the claim is fully consumed, so the payer receives nothing and no account is entitled to the assets.

Those assets are then unreachable. _getTVL() counts only the active adapter position and rebalance-deposit recovery, and every underlying-asset outflow — withdraw claim, cancellation refund, adapter deposit — moves an exact operation amount. With no sweep or rescue path the balance stays invisible to share pricing indefinitely, which is the idle-vault-balance outcome KI-005's rationale accepts other trade-offs to prevent.

depositFor(address(parentVault), amount) is the symmetric case: shares are minted to the vault, cannot be submitted for withdrawal, and strand along with their backing.

03Section · Recommendation

Recommendation

Reject the vault's own address wherever a For position is created:

diff
diff --git a/evm/src/interfaces/vaults/IParentVault.sol b/evm/src/interfaces/vaults/IParentVault.sol
@@
+ error ParentVault__InvalidBeneficiary(address beneficiary);
diff --git a/evm/src/vaults/ParentVault.sol b/evm/src/vaults/ParentVault.sol
@@ function depositFor
_revertIfZeroAddress(beneficiary);
+ if (beneficiary == address(this)) revert ParentVault__InvalidBeneficiary(beneficiary);
@@ function withdrawFor
_revertIfZeroAddress(beneficiary);
+ if (beneficiary == address(this)) revert ParentVault__InvalidBeneficiary(beneficiary);

With both guards applied the parent-vault unit suite passes 277/277 and the integration suite 121/121.

04Section · Resolution

Resolution

Fixed in 750a58b: depositFor and withdrawFor both reject the vault as beneficiary.

05Section · Affected files

Affected files

  • evm/src/vaults/ParentVault.sol#L148-L157 (depositFor)
  • evm/src/vaults/ParentVault.sol#L183-L193 (withdrawFor)
  • evm/src/libraries/vaults/ParentVaultUserEpochLib.sol#L307-L327 (_claimAsset)
Status
Fixed
Fix commit
750a58b
F-2026-0009