Audit · firm’s term: re-audit
Quantstamp reviewed OnRe (Onre - Re-Audit - 2) on 23 Jan 2026: 15 findings, 0 critical; 10 marked fixed, 5 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 | 1 |
| Medium | 1 | 0 | 2 |
| Low | 4 | 0 | 7 |
The `cancel_redemption_request` instruction accepts both `redemption_offer` and `redemption_request` as mutable accounts but does not enforce any relationship between them. There is no validation that the provided redemption request belongs to the specified redemption offer.
The `MakeRedemptionOffer` instruction defines `redemption_offer` as a PDA. However, subsequent instructions that accept the `redemption_offer` account do not revalidate its PDA derivation, which may allow incorrect redemption offer accounts to be passed in. Similarly, the `redemption_request` account is defined as a PDA in the `CreateRedemptionRequest` instruction, but its PDA constraints are not validated in other instructions that interact with it.
The `RedemptionOffer` account holds the state related to a given redemption offer, including the current `requested_redemptions` amount. This value is added to when a user creates a redemption request; however, it is not subtracted from when their redemption request is fulfilled. The inline documentation above the `fulfill_redemption_request()` function suggests that this action should take place, but the necessary logic is missing in the current implementation. This causes inconsistent accounting, making it more difficult for the protocol team to correctly ensure they provide the necessary amount of tokens to fulfill all requests.
When requesting a redemption within the protocol, a `RedemptionRequest` account is created on behalf of the user to track all state tied to their outstanding request. Once this request is either cancelled (by the user or by an admin) or fulfilled by the redemption admin, the `RedemptionRequest` account is no longer used but does not get closed. This means that every redemption request has a non-refundable rent overhead that users must pay even though their storage requirements are only temporary.
The approval verification logic reconstructs a signed approval message from the Ed25519 instruction (`signed_msg`) and separately accepts an `ApprovalMessage` argument (`msg`). While both messages are individually validated for `program_id`, `user_pubkey`, and `expiry_unix`, the implementation does not enforce that the signed message content exactly matches the message passed into the instruction. As a result, the cryptographic signature is not strictly bound to the instruction level message argument, relying instead on implicit assumptions about message construction. Since all fields are checked this additional check acts as a defence in depth.
The `add_approver()` function does not verify whether the proposed approver address is already assigned to an existing approver slot. As a result, if any approver slot is empty, the function may assign an address that is already set as the other approver, leading to duplicate approver assignments. Additionally, the `remove_approver()` function first checks whether `approver1` matches the address to be removed and returns immediately if it does, without checking `approver2`. If the same address was mistakenly assigned to both `approver1` and `approver2`, removing it would require calling the function twice.
The `calculate_fees()` function currently uses floor division when computing `token_fee_amount`. While the difference per transaction is small, repeated calls can cumulatively reduce the protocol’s collected fees.
The `verify_approval_message_generic()` function verifies an Ed25519 signature by extracting the previous instruction via its index and checking that the program ID matches the `ed25519_program`. However, the Ed25519 program is stateless, meaning the instruction should not include any accounts.
Functionality exists within the protocol to close an existing `Offer` account. In such a scenario, any `RedemptionOffer` that is tied to the closed offer will be unable to fulfill further redemption requests as the function will return an error when trying to read the now closed `Offer` state. However, the `create_redemption_request()` function will not cause an error if the `offer` stored in the `RedemptionOffer` instance does not exist, meaning redemption requests can be created that will not be fulfilled. The severity of this issue is downgraded because the `create_redemption_request()` action will require a signature from the `redemption_admin` who should handle validating that the underlying offer is still active. However, a more robust design would be helpful to remove this potential point of failure.
The `CreateRedemptionRequest` instruction includes an `expires_at` parameter that is validated against the current on chain timestamp to ensure the request has not expired. However, this expiration check is redundant because Solana transactions already include built-in replay protection via recent blockhashes. In this flow, both the `redeemer` and the `redemption_admin` sign the same transaction containing a recent blockhash. Solana only accepts transactions referencing blockhashes from the last ~150 blocks, which inherently limits how long a signed transaction can be executed. As a result, the explicit `expires_at` parameter does not provide additional security guarantees for redemption request creation.
The report names a repository or files. Open the report to see its scope.
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