Functions
DISCLAIMER // NFA // DYOR
This analysis is based on decompiled bytecode and observations of the contract behavior. We are not smart contract security experts. This document aims to explain what the contract appears to do based on the code. It should not be considered a comprehensive security audit or financial advice. Always verify critical information independently and consult with blockchain security professionals for important decisions.
⊙ generated by robots | curated by humans
| METADATA | |
|---|---|
| Proxy Address | 0x2a9848c39fff51eb184326d65f1238cc36764069 (etherscan) |
| Implementation Address | 0x435dc93869553123513148e5df4231c21dfca965 (etherscan) |
| Network | Ethereum Mainnet |
| Analysis Date | 2026-03-29 (Updated) |
Function Summary
| CATEGORY | NUM FUNCTIONS |
|---|---|
| Initialization | 1 |
| User Functions | 7 |
| Operator Functions | 8 |
| Admin Functions | 14+ |
| View Functions | 20+ |
Implementation Upgrade (2026-03-27)
The following 7 functions were added in the upgrade to implementation 0x435dc9...ca965:
VERSION(), requestPrioritySnapshot() (0x8bd94199), feeRecipient() (0x407c8855),
snapshotFee() (0x5d2e4c1c), prioritySnapshotFee() (0xc27762b5),
setFeeRecipient(address) (0x3c85102a), setPrioritySnapshotFee(uint256) (0xeb209249).
Initialization Function
initialize(address tokenAddress)
One-time initialization of the contract with the external token address.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:243-258 |
| Parameters | tokenAddress |
| Access | Anyone (but only works once) |
| FLAG | OBSERVATION |
|---|---|
| ☑ | Can only be called once (prevents re-initialization) |
| ☑ | First caller becomes admin |
| △ | No protection on who can initialize (should be called immediately after deployment) |
| △ | Token address cannot be changed after initialization |
| STEP | ACTION |
|---|---|
| 1 | Validate not already initialized |
| 2 | Validate token address is not zero |
| 3 | Store token address |
| 4 | Set initialized flag to 1 |
| 5 | Set caller as admin |
| 6 | Initialize pause states to 0 |
| 7 | Set minimum balance requirement to 0 |
| 8 | Set snapshot interval to 3600 (1 hour) |
| 9 | Set last snapshot time to block.timestamp |
| 10 | Emit initialization event |
| VARIABLE | CHANGE |
|---|---|
tokenAddress |
Set to provided token address |
initialized |
Set to 1 |
adminAddress |
Set to msg.sender |
minBalance |
Set to 0 |
snapshotInterval |
Set to 3600 |
lastSnapshotTime |
Set to block.timestamp |
| CONDITION | REVERT MESSAGE |
|---|---|
| Already initialized | "Already initialized" |
| Token address is zero | "Invalid XCL token" |
// Reconstructed from bytecode
function initialize(address tokenAddress) external {
require(initialized == 0, "Already initialized");
require(tokenAddress != address(0), "Invalid XCL token");
tokenAddress = tokenAddress;
initialized = 1;
adminAddress = msg.sender;
paused = 0;
registrationOpen = 0;
minBalanceRequired = 0;
snapshotInterval = 3600;
lastSnapshotTime = block.timestamp;
emit Initialized(tokenAddress, msg.sender);
}
User Functions
register()
Registers the caller to participate in rewards distribution based on their token holdings.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:431-458 |
| Selector | N/A (decompiled) |
| Access | External - Any eligible address |
| FLAG | OBSERVATION |
|---|---|
| ☑ | Registration is a one-time action per address |
| ☑ | Initial reward debt prevents claiming past rewards |
| △ | User's token balance is snapshot at registration time |
| △ | External token balance query could fail if token contract is malicious |
| CONDITION | REQUIREMENT |
|---|---|
| Contract state | Not paused |
| Registration | Must be open |
| Caller status | Not blacklisted |
| Caller status | Not already registered |
| Token balance | >= minimum requirement |
| STEP | ACTION |
|---|---|
| 1 | Check all preconditions |
| 2 | Query caller's token balance from external contract |
| 3 | Mark caller as registered |
| 4 | Add caller to participants array |
| 5 | Store caller's index in mapping |
| 6 | Increment participantCount |
| 7 | Store caller's balance snapshot |
| 8 | Record registration timestamp |
| 9 | Add balance to total tracked |
| 10 | Calculate initial reward debt |
| VARIABLE | CHANGE |
|---|---|
isRegistered[caller] |
Set to 1 |
participantIndex[caller] |
Set to index in array |
participants |
Caller added to array |
participantCount |
Incremented |
userBalance[caller] |
Set to token balance |
lastUpdate[caller] |
Set to block.timestamp |
totalTrackedBalance |
Increased by caller's balance |
rewardDebt[caller] |
Set to balance * rewardsPerShare / 10^18 |
| CONDITION | REVERT MESSAGE |
|---|---|
| Contract paused | "Contract is paused" |
| Registration closed | "Registration closed" |
| Caller blacklisted | "Address blacklisted" |
| Already registered | "Already registered" |
| Insufficient balance | "Insufficient XCL balance" |
| External call fails | Depends on token contract |
// Reconstructed from bytecode
function register() external {
require(paused == 0, "Contract is paused");
require(registrationOpen == 1, "Registration closed");
require(blacklisted[msg.sender] == 0, "Address blacklisted");
require(isRegistered[msg.sender] == 0, "Already registered");
uint256 balance = IERC20(tokenAddress).balanceOf(msg.sender);
require(balance >= minBalanceRequired, "Insufficient XCL balance");
isRegistered[msg.sender] = 1;
participants.push(msg.sender);
participantIndex[msg.sender] = participants.length - 1;
participantCount += 1;
userBalance[msg.sender] = balance;
lastUpdate[msg.sender] = block.timestamp;
totalTrackedBalance += balance;
rewardDebt[msg.sender] = (balance * rewardsPerShare) / 1e18;
emit Registered(msg.sender, balance);
}
flowchart TD
Start([register called])
Start --> Check1{Paused?}
Check1 -->|Yes| Revert1[Revert: Contract is paused]
Check1 -->|No| Check2{Registration Open?}
Check2 -->|No| Revert2[Revert: Registration closed]
Check2 -->|Yes| Check3{Blacklisted?}
Check3 -->|Yes| Revert3[Revert: Address blacklisted]
Check3 -->|No| Check4{Already Registered?}
Check4 -->|Yes| Revert4[Revert: Already registered]
Check4 -->|No| ExtCall[Query Token Balance]
ExtCall --> Check5{Balance >= Minimum?}
Check5 -->|No| Revert5[Revert: Insufficient balance]
Check5 -->|Yes| Process[Register User]
Process --> UpdateState[Update Storage]
UpdateState --> CalcDebt[Calculate Reward Debt]
CalcDebt --> EmitEvent[Emit Registration Event]
EmitEvent --> Success([Registration Complete])
style Start fill:#e1f5ff
style Success fill:#e1ffe1
style Revert1 fill:#ffe1e1
style Revert2 fill:#ffe1e1
style Revert3 fill:#ffe1e1
style Revert4 fill:#ffe1e1
style Revert5 fill:#ffe1e1
claimRewards()
Allows registered users to claim their accumulated ETH rewards.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:568-599 |
| Access | External - Registered users |
| FLAG | OBSERVATION |
|---|---|
| ☑ | Uses rewards-per-share accounting |
| ☑ | Multiple safety checks ensure solvency |
| △ | ETH transfer uses raw call |
| △ | State updated after external call (reentrancy risk) |
| △ | Caller must be able to receive ETH |
| CONDITION | REQUIREMENT |
|---|---|
| Contract state | Not paused |
| Caller status | Not blacklisted |
| Caller rewards | Not individually paused |
| Caller status | Is registered |
| Pending rewards | > 0 |
| Contract balance | Sufficient ETH |
| Reward pool | Sufficient balance |
| STEP | ACTION |
|---|---|
| 1 | Validate all preconditions |
| 2 | Calculate pending rewards |
| 3 | Decrease reward pool by pending amount |
| 4 | Increase totalClaimed[caller] |
| 5 | Increase totalDistributed |
| 6 | Update reward debt |
| 7 | Transfer ETH to caller |
| 8 | Emit claim event |
| VARIABLE | CHANGE |
|---|---|
rewardPool |
Decreased by claimed amount |
totalClaimed[caller] |
Increased by claimed amount |
totalDistributed |
Increased by claimed amount |
rewardDebt[caller] |
Updated to current rewardsPerShare * balance / 10^18 |
| CONDITION | REVERT MESSAGE |
|---|---|
| Contract paused | "Contract is paused" |
| Caller blacklisted | "Address blacklisted" |
| Rewards paused | "Rewards paused for this address" |
| Not registered | "Not registered" |
| No rewards | "No rewards to claim" |
| Low ETH balance | "Insufficient contract balance" |
| Low reward pool | "Insufficient reward pool" |
| Transfer failed | "ETH transfer failed" |
// Reconstructed from bytecode
function claimRewards() external {
require(paused == 0, "Contract is paused");
require(blacklisted[msg.sender] == 0, "Address blacklisted");
require(userRewardsPaused[msg.sender] == 0, "Rewards paused for this address");
require(isRegistered[msg.sender] == 1, "Not registered");
uint256 pending = (userBalance[msg.sender] * rewardsPerShare / 1e18)
- rewardDebt[msg.sender];
require(pending > 0, "No rewards to claim");
require(address(this).balance >= pending, "Insufficient contract balance");
require(rewardPool >= pending, "Insufficient reward pool");
rewardPool -= pending;
totalClaimed[msg.sender] += pending;
totalDistributed += pending;
rewardDebt[msg.sender] = (userBalance[msg.sender] * rewardsPerShare) / 1e18;
(bool success, ) = msg.sender.call{value: pending}("");
require(success, "ETH transfer failed");
emit RewardsClaimed(msg.sender, pending);
}
sequenceDiagram
participant User
participant Contract
User->>Contract: claimRewards()
Note over Contract: Validate preconditions
alt Any check fails
Contract-->>User: Revert with error
else All checks pass
Contract->>Contract: Calculate pending rewards
Note over Contract: balance * rewardsPerShare / 1e18<br/>minus rewardDebt
Contract->>Contract: Update state
Contract->>User: Transfer ETH
alt Transfer succeeds
Contract->>Contract: Emit claim event
Contract-->>User: Return amount claimed
else Transfer fails
Contract-->>User: Revert: ETH transfer failed
end
end
deregister()
Allows users to unregister from the rewards system, claiming any pending rewards first.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:640-678 |
| Access | External - Registered users |
| FLAG | OBSERVATION |
|---|---|
| ☑ | Attempts to claim rewards automatically |
| ☑ | Does not revert if reward claim fails (allows exit) |
| ☑ | Uses array compaction to avoid gaps |
| ☑ | Fully clears user state |
| △ | No cooldown or penalty for deregistering |
| CONDITION | REQUIREMENT |
|---|---|
| Contract state | Not paused |
| Caller status | Is registered |
| STEP | ACTION |
|---|---|
| 1 | Check preconditions |
| 2 | If pending rewards exist and sufficient funds, claim them |
| 3 | Decrease total tracked balance by user's balance |
| 4 | If user is not last in array, compact array |
| 5 | Remove last array element |
| 6 | Clear all user state |
| 7 | Decrement participantCount |
| 8 | Emit deregistration event |
| VARIABLE | CHANGE |
|---|---|
totalTrackedBalance |
Decreased by user's balance |
participants |
User removed, array compacted |
participantCount |
Decremented |
isRegistered[caller] |
Cleared |
userBalance[caller] |
Cleared |
lastUpdate[caller] |
Cleared |
rewardDebt[caller] |
Cleared |
participantIndex[caller] |
Cleared |
| CONDITION | REVERT MESSAGE |
|---|---|
| Contract paused | "Contract is paused" |
| Not registered | "Not registered" |
| ETH transfer fails | "ETH transfer failed" (during reward claim) |
// Reconstructed from bytecode
function deregister() external {
require(paused == 0, "Contract is paused");
require(isRegistered[msg.sender] == 1, "Not registered");
// Attempt to claim pending rewards (does not revert on failure)
uint256 pending = (userBalance[msg.sender] * rewardsPerShare / 1e18)
- rewardDebt[msg.sender];
if (pending > 0 && rewardPool >= pending && address(this).balance >= pending) {
rewardPool -= pending;
totalClaimed[msg.sender] += pending;
totalDistributed += pending;
(bool success, ) = msg.sender.call{value: pending}("");
require(success, "ETH transfer failed");
}
totalTrackedBalance -= userBalance[msg.sender];
// Array compaction: move last element to removed user's position
uint256 idx = participantIndex[msg.sender];
uint256 lastIdx = participants.length - 1;
if (idx != lastIdx) {
address lastParticipant = participants[lastIdx];
participants[idx] = lastParticipant;
participantIndex[lastParticipant] = idx;
}
participants.pop();
// Clear all user state
isRegistered[msg.sender] = 0;
userBalance[msg.sender] = 0;
lastUpdate[msg.sender] = 0;
rewardDebt[msg.sender] = 0;
participantIndex[msg.sender] = 0;
participantCount -= 1;
emit Deregistered(msg.sender);
}
flowchart TD
Start([deregister called])
Start --> Check1{Paused?}
Check1 -->|Yes| Revert1[Revert: Contract is paused]
Check1 -->|No| Check2{Registered?}
Check2 -->|No| Revert2[Revert: Not registered]
Check2 -->|Yes| Check3{Has Pending Rewards?}
Check3 -->|Yes| Check4{Sufficient Funds?}
Check4 -->|Yes| ClaimRewards[Claim Pending Rewards]
Check4 -->|No| SkipClaim[Skip Reward Claim]
Check3 -->|No| SkipClaim
ClaimRewards --> UpdateTotal[Decrease Total Balance]
SkipClaim --> UpdateTotal
UpdateTotal --> Check5{Last in Array?}
Check5 -->|Yes| RemoveLast[Remove from Array End]
Check5 -->|No| Compact[Move Last Participant<br/>to User's Position]
Compact --> RemoveLast
RemoveLast --> ClearState[Clear User State]
ClearState --> DecrementCount[Decrement Participant Count]
DecrementCount --> EmitEvent[Emit Deregistration Event]
EmitEvent --> Success([Deregistration Complete])
style Start fill:#e1f5ff
style Success fill:#e1ffe1
style Revert1 fill:#ffe1e1
style Revert2 fill:#ffe1e1
updateBalanceSnapshot()
Allows users to manually update their token balance snapshot, claiming any pending rewards first.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:601-638 |
| Access | External - Registered users |
| FLAG | OBSERVATION |
|---|---|
| ☑ | Rate-limited by snapshot interval |
| ☑ | Automatically claims pending rewards from old balance |
| △ | Essential for users whose token balance changes |
| △ | Relies on external token contract being accurate |
| CONDITION | REQUIREMENT |
|---|---|
| Contract state | Not paused |
| Caller status | Is registered |
| Time elapsed | Snapshot interval elapsed since last update |
| STEP | ACTION |
|---|---|
| 1 | Check preconditions |
| 2 | Query current token balance from external contract |
| 3 | If balance changed from stored balance |
| 4 | - If pending rewards exist, claim them |
| 5 | - Update stored balance to new balance |
| 6 | - Update timestamp |
| 7 | - Adjust total tracked balance |
| 8 | - Recalculate reward debt |
| CONDITION | REVERT MESSAGE |
|---|---|
| Contract paused | "Contract is paused" |
| Not registered | "Not registered" |
| Too soon | "Snapshot interval not elapsed" |
| External call fails | Depends on token contract |
// Reconstructed from bytecode
function updateBalanceSnapshot() external {
require(paused == 0, "Contract is paused");
require(isRegistered[msg.sender] == 1, "Not registered");
require(
block.timestamp >= lastUpdate[msg.sender] + snapshotInterval,
"Snapshot interval not elapsed"
);
uint256 newBalance = IERC20(tokenAddress).balanceOf(msg.sender);
uint256 oldBalance = userBalance[msg.sender];
if (newBalance != oldBalance) {
// Claim pending rewards based on old balance
uint256 pending = (oldBalance * rewardsPerShare / 1e18)
- rewardDebt[msg.sender];
if (pending > 0 && rewardPool >= pending && address(this).balance >= pending) {
rewardPool -= pending;
totalClaimed[msg.sender] += pending;
totalDistributed += pending;
(bool success, ) = msg.sender.call{value: pending}("");
require(success, "ETH transfer failed");
}
// Update balance and recalculate
userBalance[msg.sender] = newBalance;
lastUpdate[msg.sender] = block.timestamp;
totalTrackedBalance = totalTrackedBalance - oldBalance + newBalance;
rewardDebt[msg.sender] = (newBalance * rewardsPerShare) / 1e18;
}
}
requestPrioritySnapshot() (Added v1.0.0)
Allows any user to pay ETH to trigger an immediate balance snapshot, bypassing the normal snapshot interval cooldown.
| ATTRIBUTE | VALUE |
|---|---|
| Selector | 0x8bd94199 |
| Parameters | None |
| Access | External - Payable - Anyone |
| FLAG | OBSERVATION |
|---|---|
| △ | ETH payment flows to fee recipient EOA, not the reward pool |
| △ | Fee recipient is a separate address from admin/operator |
| △ | Bypasses the normal snapshot interval rate limit |
| ☑ | Provides users a way to update their balance without waiting |
| CONDITION | REQUIREMENT |
|---|---|
| msg.value | Must equal priority snapshot fee (currently 0.005 ETH) |
| Fee configured | Priority snapshot fee must be > 0 |
| Fee recipient | Must be configured (non-zero address) |
| STEP | ACTION |
|---|---|
| 1 | Validate msg.value matches priority snapshot fee |
| 2 | Validate fee is configured |
| 3 | Forward ETH to fee recipient address |
| 4 | Trigger balance snapshot for caller (or global) |
| CONDITION | REVERT MESSAGE |
|---|---|
| Wrong amount | "Insufficient priority snapshot f..." |
| Fee not configured | "Priority snapshot fee not config..." |
| Fee exceeds limit | "Fee exceeds maximum (1000 ETH)" |
// Reconstructed from bytecode
function requestPrioritySnapshot() external payable {
require(prioritySnapshotFee > 0, "Priority snapshot fee not configured");
require(prioritySnapshotFee <= 1000 ether, "Fee exceeds maximum (1000 ETH)");
require(msg.value == prioritySnapshotFee, "Insufficient priority snapshot fee");
require(feeRecipient != address(0), "Fee recipient not configured");
// Forward fee to recipient
(bool success, ) = feeRecipient.call{value: msg.value}("");
require(success, "Fee transfer failed");
// Trigger balance snapshot for caller (bypasses interval)
if (isRegistered[msg.sender] == 1) {
uint256 newBalance = IERC20(tokenAddress).balanceOf(msg.sender);
uint256 oldBalance = userBalance[msg.sender];
if (newBalance != oldBalance) {
userBalance[msg.sender] = newBalance;
lastUpdate[msg.sender] = block.timestamp;
totalTrackedBalance = totalTrackedBalance - oldBalance + newBalance;
rewardDebt[msg.sender] = (newBalance * rewardsPerShare) / 1e18;
}
}
emit PrioritySnapshotRequested(msg.sender, msg.value);
}
canRegister(address sender)
Checks if an address is eligible to register and returns status message.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:375-395 |
| Access | Public (view) |
| CONDITION | REQUIREMENT |
|---|---|
| Contract state | Not paused |
| Registration | Is open |
| Address status | Not blacklisted |
| Address status | Not already registered |
| Token balance | >= minimum requirement |
| TYPE | DESCRIPTION |
|---|---|
bool |
Can register |
| Multiple strings | Status/error description |
// Reconstructed from bytecode
function canRegister(address sender) public view returns (bool, string memory) {
if (paused != 0) return (false, "Contract is paused");
if (registrationOpen != 1) return (false, "Registration closed");
if (blacklisted[sender] != 0) return (false, "Address blacklisted");
if (isRegistered[sender] != 0) return (false, "Already registered");
uint256 balance = IERC20(tokenAddress).balanceOf(sender);
if (balance < minBalanceRequired) return (false, "Insufficient XCL balance");
return (true, "Eligible");
}
Operator Functions
depositRewards()
Deposits ETH into the reward pool for distribution to participants.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:210-221 |
| Access | Admin or Operator |
| Payable | Yes |
| FLAG | OBSERVATION |
|---|---|
| ☑ | ETH sent is immediately available for distribution |
| ☑ | Rewards distributed proportionally to token holdings |
| △ | If no participants, ETH accumulates without updating rewardsPerShare |
| △ | No cap on deposit amount |
| △ | Critical function for system operation |
| STEP | ACTION |
|---|---|
| 1 | Validate caller has permission |
| 2 | Validate ETH amount > 0 |
| 3 | Add ETH to reward pool |
| 4 | If participants exist, increase rewardsPerShare |
| 5 | Emit deposit event |
| VARIABLE | CHANGE |
|---|---|
rewardPool |
Increased by msg.value |
rewardsPerShare |
Increased by (10^18 * depositAmount) / totalTrackedBalance |
// Reconstructed from bytecode
function depositRewards() external payable {
require(
msg.sender == adminAddress || operators[msg.sender] == 1,
"Not operator"
);
require(msg.value > 0, "Amount must be positive");
rewardPool += msg.value;
if (totalTrackedBalance > 0) {
rewardsPerShare += (msg.value * 1e18) / totalTrackedBalance;
}
emit RewardsDeposited(msg.sender, msg.value);
}
sequenceDiagram
participant Operator
participant Contract
Operator->>Contract: depositRewards() + ETH
Note over Contract: Check operator permission
alt Not operator
Contract-->>Operator: Revert: Not operator
else Is operator
alt Amount <= 0
Contract-->>Operator: Revert: Amount must be positive
else Amount > 0
Contract->>Contract: Increase reward pool
alt Participants exist
Contract->>Contract: Calculate rewards per share
Contract->>Contract: Increase rewardsPerShare
else No participants
Note over Contract: ETH stored but not<br/>distributed yet
end
Contract->>Contract: Emit deposit event
Contract-->>Operator: Success
end
end
pause() / unpause()
Controls global pause state of the contract.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:137-153 |
| Access | Admin or Operator |
| FUNCTION | ACTION |
|---|---|
pause() |
Sets paused flag to 1 |
unpause() |
Sets paused flag to 0 |
// Reconstructed from bytecode
function pause() external {
require(
msg.sender == adminAddress || operators[msg.sender] == 1,
"Not operator"
);
paused = 1;
emit Paused(msg.sender);
}
function unpause() external {
require(
msg.sender == adminAddress || operators[msg.sender] == 1,
"Not operator"
);
paused = 0;
emit Unpaused(msg.sender);
}
openRegistration() / closeRegistration()
Controls whether new users can register.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:155-181 |
| Access | Admin or Operator |
// Reconstructed from bytecode
function openRegistration() external {
require(
msg.sender == adminAddress || operators[msg.sender] == 1,
"Not operator"
);
registrationOpen = 1;
emit RegistrationOpened(msg.sender);
}
function closeRegistration() external {
require(
msg.sender == adminAddress || operators[msg.sender] == 1,
"Not operator"
);
registrationOpen = 0;
emit RegistrationClosed(msg.sender);
}
blacklistAddress(address)
Adds an address to the blacklist, preventing registration and claims.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:273-284 |
| Access | Admin or Operator |
| FLAG | OBSERVATION |
|---|---|
| △ | Does not automatically deregister if user already registered |
| △ | Prevents future registrations and claims |
| △ | Separate from pause functionality (address-specific) |
pauseUserRewards(address)
Pauses rewards for a specific registered user.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:286-297 |
| Access | Admin or Operator |
forceDeregister(address, bool)
Forcibly deregisters a user, optionally claiming their rewards first.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:680-727 |
| Access | Admin or Operator |
| FLAG | OBSERVATION |
|---|---|
| △ | Admin/operator can remove users without their consent |
| ☑ | Can choose whether to claim user's rewards first |
| △ | Clears pause status when removing |
// Reconstructed from bytecode
function forceDeregister(address target, bool claimFirst) external {
require(
msg.sender == adminAddress || operators[msg.sender] == 1,
"Not operator"
);
require(isRegistered[target] == 1, "Not registered");
if (claimFirst) {
uint256 pending = (userBalance[target] * rewardsPerShare / 1e18)
- rewardDebt[target];
if (pending > 0 && rewardPool >= pending && address(this).balance >= pending) {
rewardPool -= pending;
totalClaimed[target] += pending;
totalDistributed += pending;
(bool success, ) = target.call{value: pending}("");
require(success, "ETH transfer failed");
}
}
totalTrackedBalance -= userBalance[target];
// Array compaction
uint256 idx = participantIndex[target];
uint256 lastIdx = participants.length - 1;
if (idx != lastIdx) {
address lastParticipant = participants[lastIdx];
participants[idx] = lastParticipant;
participantIndex[lastParticipant] = idx;
}
participants.pop();
// Clear all user state
isRegistered[target] = 0;
userBalance[target] = 0;
lastUpdate[target] = 0;
rewardDebt[target] = 0;
participantIndex[target] = 0;
userRewardsPaused[target] = 0;
participantCount -= 1;
emit ForceDeregistered(target, claimFirst);
}
globalBalanceSnapshot()
Updates token balance snapshots for all registered participants.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:491-529 |
| Access | Anyone |
| FLAG | OBSERVATION |
|---|---|
| ☑ | Can be called by anyone (public utility function) |
| ☑ | Rate-limited by interval to prevent spam |
| △ | Gas cost scales linearly with participant count |
| △ | Critical for maintaining accurate distribution |
// Reconstructed from bytecode
function globalBalanceSnapshot() external {
require(
block.timestamp >= lastSnapshotTime + snapshotInterval,
"Interval not elapsed"
);
uint256 newTotal = 0;
for (uint256 i = 0; i < participants.length; i++) {
address participant = participants[i];
uint256 oldBalance = userBalance[participant];
uint256 newBalance = IERC20(tokenAddress).balanceOf(participant);
if (newBalance != oldBalance) {
userBalance[participant] = newBalance;
emit BalanceUpdated(participant, oldBalance, newBalance);
}
newTotal += newBalance;
}
totalTrackedBalance = newTotal;
lastSnapshotTime = block.timestamp;
emit GlobalSnapshotCompleted(participants.length, newTotal);
}
flowchart TD
Start([globalSnapshot called])
Start --> Check1{Interval Elapsed?}
Check1 -->|No| Revert1[Revert: Interval not elapsed]
Check1 -->|Yes| Init[Initialize: idx=0, sum=0]
Init --> Loop{idx < participants.length?}
Loop -->|No| UpdateTotal[Update Total Tracked Balance]
Loop -->|Yes| QueryBalance[Query participant balance]
QueryBalance --> Check2{Balance Changed?}
Check2 -->|No| AddToSum[Add old balance to sum]
Check2 -->|Yes| UpdateUser[Update user balance]
UpdateUser --> EmitUserEvent[Emit balance change event]
EmitUserEvent --> AddNewToSum[Add new balance to sum]
AddToSum --> Increment[idx++]
AddNewToSum --> Increment
Increment --> Loop
UpdateTotal --> UpdateTime[Update last snapshot time]
UpdateTime --> EmitGlobal[Emit global snapshot event]
EmitGlobal --> Success([Snapshot Complete])
style Start fill:#e1f5ff
style Success fill:#e1ffe1
style Revert1 fill:#ffe1e1
Admin Functions
addOperator(address newOperator)
Grants operator role to a new address.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:260-271 |
| Access | Admin only |
| CONDITION | REQUIREMENT |
|---|---|
| Caller | Is admin |
| Address | Not zero |
| Status | Not already an operator |
// Reconstructed from bytecode
function addOperator(address newOperator) external {
require(msg.sender == adminAddress, "Not admin");
require(newOperator != address(0), "Invalid address");
require(operators[newOperator] == 0, "Already an operator");
operators[newOperator] = 1;
operatorCount += 1;
emit OperatorAdded(newOperator);
}
queueAdminTransfer(address) / acceptAdminTransfer()
Two-step admin transfer with 24-hour timelock.
| ATTRIBUTE | VALUE |
|---|---|
| Access | Admin (queue) / Pending Admin (accept) |
| Timelock | 24 hours |
| FLAG | OBSERVATION |
|---|---|
| ☑ | Two-step process prevents accidental transfers |
| ☑ | Pending admin must actively accept |
| ☑ | 24-hour delay provides warning |
// Reconstructed from bytecode
function queueAdminTransfer(address newAdmin) external {
require(msg.sender == adminAddress, "Not admin");
require(newAdmin != address(0), "Invalid address");
bytes32 key = keccak256(abi.encodePacked("adminTransfer", newAdmin));
timelockQueue[key] = block.timestamp + 24 hours;
pendingAdminAddress = newAdmin;
emit AdminTransferQueued(newAdmin, block.timestamp + 24 hours);
}
function acceptAdminTransfer() external {
require(msg.sender == pendingAdminAddress, "Not pending admin");
bytes32 key = keccak256(abi.encodePacked("adminTransfer", msg.sender));
require(timelockQueue[key] != 0, "No pending transfer");
require(block.timestamp >= timelockQueue[key], "Timelock not elapsed");
adminAddress = msg.sender;
pendingAdminAddress = address(0);
timelockQueue[key] = 0;
emit AdminTransferred(msg.sender);
}
queueOperatorRemoval(address) / executeOperatorRemoval(address, uint256)
Queues and executes operator removal with 24-hour timelock.
| ATTRIBUTE | VALUE |
|---|---|
| Access | Admin only |
| Timelock | 24 hours |
// Reconstructed from bytecode
function queueOperatorRemoval(address operator) external {
require(msg.sender == adminAddress, "Not admin");
require(operators[operator] == 1, "Not an operator");
bytes32 key = keccak256(abi.encodePacked("removeOperator", operator));
timelockQueue[key] = block.timestamp + 24 hours;
emit OperatorRemovalQueued(operator, block.timestamp + 24 hours);
}
function executeOperatorRemoval(address operator, uint256 queuedAt) external {
require(msg.sender == adminAddress, "Not admin");
bytes32 key = keccak256(abi.encodePacked("removeOperator", operator));
require(timelockQueue[key] != 0, "Not queued");
require(block.timestamp >= timelockQueue[key], "Timelock not elapsed");
operators[operator] = 0;
operatorCount -= 1;
timelockQueue[key] = 0;
emit OperatorRemoved(operator);
}
queueParameterChange(uint256) / executeParameterChange(uint256, uint256)
Queues and executes minimum balance requirement change with timelock.
| ATTRIBUTE | VALUE |
|---|---|
| Access | Admin only |
| Timelock | 24 hours |
// Reconstructed from bytecode
function queueParameterChange(uint256 newMinBalance) external {
require(msg.sender == adminAddress, "Not admin");
bytes32 key = keccak256(abi.encodePacked("paramChange", newMinBalance));
timelockQueue[key] = block.timestamp + 24 hours;
emit ParameterChangeQueued(newMinBalance, block.timestamp + 24 hours);
}
function executeParameterChange(uint256 newMinBalance, uint256 queuedAt) external {
require(msg.sender == adminAddress, "Not admin");
bytes32 key = keccak256(abi.encodePacked("paramChange", newMinBalance));
require(timelockQueue[key] != 0, "Not queued");
require(block.timestamp >= timelockQueue[key], "Timelock not elapsed");
minBalanceRequired = newMinBalance;
timelockQueue[key] = 0;
emit ParameterChanged(newMinBalance);
}
queueEmergencyWithdrawal(...) / executeEmergencyWithdrawal(...)
Queues and executes emergency withdrawal of ETH or ERC20 tokens.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:350-360 (queue), 531-566 (execute) |
| Access | Admin only |
| Timelock | 24 hours |
| FLAG | OBSERVATION |
|---|---|
| △ | Critical emergency function to recover funds |
| △ | Can withdraw any ERC20 token (including the tracked token) |
| △ | Can withdraw ETH from reward pool |
| ☑ | 24-hour timelock provides warning period |
| △ | Could be used to drain the contract |
// Reconstructed from bytecode
function queueEmergencyWithdrawal(
address token,
address recipient,
uint256 amount
) external {
require(msg.sender == adminAddress, "Not admin");
bytes32 key = keccak256(
abi.encodePacked("emergencyWithdraw", token, recipient, amount)
);
timelockQueue[key] = block.timestamp + 24 hours;
emit EmergencyWithdrawalQueued(token, recipient, amount, block.timestamp + 24 hours);
}
function executeEmergencyWithdrawal(
address token,
address recipient,
uint256 amount
) external {
require(msg.sender == adminAddress, "Not admin");
bytes32 key = keccak256(
abi.encodePacked("emergencyWithdraw", token, recipient, amount)
);
require(timelockQueue[key] != 0, "Not queued");
require(block.timestamp >= timelockQueue[key], "Timelock not elapsed");
timelockQueue[key] = 0;
if (token == address(0)) {
// Withdraw ETH
rewardPool -= amount;
(bool success, ) = recipient.call{value: amount}("");
require(success, "ETH transfer failed");
} else {
// Withdraw ERC20
IERC20(token).transfer(recipient, amount);
}
emit EmergencyWithdrawalExecuted(token, recipient, amount);
}
manualBalanceUpdate(address, uint256)
Allows admin to manually set a user's tracked token balance.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:362-373 |
| Access | Admin only |
| FLAG | OBSERVATION |
|---|---|
| △ | Powerful admin function that can manipulate reward distribution |
| △ | Bypasses external token contract query |
| △ | Could be used to fix discrepancies or manipulate system |
| △ | No validation that new balance matches actual token balance |
// Reconstructed from bytecode
function manualBalanceUpdate(address target, uint256 newBalance) external {
require(msg.sender == adminAddress, "Not admin");
require(isRegistered[target] == 1, "Not registered");
uint256 oldBalance = userBalance[target];
userBalance[target] = newBalance;
totalTrackedBalance = totalTrackedBalance - oldBalance + newBalance;
rewardDebt[target] = (newBalance * rewardsPerShare) / 1e18;
emit ManualBalanceUpdate(target, oldBalance, newBalance);
}
cancelQueuedAction(bytes32)
Cancels a queued timelock action.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:192-200 |
| Access | Admin only |
removeFromBlacklist(address)
Removes an address from the blacklist.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:233-241 |
| Access | Admin only |
unpauseUserRewards(address)
Unpauses rewards for a specific user.
| ATTRIBUTE | VALUE |
|---|---|
| Location | Implementation:223-231 |
| Access | Admin only |
setFeeRecipient(address) (Added v1.0.0)
Sets the address that receives priority snapshot fee payments.
| ATTRIBUTE | VALUE |
|---|---|
| Selector | 0x3c85102a |
| Access | Admin only |
| FLAG | OBSERVATION |
|---|---|
| △ | Can redirect all fee revenue to any address |
| △ | No timelock protection observed on this function |
| △ | Currently set to EOA 0xe1f3fbb1...37264a84 |
setPrioritySnapshotFee(uint256) (Added v1.0.0)
Sets the ETH amount required for priority snapshot requests.
| ATTRIBUTE | VALUE |
|---|---|
| Selector | 0xeb209249 |
| Access | Admin only |
| FLAG | OBSERVATION |
|---|---|
| ☑ | Maximum fee appears to be capped at 1000 ETH based on error string |
| △ | Currently set to 0.005 ETH |
View Functions
Contract State Functions
| FUNCTION | RETURNS | PURPOSE |
|---|---|---|
initialized() |
bool |
Whether contract has been initialized |
admin() |
address |
Current admin address |
pendingAdmin() |
address |
Pending admin during transfer |
paused() |
bool |
Whether contract is globally paused |
rewardPool() |
uint256 |
Total ETH available for rewards |
totalDistributed() |
uint256 |
Cumulative ETH distributed |
participantCount() |
uint256 |
Number of registered participants |
User State Functions
| FUNCTION | RETURNS | PURPOSE |
|---|---|---|
isRegistered(address) |
bool |
Whether address is registered |
blacklisted(address) |
bool |
Whether address is blacklisted |
operators(address) |
bool |
Whether address has operator role |
totalClaimed(address) |
uint256 |
Total rewards claimed by address |
participants(uint256) |
address |
Participant at specific index |
Statistics Functions
| FUNCTION | RETURNS | PURPOSE |
|---|---|---|
getStats() |
Multiple | Comprehensive contract statistics |
pendingRewards(address) |
uint256 |
Pending reward amount for address |
userStats(address) |
Multiple | Share percentage, tracked balance, current balance |
getParticipants(uint256, uint256) |
address[], uint256[] |
Paginated list of participants and balances |
Functions Added in v1.0.0 Upgrade
| FUNCTION | RETURNS | PURPOSE |
|---|---|---|
VERSION() |
string |
Implementation version ("1.0.0") |
feeRecipient() (0x407c8855) |
address |
Address receiving priority snapshot fees |
snapshotFee() (0x5d2e4c1c) |
uint256 |
Base snapshot fee (currently 0.001 ETH) |
prioritySnapshotFee() (0xc27762b5) |
uint256 |
Priority snapshot fee (currently 0.005 ETH) |