Quantum Audit Logo

Is Reserve Rights Safe?

On-chain security analysis — is it a scam or legit?

Is this your token? Publish your own audit on this page →

Reserve Rights RSR
0x3206…5d70
Ethereum
Not verifiedThis record has not gone through deep verification and is not being monitored. The score is a dated snapshot — the token’s risk can change at any time.Own this token? Put it under verification →
Last checked 2d ago 1 audit on record
Executive SummaryAI Copilot

The RSR token contract facilitates a migration from an old RSR token, incorporating pausing, ownership, and a unique 'Enchantable' spell-casting mechanism. The audit identified critical and high-severity issues related to administrative control post-migration and potential denial-of-service vectors, alongside medium and low-severity concerns. The contract exhibits a strong move towards decentralization by renouncing ownership, but this introduces significant operational risks if not managed carefully.

1 Critical1 High1 Medium1 Low2 Informational
Volume 24h
$93.3K
Liquidity
$909.3K
Price
$0.001596
Token Age
2y
Top 10 Holders
53.1%

Security Findings

Critical

Permanent Loss of Pausing Mechanism Control Post-Ownership Renouncement

C-01The `moveToWorking` function calls `_transferOwnership(address(0))`, permanently renouncing the `owner` role. After this, the `castSpell` function (which sets the `_mage` role) becomes unusable. The `onlyAdminOrPauser` modifier then relies solely on the `pauser` address. If the `pauser` subsequently calls `renouncePauser()`, the `pauser` address becomes `address(0)`. At this point, no address can satisfy the `onlyAdminOrPauser` modifier, making `pause()`, `unpause()`, `changePauser()`, and `renouncePauser()` permanently inoperable. This results in a complete and irreversible loss of control over the token's pausing functionality, which is critical for emergency situations.
IssueThe `moveToWorking` function calls `_transferOwnership(address(0))`, permanently renouncing the `owner` role. After this, the `castSpell` function (which sets the `_mage` role) becomes unusable. The `onlyAdminOrPauser` modifier then relies solely on the `pauser` address. If the `pauser` subsequently calls `renouncePauser()`, the `pauser` address becomes `address(0)`. At this point, no address can satisfy the `onlyAdminOrPauser` modifier, making `pause()`, `unpause()`, `changePauser()`, and `renouncePauser()` permanently inoperable. This results in a complete and irreversible loss of control over the token's pausing functionality, which is critical for emergency situations.
FixConsider a mechanism to allow the `pauser` role to be transferred or recovered even after ownership is renounced. For example, implement a multi-signature wallet as the `pauser` or introduce a time-locked recovery mechanism. Alternatively, if the intention is to fully decentralize, explicitly state the implications of `pauser` renouncement and ensure the community understands the irreversible nature of this action.
StatusUnresolved
High

Denial of Service in `_oldBal` due to Unbounded Loop

H-01The `_oldBal` internal view function iterates through `origins[account].length()` to calculate an account's balance. The `origins` set can grow indefinitely via the `_siphon` function, which is callable by `onlyAdmin` during the `SETUP` phase. If a large number of `from` addresses siphon to a single `newTo` address, `origins[newTo]` could become very large. When `balanceOf(newTo)` is called, the `_oldBal` loop could exceed the block gas limit, causing the transaction to revert. This would effectively make the `balanceOf` function (and any other function relying on it, like `transfer`) unusable for that account, leading to a denial of service.
IssueThe `_oldBal` internal view function iterates through `origins[account].length()` to calculate an account's balance. The `origins` set can grow indefinitely via the `_siphon` function, which is callable by `onlyAdmin` during the `SETUP` phase. If a large number of `from` addresses siphon to a single `newTo` address, `origins[newTo]` could become very large. When `balanceOf(newTo)` is called, the `_oldBal` loop could exceed the block gas limit, causing the transaction to revert. This would effectively make the `balanceOf` function (and any other function relying on it, like `transfer`) unusable for that account, leading to a denial of service.
FixImplement a pagination mechanism or a maximum limit for the `origins` set size to prevent unbounded growth. Alternatively, refactor the `_oldBal` calculation to avoid iterating over a potentially large, unbounded collection within a single transaction. Consider a 'claim' pattern where users explicitly claim their weighted balances, rather than calculating it on-the-fly for every `balanceOf` call.
StatusUnresolved
Medium

Potential `uint64` Overflow in `_siphon`

M-01The `_siphon` function contains the operation `weights[from][newTo] += weight;`. Both `weights` and `weight` are `uint64`. While Solidity 0.8.4 automatically checks for overflows, if `weights[from][newTo]` is already close to `type(uint64).max` (approximately 1.8e19) and a non-zero `weight` is added, the transaction will revert. This could lead to a denial of service for specific `siphon` operations, preventing the intended reallocation of weights.
IssueThe `_siphon` function contains the operation `weights[from][newTo] += weight;`. Both `weights` and `weight` are `uint64`. While Solidity 0.8.4 automatically checks for overflows, if `weights[from][newTo]` is already close to `type(uint64).max` (approximately 1.8e19) and a non-zero `weight` is added, the transaction will revert. This could lead to a denial of service for specific `siphon` operations, preventing the intended reallocation of weights.
FixAdd an explicit `require` check before the addition to ensure `weights[from][newTo] + weight` does not exceed `type(uint64).max`. For example: `require(weights[from][newTo] <= type(uint64).max - weight, 'RSR: weight addition overflow');`.
StatusUnresolved
Low

Allowance Migration Bypass by `approve` and `permit`

L-01The `approve` and `permit` functions directly set `allowanceCrossed[_msgSender()][spender] = true;`. This flag is intended to ensure that the `oldRSR.allowance` is migrated only once via the `ensureAllowanceCrossed` modifier. However, by setting `allowanceCrossed` to `true` directly, `approve` and `permit` bypass the initial migration of the old allowance for that specific `(owner, spender)` pair. If a user calls `approve` or `permit` before their `oldRSR.allowance` has been migrated, their old allowance will never be considered for the new RSR token, potentially leading to a lower effective allowance than expected.
IssueThe `approve` and `permit` functions directly set `allowanceCrossed[_msgSender()][spender] = true;`. This flag is intended to ensure that the `oldRSR.allowance` is migrated only once via the `ensureAllowanceCrossed` modifier. However, by setting `allowanceCrossed` to `true` directly, `approve` and `permit` bypass the initial migration of the old allowance for that specific `(owner, spender)` pair. If a user calls `approve` or `permit` before their `oldRSR.allowance` has been migrated, their old allowance will never be considered for the new RSR token, potentially leading to a lower effective allowance than expected.
FixConsider modifying `approve` and `permit` to first call `ensureAllowanceCrossed(_msgSender(), spender)` before performing the new approval. This would ensure that any existing `oldRSR` allowance is migrated before being potentially overwritten by the new approval.
StatusUnresolved
Info

Convoluted `_oldBal` Calculation Logic

I-01The `_oldBal` function's logic for determining an account's old balance is somewhat convoluted. It checks `if (!hasWeights[account])` to assign `oldRSR.balanceOf(account)` directly, otherwise it sums weighted balances. An account gains `hasWeights[account] = true` if it acts as a `from` address in `_siphon`. If an account has a direct `oldRSR` balance but also participates in `_siphon` as a `from` address, its direct `oldRSR` balance is only included if it siphons its own balance to itself (`_siphon(from, from, from, WEIGHT_ONE)`). While this logic appears to be functionally correct given the `_siphon` implementation, it is not immediately intuitive and could be a source of confusion.
IssueThe `_oldBal` function's logic for determining an account's old balance is somewhat convoluted. It checks `if (!hasWeights[account])` to assign `oldRSR.balanceOf(account)` directly, otherwise it sums weighted balances. An account gains `hasWeights[account] = true` if it acts as a `from` address in `_siphon`. If an account has a direct `oldRSR` balance but also participates in `_siphon` as a `from` address, its direct `oldRSR` balance is only included if it siphons its own balance to itself (`_siphon(from, from, from, WEIGHT_ONE)`). While this logic appears to be functionally correct given the `_siphon` implementation, it is not immediately intuitive and could be a source of confusion.
FixAdd detailed NatSpec comments to the `_oldBal` and `_siphon` functions explaining the exact conditions under which an account's direct `oldRSR` balance is included versus its weighted balances, and how `hasWeights` affects this calculation. This will improve code clarity and maintainability.
StatusUnresolved
Info

Unused `Enchantable` Features Post-Migration

I-02The `Enchantable` contract introduces a `_mage` role and a `castSpell` function, which allows the `owner` to temporarily grant `_mage` privileges to a `Spell` contract. However, the `castSpell` function is `onlyOwner`. After `moveToWorking` is called, the `owner` role is renounced (`_transferOwnership(address(0))`). This means `castSpell` can never be called again, rendering the entire `Enchantable` mechanism (including the ability to set a `_mage` or cast any `Spell`) permanently unusable after the token transitions to the `WORKING` phase.
IssueThe `Enchantable` contract introduces a `_mage` role and a `castSpell` function, which allows the `owner` to temporarily grant `_mage` privileges to a `Spell` contract. However, the `castSpell` function is `onlyOwner`. After `moveToWorking` is called, the `owner` role is renounced (`_transferOwnership(address(0))`). This means `castSpell` can never be called again, rendering the entire `Enchantable` mechanism (including the ability to set a `_mage` or cast any `Spell`) permanently unusable after the token transitions to the `WORKING` phase.
FixIf the `Enchantable` features are intended for use only during the `SETUP` phase, document this clearly. If they were intended for post-migration use, the access control for `castSpell` would need to be re-evaluated to allow a non-renounced role (e.g., `pauser` or a new dedicated role) to invoke it.
StatusUnresolved

Category Ratings

TechnicalMedium5/10

The RSR token contract (7.1 Architecture) implements a complex migration mechanism from an old token, including custom balance and allowance crossing logic. It leverages OpenZeppelin's ERC20, Pausable, Ownable, and ERC20Permit for robust foundational security (7.2 Code Security). However, the `_oldBal` function, which is part of the core balance calculation, contains an unbounded loop that could lead to denial of service for users (H-01). Additionally, the `_siphon` function's `uint64` addition lacks overflow checks, potentially causing reverts (M-01). The `approve` and `permit` functions can also bypass the intended allowance migration logic (L-01).

GovernanceMedium6/10

The contract's economic model centers on a fixed supply and a migration process from an old RSR token. A significant design choice is the renouncement of the `owner` role after the `moveToWorking` phase, intended to decentralize control (7.5 Governance). However, this leads to a critical operational risk where the pausing mechanism can become permanently inoperable if the `pauser` role is subsequently renounced (C-01). The `Enchantable` spell-casting feature also becomes unusable after ownership renouncement, limiting future administrative flexibility (7.3 Access Control). The `_siphon` mechanism, controlled by `onlyAdmin` in the `SETUP` phase, allows for reallocating 'weights' which are crucial for the migration, but this power is lost after `moveToWorking` (7.8 Operations).

UpgradesMedium6/10

The RSR contract is not designed as an upgradeable proxy (7.7 Upgrades). Its logic is immutable once deployed, which eliminates upgrade-related risks such as proxy misconfigurations or logic contract vulnerabilities introduced during upgrades. However, this also means that any identified vulnerabilities or desired feature changes cannot be addressed without a new deployment and migration.

Security Checklist

Contract VerifiedPass
Ownership RenouncedPass
No Mint FunctionFail
Liquidity LockedPass
Not a ProxyPass
HoneypotNoneBuy Tax0.0%Sell Tax0.0%

Holder Composition

10.4% in wallets42.7% in contracts
Effective Concentration27.5%

Share held by contracts — treasury, vesting, bridge or staking — is discounted against share held by wallets when the score is computed: a contract cannot decide to sell the way an anonymous holder can, though it can still be drained or voted to sell. Effective concentration is the figure the risk score is actually calculated from.

Liquidity Depth

The risk score reads depth across every pair. The volume figure and the volume-to-liquidity ratio elsewhere on this page describe only the pair this audit analysed, so the two are not directly comparable.

LP Distribution

Top-1 Unlocked Holder100.0%
Top-3 Unlocked100.0%

Key Addresses

Deployer
0x9f59…6947
Unlocked LP Held By
0x56e0…3697

No privileged address appears among these holders: the unlocked liquidity sits with independent providers, not with the deployer.

What Raised This Score

  • Mintable supply — no cap found, dilution unbounded
  • Top-10 concentration > 20% (53.1% total → 27.5% effective; 10.4% in EOAs, 42.7% in contracts — mild)
  • LP top1 unlocked holder = 100.0% (independent LP — depth risk, pool = 46% of DEX liquidity)
  • LP top3 unlocked holders = 100.0% (independent LP — depth risk, pool = 46% of DEX liquidity)
  • LP claimed locked but only 0.0% actually locked
  • 1 Critical finding(s) from audit
  • 1 High finding(s) from audit
  • 1 Medium finding(s) from audit
  • 1 Low finding(s) from audit

Each factor is an on-chain fact recorded at the time of this analysis. The score is computed from them by a deterministic function, so the same contract returns the same score for anyone who runs the audit. How scores are computed

Related Audits

MorphoHigh RiskBeamHigh RiskSuperVerse (SUPER)High Risk00 Token (00)High RiskIxs Token (IXS)High RiskAlberich Token (ALBRH)High Risk

Would You Like a More Detailed Audit of Reserve Rights?

Our AI-powered scanner gives you a deeper, real-time smart contract analysis — free, with every scoring factor shown.

Get Detailed Audit