Quantum Audit Logo

Is Graph Token Safe?

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

Graph Token GRT
0x9623…88c7
Arbitrum Not verifiedLast checked today 1 audit on record
Executive SummaryAI Copilot

The L2GraphToken contract, serving as an upgradeable ERC-20 token on Arbitrum, exhibits a critical flaw in its initialization mechanism, preventing it from being properly set up after deployment or upgrade. This issue, combined with significant centralization risks associated with the governor role, elevates the overall risk level to High. While the contract incorporates standard ERC-20 features, OpenZeppelin upgradeable patterns, and a two-step ownership transfer, the uncallable `initialize` function and the broad powers of the governor require immediate attention. Additional concerns include front-running susceptibility in the `permit` function and misleading custom upgrade logic.

1 Critical2 High1 Medium2 Low
Volume 24h
$37.1K
Liquidity
$67.1K
Price
$0.02055
Token Age
3y
Top 10 Holders
87.1%

Security Findings

Critical

Critical: `initialize` function cannot be called via proxy due to `onlyImpl` modifier

C-01The `initialize` function in `L2GraphToken` is marked with `external onlyImpl initializer`. The `onlyImpl` modifier requires `msg.sender == _implementation()`. The `_implementation()` function reads the `IMPLEMENTATION_SLOT` from the *current contract's storage*. For an implementation contract (like `L2GraphToken` itself), this slot is typically empty (0), meaning `_implementation()` would return `address(0)`. Consequently, `onlyImpl` would require `msg.sender == address(0)`, which is impossible for any transaction. This prevents the `initialize` function from ever being called through the proxy, leaving the contract uninitialized. An uninitialized contract would have its `governor`, `gatew…
IssueThe `initialize` function in `L2GraphToken` is marked with `external onlyImpl initializer`. The `onlyImpl` modifier requires `msg.sender == _implementation()`. The `_implementation()` function reads the `IMPLEMENTATION_SLOT` from the *current contract's storage*. For an implementation contract (like `L2GraphToken` itself), this slot is typically empty (0), meaning `_implementation()` would return `address(0)`. Consequently, `onlyImpl` would require `msg.sender == address(0)`, which is impossible for any transaction. This prevents the `initialize` function from ever being called through the proxy, leaving the contract uninitialized. An uninitialized contract would have its `governor`, `gatew…
FixRemove the `onlyImpl` modifier from the `initialize` function. The `initializer` modifier from OpenZeppelin is sufficient to ensure it's called only once. If `onlyImpl` was intended for a specific custom proxy interaction, this logic must be thoroughly re-evaluated and corrected to allow proxy-based initialization.
StatusUnresolved
High

High: Centralization Risk with Governor Role

H-01The `governor` role holds extensive control over critical contract functionalities. The `governor` can set the `gateway` (which controls `bridgeMint` and `bridgeBurn`), set the `l1Address`, and add/remove `minters` (who can `mint` new tokens). If the `governor`'s private key is compromised, it could lead to unauthorized minting, burning, and manipulation of bridge operations, posing a significant centralization risk (7.3 Access Control, 7.4 Economic). While a two-step ownership transfer exists, the power remains concentrated in a single entity (or multisig).
IssueThe `governor` role holds extensive control over critical contract functionalities. The `governor` can set the `gateway` (which controls `bridgeMint` and `bridgeBurn`), set the `l1Address`, and add/remove `minters` (who can `mint` new tokens). If the `governor`'s private key is compromised, it could lead to unauthorized minting, burning, and manipulation of bridge operations, posing a significant centralization risk (7.3 Access Control, 7.4 Economic). While a two-step ownership transfer exists, the power remains concentrated in a single entity (or multisig).
FixImplement a robust multi-signature wallet (e.g., Gnosis Safe) for the `governor` address to distribute control and reduce the risk of a single point of failure. Consider implementing time-locks for highly sensitive actions (e.g., `setGateway`, `addMinter`) to provide a window for intervention if a malicious transaction is initiated.
StatusUnresolved
High

High: `permit` function susceptible to front-running for transaction failure

H-02The EIP-2612 `permit` function allows users to approve token transfers off-chain. While the `nonce` mechanism prevents replay attacks, the function is susceptible to front-running. A malicious actor could observe a pending `permit` transaction and submit their own transaction with a higher gas price, causing the legitimate user's transaction to fail due to the `nonce` being incremented by the front-running transaction. Although this does not lead to direct fund loss for the `_owner`, it results in wasted gas and a poor user experience (7.2 Code Security).
IssueThe EIP-2612 `permit` function allows users to approve token transfers off-chain. While the `nonce` mechanism prevents replay attacks, the function is susceptible to front-running. A malicious actor could observe a pending `permit` transaction and submit their own transaction with a higher gas price, causing the legitimate user's transaction to fail due to the `nonce` being incremented by the front-running transaction. Although this does not lead to direct fund loss for the `_owner`, it results in wasted gas and a poor user experience (7.2 Code Security).
FixWhile inherent to the `permit` design, users should be aware of this behavior. Consider implementing a mechanism where the `permit` signature includes a `minGasPrice` or similar parameter, or advise users to use a transaction relayer that can handle such scenarios gracefully. Ensure clear documentation for users regarding potential transaction failures.
StatusUnresolved
Medium

Medium: Misleading `_implementation()` logic in `GraphUpgradeable`

M-01The `_implementation()` function in `GraphUpgradeable` reads the `IMPLEMENTATION_SLOT` from the *current contract's storage*. For an implementation contract, this slot is not used to store its own address or the proxy's implementation address; it's the proxy that stores the implementation address in this slot. This function will likely return `address(0)` or an incorrect address when called on the implementation contract, making it unsuitable for determining the actual implementation address in a standard EIP-1967 proxy context. This misleading logic contributes to the critical `initialize` issue (C-01) and can cause confusion or unexpected behavior in custom upgrade mechanisms (7.1 Archite…
IssueThe `_implementation()` function in `GraphUpgradeable` reads the `IMPLEMENTATION_SLOT` from the *current contract's storage*. For an implementation contract, this slot is not used to store its own address or the proxy's implementation address; it's the proxy that stores the implementation address in this slot. This function will likely return `address(0)` or an incorrect address when called on the implementation contract, making it unsuitable for determining the actual implementation address in a standard EIP-1967 proxy context. This misleading logic contributes to the critical `initialize` issue (C-01) and can cause confusion or unexpected behavior in custom upgrade mechanisms (7.1 Archite…
FixRefactor `_implementation()` to correctly retrieve the proxy's implementation address if it's intended to be used by the implementation contract to query its proxy. Alternatively, remove this function if its current implementation is not serving a valid purpose within the upgrade pattern, and ensure any modifiers relying on it (like `onlyImpl`) are corrected.
StatusUnresolved
Low

Low: Hardcoded `DOMAIN_SALT`

L-01The `DOMAIN_SALT` used in the EIP-712 `DOMAIN_SEPARATOR` calculation is a hardcoded `bytes32` value. While not a direct vulnerability, hardcoding this value can limit flexibility if the contract were to be deployed in multiple environments or chains where a unique salt might be desired to prevent potential domain collisions or improve clarity (7.2 Code Security).
IssueThe `DOMAIN_SALT` used in the EIP-712 `DOMAIN_SEPARATOR` calculation is a hardcoded `bytes32` value. While not a direct vulnerability, hardcoding this value can limit flexibility if the contract were to be deployed in multiple environments or chains where a unique salt might be desired to prevent potential domain collisions or improve clarity (7.2 Code Security).
FixConsider making the `DOMAIN_SALT` configurable during initialization or deriving it from a unique, immutable contract parameter (e.g., `block.chainid` combined with a deployment-specific value) to enhance flexibility and ensure uniqueness across different deployments or networks.
StatusUnresolved
Low

Low: `_initialSupply` in `_initialize` is always zero for L2GraphToken

L-02The `GraphTokenUpgradeable._initialize` function accepts an `_initialSupply` parameter to mint tokens to the owner upon initialization. However, the `L2GraphToken.initialize` function explicitly calls `GraphTokenUpgradeable._initialize(_owner, 0);`, always passing `0` for the initial supply. This means no tokens are minted to the owner during the contract's initialization, making the `_initialSupply` parameter in the base `_initialize` function effectively unused by this concrete implementation. While this might be intentional for a bridge token where supply is managed via bridging, it could be misleading or an oversight if initial supply was expected (7.8 Operations).
IssueThe `GraphTokenUpgradeable._initialize` function accepts an `_initialSupply` parameter to mint tokens to the owner upon initialization. However, the `L2GraphToken.initialize` function explicitly calls `GraphTokenUpgradeable._initialize(_owner, 0);`, always passing `0` for the initial supply. This means no tokens are minted to the owner during the contract's initialization, making the `_initialSupply` parameter in the base `_initialize` function effectively unused by this concrete implementation. While this might be intentional for a bridge token where supply is managed via bridging, it could be misleading or an oversight if initial supply was expected (7.8 Operations).
FixConfirm if the intention is indeed to start with zero initial supply and manage all token issuance via the bridge. If so, consider removing the `_initialSupply` parameter from `GraphTokenUpgradeable._initialize` or clearly documenting its intended use for other implementations. If an initial supply is desired, adjust the `L2GraphToken.initialize` call accordingly.
StatusUnresolved

Category Ratings

TechnicalHigh3/10

The contract leverages OpenZeppelin's upgradeable ERC-20 standards, including `ERC20BurnableUpgradeable` and EIP-2612 `permit` functionality, enhancing token utility and security. It employs clear role-based access control with `onlyGovernor`, `onlyGateway`, and `onlyMinter` modifiers, ensuring specific actions are restricted to authorized entities. However, a critical flaw exists where the `initialize` function cannot be called due to the `onlyImpl` modifier, preventing proper contract setup after deployment or upgrade (7.7 Upgrades). Additionally, the `permit` function is susceptible to front-running, potentially causing user transactions to fail (7.2 Code Security). The custom `_implementation()` logic in `GraphUpgradeable` is also misleading and contributes to the initialization issue (7.1 Architecture).

GovernanceHigh1/10

The contract implements a two-step ownership transfer mechanism, requiring a `pendingGovernor` to accept ownership, which is a good practice for mitigating immediate risks of accidental transfers (7.3 Access Control). It defines distinct roles for `governor`, `gateway`, and `minters`, allowing for granular control over critical token operations like minting and bridging. However, the `governor` role holds significant centralized power, capable of setting the `gateway`, `l1Address`, and managing `minters`, posing a high centralization risk if compromised (7.3 Access Control, 7.4 Economic). This single point of control could lead to unauthorized minting or manipulation of bridge operations.

UpgradesHigh1/10

The contract is designed to be upgradeable, utilizing OpenZeppelin's `Upgradeable` pattern with a `__gap` storage variable to ensure future compatibility (7.7 Upgrades). It includes custom `acceptProxy` and `acceptProxyAndCall` functions for a controlled upgrade process, requiring the proxy admin's explicit acceptance. However, a critical vulnerability prevents the `initialize` function from being called through the proxy due to an incorrect `onlyImpl` modifier, rendering the contract uninitializable after deployment or upgrade (7.7 Upgrades). This flaw fundamentally breaks the upgradeability model and prevents the contract from functioning correctly.

Security Checklist

Contract VerifiedPass
Ownership RenouncedFail
No Mint FunctionFail
Liquidity LockedFail
Not a ProxyFail
HoneypotNoneBuy Tax0.0%Sell Tax0.0%

Proxy Upgrade Controls

Proxy TypeEip1967 Transparent
AdminEOA (single key controls upgrades)
ImplementationVerified source
Upgrades (30d)0 · stable

Holder Composition

23.3% in wallets63.8% in contracts
Effective Concentration48.9%

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

Show 1 more pairShow less

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 Holder10.6%
Top-3 Unlocked28.1%

Key Addresses

Deployer
0x4528…612c
Unlocked LP Held By
0xe86d…fce00xabd5…b67e0x085a…5fb00x3399…01fb0x0cc5…149c0x94bf…54250x25cd…658d0x1179…c0bb0x9418…f3b30xcbf8…06f2

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

What Raised This Score

  • Ownership NOT renounced (admin/mint authority retained)
  • Mintable supply — no cap found, dilution unbounded
  • Proxy contract (upgradeable — admin can replace logic)
  • Admin is EOA (single key controls upgrades)
  • Top-10 concentration > 30% (87.1% total → 48.9% effective; 23.3% in EOAs, 63.8% in contracts — moderate)
  • Liquidity not locked, but no owner/deployer address holds LP — market-depth risk, not rug risk
  • 1 Critical finding(s) from audit
  • 2 High finding(s) from audit
  • 1 Medium finding(s) from audit
  • 2 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

Espresso (ESP)High RiskLivepeer Token (LPT)High RiskOrderly Network (ORDER)High RiskCurve DAO Token (CRV)High RiskODYSHigh RiskNolaHigh Risk

Would You Like a More Detailed Audit of Graph Token?

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

Get Detailed Audit