Temptation Token Info
Temptation Token is a crypto-powered voting game built on the Base blockchain. Each week, players vote $TTS on their favorite profiles in a provably fair, weighted competition. Your votes buy lottery tickets — more votes means better odds, but anyone can win. The top voter on the winning profile takes 40% of the weekly prize pool. Winners take 40%. 10% goes to the Polaris Project anti-human trafficking nonprofit. Losing votes are permanently burned — reducing TTS supply every single week.
TrustNet Score
The TrustNet Score evaluates crypto projects based on audit results, security, KYC verification, and social media presence. This score offers a quick, transparent view of a project's credibility, helping users make informed decisions in the Web3 space.
Real-Time Threat Detection
Real-time threat detection, powered by Cyvers.io,
is currently not
activated
for this project.
This advanced feature provides continuous monitoring and instant alerts to safeguard your assets from potential security threats. Real-time detection enhances your project's security by proactively identifying and mitigating risks.
For more information, click here.
Security Assessments
Summary and Final Words
No crucial issues found
The contract does not contain issues of high or medium criticality. This means that no known vulnerabilities were found in the source code.
Contract owner cannot mint
It is not possible to mint new tokens.
Contract owner cannot blacklist addresses.
It is not possible to lock user funds by blacklisting addresses.
Contract owner can set high fees
Contract owner is able to set fees above 25%. Very high fees can also prevent token transfer.
Token transfer can be locked
Owner can lock user funds with owner functions.
Token cannot be burned
There is no burning within the contract without any allowances
Ownership is not renounced
The owner retains significant control, which could potentially be used to modify key contract parameters.
Contract is not upgradeable
The contract does not use proxy patterns or other mechanisms to allow future upgrades. Its behavior is locked in its current state.
Scope of Work
This audit encompasses the evaluation of the files listed below, each verified with a SHA-1 Hash. The team referenced above has provided the necessary files for assessment.
The auditing process consists of the following systematic steps:
- Specification Review: Analyze the provided specifications, source code, and instructions to fully understand the smart contract's size, scope, and functionality.
- Manual Code Examination: Conduct a thorough line-by-line review of the source code to identify potential vulnerabilities and areas for improvement.
- Specification Alignment: Ensure that the code accurately implements the provided specifications and intended functionalities.
- Test Coverage Assessment: Evaluate the extent and effectiveness of test cases in covering the codebase, identifying any gaps in testing.
- Symbolic Execution: Analyze the smart contract to determine how various inputs affect execution paths, identifying potential edge cases and vulnerabilities.
- Best Practices Evaluation: Assess the smart contracts against established industry and academic best practices to enhance efficiency, maintainability, and security.
- Actionable Recommendations: Provide detailed, specific, and actionable steps to secure and optimize the smart contracts.
A file with a different Hash has been intentionally or otherwise modified after the security review. A different Hash may indicate a changed condition or potential vulnerability that was not within the scope of this review.
Final Words
The following provides a concise summary of the audit report, accompanied by insightful comments from the auditor. This overview captures the key findings and observations, offering valuable context and clarity.
Smart Contract Analysis Statement
Contract Analysis
The TTSVotingV3b contract is a weekly voting and prize-distribution engine that operates on top of the external Temptation Token (TTS) ERC-20. It uses Chainlink VRF v2.5 to draw a winner from a set of admin-approved profiles, applies a staking-tier multiplier from an external staking contract, mints a per-round NFT for the winning profile, and supports an optional club-referral payout. The standard prize split is 35 percent to the winning profile, 35 percent to its top voter, 10 percent to charity and 20 percent to the house wallet, or a 35 / 35 / 10 / 10 / 10 split when a registered club is linked. The overall design follows common patterns for raffle-style on-chain mechanics, but several areas need attention before the contract can be considered production-ready:
- The vote-cap math at the start of every round is mathematically impossible to satisfy, so the very first vote of every round reverts and the contract cannot accept any vote in its current form. The on-chain history of the deployed contract is consistent with this - it has been initialised and configured but has never received a successful vote transaction.
- The settlement callback runs an unbounded loop over admin-supplied profile identifiers under a fixed 500000-gas budget, and there is no admin path to reset the pending-VRF flag if the coordinator never delivers. Either condition can leave a round permanently stuck with all voter deposits locked inside the contract.
- Several values that drive payouts - charity wallet, house wallet, club routing, NFT contract - are mutable at any time and read live during the settlement callback. This creates a redirection window for up to 30 percent of a prize pool between the moment randomness is requested and the moment the callback executes.
Ownership Privileges
The ownership of the contract has been transferred to the TTSKeeper2 keeper contract, while the admin role is held by the original deployer key. Both roles are rotated in a single transaction and there is no on-chain timelock between a transfer request and its effect. The owner and admin retain the following privileges:
- The owner can start, settle and roll over voting rounds, request randomness from Chainlink VRF and set arbitrary round durations including very short or zero-length rounds.
- The admin can approve and batch-approve voting profiles, including which wallet receives the winning-profile share for each profile.
- The admin can register, update and de-register club referral wallets and link any profile to any club code at any time, including while a settlement is in flight.
- The admin can change the charity wallet, house wallet, staking contract and NFT contract addresses without any delay or event-based notification.
- The contract cannot mint TTS tokens - minting is exclusively the responsibility of the external TTS token contract, and this contract has no privileged path to it.
- The contract cannot burn user tokens at will - the only burn pathway is the transfer of any residual balance to the dEaD address at the end of a successful settlement.
- The contract cannot blacklist or freeze user addresses - there is no per-address gating beyond the standard ERC-20 allowance the user grants when they call vote.
- The contract is not upgradeable - it is deployed as a standalone contract with no proxy, no initialiser pattern and no storage gap. Any change to the logic requires a fresh deployment, a re-transfer of ownership, and an update of the off-chain integrations.
Security Features
The contract implements several positive security features:
- Randomness is sourced from Chainlink VRF v2.5 with proper coordinator gating - only the configured coordinator can call rawFulfillRandomWords, and the consumer is bound at deploy time as an immutable.
- The settlement state machine uses an explicit vrfPending flag that is set before the external VRF request, so a second settlement cannot be issued while one is in flight, and a new round cannot start until the current one is settled.
- The NFT mint at the end of settlement is wrapped in a try/catch, so a reverting NFT contract cannot prevent the prize pool from reaching voters, charity, house and club.
- The staking-tier lookup is wrapped in a try/catch and falls back to a 1x multiplier, so voters can still cast votes if the external staking contract is paused, upgraded or temporarily unavailable.
- The TTS token address is stored as an immutable, removing any post-deployment surface for an admin to redirect voter deposits into a different ERC-20.
Note - This Audit report consists of a security analysis of the TTSVotingV3b smart contract. This analysis did not include economic analysis of the contract's tokenomics, including the 30 percent aggregate non-prize share, the burn-on-rollover behaviour or the staking-multiplier table. Moreover, we only audited the main contract for the Temptation Token team. Other contracts associated with the project - including the TTS token itself, the TTSKeeper2 keeper contract, the external staking contract and the round NFT contract - were not audited by our team. We recommend investors do their own research before investing.
Files and details
Functions
public
/
State variables
public
/
Total lines
of code
/
Capabilities
Hover on items
/
Findings and Audit result
critical Issues | 1 findings
Pending
#1 critical Issue
Vote cap check prevents any vote from being cast in any round
The vote cap check at line 453 requires newProfileRaw * 10000 to be less than or equal to newRoundRaw * MAX_VOTE_CAP_BPS, where MAX_VOTE_CAP_BPS equals 4000 (40 percent). At the start of every round, p.rawVotes and r.totalRawVotes are both zero, so the very first vote of any round produces newProfileRaw equal to newRoundRaw equal to amount. The inequality reduces to 10000 less-or-equal 4000, which is always false, and every first vote of every round reverts with 'Exceeds vote cap'. Because no first vote ever lands in storage, no second vote can ever succeed either - the contract cannot accept any vote at all in its current form. The on-chain history of the deployed contract on Base is consistent with this: the contract has been deployed and configured (ownership transferred, NFT contract set) but has never received a successful vote() transaction. The contract is therefore live but functionally bricked from its primary purpose.
high Issues | 3 findings
Pending
#1 high Issue
Settlement callback can be permanently bricked by exceeding the callback gas budget
fulfillRandomWords runs an unbounded loop over r.profileIds to pick the winner. profileIds is admin-supplied via approveProfile / batchApproveProfiles and has no upper bound. Each iteration loads a dynamic string from storage and a Profile struct, then the callback performs four to six ERC-20 transfers and an external NFT mint. All of this must complete within the fixed CALLBACK_GAS_LIMIT of 500000 gas. With long profile identifiers (60 to 100 byte slugs) and dozens of approved profiles, the callback can run out of gas. Chainlink VRF v2.5 does not retry failed callbacks, so the request is consumed, the random word is delivered, and the callback reverts on chain. The round is then permanently stuck with vrfPending true and settled false. New rounds cannot start (the previous-round settled check fails) and the settlement cannot be re-issued (the !vrfPending check fails). All tokens deposited in that round are locked indefinitely.
Pending
#2 high Issue
Stuck-funds path when the winning profile has a zero wallet address
Inside fulfillRandomWords, the contract sets r.vrfPending false and r.settled true before validating that the selected winner is payable. If the early return at line 390 fires because winner.wallet is address(0) or because pool is zero, the round is marked permanently settled while every TTS deposit voters made for that round remains in the contract. approveProfile and batchApproveProfiles do not validate that the wallet is non-zero, so an admin typo, a frontend bug or an off-by-one in batch arrays can approve a profile with the zero address. If that profile then wins the random draw, the entire prize pool is trapped. On the next successful settlement, the trapped balance is transferred to the dEaD address by the burn-remainder block at line 430-433, so voters never get a refund and the prize never reaches anyone.
Pending
#3 high Issue
ERC-20 transfer return values are not checked during payouts
fulfillRandomWords performs six ERC-20 transfers (winner share, top-voter share, charity, house, optional club and the dEaD-address burn) and discards every return value. The ERC-20 standard explicitly permits a token to return false on failure rather than reverting (USDT and several other widely used tokens behave this way). If the deployed TTS token is ever upgraded behind a proxy, replaced, or has a fee-on-transfer or blacklist module added, payouts can silently fail while the contract believes settlement succeeded. The dEaD-address burn at the end of settlement still transfers any remaining balance, so a partial-failure scenario can leave winners unpaid while the contract still drains itself.
medium Issues | 7 findings
Pending
#1 medium Issue
Admin can redirect the club share between VRF request and fulfillment
setClubWallet and setProfileClub are both onlyAdmin and have no time-lock or round-lock guard. After the contract flips r.vrfPending to true, the VRF callback can take one to several blocks (longer if the subscription runs out of LINK). During that window the admin is free to call either setter, and fulfillRandomWords reads profileClub[winnerId] and clubWallets[clubCode] live at line 399-400. The admin can therefore redirect the 10 percent club share to any address simply by ordering a transaction before the callback. Additionally, profileClub is keyed only by profileId, so the same profileId reused in a future round inherits the previous round's club mapping unless the admin re-keys it.
Pending
#2 medium Issue
NFT contract address can be set to a gas-bomb without warning
setNFTContract is onlyAdmin, accepts the zero address as an intentional toggle and emits no event. A malicious or compromised admin can set nftContract to a contract whose mint function consumes all remaining callback gas. The try/catch wrapping the NFT mint at line 427 only protects against reverts, not against gas exhaustion - the catch is reached but the burn-remainder block that follows runs out of gas, the whole callback reverts, and the round becomes permanently stuck in the pending state. Off-chain monitoring also cannot detect the change because no event is fired.
Pending
#3 medium Issue
Round cannot be recovered if VRF coordinator never delivers
_requestSettlement sets r.vrfPending = true before calling the VRF coordinator. If the coordinator never delivers the callback (subscription out of LINK, network reorg, internal infrastructure failure) the flag stays true forever. _startRound is gated on the previous round being settled and _requestSettlement is gated on !vrfPending, so neither a new round nor a re-issued settlement can move forward. There is no admin function to reset vrfPending, so all tokens deposited for that round are locked indefinitely and the protocol becomes unusable.
Pending
#4 medium Issue
Single-step ownership and admin transfers, with reachable renounceOwnership
transferOwnership and transferAdmin both rotate privilege in a single transaction. A typo or compromised key permanently moves the role to an unintended address. The contract also exposes renounceOwnership() to the owner, which sets owner to address(0); after that, startRound, settleRound, requestSettlement, rolloverRound and takeMidpointSnapshot are unreachable forever. The admin role keeps working but no further rounds can ever be opened or settled, leaving any in-flight or future round permanently bricked.
Pending
#5 medium Issue
State changes after external transferFrom call in vote()
In vote(), the contract calls ttsToken.transferFrom() before updating the profile's rawVotes, totalTickets, the round-level totals and the per-voter cumulative amount. If the deployed TTS token is ever made callback-aware (ERC-777, ERC-1363, or any token with transfer hooks), the transferFrom call can re-enter the voting contract before state is updated. While the cap-check inequality itself is not bypassable through reentrancy, the topVoter logic and the cross-function reentrancy surface (through approveProfile, batchApproveProfiles, fulfillRandomWords, getProfile and the same vote function) provide enough state-window for an attacker to manipulate the topVoter recipient or to interleave transfers in unexpected ways. With a vanilla ERC-20 token without hooks the issue is latent, but the code is a single token-upgrade away from being exploitable.
Pending
#6 medium Issue
rolloverRound can be executed before the round end
rolloverRound is owner-only and flips r.settled to true without distributing the pool, but it has no requirement that the round has actually ended (no block.timestamp >= r.endTime check). The owner can therefore settle out a round seconds after it was started, before any voter has had a chance to participate. Combined with the burn-remainder block at line 430-433, this means the next settlement transfers every contribution made for the rolled-over round to the dEaD address. If the owner key is compromised, or if the keeper contract owning this role contains a bug, this is an instant rug vector. Even under benign operation it is a footgun that can destroy user funds in a single transaction.
Pending
#7 medium Issue
Payout destinations are mutable during pending VRF window
charityWallet, houseWallet and nftContract are admin-mutable at any time, including between _requestSettlement and the VRF callback. fulfillRandomWords reads charityWallet, houseWallet and nftContract live, so an admin can redirect 30 percent of a prize pool (10 percent charity plus up to 20 percent house) and the entire NFT mint to attacker-controlled destinations simply by ordering transactions before the callback. This is the same attack class as the club-routing finding but applied to the protocol-wide wallets, with a larger combined share of the pool at stake.
low Issues | 6 findings
Pending
#1 low Issue
Missing zero-address checks on constructor parameters and admin setters
The constructor stores _charityWallet, _houseWallet, _stakingContract, _ttsToken and the VRF coordinator without any zero-address validation. _ttsToken in particular is immutable, so a wrong address at deployment cannot be fixed and would brick payouts permanently. setNFTContract also accepts an arbitrary address, including the zero address. Setting any of these to address(0) by mistake silently produces broken behaviour at the next settlement and may permanently lock voter funds.
Pending
#2 low Issue
Admin setters do not emit events
setCharityWallet, setHouseWallet, setStakingContract and setNFTContract all change critical configuration but emit no event. Off-chain indexers, monitoring tools and front-end clients cannot reliably observe these changes without polling state, which is slow, expensive and error-prone. Operators also lose the ability to audit historical changes purely from logs.
Pending
#3 low Issue
Indexed string parameters in events lose their pre-image
ClubWalletSet, ProfileClubSet and Voted declare string parameters as indexed. Solidity stores indexed strings as keccak256(string) topics, so the original string is not recoverable from the topic alone. Off-chain consumers must rely on the non-indexed payload, which makes filtering logs by club code or profile id more cumbersome than the developer probably intends.
Pending
#4 low Issue
Solidity pragma is unpinned and version 0.8.20 has known compiler issues
The contract declares pragma solidity ^0.8.20. Solidity 0.8.20 has several known compiler issues (verbatim invalid deduplication, full inliner non-expression argument-evaluation order, missing side effects on selector access). The caret pragma also lets future minor versions of solc be picked up at compile time, which can introduce subtle behavioural changes between development and production builds.
Pending
#5 low Issue
profileClub mapping is global rather than per-round
profileClub is keyed by profileId only and has no round dimension. If the same profileId is approved in multiple rounds (intentionally for recurring participants, or by collision in an externally generated id scheme), the new round inherits the previous round's club mapping unless the admin explicitly re-keys it before settlement. This couples the club routing of unrelated rounds and creates an additional surface for admin error or unintended payouts.
Pending
#6 low Issue
Staking-tier lookup silently swallows all errors
_applyMultiplier wraps the call to stakingContract.getStakingTier in a try/catch with an empty catch body. If the staking contract is paused, upgraded or its ABI changes, every voter silently loses their multiplier and falls back to a 1x ticket value with no event or revert. Operators have no visibility into the fact that the integration is broken until users complain about missing rewards.
optimization Issues | 3 findings
Pending
#1 optimization Issue
Repeated storage writes inside batchApproveProfiles loop
batchApproveProfiles pushes to r.profileIds storage on every iteration of the loop and re-reads profileIds.length on each iteration of the for-loop condition. Both patterns generate avoidable storage operations and contribute to higher gas costs for the admin when batch-approving many profiles at once.
Pending
#2 optimization Issue
Use of require strings instead of custom errors
The contract uses require with string error messages in over twenty places (admin checks, round-state checks, voter checks, settlement checks). Each string error costs more gas than a custom error at both deployment time (bytecode size) and at revert time (memory expansion and string copy).
Pending
#3 optimization Issue
Magic numbers in payout split should be named bps constants
fulfillRandomWords hard-codes the payout percentages as inline literals (35 / 35 / 10 / 10 / 20). The numbers are repeated across multiple expressions, making the split harder to reason about and harder to update consistently if the economics ever change.
informational Issues | 8 findings
Pending
#1 informational Issue
Modulo bias when picking the winning ticket
Winner selection uses pick = randomWords[0] % r.totalTickets. Because 2^256 is not in general divisible by r.totalTickets, there is a tiny modulo bias. With a 256-bit source of randomness and any realistic ticket supply the bias is approximately r.totalTickets divided by 2^256 - vanishingly small in practice but technically present.
Pending
#2 informational Issue
Empty no-op functions kept for keeper compatibility
midpointSnapshot() and takeMidpointSnapshot() are empty bodies kept so the existing keeper contract can call them without reverting. They consume a transaction and gas without doing any work, and a reader unfamiliar with the keeper integration may believe the implementation is missing.
Pending
#3 informational Issue
Round duration has no minimum or maximum bounds
startRound and the (uint256) overload accept any duration value. duration of zero produces a round whose endTime equals startTime, so any vote in a later block fails the block.timestamp <= r.endTime check immediately. Very large durations lock voters out of withdrawals for the duration. Operator error in either direction can degrade user experience without any sanity check.
Pending
#4 informational Issue
Rolling over a round burns every voter's contribution
rolloverRound is an owner-only escape hatch that flips r.settled to true without distributing the pool. The pool then becomes 'remaining' tokens and is burned to the dEaD address by the next round's settlement. Voters whose contributions were collected for the rolled-over round receive nothing, with no path to a refund. This is design rather than a defect, but it materially affects the user contract between the protocol and its participants and overlaps with the premature-rollover finding M-07.
Pending
#5 informational Issue
Win probability uses boosted tickets while pool is computed from raw votes
Winner selection uses r.totalTickets and per-profile totalTickets, both of which include the staking-tier multiplier. The prize pool that is distributed at settlement is computed from winner.rawVotes (without multiplier). A tier-5 voter therefore has up to 3x the win probability of a tier-0 voter for the same raw token contribution, even though the raw contribution alone determines the size of the prize. This may be the intended incentive but is a non-obvious property of the system.
Pending
#6 informational Issue
Charity and house effective fee equals 30 percent
fulfillRandomWords hard-codes the canonical split as 35 percent winner / 35 percent top voter / 10 percent charity, and either 20 percent house or 10 percent club + 10 percent house. The aggregate non-prize share is therefore always 30 percent of every round's pool, which exceeds the 25 percent threshold commonly used to classify a token-economic system as high-fee. The percentages are constants embedded in the function with no setter, so any change requires a redeploy.
Pending
#7 informational Issue
Minimum vote amount is fixed at deploy time and cannot be tuned
MIN_VOTE is hard-coded at 5e18 TTS. If the TTS token's market value rises significantly, the minimum becomes prohibitive for normal participants. If the value crashes, the threshold becomes too low to deter spam-vote attacks. There is no path to update this value short of redeploying the contract, which limits the project's ability to adjust parameters as the ecosystem evolves.
Pending
#8 informational Issue
Tier 0 returns a 1.1x multiplier
_applyMultiplier returns amount * 110 / 100 when getStakingTier returns 0. Whether this rewards genuine stakers or accidentally rewards non-stakers depends on whether the IStaking contract returns 0 for 'user has never staked' or only for the lowest staked tier. Without that confirmation it is not possible to say whether the current behaviour matches the intended incentive design.