ApxUSD security record

Arbitrumapyx.fiX

As of 30 Oct 2026, ApxUSD has 2 audit reports by 1 security firm on Trustblock; the latest is by Quantstamp, dated 30 Apr 2026. Those reports list no open critical findings.

Critical findings
No open critical findings
Across 2 reports.
Open the latest report
Your label, live
Get the label for your site

Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.

Audits2

Newest first

AuditSecurity firmDateScopeCritical findingsReport
Apyx – ApxUSD, ApyUSD, Bridged TokenSecurity firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com
ApxUSD StablecoinSecurity firm: QuantstampDate: Scope: Not recorded (see the report)No open critical findingsOpen · certificate.quantstamp.com

Findings18

Statuses are the security firm’s own, from each report.

SeverityOpenAcknowledgedFixed
Critical000
High001
Medium002
Low429
HighStale `vestingAmount` Can Block User WithdrawalsApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

In the `LinearVestV0` contract, `pullVestedYield()` only resets `fullyVestedAmount` and `lastTransferTimestamp`; it does not update `vestingAmount`. After a pull, the contract can hold less than `vestingAmount` while still reporting the original amount as vesting. When the admin later extends the vesting period via `setVestingPeriod()`, the schedule becomes active again (period end moves to the future). When time passes, `vestedAmount()` becomes greater than zero because `newlyVestedAmount()` uses the stale `vestingAmount` and the new period. The next `pullVestedYield()` attempts to transfer more than the contract holds and reverts. The contract can become insolvent from a pull followed by a period extension. Vested yield is pulled when users withdraw `ApyUSD` for `ApxUSD`, so this revert blocks all user withdrawals until the accounting is corrected. Because `totalAssets()` is inflated by stale `vestingAmount`, ERC‑4626 `deposit()`/`mint()` mints fewer shares than a fair price, so new depositors receive less than expected. The shortfall accrues to existing shareholders (share price is overstated), making the mispricing an actual execution loss for depositors, not just a preview/UI issue. When `vestingAmount` is inflated, `ApyUSDRateView.apy()` and `annualizedYield()` return overstated values because they rely on `unvestedAmount()`, which uses the stale `vestingAmount`. **Proof of Concept:** 1. Yield is deposited into the vesting contract. 2. Vesting period passes. 3. Yield is fully pulled; `vestingAmount` is not updated and remains equal to the original deposit; contract balance is zero. 4. The admin extends the vesting period via `setVestingPeriod(newPeriod)`; there is no checkpointing and the period end moves to the future, so the schedule appears active again. 5. When time passes, `vestedAmount()` becomes greater than zero (newly vested amount is computed using the stale `vestingAmount` and the new period). 6. A user attempts to withdraw `ApyUSD` for `ApxUSD`; the vault calls `pullVestedYield()`, which reverts on transfer due to insufficient balance. Withdrawals are blocked for all users.

Open the report
Medium`RedemptionPoolV0` Incorrectly Assumes `reserveAsset` Has 18 DecimalsApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

The `RedemptionPoolV0` contract requires that its `asset` and `reserveAsset` have identical decimals and reverts if they do not. This assumes an 18-decimal format and prevents deploying the pool with common reserve assets like `USDC` (6 decimals) alongside `ApxUSD` (18 decimals). ```javascript constructor(address initialAuthority, ERC20Burnable asset_, IERC20 reserveAsset_) AccessManaged(initialAuthority) { // audit note: Requirement that asset and reserveAsset have identical decimals if (IERC20Metadata(address(asset_)).decimals() != IERC20Metadata(address(reserveAsset_)).decimals()) { revert DecimalsMismatch( IERC20Metadata(address(asset_)).decimals(), IERC20Metadata(address(reserveAsset_)).decimals() ); } asset = asset_; reserveAsset = reserveAsset_; exchangeRate = 1e18; } ```

Open the report
MediumNested `initializer` in `ERC20DenyListUpgradable` Breaks Proxy InitializationApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

`ERC20DenyListUpgradable` defines its internal init function using `initializer` instead of the `onlyInitializing` modifier. In OpenZeppelin Initializable (v5), the initializer modifier prevents re-entrancy. When the top-level `initialize()` function runs, it sets `_initializing` to `true`. When the inner `__ERC20DenyListedUpgradable_init()` is called, the initializer modifier check fails, causing the transaction to revert, preventing the `ApxUSD` and `ApyUSD` contracts from being initialized. ``` // audit note: initializer is used inside a nested initializer function function __ERC20DenyListedUpgradable_init(IAddressList initialDenyList) internal initializer { if (address(initialDenyList) == address(0)) revert InvalidAddress("initialDenyList"); DenyListStorage storage $ = _getDenyListStorage(); $._denyList = initialDenyList; } ```

Open the report
Low`_setDenyList()` Emits Incorrect Old Address in EventApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

In `ERC20DenyListUpgradable._setDenyList()`, the `DenyListUpdated` event emits the new address twice instead of (old, new). The state is updated before the event reads the storage value. ``` function _setDenyList(IAddressList newDenyList) internal { DenyListStorage storage $ = _getDenyListStorage(); $._denyList = newDenyList; emit DenyListUpdated(address($._denyList), address(newDenyList)); } ```

Open the report
Low`AddressList` `add()` and `remove()` Emit Events Without Checking Return ValueApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

`AddressList.add()` and `AddressList.remove()` call `EnumerableSet.add()` and `EnumerableSet.remove()`, which return `true` when the set was modified and `false` when it was not (e.g. `add` when the address is already in the list, `remove` when it is not). ``` function add(address user) external override restricted { if (user == address(0)) revert InvalidAddress("user"); _addresses.add(user); // audit note: This call returns a boolean emit Added(user); } ``` The contract does not check these return values and always emits `Added(user)` or `Removed(user)`. Off-chain indexers or UIs that rely on these events may incorrectly assume a state change occurred when none did.:

Open the report
Low`ApyUSD` Withdrawal Fee Is Silently Retained in Vault when `feeWallet` Is Not ConfiguredApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

In `ApyUSD._withdraw()`, the fee is always deducted from the user's shares (burning shares for `assets + fee`). However, the fee transfer to `feeWallet` is conditional on `fee > 0 && feeRecipient != address(0) && feeRecipient != address(this)`. If `unlockingFee > 0` but `feeWallet` has not been set (defaults to `address(0)`), the fee is still charged from the user but remains in the vault, effectively donating it to remaining ApyUSD holders. This is not transparent and could silently penalize early withdrawers if the admin sets a fee before configuring the fee wallet.

Open the report
Low`CommitToken` Deviates From ERC-7540 SpecificationApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

CommitToken and UnlockToken implement an `ERC-7540`-style async redeem vault and claim compliance via `supportsInterface` (returning `true` for `IERC7540Redeem` and `IERC7540Operator`). However, the implementation deviates from the spec's MUST requirements in ways that may break composability with `ERC-7540`-compliant integrations. These are intentional design choices, not bugs; the risk is that integrators performing an `ERC-165` check would assume full compliance. Deviations (MUST violations): 1. Shares are not removed from the owner on request -- Spec: *"the shares MUST be removed from the custody of owner upon requestRedeem."* The implementation only tracks requested shares in accounting (`request.shares += shares`) but leaves them in `balanceOf(owner)`. Because CommitToken is non-transferable, double-use is prevented by the balance check, so this meets the spirit of "removed from custody" but not the letter. 2. `previewRedeem()` and `previewWithdraw()` do not revert -- Spec: *"previewRedeem and previewWithdraw MUST revert for all callers and inputs."* The implementation returns 1:1 conversion. Integrators check these functions to determine if atomic redemption is possible, returning a value implies synchronous redemption is available, which is false. 3. `setOperator()` reverts and operators cannot claim -- Spec: *"MUST set the operator status to the approved value. MUST return True."* The implementation reverts with `NotSupported()`. As a consequence, operators cannot claim on behalf of a controller either (spec: *"the controller MUST be msg.sender unless the controller has approved the msg.sender as an operator"*). The implementation requires `owner == msg.sender` for all claims. Intentional for v0 simplicity. 4. `ERC-7575` `share()` not implemented -- Spec: *"Smart contracts implementing this Vault standard MUST implement the `ERC-7575` standard (in particular the share method)."* Not present. Additional note: The spec uses SHOULD (not MUST) for renaming the `owner` parameter to `controller` on the `redeem()` and `withdraw()` methods. The implementation uses `owner` in `pendingRedeemRequest()`/`claimableRedeemRequest()`. This is a recommendation-level divergence, not a spec violation.

Open the report
Low`CommitToken` Emits Duplicate `Withdraw` EventsApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

The `CommitToken._withdraw()` function calls `super._withdraw()` (OpenZeppelin `ERC4626._withdraw()` emits a `Withdraw` event), and then emits a `Withdraw` event again. ``` function _withdraw(Request storage request, address caller, address receiver, address owner) internal { ... // audit note: Emitting the first Withdraw event inside the call super._withdraw(caller, receiver, owner, assets, shares); ... // audit note: Emitting the second Withdraw event emit Withdraw(caller, receiver, owner, assets, shares); } ```

Open the report
Low`maxDeposit()` and `maxMint()` Do Not Reflect Supply Cap, Deny List, or PauseApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

`CommitToken` enforces a supply cap in `_update()` and a deny list in `_deposit()`, and inherits `whenNotPaused` on `_deposit()`. However, `maxDeposit()` and `maxMint()` inherit the `ERC4626` defaults that return `type(uint256).max`. Per `ERC-4626`, `maxDeposit` "MUST return the maximum amount of assets deposit would allow to be deposited." and "MUST NOT revert." Currently, calling `deposit(maxDeposit(receiver), receiver)` reverts when `totalSupply < supplyCap` (but non-zero), when the receiver is on the deny list, or when the contract is paused. Integrators that rely on `maxDeposit()` and `maxMint()` to size their calls will experience unexpected reverts.

Open the report
Low`mintStatus()` Can Return `MintStatus.Ready` when `AccessManager` Schedule Has ExpiredApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

`mintStatus()` uses `getSchedule(operationId)` to decide if an operation is executable. AccessManager's `getSchedule()` returns `0` when the operation is not scheduled, executed, cancelled, or expired (past `timepoint + expiration()`, default 1 week). The code only treats `scheduleTime > currentTime` or `currentTime < order.notBefore` as "not ready" and otherwise returns `Ready`. It never treats `scheduleTime == 0` as expired. So when the AccessManager schedule has expired but the order's `notAfter` has not, `mintStatus()` still returns `Ready` even though `executeMint()` would revert with `AccessManagerExpired`. This only occurs when the order's validity window is longer than the AccessManager execution window (e.g. `notAfter` set to a long expiry or `type(uint48).max`). For typical short-lived orders (e.g. notAfter within a day or a week), order expiry is hit first and the bug does not appear.

Open the report
Show 2 more
Low`redeem()` Has No Slippage ProtectionApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

`RedemptionPoolV0.redeem()` uses the current admin-controlled `exchangeRate` to compute the reserve amount via `previewRedeem()`. There is no minimum-output parameter; if the admin updates the exchange rate (e.g. lowers it) after the user has submitted a transaction but before it is executed, the user receives less reserve asset than expected. Because the rate is set by the `ADMIN_ROLE` and can updated at any time, users have no on-chain guarantee on the outcome of a redeem call.

Open the report
LowImprove ValidationsApxUSD Stablecoin · Quantstamp · 13 Feb 2026Fixed

1. The `ApxUSD.setDenyList()` and `ApyUSD.setDenyList()` functions do not validate `newDenyList`, while `CommitToken.setDenyList()` has this validation. Consider validating against `address(0)` and unifying the function implementations. 2. `CommitToken.setUnlockingDelay()` allows zero: The constructor requires `unlockingDelay_ > 0`, but `setUnlockingDelay()` accepts any `uint48` including `0`. Setting the delay to zero makes all redeem requests instantly claimable, bypassing the cooldown. While this requires the `ADMIN_ROLE`, it contradicts the constructor invariant. Add validation in `setUnlockingDelay()`: if `(newUnlockingDelay == 0)` revert `InvalidAmount("unlockingDelay", newUnlockingDelay)`. 3. The `CommitToken.requestRedeem()` and `CommitToken.requestWithdraw()` functions allow `0`-value requests. Consider validating the assets and shares parameters against `0`.

Open the report

Contracts

No deployed address is recorded for these audits.

The reports name a repository or files. Open a report to see its scope.

The repositories and files the reports name

Show this on your site

Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.

Content

Facts

Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.

Fixed as
Language

Style

Layout
Theme
Accent
Corners

Format

Size
Date
Embed as
HTML
<head>
<script src="https://widget.trustblock.run/project-widget.min.js" defer></script>
</head>
<body>
<div
data-tb-project-widget
data-tb-project-slug="apxusd"
></div>
</body>

Paste the code where the label should appear. It uses no API credit and updates by itself.

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us