// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; interface IAccessStakeToken { function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); } /// @notice Non-custodial access stake for RelayVault bandwidth contributors. /// @dev The token and minimum are immutable. There is no owner or emergency /// withdrawal path: only the staker can withdraw their own balance. contract BandwidthAccessStaking { error EmptyAddress(); error ZeroAmount(); error TransferFailed(); error InsufficientStake(); error MinimumStakeRequired(); error Reentrancy(); event Staked(address indexed account, uint256 amount, uint256 newBalance); event Unstaked(address indexed account, uint256 amount, uint256 newBalance); IAccessStakeToken public immutable stakeToken; uint256 public immutable minimumStake; mapping(address => uint256) public stakedBalance; uint256 private locked; modifier nonReentrant() { if (locked == 1) revert Reentrancy(); locked = 1; _; locked = 0; } constructor(address stakeToken_, uint256 minimumStake_) { if (stakeToken_ == address(0) || minimumStake_ == 0) revert EmptyAddress(); stakeToken = IAccessStakeToken(stakeToken_); minimumStake = minimumStake_; } function isEligible(address account) public view returns (bool) { return stakedBalance[account] >= minimumStake; } function stake(uint256 amount) external nonReentrant { if (amount == 0) revert ZeroAmount(); if (!_callToken(abi.encodeWithSelector(IAccessStakeToken.transferFrom.selector, msg.sender, address(this), amount))) { revert TransferFailed(); } uint256 nextBalance = stakedBalance[msg.sender] + amount; stakedBalance[msg.sender] = nextBalance; emit Staked(msg.sender, amount, nextBalance); } function unstake(uint256 amount) external nonReentrant { if (amount == 0) revert ZeroAmount(); uint256 current = stakedBalance[msg.sender]; if (amount > current) revert InsufficientStake(); uint256 nextBalance = current - amount; if (nextBalance != 0 && nextBalance < minimumStake) revert MinimumStakeRequired(); stakedBalance[msg.sender] = nextBalance; if (!_callToken(abi.encodeWithSelector(IAccessStakeToken.transfer.selector, msg.sender, amount))) { revert TransferFailed(); } emit Unstaked(msg.sender, amount, nextBalance); } function _callToken(bytes memory data) internal returns (bool) { (bool ok, bytes memory returndata) = address(stakeToken).call(data); if (!ok) return false; if (returndata.length == 0) return true; if (returndata.length < 32) return false; return abi.decode(returndata, (bool)); } }