Address
0x00a52f8564d6a43153fbc7682bc6a61bbb26f9bcCurrent Holdings
$0.00
TXs sent
0
First Active
2026-06-27
block 26,886,338
Last Active
today
block 27,544,261
Funded By
not identified
Net worth historyi
119 snapshots · to block 27,544,262coverage change 26 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Aug
exact matchTetraStakingsolc 0.8.24+commit.e11b9ed9runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
/// @title TetraStaking
/// @notice Flexible (no-lock) staking of $TETRAp on PulseChain that distributes
/// multi-token revenue rewards (e.g. WPLS, DAI) to stakers on a weekly cycle.
/// @dev Immutable, not upgradeable, NOT pausable, Ownable2Step. The owner can ONLY
/// add/remove whitelist tokens and set the funding contract — no pause, no
/// rescue; staked principal and owed rewards are never touchable by the owner.
///
/// Reward accounting is a MasterChef-style per-token accumulator (`accRewardPerShare`)
/// over *eligible* stake, advanced only at weekly boundaries inside _settle. New stake
/// waits one epoch in a `pending` bucket before becoming eligible. The accumulator is
/// stored as a sparse Checkpoints trace keyed by epoch, so (a) settling a long dormancy
/// gap costs O(elapsed) cheap reads rather than O(elapsed) storage writes, and (b) an
/// inactive staker's pending stake can be credited from the exact accumulator value at
/// the epoch it became eligible. Undistributed integer-division remainders are carried
/// forward (never stranded). claimable() faithfully simulates _settle so the view never
/// disagrees with claim().
contract TetraStaking is Ownable2Step, ReentrancyGuard {
using SafeERC20 for IERC20;
using Checkpoints for Checkpoints.Trace256;
// ----------------------------- Constants ---------------------------- //
uint256 public constant WEEK = 7 days;
uint256 private constant PRECISION = 1e18;
uint256 public constant MAX_SUPPORTED_TOKENS = 20;
uint256 public constant MAX_REWARD_TOKENS = 20;
/// @dev Caps how far in the past `genesis` may be set at deploy, bounding the
/// initial epoch so the very first interaction can never be a gas bomb.
uint256 public constant MAX_GENESIS_BACKDATE = 2 * WEEK;
/// @dev Caps how far in the FUTURE `genesis` may be set at deploy. `genesis` is
/// immutable with no setter, so a fat-fingered far-future value would silently
/// disable all payouts forever; this bounds the launch window to ~1 year.
uint256 public constant MAX_GENESIS_FORWARD = 52 * WEEK;
// ----------------------------- Immutables --------------------------- //
IERC20 public immutable stakingToken; // $TETRAp — principal only, never a reward token
uint256 public immutable genesis; // a Friday 17:00 UTC anchor; payouts at genesis + k*WEEK
// ------------------------------- Config ----------------------------- //
address public fundingContract; // the only address allowed to deposit rewards
// -------------------------- Token registries ------------------------ //
address[] private _supportedTokens; // active whitelist (deposit gate)
mapping(address => bool) public isSupportedToken;
address[] private _rewardTokens; // every token ever funded (append-only; claims survive de-whitelisting)
mapping(address => bool) private _isRewardToken;
// ----------------------------- Stake state -------------------------- //
struct UserInfo {
uint256 eligibleStake; // earns the upcoming payout
uint256 pendingStake; // staked this epoch; eligible at `pendingEligibleEpoch`
uint256 pendingEligibleEpoch; // epoch at which pendingStake promotes to eligible
}
mapping(address => UserInfo) public users;
uint256 public totalEligibleStake;
uint256 public totalPendingStake;
// ----------------------------- Reward state ------------------------- //
mapping(address => Checkpoints.Trace256) private _acc; // token => acc-per-share trace (key=epoch, value scaled by PRECISION); uint256 value cannot overflow
mapping(address => uint256) public currentEpochPool; // token => rewards awaiting distribution
mapping(address => uint256) public totalDeposited; // token => lifetime deposited
mapping(address => uint256) public totalClaimed; // token => lifetime claimed
mapping(address => mapping(address => uint256)) public rewardDebt; // user => token => debt
mapping(address => mapping(address => uint256)) public storedClaimable; // user => token => settled, unclaimed
uint256 public lastSettledEpoch; // highest weekly boundary global settlement has processed
// ------------------------------- Events ----------------------------- //
event Staked(address indexed user, uint256 amount, uint256 eligibleEpoch);
event Unstaked(address indexed user, uint256 amount);
event RewardsClaimed(address indexed user, address indexed token, uint256 amount);
event RewardsDeposited(address indexed funder, address indexed token, uint256 amount, uint256 epoch);
event Settled(uint256 indexed toEpoch);
event SupportedTokenAdded(address indexed token);
event SupportedTokenRemoved(address indexed token);
event FundingContractChanged(address indexed previous, address indexed current);
// ------------------------------- Errors ----------------------------- //
error ZeroAmount();
error ZeroAddress();
error InvalidToken();
error InvalidGenesis();
error InvalidFundingContract();
error TokenAlreadySupported();
error TokenNotSupported();
error TooManySupportedTokens();
error TooManyRewardTokens();
error NotFundingContract();
error InsufficientStake();
error NotSelf();
// ----------------------------- Constructor -------------------------- //
constructor(
address _stakingToken,
uint256 _genesis,
address _fundingContract,
address _owner,
address[] memory _initialTokens
) Ownable(_owner) {
if (_stakingToken == address(0) || _fundingContract == address(0)) revert ZeroAddress();
// genesis must be non-zero, not back-dated beyond the cap (prevents a
// misconfigured deploy from creating an unbounded initial settlement gap), and
// not set absurdly far in the future (which would permanently disable payouts).
if (
_genesis == 0 || _genesis + MAX_GENESIS_BACKDATE < block.timestamp
|| _genesis > block.timestamp + MAX_GENESIS_FORWARD
) revert InvalidGenesis();
if (_fundingContract == _stakingToken) revert InvalidFundingContract();
stakingToken = IERC20(_stakingToken);
genesis = _genesis;
fundingContract = _fundingContract;
emit FundingContractChanged(address(0), _fundingContract);
uint256 n = _initialTokens.length;
for (uint256 i; i < n; ++i) {
_addSupportedToken(_initialTokens[i]);
}
}
// ------------------------- User: stake / unstake -------------------- //
/// @notice Stake `amount` of $TETRAp. New stake becomes eligible at the next
/// weekly boundary (it "rolls" to next week if past this week's cutoff).
function stake(uint256 amount) external nonReentrant {
if (amount == 0) revert ZeroAmount();
_settle();
_harvest(msg.sender);
uint256 before = stakingToken.balanceOf(address(this));
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = stakingToken.balanceOf(address(this)) - before;
if (received == 0) revert ZeroAmount();
UserInfo storage u = users[msg.sender];
uint256 eligEpoch = _eligibleEpochForNow();
if (eligEpoch <= lastSettledEpoch) {
// pre-genesis stake: eligible immediately
u.eligibleStake += received;
totalEligibleStake += received;
} else {
u.pendingStake += received;
u.pendingEligibleEpoch = eligEpoch;
totalPendingStake += received;
}
_resetDebt(msg.sender);
emit Staked(msg.sender, received, eligEpoch);
}
/// @notice Unstake `amount` of $TETRAp. Removed from PENDING (not-yet-earning)
/// stake first, then from eligible stake — so a user pulling only their
/// brand-new stake keeps their eligible rewards; removing eligible stake
/// proportionally forfeits the current week's reward to those still staked.
function unstake(uint256 amount) external nonReentrant {
_unstake(msg.sender, amount);
}
function _unstake(address user, uint256 amount) private {
if (amount == 0) revert ZeroAmount();
_settle();
_harvest(user);
UserInfo storage u = users[user];
if (amount > u.eligibleStake + u.pendingStake) revert InsufficientStake();
uint256 fromPending = amount <= u.pendingStake ? amount : u.pendingStake;
uint256 fromEligible = amount - fromPending;
if (fromPending != 0) {
u.pendingStake -= fromPending;
totalPendingStake -= fromPending;
}
if (fromEligible != 0) {
u.eligibleStake -= fromEligible;
totalEligibleStake -= fromEligible;
}
_resetDebt(user);
stakingToken.safeTransfer(user, amount);
emit Unstaked(user, amount);
}
// ----------------------------- User: claim -------------------------- //
/// @notice Claim all accrued rewards. A reward token whose transfer reverts
/// (e.g. a blacklisting token) is skipped — it never blocks the others.
function claim() external nonReentrant {
_settle();
_harvest(msg.sender);
uint256 elig = users[msg.sender].eligibleStake;
address[] memory toks = _rewardTokens;
uint256 n = toks.length;
for (uint256 i; i < n; ++i) {
address t = toks[i];
rewardDebt[msg.sender][t] = Math.mulDiv(elig, _curAcc(t), PRECISION); // reset debt (overflow-safe)
_tryPayout(msg.sender, t); // skip-on-failure
}
}
/// @notice Claim accrued rewards of a single token (reverts if that token's transfer fails).
function claim(address token) external nonReentrant {
_settle();
_harvest(msg.sender);
_resetDebt(msg.sender);
uint256 amount = storedClaimable[msg.sender][token];
if (amount != 0) {
uint256 bal = IERC20(token).balanceOf(address(this));
uint256 pay = amount <= bal ? amount : bal; // cap at available (no hard revert on a dust shortfall)
if (pay != 0) {
storedClaimable[msg.sender][token] = amount - pay;
totalClaimed[token] += pay;
IERC20(token).safeTransfer(msg.sender, pay);
emit RewardsClaimed(msg.sender, token, pay);
}
}
}
/// @notice Claim everything (skip-on-failure) and unstake the full principal in one call.
function exit() external nonReentrant {
_settle();
_harvest(msg.sender);
UserInfo storage u = users[msg.sender];
uint256 principal = u.eligibleStake + u.pendingStake;
if (u.eligibleStake != 0) totalEligibleStake -= u.eligibleStake;
if (u.pendingStake != 0) totalPendingStake -= u.pendingStake;
u.eligibleStake = 0;
u.pendingStake = 0;
address[] memory toks = _rewardTokens;
uint256 n = toks.length;
for (uint256 i; i < n; ++i) {
rewardDebt[msg.sender][toks[i]] = 0;
_tryPayout(msg.sender, toks[i]);
}
if (principal != 0) {
stakingToken.safeTransfer(msg.sender, principal);
emit Unstaked(msg.sender, principal);
}
}
// ----------------------- Funding contract: deposit ------------------ //
/// @notice Fund the rewards pool. Callable ONLY by `fundingContract`, only for
/// whitelisted tokens. Distributed to eligible stakers at the next boundary.
function depositRewards(address token, uint256 amount) external nonReentrant {
if (msg.sender != fundingContract) revert NotFundingContract();
if (!isSupportedToken[token]) revert TokenNotSupported();
if (amount == 0) revert ZeroAmount();
_settle();
if (!_isRewardToken[token]) {
if (_rewardTokens.length >= MAX_REWARD_TOKENS) revert TooManyRewardTokens();
_isRewardToken[token] = true;
_rewardTokens.push(token);
}
uint256 before = IERC20(token).balanceOf(address(this));
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
uint256 received = IERC20(token).balanceOf(address(this)) - before;
currentEpochPool[token] += received;
totalDeposited[token] += received;
emit RewardsDeposited(msg.sender, token, received, lastSettledEpoch);
}
// -------------------------- Owner: the only admin ------------------- //
function addSupportedToken(address token) external onlyOwner {
_addSupportedToken(token);
}
function removeSupportedToken(address token) external onlyOwner {
if (!isSupportedToken[token]) revert TokenNotSupported();
isSupportedToken[token] = false;
uint256 n = _supportedTokens.length;
for (uint256 i; i < n; ++i) {
if (_supportedTokens[i] == token) {
_supportedTokens[i] = _supportedTokens[n - 1];
_supportedTokens.pop();
break;
}
}
emit SupportedTokenRemoved(token);
}
function setFundingContract(address newFundingContract) external onlyOwner {
if (newFundingContract == address(0) || newFundingContract == address(this)
|| newFundingContract == address(stakingToken)) revert InvalidFundingContract();
emit FundingContractChanged(fundingContract, newFundingContract);
fundingContract = newFundingContract;
}
// ------------------------------- Settlement ------------------------- //
/// @notice Permissionless: advance settlement to the current epoch. Useful to keep
/// a dormant contract current; not required for correctness.
function poke() external {
_settle();
}
/// @dev Roll global state forward across every elapsed weekly boundary. At each
/// boundary: distribute the pool over eligible stake (carrying the integer
/// remainder forward; carrying the whole pool if there is no eligible stake),
/// checkpoint the accumulator only when it actually changes, then promote
/// pending stake. Storage writes happen only on real distributions/promotions,
/// so settling a long gap is O(elapsed) cheap reads, not O(elapsed) writes.
function _settle() internal {
uint256 epoch = _currentEpoch();
uint256 last = lastSettledEpoch;
if (epoch <= last) return;
address[] memory toks = _rewardTokens;
uint256 nTokens = toks.length;
for (uint256 k = last + 1; k <= epoch; ++k) {
bool didWork = false;
uint256 te = totalEligibleStake;
if (te != 0) {
for (uint256 i; i < nTokens; ++i) {
address t = toks[i];
uint256 pool = currentEpochPool[t];
if (pool == 0) continue;
uint256 cur = _curAcc(t);
// mulDiv: full 512-bit intermediate so a huge pool can't spuriously
// overflow `pool * PRECISION` and brick the non-pausable contract.
uint256 add = Math.mulDiv(pool, PRECISION, te);
if (add == 0) continue; // too small to distribute this round — carry forward
uint256 distributed = Math.mulDiv(add, te, PRECISION); // <= pool
if (distributed == 0) continue; // would move zero tokens — carry whole pool, never inflate acc
_acc[t].push(k, cur + add);
currentEpochPool[t] = pool - distributed; // carry the remainder
didWork = true;
}
}
// promote pending → eligible for the new epoch
if (totalPendingStake != 0) {
totalEligibleStake += totalPendingStake;
totalPendingStake = 0;
didWork = true;
}
// Once a boundary neither distributes nor promotes, eligible stake and the
// pools are stable, so every remaining boundary is a no-op. Stop the loop
// (the skipped epochs change nothing) — this bounds settlement gas to O(1)
// in steady state, defusing any dormancy gas bomb.
if (!didWork) break;
}
lastSettledEpoch = epoch;
emit Settled(epoch);
}
/// @dev Move a user's accrued rewards (eligible earnings + any now-due pending
/// promotion) into storedClaimable, and promote their pending stake. Does NOT
/// reset rewardDebt — callers follow with _resetDebt (or set debt themselves).
function _harvest(address user) internal {
UserInfo storage u = users[user];
address[] memory toks = _rewardTokens;
uint256 nTokens = toks.length;
uint256 elig = u.eligibleStake;
uint256 pending = u.pendingStake;
uint256 pe = u.pendingEligibleEpoch;
bool promote = pending != 0 && lastSettledEpoch >= pe;
for (uint256 i; i < nTokens; ++i) {
address t = toks[i];
uint256 cur = _curAcc(t);
uint256 accrued = Math.mulDiv(elig, cur, PRECISION) - rewardDebt[user][t];
if (promote) {
accrued += Math.mulDiv(pending, cur - _accAt(t, pe), PRECISION);
}
if (accrued != 0) storedClaimable[user][t] += accrued;
}
if (promote) {
u.eligibleStake = elig + pending;
u.pendingStake = 0;
}
}
function _resetDebt(address user) internal {
uint256 elig = users[user].eligibleStake;
address[] memory toks = _rewardTokens;
uint256 nTokens = toks.length;
for (uint256 i; i < nTokens; ++i) {
address t = toks[i];
rewardDebt[user][t] = Math.mulDiv(elig, _curAcc(t), PRECISION);
}
}
/// @dev Pay a user's stored claimable for one token, skipping it (no state change)
/// if EITHER the balance read OR the transfer reverts — so one bad reward token
/// cannot brick a batch claim()/exit(). The balanceOf read AND the transfer run
/// inside the self-call wrapped by try/catch; the previous version read balanceOf
/// OUTSIDE the guard, so a balanceOf-reverting token (self-destructed / paused /
/// reverting-proxy) could brick the whole loop. Accounting updates only on a real
/// payout, so a skipped token stays fully claimable later.
function _tryPayout(address user, address token) private {
uint256 amount = storedClaimable[user][token];
if (amount == 0) return;
try this.__capTransferReward(token, user, amount) returns (uint256 paid) {
if (paid != 0) {
storedClaimable[user][token] = amount - paid; // keep any un-payable wei claimable later
totalClaimed[token] += paid;
emit RewardsClaimed(user, token, paid);
}
} catch {
// balance read or transfer reverted — skip this token, leave it fully claimable
}
}
/// @dev External self-call wrapper: read the live balance, cap the payout at it
/// (so floored-accumulator wei-dust can never strand a claimer), then SafeERC20
/// transfer; returns the amount actually paid. Only callable by self, so any
/// revert in the balance read or the transfer is isolated by the caller's
/// try/catch. (Mutating entry points are nonReentrant, so a reentrant token's
/// re-entry reverts and is swallowed here with no state change.)
function __capTransferReward(address token, address to, uint256 amount) external returns (uint256 paid) {
if (msg.sender != address(this)) revert NotSelf();
uint256 bal = IERC20(token).balanceOf(address(this));
paid = amount <= bal ? amount : bal;
if (paid != 0) IERC20(token).safeTransfer(to, paid);
}
function _addSupportedToken(address token) internal {
if (token == address(0) || token == address(stakingToken)) revert InvalidToken();
if (isSupportedToken[token]) revert TokenAlreadySupported();
if (_supportedTokens.length >= MAX_SUPPORTED_TOKENS) revert TooManySupportedTokens();
isSupportedToken[token] = true;
_supportedTokens.push(token);
emit SupportedTokenAdded(token);
}
// ------------------------- Internal: accumulator -------------------- //
function _curAcc(address token) internal view returns (uint256) {
return _acc[token].latest();
}
function _accAt(address token, uint256 epoch) internal view returns (uint256) {
return _acc[token].upperLookup(epoch);
}
// --------------------------- Internal: time ------------------------- //
function _currentEpoch() internal view returns (uint256) {
if (block.timestamp <= genesis) return 0;
return (block.timestamp - genesis) / WEEK;
}
function _eligibleEpochForNow() internal view returns (uint256) {
if (block.timestamp < genesis) return 0;
return (block.timestamp - genesis) / WEEK + 1;
}
// ------------------------------- Views ------------------------------ //
function stakedBalanceOf(address user) public view returns (uint256) {
UserInfo storage u = users[user];
return u.eligibleStake + u.pendingStake;
}
function eligibleStakeOf(address user) external view returns (uint256) {
return users[user].eligibleStake;
}
function pendingStakeOf(address user) external view returns (uint256) {
return users[user].pendingStake;
}
function totalStaked() public view returns (uint256) {
return totalEligibleStake + totalPendingStake;
}
function accRewardPerShare(address token) external view returns (uint256) {
return _curAcc(token);
}
/// @notice Rewards of `token` currently claimable by `user`. Faithfully simulates
/// _settle + _harvest, so it always equals what claim() would pay.
function claimable(address user, address token) public view returns (uint256 amount) {
UserInfo storage u = users[user];
amount = storedClaimable[user][token];
uint256 curE = _currentEpoch();
uint256 last = lastSettledEpoch;
uint256 simAcc = _curAcc(token);
uint256 simTe = totalEligibleStake;
uint256 simPending = totalPendingStake;
uint256 simPool = currentEpochPool[token];
uint256 pe = u.pendingEligibleEpoch;
bool promote = u.pendingStake != 0 && curE >= pe;
bool peResolved;
uint256 accAtPe;
if (promote && pe <= last) {
accAtPe = _accAt(token, pe);
peResolved = true;
}
// simulate each elapsed boundary exactly as _settle would (same early-exit)
for (uint256 k = last + 1; k <= curE; ++k) {
bool didWork = false;
if (simTe != 0 && simPool != 0) {
uint256 add = Math.mulDiv(simPool, PRECISION, simTe); // mirror _settle (overflow-safe)
if (add != 0) {
uint256 dist = Math.mulDiv(add, simTe, PRECISION);
if (dist != 0) {
simAcc += add;
simPool -= dist;
didWork = true;
}
}
}
if (promote && !peResolved && k == pe) {
accAtPe = simAcc;
peResolved = true;
}
if (simPending != 0) {
simTe += simPending;
simPending = 0;
didWork = true;
}
if (!didWork) break;
}
amount += Math.mulDiv(u.eligibleStake, simAcc, PRECISION) - rewardDebt[user][token];
if (promote) {
if (!peResolved) accAtPe = simAcc;
amount += Math.mulDiv(u.pendingStake, simAcc - accAtPe, PRECISION);
}
}
function claimableAll(address user)
external
view
returns (address[] memory tokens, uint256[] memory amounts)
{
uint256 n = _rewardTokens.length;
tokens = new address[](n);
amounts = new uint256[](n);
for (uint256 i; i < n; ++i) {
tokens[i] = _rewardTokens[i];
amounts[i] = claimable(user, _rewardTokens[i]);
}
}
function isEligibleForNextPayout(address user) public view returns (bool) {
UserInfo storage u = users[user];
uint256 elig = u.eligibleStake;
if (u.pendingStake != 0 && _currentEpoch() >= u.pendingEligibleEpoch) elig += u.pendingStake;
return elig != 0;
}
function nextPayoutTime() public view returns (uint256) {
return genesis + (_currentEpoch() + 1) * WEEK;
}
function snapshotCutoffTime() public view returns (uint256) {
return genesis + _currentEpoch() * WEEK;
}
function currentEpoch() external view returns (uint256) {
return _currentEpoch();
}
function supportedTokens() external view returns (address[] memory) {
return _supportedTokens;
}
function rewardTokens() external view returns (address[] memory) {
return _rewardTokens;
}
/// @dev Saturating: a direct token donation can raise the balance-capped payout so
/// cumulative `totalClaimed` slightly exceeds `totalDeposited` (donated wei). Guard
/// the subtraction so this view can never underflow-revert.
function outstandingRewards(address token) external view returns (uint256) {
uint256 dep = totalDeposited[token];
uint256 claimed = totalClaimed[token];
return dep >= claimed ? dep - claimed : 0;
}
// ---------------------- Aggregator views (dApp) --------------------- //
struct UserView {
uint256 eligibleStake;
uint256 pendingStake;
uint256 totalStake;
bool eligibleNextPayout;
address[] tokens;
uint256[] claimableAmounts;
}
struct GlobalView {
uint256 totalStaked;
uint256 totalEligibleStake;
uint256 totalPendingStake;
uint256 currentEpoch;
uint256 nextPayoutTime;
uint256 snapshotCutoffTime;
address[] supportedTokens;
address[] rewardTokens;
}
function getUserInfo(address user) external view returns (UserView memory v) {
UserInfo storage u = users[user];
v.eligibleStake = u.eligibleStake;
v.pendingStake = u.pendingStake;
v.totalStake = u.eligibleStake + u.pendingStake;
v.eligibleNextPayout = isEligibleForNextPayout(user);
uint256 n = _rewardTokens.length;
v.tokens = new address[](n);
v.claimableAmounts = new uint256[](n);
for (uint256 i; i < n; ++i) {
v.tokens[i] = _rewardTokens[i];
v.claimableAmounts[i] = claimable(user, _rewardTokens[i]);
}
}
function getGlobalInfo() external view returns (GlobalView memory v) {
v.totalStaked = totalStaked();
v.totalEligibleStake = totalEligibleStake;
v.totalPendingStake = totalPendingStake;
v.currentEpoch = _currentEpoch();
v.nextPayoutTime = nextPayoutTime();
v.snapshotCutoffTime = snapshotCutoffTime();
v.supportedTokens = _supportedTokens;
v.rewardTokens = _rewardTokens;
}
}