Skip to main content

BCOStaking

UUPS upgradeable staking contract based on the Synthetix StakingRewards model. Users deposit BCO to earn rewards from a pre-funded pool.

PropertyValue
Solidity0.8.28
ProxyUUPS (EIP-1822)
ModelSynthetix StakingRewards
AddressTLXMq6XnwCyS9z3B8tbuNA82JJfjUnDNFe
Storage Gap__gap[48] reserved for upgrades

Roles

RoleAssigned ToPurpose
DEFAULT_ADMIN_ROLEMulti-sig 3/5Manage roles, configure parameters
REWARD_MANAGER_ROLEMulti-sig 3/5Fund reward periods
PAUSER_ROLEMulti-sig 3/5Emergency pause
UPGRADER_ROLEMulti-sig via timelockAuthorize upgrades

How Staking Works

Reward Distribution

1. Reward manager calls notifyRewardAmount(amount)
2. Contract calculates: rewardRate = amount / rewardDuration
3. Rewards accrue per second, proportional to each staker's share
4. Stakers call getReward() to claim
5. After rewardDuration, rewards stop automatically

Rewards come from a pre-funded pool — no new tokens are minted. This preserves the BCO supply invariant.

Example

Reward period: 100,000 BCO over 90 days (default duration)
Alice stakes: 1,000 BCO (25% of pool)
Bob stakes: 3,000 BCO (75% of pool)

Alice earns: ~25,000 BCO over 90 days
Bob earns: ~75,000 BCO over 90 days

Key Functions

Staking

/// @notice Deposits BCO with a lock period.
function deposit(uint256 amount, uint48 lockDuration) external;

/// @notice Withdraws staked BCO. Requires lock to be expired. Claims rewards automatically.
function withdraw(uint256 amount) external;

/// @notice Claims accumulated rewards without withdrawing.
function claimRewards() external;

/// @notice Emergency withdraw — bypasses lock but forfeits all pending rewards.
function emergencyWithdraw() external;

Reward Management

/// @notice Starts or extends a reward period. Only REWARD_MANAGER_ROLE.
/// @dev Tokens must be transferred to the contract before calling.
function notifyRewardAmount(uint256 amount) external;

/// @notice Sets duration for future reward periods. Cannot change during active period.
function setRewardDuration(uint256 duration) external;

/// @notice Updates lock period parameters. Validates min > 0 and min <= max.
function setLockParameters(uint48 newMinLock, uint48 newMaxLock) external;

View Functions

function totalStaked() external view returns (uint256);
function earned(address account) external view returns (uint256);
function pendingReward(address account) external view returns (uint256);
function rewardRate() external view returns (uint256);
function rewardDuration() external view returns (uint256);
function periodFinish() external view returns (uint256);
function rewardPerToken() external view returns (uint256);
function lastTimeRewardApplicable() external view returns (uint256);
function totalUnclaimedRewards() external view returns (uint256);
function excessBCO() external view returns (uint256);
function minLockPeriod() external view returns (uint48);
function maxLockPeriod() external view returns (uint48);

Configuration

function setTimelock(address newTimelock) external;
function setDirectUpgradeSupplyLimit(uint256 newLimit) external;

Lock Defaults

ParameterDefault
Min lock7 days
Max lock365 days

Recovery

/// @notice Recovers force-deposited native currency.
function recoverNative() external;

/// @notice Recovers ERC-20 tokens. For BCO, limited to excessBCO().
function recoverERC20(address token, uint256 amount) external;

Staker Fund Protection

The recoverERC20() function includes a guard when recovering BCO tokens:

recoverable BCO = contract balance - totalStaked - totalUnclaimedRewards

This ensures the admin can never drain staker funds or unclaimed rewards. Only genuinely excess tokens (e.g., accidentally sent) can be recovered.

Events

event Deposited(address indexed user, uint256 amount, uint48 lockUntil);
event Withdrawn(address indexed user, uint256 amount);
event RewardClaimed(address indexed user, uint256 reward);
event EmergencyWithdrawn(address indexed user, uint256 amount);
event RewardAdded(uint256 amount, uint256 rewardRate, uint256 periodFinish);
event RewardDurationUpdated(uint256 newDuration);
event LockParametersUpdated(uint48 minLock, uint48 maxLock);

Key Design Decisions

DecisionRationale
No infinite mintRewards from pool, not emissions — preserves invariant
Finite periodsRewards stop automatically, no runaway distribution
Pull modelStakers claim explicitly, no gas-heavy push distribution
Excess guardAdmin cannot drain staker funds via recovery
Progressive timelockUpgrades require delay when supply > 1M BCO