Guardian's review of Onchain Minting for Ethena, published July 2026. The report records 21 findings across 2 review rounds, including 3 medium and 9 low.
- Published
- Review window
- June 10 to 25, 2026
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Ethereum
- Sector
- Stablecoins
- 0 Critical
- 0 High
- 3 Medium
- 9 Low
- 9 Informational
Findings 21
Main Review
9 findings · June 10 to 12, 2026-
M-01 Medium Token Blacklist Does Not Freeze PSM Routing Validation Acknowledged
Description
xUSD/OFT blocks blacklisted operators from moving tokens through approvals by checking msg.sender, from, and to in _update(). This prevents a blacklisted spender from calling transferFrom() against approval granted by a clean address. The documentation states that the msg.sender blacklist check exists to prevent blacklisted addresses from circumventing restrictions through approvals.
PSM maintains separate routing authority through approved beneficiaries and delegated signers, but does not integrate that authority with the xUSD/OFT blacklist. A blacklisted active benefactor can still approve a clean beneficiary, and a blacklisted address can become, remain, or confirm as a delegated signer without any blacklist check.
When a PSM swap executes, the xUSD transfer is performed by the PSM. The token therefore sees msg.sender == address(psm) and only the transfer endpoints as from and to. If those endpoints are clean, the transfer succeeds even though the actor controlling the PSM routing path is blacklisted.
For swapForAsset, xUSD moves from the asset send custodian to the clean beneficiary, so a blacklisted benefactor or delegated signer can route custodian xUSD as long as the PSM-level beneficiary/signing checks pass. For swapForCollateral, a blacklisted delegated signer can initiate xUSD movement from a clean benefactor to the asset receive custodian.
As a result, token blacklisting alone does not freeze PSM-mediated routing authority. This undermines the documented msg.sender blacklist protection intended to stop blacklisted actors from moving tokens through clean accounts or approvals.
Recommendation
Add PSM-level blacklist checks at swap execution for blacklisted PSM actors that are not necessarily visible to xUSD/OFT during the token transfer. At minimum, swap() should reject when msg.sender is blacklisted, and should also reject swapForAsset when order.benefactor is blacklisted, because in that direction xUSD moves from the asset send custodian to the beneficiary and the token does not see the benefactor. Existing xUSD/OFT from / to checks already cover token-visible endpoints such as the beneficiary in swapForAsset and the benefactor in swapForCollateral.
-
L-01 Low Self-transfer allows free swaps Trust Assumptions Acknowledged
Description
The PSM assumes every swap transfers input value from the benefactor into the input custodian before sending output value from the opposite custodian. This assumption breaks if the input custodian is also an active benefactor.
if (_isSwapForAsset) { IERC20(order.collateral) .safeTransferFrom(order.benefactor, _collateralConfig.receiveCustodianAddress, order.amountIn); asset.safeTransferFrom(assetSendCustodianAddress, order.beneficiary, amountOut); } else { asset.safeTransferFrom(order.benefactor, assetReceiveCustodianAddress, order.amountIn); IERC20(order.collateral) .safeTransferFrom(_collateralConfig.sendCustodianAddress, order.beneficiary, amountOut); }When
order.benefactorequals the collateral receive custodian, the first transfer is a self-transfer and does not increase custodian collateral balances, while the asset custodian still paysamountOut. The same issue exists forswapForCollateralwhenorder.benefactorequalsassetReceiveCustodianAddress.If a custodian address is accidentally enabled as a benefactor, it can extract value from the opposite custodian without providing net input value.
Recommendation
Prevent custodian addresses from being used as benefactors in the corresponding swap direction.
-
L-02 Low Misleading collateral config update event Events Acknowledged
Description
updateCollateralConfigpreserves the existing active status in storage, but emits the raw input config instead of the final stored config. Ifconfig.isActivediffers fromwasActive, the emittedCollateralConfigUpdatedevent reports a state that was never stored. Consequently, off-chain indexers and monitoring systems can track an incorrect collateral state.Recommendation
Emit the stored config instead of the calldata config, because storage already contains the normalized
isActivevalue. -
L-03 Low Self-approval getter mismatch Unexpected Behavior Acknowledged
Description
The swap validation treats a benefactor as implicitly approved when the benefactor is also the beneficiary, but
isApprovedBeneficiaryonly returns the explicit mapping value.if (order.benefactor != order.beneficiary && !_benefactorState.config.approvedBeneficiaries[order.beneficiary]) { revert BeneficiaryNotApproved(order.beneficiary); }As a result, the getter can return false even though the same benefactor-beneficiary pair is valid for swaps. Frontends or integrators relying on this getter may incorrectly block valid self-beneficiary swaps.
Recommendation
Consider updating
isApprovedBeneficiaryto return true whenbenefactor == beneficiary. -
I-01 Informational Unknown collateral can be enabled Unexpected Behavior Acknowledged
Description
enableCollateralcan mark any non-zero address as active without verifying that the collateral was previously added or configured. Consequently, an unknown collateral can mistakenly end up withisActive == truewhile all other config fields remain zero, including the oracle and custodian addresses. Later,swaponly checks the active flag before reading the oracle, so it will attempt to callgetPriceonaddress(0)and revert instead of failing with the expectedCollateralNotSupportederror.Recommendation
Consider requiring that the collateral already has a valid stored config before enabling it.
-
I-02 Informational Weak granularity in PSM events Events Acknowledged
Description
Many PSM configuration events do not include the previous value, and some emit only a generic marker for several different state changes. This affects collateral configuration, benefactor configuration, global/default limits, peg price updates, and lifecycle toggles.
As a result, off-chain monitors cannot reconstruct the exact state transition from logs alone. They must query storage after the transaction and may miss the old value, the specific changed field, or the precise semantics of the update.
Recommendation
Consider emitting specific events that include both old and new values for every PSM configuration change.
-
I-03 Informational Underdocumented delegated signer power Documentation Acknowledged
Description
The PSM docs mention delegation only as part of benefactor validation and nonce sharing, but they do not clearly document that an accepted delegated signer can execute swaps with effectively the same swap authority as the benefactor.
Once accepted, a delegated signer can execute any valid swap for the benefactor, subject to the benefactor’s approved beneficiaries, limits, balances, and token allowances. This includes consuming benefactor nonces and moving benefactor funds. As a result, integrators or operators may underestimate the trust level required for delegated signers and treat them as lower-risk accounts than they actually are.
Recommendation
Consider documenting that an accepted delegated signer has full swap execution authority for the benefactor within configured PSM constraints, and adding the same warning to the relevant NatSpec for delegation functions.
-
I-04 Informational Zero Custom Fee Falls Back to Default Documentation Acknowledged
Description
The PSM implementation reserves a custom fee value of 0 as an unset sentinel. When no zero-fee exemption is set, a custom fee of 0 falls back to the collateral default fee rather than producing a zero-fee swap. However, the docs state that custom fees can be set to zero for fee-free operations. This can cause operators or integrators to believe that calling setBenefactorSwapForAssetFee(benefactor, collateral, 0) or setBenefactorSwapForCollateralFee(benefactor, collateral, 0) grants a zero custom fee. In practice, zero-fee behavior requires either a zero collateral default fee or the explicit zero-fee exemption flag. As a result, fee policy may be misunderstood or misconfigured by operators relying on the broader documentation instead of the implementation-specific NatSpec.
Recommendation
Clarify the fee documentation to state that 0 custom fee means “unset / use collateral default”, and that fee-free per-benefactor swaps require the explicit zero-fee exemption flag unless the collateral default fee is also zero.
-
I-05 Informational Quote calls pollute oracle events Events Acknowledged
Description
getQuoteis externally callable and routes through oracle validation before returning the quoted amounts. The_validateOraclePriceshared validation helper emitsOraclePriceValidatedwhenever the oracle data passes validation. As a result, anyone can generate oracle validation logs by callinggetQuotewithout executing a proper swap. This can pollute monitoring and indexers that treatOraclePriceValidatedas evidence of swap-time oracle validation.Recommendation
Consider splitting oracle validation into a non-emitting helper for
getQuoteand an emitting path for actual swaps only.
Remediation Review
12 findings · June 23 to 25, 2026-
M-01 Medium Stale Peg Enables PSM Arbitrage Gaming Acknowledged
Description
The
swapfunction prices the primary asset using the currently configuredpegPrice. The collateral price is fetched from an oracle and validated during the swap, but the asset side relies on the last value set by the peg manager. The protocol includessetPegPrice, which allowsPEG_MANAGER_ROLEto update the asset peg when the asset permanently depegs to a new equilibrium.However, this creates a race condition during asset repricing events. If the asset trades at $0.80 while
pegPriceis still $1, an attacker can buy the asset externally and callswapForCollateralbefore the peg manager’s update is executed, receiving close to $1 of collateral for each discounted asset token. If the asset trades at $1.20 whilepegPriceis still $1, an attacker can callswapForAssetbefore the update, sending $1 of collateral and receiving asset worth about $1.20.The existence of
PEG_MANAGER_ROLEmitigates long-term stale pricing, but it does not prevent short stale price windows. Malicious users can monitor the market and mempool and execute profitable swaps before the peg update is executed.Recommendation
Add asset-side price protection so swaps revert when the asset market price diverges from the configured
pegPrice. -
M-02 Medium Blacklist Redirect Queues OFT Compose Logical Error Acknowledged
Description
xUSDOFTUpgradeable._creditredirects a blacklisted destination's tokens torescueRecipientbut returns the positive amount credited there. The inherited OFT receive path then uses the original decodedtoAddressforsendComposeandOFTReceivedevent, encoding that positiveamountReceivedLDin the compose payload even though the original recipient received no xUSD.address toAddress = _message.sendTo().bytes32ToAddress(); // @dev Credit the amountLD to the recipient and return the ACTUAL amount the recipient received in local decimals uint256 amountReceivedLD = _credit(toAddress, _toLD(_message.amountSD()), _origin.srcEid); if (_message.isComposed()) { // @dev Proprietary composeMsg format for the OFT. bytes memory composeMsg = OFTComposeMsgCodec.encode( _origin.nonce, _origin.srcEid, amountReceivedLD, _message.composeMsg() ); // @dev Stores the lzCompose payload that will be executed in a separate tx. // Standardizes functionality for executing arbitrary contract invocation on some non-evm chains. // @dev The off-chain executor will listen and process the msg based on the src-chain-callers compose options passed. // @dev The index is used when a OApp needs to compose multiple msgs on lzReceive. // For default OFT implementation there is only 1 compose msg per lzReceive, thus its always 0. endpoint.sendCompose(toAddress, _guid, 0 /* the index of the composed message*/, composeMsg); } emit OFTReceived(_guid, _origin.srcEid, toAddress, amountReceivedLD);Consequently, a blacklisted compose receiver can still receive an authenticated LayerZero compose callback from the xUSD OFT reporting a positive received amount while its xUSD balance remains zero. Compose-based integrations or accounting that trust the OFT callback as receipt proof can release or credit downstream assets despite blacklist redirection.
Recommendation
When a blacklist redirect occurs, suppress compose/event attribution to the original recipient, redirect compose and events to
rescueRecipient, or make the receive path encode/return zero for the blacklisted original recipient. -
L-01 Low Fee Events Overstate Fees Events Resolved
Description
The PSM computes
feeAmountbefore calculating the final swap output. The peg-based quote applies this fee by usingnetAmountIn, but the oracle-based quote uses the full grossamountIn. Since the final output is the minimum of the two quotes, the oracle-based quote can win even though it did not apply the computed fee.When the peg-based path wins, the emitted
feeAmountcorresponds to the reduced output. When the oracle-based path wins,SwapExecutedmay emit a non-zerofeeAmounteven though the user output was calculated from the gross input amount. This can happen during collateral price deviations, where the oracle path is selected to protect the protocol. Consequently, off-chain accounting systems that rely onSwapExecuted.feeAmountcan overstate actually collected fees.Recommendation
Emit the effective collected fee instead of the precomputed fee. If the oracle path wins, set the emitted fee to 0, or split the event into
quotedFeeAmountandeffectiveFeeAmountso monitoring systems can distinguish configured fees from fees actually applied. -
L-02 Low Oracle Bounds Can Freeze PSM Swaps DoS Acknowledged
Description
The aggregate oracle applies its own minimum and maximum price bounds before returning a price to the PSM. These bounds are immutable, while the PSM collateral configuration can be updated by the collateral manager. As a result, if a collateral asset reaches a new valid market equilibrium outside the aggregate oracle’s deployment-time bounds, the PSM may remain unable to swap even after its own collateral bounds are updated. Because the PSM calls the configured oracle feed before applying its own collateral bounds, a revert inside the aggregate oracle prevents the updated PSM bounds from being evaluated.
Recommendation
Consider making aggregate oracle bounds mutable through an admin-gated function, or ensure recovery procedures replace the collateral’s oracle feed with a newly deployed feed using wider bounds.
-
L-03 Low Old Oracle Timestamp Can Halt PSM Swaps Oracle Acknowledged
Description
AggregateOracleFeed filters oracle members using its own maxStaleness, calculates the median price from the remaining valid prices, and returns the earliest timestamp among those valid members. PSM consumes that returned timestamp as the oracle updatedAt value and applies a separate collateral-level maxOracleAge check. If the aggregate oracle uses a wider staleness window than the PSM collateral config, one lagging oracle can make PSM reject swaps even when the aggregate median is valid and enough fresher oracle members exist. For example, with AggregateOracleFeed.maxStaleness = 24 hours and PSM.CollateralConfig.maxOracleAge = 1 hour, two fresh feeds plus one 2-hour-old feed return a valid median price but an old aggregate timestamp. PSM then reverts with OraclePriceTooOld. As a result, a single lagging-but-aggregate-valid oracle member can halt swaps for the affected collateral until the oracle updates, the member is removed, aggregate staleness is reduced, or PSM max age is widened.
Recommendation
Enforce freshness-window alignment for PSM collaterals using aggregate feeds. At minimum, ensure AggregateOracleFeed.maxStaleness <= CollateralConfig.maxOracleAge during deployment and config updates, or make the aggregate oracle apply the PSM freshness threshold directly before returning a timestamp.
-
L-04 Low Re-Adding Benefactor Revives Old Authorizations Warning Acknowledged
Description
removeBenefactor() deletes benefactorState[benefactor].config, but Solidity does not clear nested mapping entries inside the deleted struct. Old delegatedSigners, approvedBeneficiaries, custom fee mappings, and zero-fee exemption mappings remain in storage. When the same address is later re-added through addBenefactor(), the function only sets isActive = true. The old mapping entries therefore become live again. In particular, a previously accepted delegated signer can immediately submit swaps for the re-added benefactor and route output to a previously approved beneficiary without a fresh setDelegatedSigner() / confirmDelegatedSigner() flow or new beneficiary approval. The scope documentation acknowledges that some old benefactor state can become active again on re-add, but the listed examples focus on epoch/period usage, nonces, and historical data. It does not make clear that executable authorization and fee/exemption mappings also revive.
Recommendation
If re-adding a benefactor is intended to preserve prior authorization/configuration state, update the documentation to explicitly state that old delegated signers, approved beneficiaries, custom fees, and zero-fee exemptions become active again on re-add, and require operators to review or revoke them before reactivation. If re-add is intended to behave like fresh onboarding, track benefactor onboarding generations separately from isActive and key authorization/config mappings by the current generation, or otherwise reset those mappings during re-add.
-
L-05 Low Aggregate Oracle Counts Duplicate Sources Oracle Acknowledged
Description
AggregateOracleFeed enforces quorum by counting oracle wrapper contracts, but it does not verify that those wrappers point to distinct underlying data sources. Different ChainlinkOracleFeed contracts can wrap the same Chainlink aggregator/proxy, and different PythOracleFeed contracts can wrap the same (oracle, priceId) pair. As a result, minNumberOfOracles = 2 can be satisfied by two wrappers that both read from the same underlying source. A PSM collateral using this aggregate feed can pass oracle quorum and execute swaps with less source diversity than operators expect. For example, two distinct ChainlinkOracleFeed wrappers can both be deployed with the same Chainlink aggregator address. AggregateOracleFeed accepts both because their wrapper addresses differ, getPrice() counts both successful reads, and the PSM then treats the aggregate feed as satisfying a two-oracle quorum even though only one underlying Chainlink source is used.
Recommendation
Track and reject duplicate underlying oracle identities when adding feeds, such as the Chainlink aggregator/proxy address or the Pyth (oracle, priceId) pair. Alternatively, document that aggregate quorum counts adapter contracts only and that source uniqueness must be enforced operationally during oracle onboarding.
-
L-06 Low PSM Ignores Pyth Confidence Bounds Oracle Acknowledged
Description
PSM depeg checks use the Pyth center price after PythOracleFeed validates only that confidence / price <= maxConfidenceBps. A swap can pass even when the conservative price including confidence is outside the configured minOraclePrice or maxOraclePrice bound. For example, with center price 1.00, confidence 1.5%, maxConfidenceBps = 200, minOraclePrice = 0.99e18, and maxOraclePrice = 1.01e18, the adapter returns 1.00e18. Both PSM directions can execute even though price - confidence = 0.985e18 is below the swapForAsset floor and price + confidence = 1.015e18 is above the swapForCollateral ceiling.
Recommendation
Enforce an alignment rule between Pyth maxConfidenceBps and each collateral’s PSM bounds, or use adverse-side confidence-adjusted prices for depeg checks. If center-price checks are intentional, document that PSM bounds are center-price bounds and must include enough confidence buffer.
-
I-01 Informational PSM Deploy Config Values Can Wrap Informational Acknowledged
Description
DeployPSM reads numeric environment variables as uint256 but casts them to uint128 before validation. The affected values are MAX_SWAP_FOR_ASSET_PER_EPOCH, MAX_SWAP_FOR_COLLATERAL_PER_EPOCH, and PEG_PRICE. Because explicit Solidity downcasts do not revert, an oversized environment value can wrap to a smaller value before the PSM constructor receives it. This can deploy the PSM with unexpectedly tiny swap limits, or allow an oversized PEG_PRICE input to wrap into a valid-looking value before the constructor’s peg-price bound is checked. As a result, a malformed deployment environment can silently produce materially different PSM configuration than the operator intended, instead of reverting during deployment.
Recommendation
Validate deployment environment values as uint256 before downcasting. Ensure each value is <= type(uint128).max, and validate PEG_PRICE against the PSM maximum before casting. Also compute derived period limits in uint256 and validate they fit in uint128 before assigning them to GlobalConfig.
-
I-02 Informational Stale Oracle Aggregation Docs Documentation Acknowledged
Description
The documentation still describes
AggregateOracleFeedas using arithmetic mean / average aggregation.docs/SCOPE.mdstates that the protocol uses arithmetic mean rather than median,docs/ARCHITECTURE.mdlists average price calculation as an aggregate oracle feature, anddocs/FORK_TESTING.mdsays the fork test verifies the average of all four oracles.This is stale. The current
AggregateOracleFeedimplementation and interface document median aggregation, and the code returnsMath.median(validPricesArray).Recommendation
Update the documentation and comments to state that AggregateOracleFeed uses median aggregation, not arithmetic mean / average.
-
I-03 Informational Inactive Benefactor Can Be Spent As Custodian Informational Acknowledged
Description
PSM prevents a custodian address from being an active benefactor, but the check only looks at benefactorState[address].config.isActive. After a benefactor is disabled or removed, the same address can be configured as an asset or collateral send custodian while any old ERC20 approval to the PSM remains live. Normal swaps by another active benefactor can then pull tokens from that inactive/offboarded benefactor through the send-custodian leg. This is possible when disabled or removed benefactors do not immediately revoke approvals, and it weakens the intended emergency/offboarding separation between benefactors and custodians. It requires privileged custodian configuration, so the impact is limited to a role-transition and allowance-reuse hazard rather than an unprivileged drain.
Recommendation
Track benefactor registration/history separately from active status and reject current or formerly registered benefactors as send custodians unless an explicit admin migration/acknowledgement clears the relationship. Emergency offboarding runbooks should also require revoking PSM allowances from disabled or removed benefactors before any custodian reuse.
-
I-04 Informational Unsafe Downcast Before Quote Selection Math Acknowledged
Description
OnChainMinting and PSM compute peg-based and oracle-based quote branches, cast each branch to uint128, and only then select the lower value. If the branch that should be discarded exceeds uint128,
SafeCastreverts before the safe quote can be chosen.For the intended stablecoin use case, such an extreme oracle value is unlikely under normal market conditions. The main concern is
_collateralConfig.oracleFeedmisconfiguration or a faulty oracle adapter, such as a wrong feed address, incorrect decimal scaling, or a custom feed returning extreme values.AggregateOracleFeedreduces this risk by applying min/max price filtering before returning a price.Recommendation
Consider keeping the quote branches as uint256, select the lower value in uint256, and downcast only the final selected
amountOutto uint128.
No findings match.
More from Ethena
All 7 reportsPut your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.