Skip to main content

Overview

The FenineSystem contract is the heart of Fenines Network’s FPoS (Finality Proof of Stake) consensus. It manages validator registration, staking, delegation, and reward distribution through a unique proximity-based system.
Contract Address: 0x0000000000000000000000000000000000001000 (System Contract)

Architecture

NOT_EXIST → CREATED → STAKED → VALIDATED → UNSTAKED → CREATED
                ↓         ↓         ↓
           Register   Stake    Activated
  • NOT_EXIST: Address has never registered
  • CREATED: Registered with metadata, not yet staked
  • STAKED: Staked ≥10,000 FEN, waiting for epoch activation
  • VALIDATED: Active validator, earning rewards
  • UNSTAKED: Initiated unstake, in lock period
Fenines implements an 8-level proximity (referral) system where:
  • When a delegator claims rewards, a portion goes to their uplines (previous stakers to the same validator)
  • Default distribution: 30% of rewards distributed across 8 levels
  • Per-level allocation: [7%, 5%, 4%, 3.5%, 3%, 2.5%, 2.5%, 2.5%]
  • Remainder split 50/50 between claimer and validator
  • Anti-Sybil: Minimum stake requirements increase per level
  • Epoch Length: 200 blocks (~10 minutes at 3s block time)
  • System Transactions: Executed automatically at block % 200 == 0
    1. updateValidatorCandidates() - Activate STAKED → VALIDATED
    2. distributeBlockReward() - Distribute accumulated rewards
  • Validator Set Updates: Only at epoch boundaries

Constants

ConstantValueDescription
MAX_VALIDATORS101Maximum active validators
MIN_VA_STAKE10,000 FENMinimum validator self-stake
MIN_DC_STAKE1,000 FENMinimum delegator stake
VA_LOCK_PERIOD50,000 blocks~7 days unstake lock
DC_LOCK_PERIOD25,000 blocks~3.5 days unstake lock
DEFAULT_COMMISSION500 (5%)Default commission rate
MAX_COMMISSION1000 (10%)Maximum commission rate
BLOCK_EPOCH200Blocks per epoch
PROXIMITY_DEPTH8Proximity levels

Validator Functions

registerValidator

Register as a validator (expression of intent).
function registerValidator(
    address payable rewardAddress,
    string calldata moniker,
    string calldata phoneNumber,
    string calldata details,
    uint256 commissionRate
) external returns (bool)
rewardAddress
address payable
required
Address to receive rewards (can differ from msg.sender)
moniker
string
required
Validator name/identifier (1-128 characters)
phoneNumber
string
Contact phone number (optional)
details
string
Additional metadata (optional)
commissionRate
uint256
required
Commission in basis points (500-1000 = 5-10%)
const tx = await systemContract.methods.registerValidator(
  rewardAddress,
  'MyValidator',
  '+1234567890',
  'Best validator in town',
  500 // 5% commission
).send({ from: validatorAddress });
You must register before staking. Status transitions: NOT_EXIST → CREATED

stakeValidator

Stake as validator (minimum 10,000 FEN).
function stakeValidator() external payable returns (bool)
const stakeAmount = web3.utils.toWei('10000', 'ether');

const tx = await systemContract.methods.stakeValidator().send({
  from: validatorAddress,
  value: stakeAmount
});
Requirements:
  • Status must be CREATED or UNSTAKED
  • msg.value >= 10,000 FEN
  • Contract not paused
Effects:
  • Adds to selfStake and totalStake
  • Status → STAKED
  • Added to validatorCandidates array
  • Will be activated at next epoch

addValidatorStake

Add more stake to existing validator (top-up).
function addValidatorStake() external payable returns (bool)
const additionalStake = web3.utils.toWei('5000', 'ether');

await systemContract.methods.addValidatorStake().send({
  from: validatorAddress,
  value: additionalStake
});
Requirements:
  • Status must be STAKED or VALIDATED
  • msg.value > 0
Effects:
  • Increases selfStake and totalStake
  • Does NOT reset stake age or status

unstakeValidator

Initiate validator unstake process.
function unstakeValidator() external returns (bool)
await systemContract.methods.unstakeValidator().send({
  from: validatorAddress
});
Requirements:
  • Status must be STAKED or VALIDATED
  • Not already unstaking
Effects:
  • Status → UNSTAKED
  • Sets unstakeBlock = current block
  • Removed from activeValidatorSet
  • Removed from validatorCandidates
  • Lock period: 50,000 blocks (~7 days)

withdrawValidatorStake

Withdraw staked FEN after lock period.
function withdrawValidatorStake() external returns (bool)
await systemContract.methods.withdrawValidatorStake().send({
  from: validatorAddress
});
Requirements:
  • unstakeBlock > 0 (unstake initiated)
  • block.number >= unstakeBlock + 50000 (lock period passed)
  • selfStake > 0
Effects:
  • Transfers selfStake to validator
  • Resets selfStake = 0
  • Status → CREATED
  • Can re-stake later

claimValidatorReward

Claim accumulated validator rewards.
function claimValidatorReward() external returns (bool)
// Check rewards first
const info = await systemContract.methods.getValidatorInfo(validatorAddress).call();
console.log('Claimable:', web3.utils.fromWei(info.claimableReward, 'ether'), 'FEN');

// Claim
await systemContract.methods.claimValidatorReward().send({
  from: validatorAddress
});
Requirements:
  • Status must be VALIDATED
  • claimableReward > 0
Effects:
  • Transfers rewards to rewardAddress
  • Applies tax via TaxManager
  • Resets claimableReward = 0
Tax Flow:
Gross Reward
  ├─> Tax (burn + dev)
  └─> Net → rewardAddress

updateValidatorMetadata

Update validator information.
function updateValidatorMetadata(
    string calldata moniker,
    string calldata phoneNumber,
    string calldata details
) external returns (bool)
await systemContract.methods.updateValidatorMetadata(
  'NewValidatorName',
  '+9876543210',
  'Updated description'
).send({ from: validatorAddress });

updateCommissionRate

Update commission rate (only before activation).
function updateCommissionRate(uint256 newRate) external returns (bool)
await systemContract.methods.updateCommissionRate(750).send({
  from: validatorAddress
}); // Set to 7.5%
Can only update when status is STAKED (not yet activated). Commission is locked after activation.

Delegator Functions

stakeToValidator

Stake FEN to a validator as a delegator.
function stakeToValidator(address vaAddr) external payable returns (bool)
vaAddr
address
required
Validator address to stake to
const validatorAddress = '0x1234...';
const stakeAmount = web3.utils.toWei('1000', 'ether');

await systemContract.methods.stakeToValidator(validatorAddress).send({
  from: delegatorAddress,
  value: stakeAmount
});
Requirements:
  • Validator status must be VALIDATED
  • msg.value >= 1,000 FEN
  • Not currently unstaking
  • Whitelisted via NFTPassport contract
Effects:
  • First-time: Added to validator’s stakers array (proximity position)
  • Increases stakeAmount and validator’s totalStake
  • Starts earning rewards
Proximity Position: Your position in the stakers array determines proximity rewards. Earlier stakers are deeper in the chain and receive rewards from later stakers.

addDelegatorStake

Add more stake to existing delegation.
function addDelegatorStake(address vaAddr) external payable returns (bool)
await systemContract.methods.addDelegatorStake(validatorAddress).send({
  from: delegatorAddress,
  value: web3.utils.toWei('500', 'ether')
});

unstakeDelegator

Initiate delegator unstake process.
function unstakeDelegator(address vaAddr) external returns (bool)
await systemContract.methods.unstakeDelegator(validatorAddress).send({
  from: delegatorAddress
});
Requirements:
  • stakeAmount > 0
  • Not already unstaking
Effects:
  • Removed from stakers array (proximity chain)
  • Status → UNSTAKING
  • Sets unstakeBlock = current block
  • Lock period: 25,000 blocks (~3.5 days)
  • Pending rewards preserved

withdrawDelegatorStake

Withdraw delegated stake after lock period.
function withdrawDelegatorStake(address vaAddr) external returns (bool)
// Check if lock period passed
const info = await systemContract.methods.getDelegatorInfo(
  delegatorAddress,
  validatorAddress
).call();

const currentBlock = await web3.eth.getBlockNumber();
const lockPeriod = 25000;

if (currentBlock >= info.unstakeBlock + lockPeriod) {
  await systemContract.methods.withdrawDelegatorStake(validatorAddress).send({
    from: delegatorAddress
  });
}

claimDelegatorReward

Claim rewards with proximity distribution.
function claimDelegatorReward(address vaAddr) external returns (bool)
// Estimate rewards first
const estimate = await systemContract.methods.getEstimatedDelegatorReward(
  delegatorAddress,
  validatorAddress
).call();

console.log('Pending:', web3.utils.fromWei(estimate.pending, 'ether'));
console.log('After proximity:', web3.utils.fromWei(estimate.afterProximity, 'ether'));
console.log('After tax:', web3.utils.fromWei(estimate.afterTax, 'ether'));

// Claim
await systemContract.methods.claimDelegatorReward(validatorAddress).send({
  from: delegatorAddress
});
Reward Flow:
Pending Reward (100%)
  ├─> Proximity Distribution (30% default)
  │     ├─> Level 1 upline (7%)
  │     ├─> Level 2 upline (5%)
  │     └─> ... 8 levels
  ├─> Residual split (if uplines ineligible)
  │     ├─> 50% to claimer (bonus)
  │     └─> 50% to validator
  └─> Net to Claimer (70% + bonuses)
        ├─> Tax (burn + dev)
        └─> Final Amount

View Functions

getActiveValidators

Get list of active validators.
function getActiveValidators() external view returns (address[] memory)
const validators = await systemContract.methods.getActiveValidators().call();
console.log('Active validators:', validators);

getValidatorInfo

Get detailed validator information.
function getValidatorInfo(address vaAddr) external view returns (
    VAStatus status,
    uint256 selfStake,
    uint256 totalStake,
    uint256 commissionRate,
    uint256 claimableReward,
    uint256 stakerCount
)
const info = await systemContract.methods.getValidatorInfo(validatorAddress).call();

console.log({
  status: ['NOT_EXIST', 'CREATED', 'STAKED', 'VALIDATED', 'UNSTAKED'][info.status],
  selfStake: web3.utils.fromWei(info.selfStake, 'ether'),
  totalStake: web3.utils.fromWei(info.totalStake, 'ether'),
  commission: info.commissionRate / 100 + '%',
  claimable: web3.utils.fromWei(info.claimableReward, 'ether'),
  delegators: info.stakerCount
});

getDelegatorInfo

Get delegator stake information.
function getDelegatorInfo(address dcAddr, address vaAddr) external view returns (
    DCStatus status,
    uint256 stakeAmount,
    uint256 pendingRewards,
    uint256 joinedAt,
    uint256 stakerIndex
)

getEstimatedDelegatorReward

Calculate estimated rewards (preview before claiming).
function getEstimatedDelegatorReward(address dcAddr, address vaAddr) 
    external view returns (
        uint256 pending,
        uint256 afterProximity,
        uint256 afterTax
    )

getProximityConfig

Get current proximity distribution configuration.
function getProximityConfig() external view returns (
    uint16 totalBps,
    uint8 depth,
    uint16[8] memory rewardBps
)
const config = await systemContract.methods.getProximityConfig().call();

console.log('Total proximity:', config.totalBps / 100, '%');
console.log('Levels:', config.depth);
console.log('Distribution:', config.rewardBps.map(bps => bps / 100 + '%'));

getCurrentEpoch

Get current epoch number.
function getCurrentEpoch() external view returns (uint256)

getNextEpochReward

Get pending reward pool for next epoch.
function getNextEpochReward() external view returns (uint256)

Admin Functions

Admin functions are restricted to the contract admin (set during genesis). These should only be used for emergency situations or governance-approved changes.

setProximityConfig

Update proximity reward distribution.
function setProximityConfig(
    uint16 newTotalBps,
    uint16[8] calldata newRewardBps
) external onlyAdmin returns (bool)
Requirements:
  • Only admin can call
  • newTotalBps <= 8700 (87% max)
  • Array must sum to newTotalBps

pause / unpause

Emergency pause/unpause contract (inherited from EmergencyControl).
function pause() external onlyAdmin
function unpause() external onlyAdmin

changeAdmin

Transfer admin role (inherited from EmergencyControl, with 7-day timelock).
function changeAdmin(address newAdmin) external onlyAdmin

Events

event ValidatorRegistered(address indexed va, string moniker, uint256 timestamp)
event ValidatorStaked(address indexed va, uint256 amount, uint256 totalStake, uint256 timestamp)
event ValidatorActivated(address indexed va, uint256 indexed epochNumber)
event ValidatorUnstaked(address indexed va, uint256 amount, uint256 unlockBlock)
event ValidatorWithdrawn(address indexed va, uint256 amount)
event ValidatorRewardClaimed(address indexed va, uint256 gross, uint256 net, uint256 tax)
event MetadataUpdated(address indexed va)
event CommissionUpdated(address indexed va, uint256 oldRate, uint256 newRate)
event DelegatorStaked(address indexed dc, address indexed va, uint256 amount, uint256 totalStake, uint256 timestamp)
event DelegatorUnstaked(address indexed dc, address indexed va, uint256 unlockBlock)
event DelegatorWithdrawn(address indexed dc, address indexed va, uint256 amount)
event DelegatorRewardClaimed(address indexed dc, address indexed va, uint256 gross, uint256 net, uint256 tax)
event ProximityRewardsApplied(address indexed claimer, address indexed va, uint256 totalDistributed)
event ProximityRewardAllocated(address indexed recipient, uint256 amount, uint8 level)
event RewardDistributed(uint256 epochReward, uint256 validatorCount, uint256 indexed epochNumber, uint256 timestamp)
event ValidatorSetUpdated(address[] validators, uint256 indexed epochNumber)
event EpochProcessed(uint256 indexed blockNumber, address indexed caller, uint256 reward, bool success)

Integration Examples

// Step 1: Register
await systemContract.methods.registerValidator(
  rewardAddress,
  'MyValidator',
  '',
  'Professional validator',
  500
).send({ from: validatorAddress });

// Step 2: Stake (minimum 10,000 FEN)
await systemContract.methods.stakeValidator().send({
  from: validatorAddress,
  value: web3.utils.toWei('10000', 'ether')
});

// Step 3: Wait for next epoch (automatic activation)
// Status will change: CREATED → STAKED → VALIDATED

// Step 4: Monitor rewards
setInterval(async () => {
  const info = await systemContract.methods.getValidatorInfo(validatorAddress).call();
  console.log('Claimable:', web3.utils.fromWei(info.claimableReward, 'ether'));
}, 60000); // Check every minute

Security Considerations

All state-changing functions that transfer ETH use the nonReentrant modifier to prevent reentrancy attacks.
  • User functions: Anyone can call
  • System functions: Only block.coinbase (consensus layer)
  • Admin functions: Only contract admin with timelock
  • Minimum stake requirements per proximity level prevent sybil attacks
  • Stake age requirement (100 blocks minimum)
  • Unstaking addresses cannot receive proximity rewards
  • Pause functionality (inherited from EmergencyControl)
  • 7-day timelock for admin changes
  • Emergency withdrawal capability with hooks

Next Steps

NFT Passport Contract

Whitelist and referral system

Tax Manager Contract

Reward tax and burn mechanism

Integration Examples

Complete integration examples

Quick Reference

Common queries and selectors