Subscription Token Protocol V2 security record

As of 24 Oct 2026, Subscription Token Protocol V2 has 2 audit reports by 1 security firm on Trustblock; the latest is by Quantstamp, dated 30 May 2024. 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
Subscription Token Protocol V2Security firm: QuantstampDate: Scope: Repository at commit 2b993d1No open critical findingsOpen · certificate.quantstamp.com
Subscription Token - FabricSecurity firm: QuantstampDate: Scope: Repository at commit 504dd06No open critical findingsOpen · certificate.quantstamp.com

Findings25

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

SeverityOpenAcknowledgedFixed
Critical000
High002
Medium023
Low0612
HighSlashing Mechanism May Allow Some Users to Overclaim Rewards and Cause DoS for Other UsersSubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

In essence, the calculation of points is incorrect during slashing as it does not take into account the variable `totalRewardsEgress` (i.e. the function `RewardsPoolLib.burn()`). This can cause two undesired outcomes when a user tries to claim their rewards: 1. He may be denied claiming the rewards as there are not enough funds left in the contract. 1. If there are enough funds left in the contract, he may be able to claim more rewards than he is entitled to. <br> To illustrate the issue, consider the following example: ``` Assume `Expiry Time = 1 block` and no slashing grace period. Block 1: (Multiplier = 2) Bob purchases 50 tokens => 100 shares cuz of 2x multiplier Total Share = 100 pointsPerShare = 50/100 = 0.5 totalRewardIngress = 50 totalRewardEgress = 0 Bob has 100 shares, and Bob's pointsCorrection = 0 Alice has 0 shares, and Alice's pointsCorrection = 0 Block 2: (Multiplier decreases to 1) Alice purchases 50 tokens => 50 shares cuz of 1x multiplier Total Share = 150 pointsPerShare += 50/150 = 0.8333... totalRewardIngress = 100 totalRewardEgress = 0 Bob has 100 shares, Bob's pointsCorrection = 0, and Bob rewardsWithdrawn = 0 Alice has 50 shares, Alice's pointsCorrection = -0.5*50 = -25, and Alice rewardsWithdrawn = 0 Block 3: Alice claims her rewards with `STPV2.transferRewardsFor(Alice)`. Alice claims (0.83 * 50) - 25 = 16.66666.. tokens as rewards. Total Share = 150 pointsPerShare += 50/150 = 0.8333... totalRewardIngress = 100 totalRewardEgress = 16.66666... Bob has 100 shares, Bob pointsCorrection = 0, and Bob rewardsWithdrawn = 0 Alice has 50 shares, Alice pointsCorrection = -0.5*50 = -25, and Alice rewardsWithdrawn = 16.6666666 Note: Bob (can) claim 83.333333 here, and can possibly be slashed. But suppose these do not happen here. Block 4: Alice is slashed, but Bob is not slashed. Total Share = 100 pointsPerShare = 100/100 = 1 totalRewardIngress = 100 totalRewardEgress = 16.66666... Bob has 100 shares, Bob pointsCorrection = 0, and Bob rewardsWithdrawn = 0 Alice has 0 shares, Alice pointsCorrection = 0, and Alice rewardsWithdrawn = 0 Block 5: Bob claims his rewards with `STPV2.transferRewardsFor(Bob)`. Bob claims (1 * 100) - 0 = 100 tokens as rewards. Total Share = 100 pointsPerShare = 1 totalRewardIngress = 100 totalRewardEgress = 116.66666... Bob has 100 shares, Bob pointsCorrection = 0, and Bob rewardsWithdrawn = 100 Alice has 0 shares, Alice pointsCorrection = 0, and Alice rewardsWithdrawn = 0 However, this will revert as there are only 83.33 tokens in the contract. ``` This is especially problematic because anyone can call slashing on behalf of other users, greatly amplifying the risks of the above situation happening.

Open the report
HighFunds Allocated for Rewards Can Be Locked in the ContractSubscription Token - Fabric · Quantstamp · 25 Oct 2023Fixed

A security issue has been identified within the reward redemption and total reward points management system of the platform. This issue is critical because it can result in unfair reward distribution, where users who have accumulated reward points may not receive the correct amount of tokens from the reward pool. The issue arises from the formula used for redemption and the dynamics of total reward points. Analysis: 1. Reward Redemption Formula: - The platform uses a redemption formula to calculate the number of tokens a user receives from the reward pool when redeeming their reward points. - The formula is: `rewardPoolTotal * (userRewardPoints / totalRewardPoints)`. - `rewardPoolTotal` represents the total amount of tokens allocated for rewards (not the current reward pool balance). - `totalRewardPoints` is the sum of reward points in circulation. 2. Total Reward Points Dynamics: - `totalRewardPoints` increases when users purchase subscription time. - `totalRewardPoints` decreases when a user's subscription expires, and their reward points are slashed by other users. - The formula for reward points redemption can potentially fail to accurately distribute rewards when `totalRewardPoints` decreases, leading to funds blocked in the contract. When a user subscription expires, other users can slash and burn those reward points, so `totalRewardPoints` will decrease and users will get a higher share per reward token on the reward pool. `rewardPoolBalance` will not have enough funds for the users, and some withdrawals will revert, locking the funds in the contract.

Open the report
MediumUsers May Force One Another Out of TiersSubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

This vulnerability was brought to our attention by the Subscription Token Team. `STPV2.mintAdvanced()` allows callers to specify both the recipient and the tier when purchasing a subscription. This creates a situation in which users can purchases subscriptions for another user with the intent of blocking them out of a favorable subscription tier. This becomes problematic in the situation where there is a highly exclusive subscription tier. The potential reward benefits from joining that tier could be more profitable than it would be to purchase subscriptions of a lower tier for an address that one may suspect to try to join the higher tier.

Open the report
MediumChecks-Effects-Interactions Pattern ViolationSubscription Token - Fabric · Quantstamp · 25 Oct 2023Fixed

The [Checks-Effects-Interactions](https://solidity.readthedocs.io/en/latest/security-considerations.html#use-the-checks-effects-interactions-pattern) coding pattern is meant to mitigate any chance of other contracts manipulating the state of the blockchain in unexpected and possibly malicious ways before control is returned to the original contract. As the name implied, only after checking whether appropriate conditions are met and acting internally on those conditions should any external calls to, or interactions with, other contracts be done. The following instance(s) have been identified: <br> * `_refund()` <br> ``` uint256 tokens = balance * _tokensPerSecond; if (balance > 0) { sub.secondsPurchased -= balance; _transferOut(account, tokens); <== External Call } _subscriptions[account] = sub; <== State Change ``` <br> * `_fetchSubscription()` <br> ``` if (sub.tokenId == 0) { require(_supplyCap == 0 || _tokenCounter < _supplyCap, "Supply cap reached"); _tokenCounter += 1; sub = Subscription(_tokenCounter, 0, 0, block.timestamp, block.timestamp, 0, 0, 0); <== This state gets updated in `purchaseTime()` _safeMint(account, sub.tokenId); <== External Call } ```

Open the report
MediumLoss of Private Keys Leads to Rewards and Fees Allocation LockSubscription Token - Fabric · Quantstamp · 25 Oct 2023Fixed

A scenario where a content creator has lost access to their private keys, and did not set a designated transfer recipient (`_transferRecipient`) will lead to funds locked in the contract. This oversight results in rewards (if activated) and fees not being correctly allocated, causing these funds to remain locked within the contract indefinitely. Rewards and fees are allocated when `withdrawTo()` is executed. Note that this function can only be called by the contract creator. Users will not be able to claim any reward tokens by redeeming their reward points. Although this can be seen as a contract creator's responsibility, final user funds will be locked, potentially harming the protocol's reputation.

Open the report
LowChecks-Effects-Interactions Pattern Not Enforced, Exposing the System to ReentranciesSubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

While reentrancies may not always negatively impact a system, their existence should be highlighted and their potential consequences mitigated. Here are the instances identified: 1. via `_checkOnERC721Received()`: - In `STPV2._purchase()`, a token can be minted via `_safeMint()`. It can trigger the execution of `_checkOnERC721Received()` that would be able to interact with the system in a read or write access in a state that may be inconsistent since any storage variable that remains to be updated after the call to `_safeMint()` are still in their initial state. - Same remark for `STPV2.grantTime()`. 2. via `CurrencyLib.capture()`, a reentrancy could happen in `STPV2.yieldRewards()` and `STPV2._purchase()`. 3. via `CurrencyLib.transfer()`, a reentrancy could happen in `SPTV2Factory.deploySubscription()`, `SPTV2._transferFee()` and `SPTV2._purchase()`. 4. via `GateLib.balance()`, a reentrancy could happen in `TierLib.checkJoin()`.

Open the report
LowCurve Is Not Monotonically DecreasingSubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

After a certain time (i.e. `periods > curve.numPeriods`), the curve multiplier will be lower bounded by `minMultiplier`, as indicated by `RewardCurveLib.currentMultiplier()`. However, it is possible that for periods before `curve.numPeriods`, the multiplier calculated using `uint256(curve.formulaBase) ** (curve.numPeriods - periods)` is lower than the `minMultiplier`. This means for some misconfigured curves as described above, there may be undesired cases where a user is incentivized to delay purchasing the subscription due to an expected increase in `currentMultiplier()` in later periods (i.e. when `periods > curve.numPeriods`).

Open the report
LowDowncasting May Lead to Unsafe ConsequencesSubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

At several locations, values are downcasted. Some of them may have an initial value higher than the max value supported by the destination type and this could lead to unsafe consequences instead of reverting. It is the case in the contracts: * `SubscriberLib.sol`; * `GateLib.sol`; * `TierLib.sol`; * `SubscriptionLib.sol`; * `RewardPoolLib.sol`;

Open the report
LowEvents Emitted by the Contracts Could Be ImprovedSubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

To validate the proper deployment and initialization of the contracts, it is a good practice to emit events. Also, any important state transitions can be logged, which is beneficial for monitoring the contract and tracking eventual bugs or hacks. Below is a list of events that could be improved: 1. In `STPV2.updateProtocolFeeRecipient()` and `STPV2.updateClientFeeRecipient()`, the new address is not logged and the same event `FeeRecipientChange` is emitted in both functions. 2. In `STPV2Factory.setDeployFee()`, no event is emitted.

Open the report
LowEvents Related to the Support of `ERC4906` and `ERC5192` Can Be Emitted Even if No Change HappensSubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

`ERC4906` expects the event `MetadataUpdate` to be emitted when an update happens. However, in the functions `SubscriberLib.extend()` and `SubscriberLib.retract()`, these events may be emitted even if the new value is the same as the old one. `ERC5192` expects events `Locked` and `Unlocked` to be emitted when the locked status of an object is updated. However: * in the function `emitSwitchEvents()`, it is possible to emit the same event in a row depending on the value of `transferrable`. * when a tier is updated and its attribute `transferrable` is updated, no event is emitted for existing tokens. * when tokens are minted for a given tier, no `ERC5192` event is emitted. This could impact the operations of external observers of what happens on-chain.

Open the report
Show 7 more
LowImpersonation Attack Is Possible for Contracts Deployed by the FactorySubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

The function `STPV2Factory.deploySubscription()` emits `Deployment` events with the field `bytes deployKey` when a user deploys a subscription. According to `Factory.sol`, this field is "an identifier to help track deployments via Deploy event". However, since this value is provided by the user deploying the contract and in the absence of a unicity check, anyone can front-run or back-run a subscription deployment with the same `deployKey` value. As a result, external observers could be misled and subscribe to the contract deployed by the impersonator. This risk should be documented to creators and subscribers.

Open the report
LowMissing Input ValidationSubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

It is important to validate inputs, even if they only come from trusted addresses, to avoid human error: In `STPV2.sol`: - qs-badge-acknowledged `setGlobalSupplyCap()`, `supplyCap` can be set to 0; - qs-badge-acknowledged Any address can be used as the `_currency`. Advise users to check the address used as the currency of a given subscription before performing transactions with this contract. - qs-badge-fixed *NOTE: This missing input validation was brought to our attention by the Subscription Token Team.* `STPV2.initialize()` does not ensure that the specified client or protocol recipient are different from `address(0)` in the case where the fees defined are greater than zero. If a subscription can be created with a non-zero fee and a fee recipient of the zero address, then all mints of that subscription will fail, as the `CurrencyLib` prevents transfers to the zero address. - qs-badge-fixed `issueRewardShares()` does not ensure that the `account` is different from `address(0)`, meaning rewards could be issued to the zero address <br> In `ReferralLib.sol`: - qs-badge-fixed `setReferral()`, `settings.basisPoints` can have a value of maximum `10000`. <br> In `SubscriptionLib.sol` - qs-badge-fixed *NOTE: This missing input validation was brought to our attention by the Subscription Token Team.* In-line comments in the struct definition for `Tier` in `Index.sol` describe that a `maxSupply` of zero indicates no maximum value which supports an unlimited number of subscribers. However, In `updateTier()` if there are active subscriptions on the tier, and the `maxSupply` is being updated to zero, the update will revert as the `subCount` exceeds zero. Consider using `type(uint256).max` to indicate an unlimited supply, or, check against `maxSupply` equaling zero. <br> In `TierLib.sol`: - qs-badge-acknowledged In `validate()`: - the `periodDurationSeconds` can be very small; - `rewardCurveId` is not checked to match an existing curve; - the immutability of some parameters could lead to abuses by the creator. For instance, `transferrable` and `maxSupply` can be updated once people already bought a subscription, which could affect the value of that subscription. <br> In `Rewards.sol`: - qs-badge-acknowledged The fact that rewards can be slashable with a `slashGracePeriod` of `0` should be confirmed to be the documented behavior. The same recommendation is for the mutability nature of these two parameters.

Open the report
LowPartial Support of ERC-1155-Based Gates Is Not DocumentedSubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

The function `GateLib.validate()` reverts if the `gate` used an address of type `ERC-1155` and `tokenId == 0`. However, some ERC-1155 may have a valid `tokenId == 0`. This should be documented to creators.

Open the report
LowSecurity Considerations for `AccessControlled`Subscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

The contract `AccessControlled.sol` implements access control mechanisms using role bitmaps. A few security considerations have been identified: 1. Roles identifiers should not overlap. Calling `_checkRoles()` can return true for another role than the one checked. For instance, if we have two roles `r1` represented by `100` and `r2` represented by `110`, and Bob has the role `r1`, calling `_checkRoles(r2)` for Bob will return `true`. Same remark for `_checkOwnerOrRoles()`. 2. The function `setRoles()` plays several roles with the same parameter, and the parameter `roles` should not only capture the new role to add but also the old roles that should remain active for the given address. Consider distinguishing two functions, one adding roles and keeping the previous ones (or binary operation) and another one fully overriding the existing roles with the new ones. 3. A role with the identifier `0` cannot be checked to be owned by someone Note that each role is defined by a `uint16` integer. This means there can be at most 16 distinct roles in the system. Furthermore, when setting a new role in the system, we have to ensure the value is a power of 2 to avoid having unintended role overlaps. For example, in `STPV2.sol`, if the team introduces a new role in the future (see the snippet below), the new role should have the value `4` instead of `3`: ``` /// @dev The manager role can do most things, except calls that involve money (except tier management with rewardbps) uint16 private constant ROLE_MANAGER = 1; /// @dev The agent can only grant and revoke time uint16 private constant ROLE_AGENT = 2; /// NEW ROLE uint16 private constant ROLE_NEW = 4; // This should be 4 instead of 3. ```

Open the report
LowSeveral Functions Related to the Reward Mechanism Are Exposed to Race ConditionsSubscription Token Protocol V2 · Quantstamp · 30 May 2024Fixed

According to the reward mechanism implemented in `RewardPoolLib.sol`, the reward balance of a user depends on his current shares, the value of `state.pointsPerShare` and `holder.pointsCorrection`. All functions impacting or interacting with these values (read or write access) are exposed to race conditions. In other words, the number of shares of a given user will depend on whether his transaction is executed before or after another transaction impacting the number of shares of another user.

Open the report
LowFunds Sent Can Bypass the `receive()` FunctionSubscription Token - Fabric · Quantstamp · 25 Oct 2023Fixed

Funds can be sent to the contract using the function `selfDestruct`, funds sent this way will not invoke the code inside the `receive()` function.

Open the report
LowMissing Input ValidationSubscription Token - Fabric · Quantstamp · 25 Oct 2023Fixed

It is essential to validate inputs, even if they only come from trusted addresses, to avoid human error. The following missing input validations have been identified: * `initialize()` * `params.name`: validate to be a non-empty string. * `params.symbol`: validate to be a non-empty string. * `params.contractUri`: validate to be a non-empty string. * `params.tokenUri`: validate to be a non-empty string. * `mintWithReferralFor()` * `account`: validate to be a non-zero address. * `referrer`: validate to be a non-zero address. * `withdrawTo()` * `account`: validate to be a non-zero address. * `refund()` * `accounts`: validate to be a non-empty list. * `updateMetadata()` * `_contractURI`: validate to be a non-empty string. * `_tokenURI`: validate to be a non-empty string. * `_grantTime()` * `numSeconds`: validate to be a non-zero value. * `createReferralCode()` relies on verifying that `_referralCodes[code].bps` is zero to know if a referral code exists or not. However, it does not revert if `bps` input parameter is zero. This can confuse external systems listening to `ReferralCreated` event. Validate `bps` to be a non-zero value.

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="subscription-token-protocol-v2"
></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