The person who launches an agent cannot rug a token built on AgentTokenV4. The template exposes no mint function after deployment, no pause switch, and gives the creator no blacklist or liquidity powers. Those powers do exist — blacklisting, raising trading tax without a ceiling, and withdrawing liquidity that is nominally locked for ten years — but they belong to Virtuals Protocol administrative keys shared across every agent token from the same factory. On-chain inspection resolves those keys to two externally owned accounts: one holds token ownership together with the factory roles that can blacklist, retax and pull liquidity, and the other can replace the factory logic outright. Neither is a multisig and neither sits behind a timelock. The risk is therefore not token-specific: it is protocol-wide, and it rests on two private keys.
The ERC20 template cloned for every agent token launched through Virtuals Protocol on Base.
Yes, but not by the agent creator.
_beforeTokenTransfer reverts when blacklists[to] is true. It checks only the recipient. Because a sell transfers tokens *to* the Uniswap V2 pair, blacklisting the pair address would make every sell revert while buys continued to work. addBlacklistAddress is onlyOwnerOrFactory, and the owner is assigned by the factory rather than by the launcher.
Controlled by: 0xe220329659d41b2a9f26e83816b424bdacf62567, an externally owned account, as token owner; and AgentFactoryV7 via BONDING_ROLE
Yes, with no upper bound in code.
setProjectTaxRates carries the comment "subject to only ever decreasing", but the implementation contains no such check — it assigns both basis-point values directly. Against BP_DENOM = 10000, a 100% sell tax is reachable. Subtraction of the tax happens inside an unchecked block, so values above the denominator are not arithmetically guarded either. Same access control as the blacklist. The clone inspected for this review was running 100 basis points (1%) on both buy and sell.
Controlled by: 0xe220329659d41b2a9f26e83816b424bdacf62567 as token owner; and AgentFactoryV7
One externally owned account, shared across every token from the factory.
AgentFactoryV7._createNewAgentToken passes [_tokenAdmin, _uniswapRouter, assetToken] into initialize; the first element becomes the argument to _transferOwnership. _tokenAdmin is one factory-wide parameter set through setParams by DEFAULT_ADMIN_ROLE, so every clone from that factory answers to the same address. Calling owner() on a deployed clone returned 0xe220329659d41b2a9f26e83816b424bdacf62567, which holds no contract code. That same account also holds DEFAULT_ADMIN_ROLE on the factory, meaning it can grant itself any remaining role, reassign _tokenAdmin, and change the implementation used for future tokens. Ownership uses Ownable2StepUpgradeable, so transfers require acceptance — good practice, but it does not change who holds the key.
Controlled by: 0xe220329659d41b2a9f26e83816b424bdacf62567 (DEFAULT_ADMIN_ROLE on AgentFactoryV7)
Not enforceably.
LP tokens are staked into AgentVeTokenV2 with a matureAt timestamp, and withdraw does block the founder from dropping below initialLock before that date. Two protocol levers override it. setMatureAt lets any holder of ADMIN_ROLE on the AgentNft move the date arbitrarily. removeLpLiquidity withdraws the founder’s LP to a caller-supplied recipient with caller-supplied slippage bounds; its own docstring states it "intentionally BYPASSES matureAt". Virtuals built it to drain projects that have already rugged, but the mechanism itself does not distinguish intent. REMOVE_LIQUIDITY_ROLE on the factory resolves to the same externally owned account that owns the tokens.
Controlled by: 0xe220329659d41b2a9f26e83816b424bdacf62567 (REMOVE_LIQUIDITY_ROLE); ADMIN_ROLE on AgentNft for setMatureAt
Yes. The factory is an upgradeable proxy.
AgentFactoryV7 is deployed behind an EIP-1967 transparent proxy at 0xc169a24010fd4d47ca7be4b4c7863b76520442a3, currently delegating to implementation 0x5e357d0219e3a15674f18a5a197fb45e93568ae9. Its ProxyAdmin at 0x5d47e482a05980b7ef093e3ed8a2d428942ffbc0 is owned by 0xc31cf1168b2f6745650d7b088774041a10d76d55, an account with no contract code. Whoever holds that key can replace the factory implementation with arbitrary logic, at which point every statement on this page about how the factory behaves would need re-checking. Tokens already deployed keep their own logic, since they are fixed-target clones, but the factory’s powers over them run through onlyOwnerOrFactory.
Controlled by: 0xc31cf1168b2f6745650d7b088774041a10d76d55 (owner of the ProxyAdmin)
Not for tokens already deployed.
Tokens are EIP-1167 minimal proxies created with Clones.cloneDeterministic, delegating to a fixed implementation address baked into the clone bytecode. setImplementations on the factory changes the target for *future* clones only. Existing tokens keep the logic they launched with. The clone inspected for this review carries the standard 45-byte EIP-1167 runtime, confirming the pattern.
No.
_mint is internal and is only reached from _mintBalances, called inside initialize, which is guarded by the initializer modifier and therefore runs once. No external mint entry point exists. burn and burnFrom only reduce the caller’s own balance, the latter requiring an allowance.
Control resolves to two externally owned accounts on Base, neither of them a multisig or a timelock. The first, 0xe220329659d41b2a9f26e83816b424bdacf62567, is the _tokenAdmin that owns every AgentTokenV4 clone, and on AgentFactoryV7 (0xc169a24010fd4d47ca7be4b4c7863b76520442a3) it also holds DEFAULT_ADMIN_ROLE, REMOVE_LIQUIDITY_ROLE and WITHDRAW_ROLE. It does not currently hold BONDING_ROLE, but DEFAULT_ADMIN_ROLE lets it grant itself that role in a single transaction, so the distinction is procedural rather than protective. The second, 0xc31cf1168b2f6745650d7b088774041a10d76d55, owns the ProxyAdmin behind the factory and can therefore replace the factory implementation entirely. Both are active operational keys rather than dormant ones, with 2,830 and 314 transactions sent respectively at the time of checking.
Read from a deployed clone on Base on 2026-09-21 via https://mainnet.base.org.
Sample token LLX at 0xB9Dd507a5b352783b25e14c9b6E77D9f0067380f,
delegating to implementation 0x950ff75579560e8f391406274790fcb8bf749aa8.
| Call | Value |
|---|---|
| Clone bytecode | EIP-1167 minimal proxy, 45 bytes, delegating to 0x950ff75579560e8f391406274790fcb8bf749aa8Confirms the deployed token is a clone rather than an independently deployed contract. |
| taxAccountingAdapter() | 0xe700baca9fbe5e4cbbc9ef0347b9b5b7a0864eb9This getter exists only on AgentTokenV4, which is what identifies the template version. |
| pairToken() | 0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b (VIRTUAL) |
| projectBuyTaxBasisPoints() / projectSellTaxBasisPoints() | 100 / 100 (1% each)Both are writable without an upper bound by the owner key. |
| owner() | 0xe220329659d41b2a9f26e83816b424bdacf62567No contract code at this address: an externally owned account, not a multisig or timelock. 2,830 transactions sent and a funded ETH balance at the time of checking — an active operational key. |
Factory (private _factory, read from storage slot 16) |
0xc169a24010fd4d47ca7be4b4c7863b76520442a3The layout was confirmed by matching adjacent slots against their public getters before trusting this one. |
| hasRole(DEFAULT_ADMIN_ROLE, owner) on the factory | trueThe token owner can also grant and revoke every other factory role, including BONDING_ROLE, which it does not currently hold. |
| hasRole(REMOVE_LIQUIDITY_ROLE, owner) / hasRole(WITHDRAW_ROLE, owner) | true / trueThe same key that owns the tokens can trigger the liquidity withdrawal path that bypasses the ten-year lock. |
| Factory EIP-1967 implementation / ProxyAdmin | 0x5e357d0219e3a15674f18a5a197fb45e93568ae9 / 0x5d47e482a05980b7ef093e3ed8a2d428942ffbc0The factory is a transparent upgradeable proxy, so its logic is replaceable. |
| ProxyAdmin.owner() | 0xc31cf1168b2f6745650d7b088774041a10d76d55A second externally owned account, with 314 transactions sent. It can replace the factory implementation with arbitrary logic. |
| paused() on the factory | falseNew agent launches were not halted at the time of checking. |
_tokenAdmin is a single factory-wide parameter — it was not confirmed by sampling a second token. Check any specific token on its own before relying on this._tokenAdmin address can also be reassigned at any time. Every reading here is a snapshot of the date shown, not a standing guarantee.main branch of the protocol repository at the blob revisions listed below. The clone inspected on Base was identified by its function interface, not by a byte-for-byte comparison against a compiled build of that source.c912d54f4e152a862ec082f924ddb902b13dReviewing a specific token deployed from this template? Run an automated audit on its contract address →