Quantstamp reviewed ApxUSD (Apyx – ApxUSD, ApyUSD, Bridged Token) on 30 Apr 2026: 4 findings, 0 critical; 2 acknowledged, 2 open.
Report hosted on certificate.quantstamp.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 0 |
| Medium | 0 | 0 | 0 |
| Low | 2 | 2 | 0 |
`ApyUSD.initialize()` does not accept or set an `unlockToken` address. As documented in the NatSpec for `initialize()` (`@dev UnlockToken must be set after deployment using setUnlockToken()`), this is a post-deployment configuration step performed by `ADMIN_ROLE`. Until `setUnlockToken()` is called, every withdraw and redeem call reverts unconditionally: ```solidity // src/ApyUSD.sol:290-293 // Require unlockToken is set if (address($.unlockToken) == address(0)) { revert AddressNotSet("unlockToken"); } ``` The `vesting`, `unlockingFee`, and `feeWallet` parameters share this deferred-configuration pattern, but only the absence of `unlockToken` makes the vault non-functional for holders. The others have safe zero-value defaults (no yield accrual, no fee, fee retained in vault, respectively). This creates a window between deployment and `setUnlockToken` being called during which `deposit()` and `mint()` succeed, but `withdraw()` and `redeem()` are completely blocked.
`ApyUSD.setVesting()` replaces the stored vesting contract by overwriting `$.vesting` directly, with no prior claim of accrued yield from the outgoing contract: ```solidity // src/ApyUSD.sol:387-395 function setVesting(IVesting newVesting) external restricted { ApyUSDStorage storage $ = _getApyUSDStorage(); IVesting oldVesting = $.vesting; // Update Vesting reference $.vesting = newVesting; emit VestingUpdated(address(oldVesting), address(newVesting)); } ``` `LinearVestV0.pullVestedYield()` is gated by `onlyBeneficiary`, which checks `msg.sender == beneficiary`. The beneficiary is the `ApyUSD` vault address, set immutably at construction time. Once `$.vesting` is overwritten, the vault's `_withdraw` and `totalAssets` paths reference only the new contract. There is no protocol-level mechanism that will ever call `pullVestedYield()` on the old contract again, leaving its remaining balance permanently stranded under normal operation. The impact is two-fold: 1. **Immediate share price discontinuity:** `totalAssets()` includes `$.vesting.vestedAmount()` as phantom yield, tokens physically held by the vesting contract but logically attributed to the vault's share price: ```solidity // src/ApyUSD.sol:200-211 function totalAssets() public view override returns (uint256) { ... if (address($.vesting) != address(0)) { vestedYield = $.vesting.vestedAmount(); } return vaultBalance + vestedYield; } ``` At the moment `setVesting` executes, `$.vesting` changes atomically. If the outgoing contract held `V` in `vestedAmount()`, `totalAssets()` drops by `V` in the same transaction. Because `totalSupply()` is unchanged, each outstanding share is immediately redeemable for fewer assets. A loss of value for all current holders proportional to `V / totalSupply`. 2. **Stranded unvested yield:** Beyond the already-vested portion, any yield still in the unvested tranche (`LinearVestV0.unvestedAmount()`) is locked in the old contract indefinitely. There is no admin function on `ApyUSD` to trigger `pullVestedYield` on an arbitrary address, and `LinearVestV0.pullVestedYield` cannot be called by anyone other than the registered beneficiary. The tokens remain in the old contract, inaccessible through any normal protocol flow.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (5)
236 projects reviewed · 332 reports · latest Oct 2026
See Quantstamp’s auditsSomething 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