Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · July 2026

Onchain Minting

for Ethena

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

1 resolved · 20 acknowledged

Findings 21

Main Review

9 findings · June 10 to 12, 2026
  1. M-01 Medium Token Blacklist Does Not Freeze PSM Routing Validation Acknowledged
    Location
    src/swap/PSM.sol:266
    Round
    Main Review

    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.

  2. L-01 Low Self-transfer allows free swaps Trust Assumptions Acknowledged
    Location
    src/swap/PSM.sol:323-331
    Round
    Main Review

    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.benefactor equals the collateral receive custodian, the first transfer is a self-transfer and does not increase custodian collateral balances, while the asset custodian still pays amountOut. The same issue exists for swapForCollateral when order.benefactor equals assetReceiveCustodianAddress.

    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.

  3. L-02 Low Misleading collateral config update event Events Acknowledged
    Location
    src/swap/PSM.sol:550-553
    Round
    Main Review

    Description

    updateCollateralConfig preserves the existing active status in storage, but emits the raw input config instead of the final stored config. If config.isActive differs from wasActive, the emitted CollateralConfigUpdated event 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 isActive value.

  4. L-03 Low Self-approval getter mismatch Unexpected Behavior Acknowledged
    Location
    src/swap/PSM.sol:1143-1145
    Round
    Main Review

    Description

    The swap validation treats a benefactor as implicitly approved when the benefactor is also the beneficiary, but isApprovedBeneficiary only 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 isApprovedBeneficiary to return true when benefactor == beneficiary.

  5. I-01 Informational Unknown collateral can be enabled Unexpected Behavior Acknowledged
    Location
    src/swap/PSM.sol:449-461
    Round
    Main Review

    Description

    enableCollateral can 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 with isActive == true while all other config fields remain zero, including the oracle and custodian addresses. Later, swap only checks the active flag before reading the oracle, so it will attempt to call getPrice on address(0) and revert instead of failing with the expected CollateralNotSupported error.

    Recommendation

    Consider requiring that the collateral already has a valid stored config before enabling it.

  6. I-02 Informational Weak granularity in PSM events Events Acknowledged
    Location
    PSM.sol
    Round
    Main Review

    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.

  7. I-03 Informational Underdocumented delegated signer power Documentation Acknowledged
    Location
    Global
    Round
    Main Review

    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.

  8. I-04 Informational Zero Custom Fee Falls Back to Default Documentation Acknowledged
    Location
    src/swap/PSM.sol:1150
    Round
    Main Review

    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.

  9. I-05 Informational Quote calls pollute oracle events Events Acknowledged
    Location
    src/swap/PSM.sol:1484
    Round
    Main Review

    Description

    getQuote is externally callable and routes through oracle validation before returning the quoted amounts. The _validateOraclePrice shared validation helper emits OraclePriceValidated whenever the oracle data passes validation. As a result, anyone can generate oracle validation logs by calling getQuote without executing a proper swap. This can pollute monitoring and indexers that treat OraclePriceValidated as evidence of swap-time oracle validation.

    Recommendation

    Consider splitting oracle validation into a non-emitting helper for getQuote and an emitting path for actual swaps only.

Remediation Review

12 findings · June 23 to 25, 2026
  1. M-01 Medium Stale Peg Enables PSM Arbitrage Gaming Acknowledged
    Location
    src/swap/PSM.sol:1571-1631
    Round
    Remediation Review

    Description

    The swap function prices the primary asset using the currently configured pegPrice. 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 includes setPegPrice, which allows PEG_MANAGER_ROLE to 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 pegPrice is still $1, an attacker can buy the asset externally and call swapForCollateral before 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 while pegPrice is still $1, an attacker can call swapForAsset before the update, sending $1 of collateral and receiving asset worth about $1.20.

    The existence of PEG_MANAGER_ROLE mitigates 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.

  2. M-02 Medium Blacklist Redirect Queues OFT Compose Logical Error Acknowledged
    Location
    src/oft/xUSDOFTUpgradeable.sol:402-416
    Round
    Remediation Review

    Description

    xUSDOFTUpgradeable._credit redirects a blacklisted destination's tokens to rescueRecipient but returns the positive amount credited there. The inherited OFT receive path then uses the original decoded toAddress for sendCompose and OFTReceived event, encoding that positive amountReceivedLD in 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.

  3. L-01 Low Fee Events Overstate Fees Events Resolved
    Location
    src/swap/PSM.sol:1598-1631
    Round
    Remediation Review

    Description

    The PSM computes feeAmount before calculating the final swap output. The peg-based quote applies this fee by using netAmountIn, but the oracle-based quote uses the full gross amountIn. 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 feeAmount corresponds to the reduced output. When the oracle-based path wins, SwapExecuted may emit a non-zero feeAmount even 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 on SwapExecuted.feeAmount can 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 quotedFeeAmount and effectiveFeeAmount so monitoring systems can distinguish configured fees from fees actually applied.

  4. L-02 Low Oracle Bounds Can Freeze PSM Swaps DoS Acknowledged
    Location
    src/oracle/AggregateOracleFeed.sol:235-266
    Round
    Remediation Review

    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.

  5. L-03 Low Old Oracle Timestamp Can Halt PSM Swaps Oracle Acknowledged
    Location
    AggregateOracleFeed.sol
    Round
    Remediation Review

    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.

  6. L-04 Low Re-Adding Benefactor Revives Old Authorizations Warning Acknowledged
    Location
    PSM.sol
    Round
    Remediation Review

    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.

  7. L-05 Low Aggregate Oracle Counts Duplicate Sources Oracle Acknowledged
    Location
    AggregateOracleFeed.sol
    Round
    Remediation Review

    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.

  8. L-06 Low PSM Ignores Pyth Confidence Bounds Oracle Acknowledged
    Location
    PythOracleFeed.sol, PSM.sol
    Round
    Remediation Review

    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.

  9. I-01 Informational PSM Deploy Config Values Can Wrap Informational Acknowledged
    Location
    scripts/deploy/DeployPSM.s.sol
    Round
    Remediation Review

    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.

  10. I-02 Informational Stale Oracle Aggregation Docs Documentation Acknowledged
    Location
    docs
    Round
    Remediation Review

    Description

    The documentation still describes AggregateOracleFeed as using arithmetic mean / average aggregation. docs/SCOPE.md states that the protocol uses arithmetic mean rather than median, docs/ARCHITECTURE.md lists average price calculation as an aggregate oracle feature, and docs/FORK_TESTING.md says the fork test verifies the average of all four oracles.

    This is stale. The current AggregateOracleFeed implementation and interface document median aggregation, and the code returns Math.median(validPricesArray).

    Recommendation

    Update the documentation and comments to state that AggregateOracleFeed uses median aggregation, not arithmetic mean / average.

  11. I-03 Informational Inactive Benefactor Can Be Spent As Custodian Informational Acknowledged
    Location
    PSM.sol
    Round
    Remediation Review

    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.

  12. I-04 Informational Unsafe Downcast Before Quote Selection Math Acknowledged
    Location
    https://github.com/GuardianOrg/onchain-minting-internal-team1-1782152411467/blob/354bc9b8019586b8e1d75fc8c8998a8507b55a1b/src/swap/PSM.sol#L1617-L1619 https://github.com/GuardianOrg/onchain-minting-internal-team1-1782152411467/blob/354bc9b8019586b8e1d75fc8c8998a8507b55a1b/src/minting/OnChainMinting.sol#L1127-L1132
    Round
    Remediation Review

    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, SafeCast reverts 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.oracleFeed misconfiguration or a faulty oracle adapter, such as a wrong feed address, incorrect decimal scaling, or a custom feed returning extreme values. AggregateOracleFeed reduces 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 amountOut to uint128.

More from Ethena

All 7 reports
  1. PSM Adapter

    8 findings 8 findings: 1 low, 7 informational
  2. Ethena Pay Updates

    81 findings3 high 81 findings: 3 high, 14 medium, 39 low, 25 informational
  3. Ethena Pay

    34 findings 34 findings: 3 medium, 7 low, 24 informational
  4. Execution Guard

    12 findings 12 findings: 2 medium, 4 low, 6 informational

Put 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.

Get a quote