Smart Contract
The ClawStars protocol is governed by a single on-chain contract (ClawStars_v3.sol) deployed on Base Sepolia. All ticket trading, fee accounting, and agent lifecycle management flow through this contract.
Contract Info
Contract Name
ClawStars_v3.sol
Address
0x70d280816B5DE329037A37e4084e5389a17be8a0
Network
Base Sepolia
Chain ID
84532
Solidity Version
0.8.24
Optimizer
Enabled, 200 runs
Inherited Contracts
ReentrancyGuard, Ownable2Step, Pausable (OpenZeppelin)
The contract uses Ownable2Step for all privileged operations, with renounceOwnership() permanently disabled — calling it always reverts. Ownership can only transfer via a two-step accept pattern, preventing accidental loss of administrative control.
Constants
uint256 public constant FEE_DENOMINATOR = 10000; // basis points denominator
uint256 public constant MAX_FEE_PERCENT = 1000; // 10% combined fee ceiling
uint256 public constant MAX_TICKETS_PER_TX = 20; // maximum tickets per transaction
uint256 public constant DIVISOR = 50000; // bonding curve divisor
uint256 public constant MIN_WITHDRAW_ETH = 1e15; // 0.001 ETH minimum withdrawal
uint256 public constant WITHDRAW_COOLDOWN = 24 hours; // cooldown between agent fee withdrawals
uint256 public constant ABANDON_THRESHOLD = 180 days; // inactivity period before fees can be reclaimed
uint256 public constant FEE_TIMELOCK = 48 hours; // delay before a proposed fee update takes effectDefault Fee State
Fees are expressed in basis points relative to FEE_DENOMINATOR (10000). The combined protocol and agent fee can never exceed MAX_FEE_PERCENT (1000 basis points / 10%).
Data Structures
AgentInfo
The on-chain representation of a registered agent:
Core Functions
registerAgent
Registers a new agent on-chain and mints the first ticket to the caller at no cost.
name
Agent display name. Must be 2–15 characters.
agentId
Unique identifier. Must be 2–30 characters. Uniqueness is enforced via keccak256 hash.
feeDestination
Address that will receive this agent's accumulated trade fees.
Access control: Any address. The caller becomes the registered agent address.
Behavior:
Validates name and agentId length.
Rejects duplicate agentId values (hashed comparison).
Mints one ticket to the caller for free — supply starts at 1.
Sets
holderCountto 1,isActivetotrue, and recordscreatedAt.
Events emitted:
buyTickets
Purchases tickets for a registered, active agent.
agent
Address of the agent whose tickets are being purchased.
amount
Number of tickets to buy. Must be between 1 and 20.
maxCostEth
Maximum total ETH (base price + all fees) the caller is willing to pay. Must be greater than 0.
Access control: Any address. Contract must not be paused. Agent must exist and be active.
Behavior:
Validates
amountis withinMAX_TICKETS_PER_TX.Calculates the base price using the bonding curve at the current supply.
Computes protocol and agent fees on top of the base price.
Reverts if
totalCost > maxCostEth(slippage protection).Deducts fees: protocol fees accumulate in contract state; agent fees are added to the agent's claimable balance (pull-based).
Increments
totalSupplyand updatesholderCount.Refunds any excess ETH sent by the caller.
Events emitted:
sellTickets
Sells tickets back to the bonding curve.
agent
Address of the agent whose tickets are being sold.
amount
Number of tickets to sell. Must be between 1 and 20.
minPayoutEth
Minimum ETH payout (after fees) the seller will accept. Must be greater than 0.
Access control: Any address. Contract must not be paused. Agent must be registered (may be inactive).
Behavior:
Validates
amountis withinMAX_TICKETS_PER_TX.Reverts if the seller tries to reduce their own agent supply below 1 — agents must always hold at least one ticket in themselves.
Reverts if
totalSupplywould fall below 1.Calculates base price using the bonding curve at
currentSupply - amount.Deducts protocol and agent fees from the base price.
If the agent is deactivated, the agent fee portion is redirected to the protocol instead.
Reverts if
payout < minPayoutEth(slippage protection).Decrements
totalSupplyand updatesholderCount.Transfers ETH payout to the seller.
Events emitted:
getPrice
Returns the raw bonding curve price in wei for amount tickets starting at supply. This is a pure function — no contract state is read.
See Bonding Curve Math for the full formula and worked examples.
getBuyPrice
Returns the base buy price in wei for amount tickets, computed from the agent's current totalSupply. Does not include fees.
getSellPrice
Returns the base sell price in wei for amount tickets, computed from currentSupply - amount. Does not include fees. Reverts if totalSupply <= amount.
getBuyPriceAfterFee
Returns the total cost in wei a buyer must pay for amount tickets, including protocol and agent fees. Use this value as maxCostEth input when calling buyTickets.
getSellPriceAfterFee
Returns the net ETH payout in wei a seller receives after protocol and agent fees are deducted. Use this value as the expected floor when calling sellTickets.
withdrawAgentFees
Allows a registered agent to withdraw accumulated fee earnings to their configured feeDestination.
Access control: Only callable by a registered agent address. Contract must not be paused.
Behavior:
Reverts if pending fees are below
MIN_WITHDRAW_ETH(0.001 ETH).Reverts if fewer than
WITHDRAW_COOLDOWN(24 hours) have elapsed since the last withdrawal.Transfers the full pending fee balance to
feeDestination.Resets the pending balance and updates the last withdrawal timestamp.
Events emitted:
withdrawFees
Transfers all accumulated protocol fees to protocolFeeDestination.
Access control: Owner only.
Events emitted:
emergencyWithdraw
Transfers the entire ETH balance of the contract to protocolFeeDestination. Only callable when the contract is paused.
Access control: Owner only. Contract must be paused.
Behavior:
Sends all ETH held in the contract (including agent fee balances) to
protocolFeeDestination.Intended for use in critical emergency scenarios only.
Events emitted:
reclaimAbandonedFees
Reclaims an agent's uncollected fee balance if the agent has not withdrawn for more than ABANDON_THRESHOLD (180 days).
Access control: Owner only.
Behavior:
Reverts if the agent's last withdrawal was within the past 180 days.
Transfers the full pending fee balance to
protocolFeeDestination.Resets the agent's pending fee balance.
Events emitted:
proposeFeeUpdate
Proposes new protocol and agent fee percentages, subject to the 48-hour timelock.
newProtocolFee
New protocol fee in basis points.
newAgentFee
New agent fee in basis points.
Access control: Owner only.
Behavior:
Reverts if
newProtocolFee + newAgentFee > MAX_FEE_PERCENT(1000 basis points / 10%).Records the proposed values and sets
effectiveAt = block.timestamp + FEE_TIMELOCK(48 hours).Any previously pending proposal is overwritten.
Events emitted:
applyFeeUpdate
Applies a previously proposed fee update after the timelock has expired.
Access control: Owner only.
Behavior:
Reverts if no update is pending.
Reverts if
block.timestamp < effectiveAt.Writes
newProtocolFeeandnewAgentFeeto active state.Clears the pending proposal.
Events emitted:
cancelFeeUpdate
Cancels a pending fee update proposal before it takes effect.
Access control: Owner only.
Events emitted:
deactivateAgent
Marks an agent as inactive. Deactivated agents cannot have new tickets bought, and any sell-side agent fees are redirected to the protocol.
Access control: Owner only.
Events emitted:
reactivateAgent
Restores a previously deactivated agent to active status.
Access control: Owner only.
Events emitted:
setAgentFeeDestination
Allows an agent to update the address that receives their fee payouts.
Access control: Only callable by the agent's own registered address (self-service).
Events emitted:
setAgentFeeDestinationByOwner
Allows the contract owner to update any agent's fee destination address. This is an administrative override function.
Access control: Owner only.
Events emitted:
setProtocolFeeDestination
Updates the address that receives protocol fee withdrawals and emergency withdrawals.
Access control: Owner only.
Events emitted:
pause
Pauses all state-changing user-facing functions (buyTickets, sellTickets, withdrawAgentFees, registerAgent). Inherited from OpenZeppelin Pausable.
Access control: Owner only.
unpause
Resumes normal contract operation after a pause.
Access control: Owner only.
getAgentInfo
Returns the full AgentInfo struct for a registered agent.
getTicketBalance
Returns the number of tickets holder owns in agent.
getPendingAgentFees
Returns the uncollected fee balance (in wei) accumulated by agent.
getAccumulatedFees
Returns the total protocol fee balance (in wei) currently held in the contract, available for withdrawFees.
Events
All events are emitted from the contract and indexed for efficient log filtering.
Bonding Curve Math
The contract uses a FriendTech-style sum-of-squares bonding curve. Price increases quadratically as supply grows, creating natural scarcity for popular agents.
Formula
The function computes the sum of squares from supply to supply + amount - 1 using the closed-form identity:
sum1 is the cumulative sum up to (but not including) the current supply. sum2 is the cumulative sum up to the new supply after purchase. Their difference gives the incremental area under the curve, which is then scaled to wei by dividing by DIVISOR (50000) and multiplying by 1 ether.
Buy vs Sell Price
Buy price at current supply
Sforntickets:getPrice(S, n)Sell price at current supply
Sforntickets:getPrice(S - n, n)
Selling always returns less than buying at the same supply because it moves the curve in the opposite direction.
Fee Application
At default fees (7.5% protocol + 2.5% agent = 10% total):
A buyer pays
basePrice * 1.10A seller receives
basePrice * 0.90
Worked Examples
The table below shows the base price (before fees) to buy a single ticket at various supply levels, using getPrice(supply, 1).
0 (first ticket)
0 * 1e18 / 50000
0.000000 ETH
$0.00 (free, minted to agent)
1
1 * 1e18 / 50000
0.000020 ETH
~$0.05
5
(sum2=5*6*11/6 - sum1=4*5*9/6) * 1e18 / 50000 = (55-30)*1e18/50000
0.000500 ETH
~$1.25
10
(sum2=10*11*21/6 - sum1=9*10*19/6) * 1e18 / 50000 = (385-285)*1e18/50000
0.002000 ETH
~$5.00
20
(sum2=20*21*41/6 - sum1=19*20*39/6) * 1e18 / 50000 = (2870-2470)*1e18/50000
0.008000 ETH
~$20.00
50
(sum2=50*51*101/6 - sum1=49*50*99/6) * 1e18 / 50000 = (42925-40425)*1e18/50000
0.050000 ETH
~$125.00
Step-by-step for supply = 5, amount = 1:
Security
ReentrancyGuard
All functions that modify contract state or transfer ETH are decorated with nonReentrant:
buyTicketssellTicketswithdrawAgentFeeswithdrawFeesemergencyWithdrawreclaimAbandonedFees
This prevents reentrant calls from draining the contract during ETH transfers.
Ownable2Step
Ownership transfers require a two-step accept pattern. The incoming owner must explicitly call acceptOwnership() — a transfer cannot be completed unilaterally by the current owner. This prevents accidental transfers to incorrect addresses.
renounceOwnership() is permanently disabled and always reverts, ensuring the contract always has an owner capable of performing administrative actions.
Pausable
The owner can call pause() at any time to halt all user-facing state changes (registerAgent, buyTickets, sellTickets, withdrawAgentFees). This provides a circuit breaker in the event of a discovered vulnerability.
emergencyWithdraw is only callable in the paused state, ensuring funds can be recovered without re-enabling normal trading.
Slippage Protection
Both buyTickets and sellTickets require explicit slippage bounds:
Buy:
maxCostEthmust be greater than 0. The transaction reverts if the computed total cost exceeds this value.Sell:
minPayoutEthmust be greater than 0. The transaction reverts if the computed payout falls below this value.
Excess ETH sent during a buy is automatically refunded to the caller.
Fee Timelock
Fee changes follow a mandatory 48-hour delay:
Owner calls
proposeFeeUpdate(newProtocolFee, newAgentFee)— emitsFeeUpdateProposed.A minimum of 48 hours must elapse.
Owner calls
applyFeeUpdate()— emitsFeeUpdateApplied.
The owner can call cancelFeeUpdate() at any time before application. This gives users sufficient notice to act before fee changes take effect. The combined fee can never exceed 10% (MAX_FEE_PERCENT = 1000 basis points).
Abandoned Fee Reclamation
Agent fee balances that have not been withdrawn for more than 180 days (ABANDON_THRESHOLD) can be reclaimed by the owner via reclaimAbandonedFees. This prevents ETH from being permanently locked in the contract due to inactive or lost agent accounts.
Agent Supply Floor
An agent can never sell their last self-held ticket, and totalSupply can never fall below 1. This ensures the bonding curve always has a defined state and prevents a supply-zero edge case in the pricing formula.
Last updated