Address
0x25b714ad96322cfcefcf11eb501d2fb2152173b4Current Holdings
$0.00
TXs sent
not counted
First Active
2026-07-17
block 27,055,369
Last Active
15 days ago
block 27,443,657
Funded By
not identified
Net worth historyi
No net-worth snapshots recorded yet
partial matchBXUSDsolc 0.8.36+commit.8a079791runtime partial · creation not verified
/*
____ _ _ __ __
| _ \ (_) | | \ \ / /
| |_) |_ __ _ __| | __ _ ___ \ V /
| _ <| '__| |/ _` |/ _` |/ _ \ > <
| |_) | | | | (_| | (_| | __// . \
|____/|_| |_|\__,_|\__, |\___/_/ \_\
__/ |
|___/
Trustless Bridge Secure Layer + Secure Liquidity Farming Protocol + Treasury
BridgeX.win
Copyright (c) 2026 Intarsia Network BridgeX.win
----------------------------------------------------------------------------
Business Source License 1.1
----------------------------------------------------------------------------
Licensor: Intarsia Network
Licensed Work: BridgeX.sol / BridgeXDSL.sol / BridgeXLib.sol / BridgeXTreasury.sol / BridgeXSecureLayerUSD.sol / BridgeXSecureLayerETH.sol / BridgeXSecureLayerBTC.sol / BXUSD.sol / BXETH.sol / BXBTC.sol
Additional Use Grant: None
Change Date: 2029-07-02 (3 years from deployment)
Change License: GNU General Public License v3.0 or later
Use of this software is governed by the Business Source License 1.1
included below.
Terms
-----
The Licensor hereby grants you the right to copy, modify, and distribute the
Licensed Work and products derived from it, in each case for any purpose
(commercial or non-commercial), subject to the following conditions:
1. You may not use the Licensed Work for any purpose other than to develop,
deploy, and operate the BridgeX / Intarsia Protocol or derivatives thereof.
2. The Change Date provision in this License applies as set forth above.
3. Upon the Change Date, this License will automatically convert to the
GNU General Public License, Version 3.0 or any later version.
THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND
NON-INFRINGEMENT. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
*/
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.36;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Iv3PosManager} from "./interfaces/Iv3PosManager.sol";
import {ILibertyV3Pool} from "./interfaces/ILibertyV3Pool.sol";
import {ISecureLayerToken} from "./interfaces/ISecureLayerToken.sol";
import {BridgeXLib} from "./libraries/BridgeXLib.sol";
import "./tokens/BXUSD.sol";
import "./tokens/BXETH.sol";
import "./tokens/BXBTC.sol";
/// @title BridgeXDSL (BridgeX Destination Secure Layer)
/// @notice Trustless bridge staking/farming.
/// Receipt token ownership is permanently locked to this contract.
/// @dev Immutable, non-upgradeable contract designed for maximum decentralization
/// and fairness. Combines a trustless bridge (wrap/unwrap) with Uniswap V3
/// LP staking that enforces active price ranges for rewards and fees.
///
/// Key security and fairness principles:
/// - There is **no emergency owner withdraw function**. The owner cannot
/// drain or rescue funds under any circumstances.
///
/// **Auditor / Reviewer Note:**
/// Plain `revert();` (without error messages) is intentionally used in multiple
/// locations for maximum gas savings on the most frequent failure paths.
///
/// The overall design prioritizes user protection, decentralization of
/// critical decisions, and transparency over owner convenience.
contract BridgeXDSL is
IERC721Receiver,
Ownable2Step,
ReentrancyGuard
{
using SafeERC20 for IERC20;
using BridgeXLib for *;
enum Action_ {Claim, CollectFees, ClaimAndCollect}
enum Tokens_ {Invalid_, SLusd_, SLeth_, SLbtc_}
struct PoolInfo {
uint256 accRewardPerShare;
uint256 totalStaked;
uint256 totalTokens;
address token0;
address token1;
address rewardToken;
uint96 rewardRate;
uint40 lastRewardTime;
}
struct StakedPosition {
uint128 rewardDebt;
uint128 liquidity;
uint128 tokens;
address owner;
uint40 stakeTime;
uint8 poolId;
}
struct UserDeposit {
uint128 deposits;
uint128 withdrawals;
Tokens_ token;
}
struct PositionRef {
uint8 pid;
uint64 tid;
}
// === MAPPINGS ===
mapping(uint8 => mapping(uint64 => StakedPosition)) public stakedPositions_;
mapping(address => mapping(Tokens_ => UserDeposit)) public userDeposits_;
mapping(address => address) public userReferrer_;
mapping(address => address) public tokenWrapPairs_;
mapping(address => uint16) public referralsPerUser_;
mapping(address => uint8) public tokensIds_;
mapping(address => PositionRef[]) private userStakedRefs_;
// === PUBLIC_ARRAYS ===
PoolInfo[5] public poolInfo;
uint16[5] public targetAprBps;
uint40[5] public lastAutoUpdate;
uint64[5] public initialPositionIds;
bool public positionsInitialized;
// === CONSTANTS ===
uint256 public constant START_TIMESTAMP = 1784419200; // Sun Jul 19 2026 00:00:00 UTC
uint256 public constant BONUS_APR_ENDS = START_TIMESTAMP + 100 days;
uint256 public constant RATE_COOLDOWN = 1 hours;
uint16 public constant MAX_NORMAL_APR = 6400; // 64%
uint16 public constant MAX_BONUS_APR = MAX_NORMAL_APR * 2; // 128%
uint16 public constant MAX_USER_STAKES = 21;
uint24 private constant LP_FEE_TIER = 100;
uint256 private constant PRECISION = 1e18;
uint256 private constant SECONDS_PER_YEAR = 365 days;
uint256 private constant SECONDS_PER_MONTH = 30 days;
uint256 private constant MAX_30D_RATE_BXUSD = 20e21; // $20,000
uint256 private constant MAX_30D_RATE_BXETH = 10e18; // 10 Ether
uint256 private constant MAX_30D_RATE_BXBTC = 25e16; // 0.25 Bitcoin
/// @notice Default BridgeX Treasury / Referral Address
/// @dev Receives referral rewards when no custom referrer is provided.
address private constant BX_TREASURY = 0xa22061D4131E25f47892F6b925993C8d2e53512d;
/// @notice Address allowed to bypass the `START_TIMESTAMP` check in `wrap` and `stake`
/// functions. Used exclusively during deployment to verify and initialize the contract.
address private constant DEPLOYER = 0xB19f4592139E7c69881265aD66615c28C0D47a92;
// ================================================
// Liberty Swap V3 Contracts (PulseChain)
// ================================================
/// @notice Liberty Swap V3 Non-fungible Position Manager
/// @dev Official contract for creating, managing, and interacting with liquidity positions
address private constant POS_MANAGER = 0x19b1347900840e7a299F339868881750918FAD91;
/// @notice Liberty Swap V3 Factory
/// @dev Official factory for creating new liquidity pools and querying existing ones
address private constant FACTORY = 0x796fcbDC956b85797EFe21145Aa97599B7FB36a6;
// BridgeX Secure Layer Tokens
address private constant SLUSD_TOKEN = 0xBd61f6317ec668eea35456A4f593A498066BE471;
address private constant SLETH_TOKEN = 0x847821eeE732D6E549846C43Db9FEEc6B4667D88;
address private constant SLBTC_TOKEN = 0x5BdaD48D0483fb249047690cd7C23Eeb3E8A20f5;
// === IMMUTABLE ===
// Official BridgeX USD, ETH, and BTC tokens
address public immutable BXUSD_TOKEN;
address public immutable BXETH_TOKEN;
address public immutable BXBTC_TOKEN;
/* ==============================
:::: EVENTS ::::
============================== */
event WrapEvent(
address indexed user,
address token0,
address token1,
uint256 amount,
bool isWrap
);
event StakeEvent(
uint256 indexed tokenId,
uint8 poolId,
uint256 amount,
uint8 action
);
event RewardRateUpdated(
uint8 poolId,
uint256 newRate
);
/* ==============================
:::: MODIFIERS ::::
============================== */
/// @dev Allows normal users only after START_TIMESTAMP.
/// DEPLOYER can interact immediately for setup and verification.
modifier isActive() {
if (
block.timestamp < START_TIMESTAMP &&
msg.sender != DEPLOYER
) {
revert();
}
_;
}
/* ==============================
:::: CONSTRUCTOR ::::
============================== */
/// @notice Deploys and initializes the contract, creating token contracts and configuring all pools and token mappings.
/// @dev In the constructor:
/// - Sets the contract owner to `msg.sender`.
/// - Deploys BXUSD/BXETH/BXBTC token contracts.
/// - Initializes pool metadata for all `poolId` in `[0..BridgeXLib.MAX_POOL_ID]`, including token addresses and reward rate/target APR.
/// - Sets token IDs and wrap pairs for SLUSD/ SLETH/ SLBTC.
/// - Mints initial balances of BXUSD/BXETH/BXBTC to this contract to back the initial LP/core position creation.
constructor() Ownable(msg.sender) {
// ==== INITIALIZE_TOKENS ====
BXUSD_TOKEN = address(new BXUSD());
BXETH_TOKEN = address(new BXETH());
BXBTC_TOKEN = address(new BXBTC());
// ==== INITIALIZE_POOLS ====
for (uint8 i = 0; i <= BridgeXLib.MAX_POOL_ID;) {
(address tok0, address tok1, bool isLower) = BridgeXLib._getPoolTokensAndType(i, BXUSD_TOKEN, BXETH_TOKEN, BXBTC_TOKEN);
( , , , uint256 startingRate, ) = BridgeXLib._getCoreConfigs(i, BXUSD_TOKEN, BXETH_TOKEN, BXBTC_TOKEN, true);
_initPool(i, tok0, tok1, isLower ? tok0 : tok1, MAX_BONUS_APR, startingRate);
unchecked {++i;}
}
// ==== INITIALIZE_TOKEN_IDS ====
tokensIds_[SLUSD_TOKEN] = uint8(Tokens_.SLusd_);
tokensIds_[SLETH_TOKEN] = uint8(Tokens_.SLeth_);
tokensIds_[SLBTC_TOKEN] = uint8(Tokens_.SLbtc_);
// ==== INITIALIZE_TOKEN_PAIRS ====
tokenWrapPairs_[SLUSD_TOKEN] = BXUSD_TOKEN;
tokenWrapPairs_[SLETH_TOKEN] = BXETH_TOKEN;
tokenWrapPairs_[SLBTC_TOKEN] = BXBTC_TOKEN;
// === MINT_AMOUNTS_FOR_LP ===
ISecureLayerToken(BXUSD_TOKEN).mint(address(this), (BridgeXLib.MINT_CORE_V3LIQ_BXUSD * 3) + (BridgeXLib.MINT_CORE_STAKE_BXUSD * 3));
ISecureLayerToken(BXETH_TOKEN).mint(address(this), BridgeXLib.MINT_CORE_V3LIQ_BXETH + BridgeXLib.MINT_CORE_STAKE_BXETH);
ISecureLayerToken(BXBTC_TOKEN).mint(address(this), BridgeXLib.MINT_CORE_V3LIQ_BXBTC + BridgeXLib.MINT_CORE_STAKE_BXBTC);
// === SET_ALL_APPROVALS_MAX ===
IERC20(BXUSD_TOKEN).approve(POS_MANAGER, type(uint256).max);
IERC20(BXETH_TOKEN).approve(POS_MANAGER, type(uint256).max);
IERC20(BXBTC_TOKEN).approve(POS_MANAGER, type(uint256).max);
}
/* ==============================
:::: EXTERNAL_PUBLIC ::::
============================== */
/// @notice Wraps bridged tokens (iBUSD/BXETH/BXBTC) into their corresponding secure layer tokens (BXUSD/BXETH/BXBTC)
/// @dev This is the entry point for the trustless bridge. The bridged token is transferred
/// to this contract and an equivalent amount of the BX Token is minted directly to the user.
/// Deposit tracking is updated for future withdrawal limits.
/// @param token The bridged token to wrap (must be one of the supported SL* tokens)
/// @param amount The amount of tokens to wrap (must be > 0)
function wrapTokens(
IERC20 token,
uint128 amount
)
external
isActive
nonReentrant
{
if (amount == 0) revert();
address tokenAddr = address(token);
if (tokenAddr == address(0)) revert();
address sender = msg.sender;
uint8 id = tokensIds_[tokenAddr];
BridgeXLib._requireWrapAmount(id, amount);
token.safeTransferFrom(sender, address(this), amount);
Tokens_ tokenEnum = Tokens_(id);
UserDeposit storage ud = userDeposits_[sender][tokenEnum];
address mintToken = tokenWrapPairs_[tokenAddr];
if (mintToken == address(0)) revert();
unchecked {
ud.deposits += amount;
}
ISecureLayerToken(mintToken).mint(sender, amount);
emit WrapEvent(sender, tokenAddr, mintToken, amount, true);
}
/// @notice Unwraps secure layer tokens (BXUSD/BXETH/BXBTC) back into their corresponding bridged tokens (iBUSD/BXETH/BXBTC)
/// @dev Burns the BX Token from the user and transfers the original bridged token back.
/// Enforces that the user cannot withdraw more than they have deposited (net of prior withdrawals).
/// @param token The bridged token to receive (must be one of the supported SL* tokens)
/// @param amount The amount of tokens to unwrap (must be > 0 and within user's deposit limit)
function unwrapTokens(
IERC20 token,
uint128 amount
)
external
nonReentrant
{
if (amount == 0) revert();
address tokenAddr = address(token);
if (tokenAddr == address(0)) revert();
address sender = msg.sender;
uint8 id = tokensIds_[tokenAddr];
BridgeXLib._requireWrapAmount(id, amount);
Tokens_ tokenEnum = Tokens_(id);
UserDeposit storage ud = userDeposits_[sender][tokenEnum];
uint256 totalWith = ud.withdrawals + amount;
if (ud.deposits < totalWith) revert();
address burnToken = tokenWrapPairs_[tokenAddr];
if (burnToken == address(0)) revert();
unchecked {
ud.withdrawals += amount;
}
ISecureLayerToken(burnToken).burn(sender, amount);
token.safeTransfer(sender, amount);
emit WrapEvent(sender, burnToken, tokenAddr, amount, false);
}
/// @notice Updates all pools by accruing pending rewards and adjusting reward rates if cooldown has passed.
/// @dev This is a convenience function that allows anyone to trigger reward updates across all pools.
/// Useful for keeping reward calculations fresh between user interactions, especially during
/// periods of low activity. Calls _updatePool and _adjustRateIfNeeded for every pool.
///
/// This function is intentionally permissionless.
function updateAllPools()
external
nonReentrant
{
for (uint8 i = 0; i <= BridgeXLib.MAX_POOL_ID;) {
_updatePool(i);
_adjustRateIfNeeded(i);
unchecked { ++i; }
}
}
/// @notice Triggers an automatic reward-rate update for a pool if the cooldown has passed.
/// @dev Reverts if called before RATE_COOLDOWN has elapsed since lastAutoUpdate for the pool. Calls _performAutoAdjust with shouldUpdatePool=true.
/// @param poolId The pool id (0..2) to update.
function updateRewardRateAuto(
uint8 poolId
)
external
nonReentrant
{
uint256 nextUpdate = lastAutoUpdate[poolId] + RATE_COOLDOWN;
if (block.timestamp < nextUpdate) revert();
_performAutoAdjust(poolId, true);
}
/// @notice ERC721 token receiver hook ? required to accept safeTransferFrom for ERC721 tokens.
/// @dev Returns the selector to confirm the contract accepts ERC721 tokens. Parameters are unused.
function onERC721Received(
address,
address,
uint256,
bytes calldata
)
external
pure
override
returns(bytes4)
{
return this.onERC721Received.selector;
}
/// @notice Stakes liquidity into a Uniswap V3 pool by minting a new LP position NFT
/// @dev **IMPORTANT**: Staking is **only allowed** when the current price is within the
/// position's tick range (in-bounds / "fair ratio").
/// If the price is out of range, the transaction will revert inside `_poolRatio()`.
/// This prevents users from starting a stake when the position would not be active.
///
/// @param poolId The pool ID to stake into (must be < 3)
/// @param amount0Desired Desired amount of the primary token to stake
/// @param amount1Desired Desired amount of the secondary token to stake
/// @param slippageBps Maximum allowed slippage in basis points (e.g. 500 = 5%)
/// @param referrer Address of the referrer (can be `address(0)` or self)
function stake(
uint8 poolId,
uint256 amount0Desired,
uint256 amount1Desired,
uint256 slippageBps,
address referrer
)
external
isActive
nonReentrant
{
if (userStakedRefs_[msg.sender].length >= MAX_USER_STAKES) revert();
(address token0, address token1, bool isLower) = BridgeXLib._getPoolTokensAndType(poolId, BXUSD_TOKEN, BXETH_TOKEN, BXBTC_TOKEN);
BridgeXLib._requireStakeValidations(poolId, amount0Desired, amount1Desired, token0, token1, slippageBps);
_requireFairRatio(poolId);
_setRef(referrer, msg.sender);
{
address sa = msg.sender;
address ta = address(this);
address pm = POS_MANAGER;
IERC20 t0 = IERC20(token0);
IERC20 t1 = IERC20(token1);
t0.approve(pm, 0);
t1.approve(pm, 0);
t0.approve(pm, amount0Desired);
t1.approve(pm, amount1Desired);
t0.safeTransferFrom(sa, ta, amount0Desired);
t1.safeTransferFrom(sa, ta, amount1Desired);
}
uint256 tokenId;
uint256 liquidity;
uint256 amount0;
uint256 amount1;
{
Iv3PosManager.MintParams memory params = Iv3PosManager.MintParams({
token0: token0,
token1: token1,
fee: LP_FEE_TIER,
tickLower: BridgeXLib._getStakeTick(poolId, false, isLower),
tickUpper: BridgeXLib._getStakeTick(poolId, true, isLower),
amount0Desired: amount0Desired,
amount1Desired: amount1Desired,
amount0Min: (amount0Desired * (10000 - slippageBps)) / 10000,
amount1Min: (amount1Desired * (10000 - slippageBps)) / 10000,
recipient: address(this),
deadline: block.timestamp + 1 hours
});
(tokenId, liquidity, amount0, amount1) = Iv3PosManager(POS_MANAGER).mint(params);
}
BridgeXLib._finalStakingChecks(tokenId, liquidity, amount0, amount1);
unchecked {
if (amount0 < amount0Desired) {
uint256 refund0 = amount0Desired - amount0;
uint8 tknDec0 = BridgeXLib._getTknDec(poolId, token0);
uint256 minDust0 = BridgeXLib._minDust(tknDec0);
if (refund0 > minDust0) IERC20(token0).safeTransfer(msg.sender, refund0);
}
if (amount1 < amount1Desired) {
uint256 refund1 = amount1Desired - amount1;
uint8 tknDec1 = BridgeXLib._getTknDec(poolId, token1);
uint256 minDust1 = BridgeXLib._minDust(tknDec1);
if (refund1 > minDust1) IERC20(token1).safeTransfer(msg.sender, refund1);
}
}
_updatePool(poolId);
{
// Total Amount is normalized to 18 decimals
(uint256 bonus, uint256 totalAmount) = BridgeXLib._calcBonusAndTotal(poolId, amount0, amount1, token0, token1, isLower, liquidity);
if (bonus > 0) {
emit StakeEvent(tokenId, poolId, bonus, 3);
unchecked {
liquidity += bonus;
}
}
_updateStakePosition(poolId, tokenId, liquidity, totalAmount);
}
_adjustRateIfNeeded(poolId);
emit StakeEvent(tokenId, poolId, liquidity, 0);
}
/// @notice Harvests rewards and/or collects trading fees from a staked Uniswap V3 position
/// @dev IMPORTANT: Harvesting is **only allowed** when the position's price range is in bounds
/// (i.e. "fair ratio"). If the position is out of range, `_requireFairRatio` will revert.
/// This prevents harvesting when the LP position is not actively earning fees/rewards.
///
/// @param tokenId The ID of the staked Uniswap V3 NFT position
/// @param poolId The pool ID (must be < 3)
/// @param action The type of action to perform:
/// - `Claim`: Claim only protocol rewards (with referral logic)
/// - `CollectFees`: Collect only Uniswap trading fees
/// - `ClaimAndCollect`: Do both
function harvest(
uint256 tokenId,
uint8 poolId,
Action_ action
)
external
nonReentrant
{
_updatePool(poolId);
address sender = msg.sender;
if (tokenId == 0 || poolId > BridgeXLib.MAX_POOL_ID) revert();
if (action > Action_.ClaimAndCollect) revert();
if (IERC721(POS_MANAGER).ownerOf(tokenId) != address(this)) revert();
_requireFairRatio(poolId);
uint64 sid = uint64(tokenId);
StakedPosition storage pos = stakedPositions_[poolId][sid];
if (pos.owner != sender) revert();
if (pos.liquidity == 0) revert();
bool shouldClaim = (action == Action_.Claim || action == Action_.ClaimAndCollect);
bool shouldCollect = (action == Action_.CollectFees || action == Action_.ClaimAndCollect);
// === Claim Rewards ===
if (shouldClaim) _claimRewards(pos, sid, sender, poolId);
// === Collect Trading Fees ===
if (shouldCollect) {
Iv3PosManager.CollectParams memory params = Iv3PosManager.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
});
(uint256 amount0, uint256 amount1) = Iv3PosManager(POS_MANAGER).collect(params);
address token0 = poolInfo[poolId].token0;
address token1 = poolInfo[poolId].token1;
if (token0 == address(0) || token1 == address(0)) revert();
if (amount0 > 0) IERC20(token0).safeTransfer(sender, amount0);
if (amount1 > 0) IERC20(token1).safeTransfer(sender, amount1);
}
_adjustRateIfNeeded(poolId);
}
/// @notice Unstake a position. User can choose to forfeit pending rewards.
/// @param tokenId The NFT position ID
/// @param pid Pool ID
/// @param acceptForfeit If true, user deliberately forfeits any pending rewards
function unstake(
uint256 tokenId,
uint8 pid,
bool acceptForfeit
)
external
nonReentrant
{
_updatePool(pid);
if (tokenId == 0 || pid > BridgeXLib.MAX_POOL_ID) revert();
if (IERC721(POS_MANAGER).ownerOf(tokenId) != address(this)) revert();
StakedPosition storage pos = stakedPositions_[pid][uint64(tokenId)];
if (pos.owner != msg.sender || pos.liquidity == 0) revert();
uint256 pending = _pendingRewards(uint64(tokenId), pid);
if (!acceptForfeit) {
if (!BridgeXLib._isBelowUnstakeThreshold(pid, pending)) revert();
}
// Update pool totals
if (poolInfo[pid].totalStaked >= pos.liquidity) {
poolInfo[pid].totalStaked -= pos.liquidity;
poolInfo[pid].totalTokens -= pos.tokens;
} else {
poolInfo[pid].totalStaked = 0;
poolInfo[pid].totalTokens = 0;
}
delete stakedPositions_[pid][uint64(tokenId)];
// Remove from user references
PositionRef[] storage refs = userStakedRefs_[msg.sender];
for (uint256 i = 0; i < refs.length; ++i) {
if (refs[i].pid == pid && refs[i].tid == uint64(tokenId)) {
refs[i] = refs[refs.length - 1];
refs.pop();
break;
}
}
IERC721(POS_MANAGER).safeTransferFrom(address(this), msg.sender, tokenId);
emit StakeEvent(tokenId, pid, 0, 2);
// Auto-adjust reward rate if needed
_adjustRateIfNeeded(pid);
}
/* ==============================
:::: PUBLIC EXTERNAL VIEW ::::
============================== */
/// @notice Returns multiple pieces of pool state in one call.
/// @return pools Array of PoolInfo structs for all pools (3).
/// @return targetAPRs Array of target APRs in basis points for each pool.
/// @return initialIds Array of initial position token IDs for each pool.
/// @return lastUpdates Array of last auto-update timestamps for each pool.
function getPoolData()
external
view
returns
(
PoolInfo[5] memory pools,
uint16[5] memory targetAPRs,
uint64[5] memory initialIds,
uint40[5] memory lastUpdates
) {
return (
poolInfo,
targetAprBps,
initialPositionIds,
lastAutoUpdate
);
}
/// @notice Calculates current APR for a given pool in basis points (with 2 decimal places: e.g., 1234 = 12.34%).
/// @param poolId The pool id (0..2).
/// @return aprBps Current APR expressed in basis points with two implied decimals.
function getCurrentAPR(
uint8 poolId
)
external
view
returns(uint256 aprBps)
{
if (poolId > BridgeXLib.MAX_POOL_ID) revert();
if (poolInfo[poolId].totalTokens == 0) revert();
uint256 rate = poolInfo[poolId].rewardRate;
uint256 tvl = poolInfo[poolId].totalTokens;
uint256 annualRewards = rate * SECONDS_PER_YEAR;
uint256 calculated = (annualRewards * 10000) / tvl;
return calculated;
}
/// @notice Checks whether a staked NFT can be unstaked without forfeiting rewards.
/// @param tokenId The staked NFT token id.
/// @param pid The pool id the token is staked in.
/// @return canUnstakeWithoutForfeit True if pending rewards are <= unstake threshold.
/// @return pendingRewardsAmount Current pending rewards for the position.
/// @return unstakeThreshold The threshold used to decide forfeiture.
function canUnstakeSafely(
uint64 tokenId,
uint8 pid
)
external
view
returns(
bool canUnstakeWithoutForfeit,
uint256 pendingRewardsAmount,
uint256 unstakeThreshold
)
{
if (tokenId == 0 || pid > BridgeXLib.MAX_POOL_ID) return (false, 0, 0);
if (IERC721(POS_MANAGER).ownerOf(tokenId) != address(this)) return (false, 0, 0);
StakedPosition memory pos = stakedPositions_[pid][tokenId];
if (pos.owner != msg.sender || pos.liquidity == 0) return (false, 0, 0);
uint256 pending = _pendingRewards(tokenId, pid);
uint256 threshold = BridgeXLib._getUnstakeThreshold(pid);
return (pending <= threshold, pending, threshold);
}
/// @notice Returns all staked positions for a user.
function getUserData(
address user
)
external
view
returns (
StakedPosition[] memory positions,
uint64[] memory tokenIds
)
{
PositionRef[] memory refs = userStakedRefs_[user];
uint256 len = refs.length;
positions = new StakedPosition[](len);
tokenIds = new uint64[](len);
for (uint256 i = 0; i < len; ) {
PositionRef memory r = refs[i];
positions[i] = stakedPositions_[r.pid][r.tid];
tokenIds[i] = r.tid;
unchecked { ++i; }
}
}
/// @notice Returns arrays of staked token ids and their pool ids for a user.
/// @param user The user address to query.
/// @return tokenIds Array of staked NFT token ids owned by the user (as uint64).
/// @return pids Array of pool ids corresponding to each token id.
function getUserStakedTokenIds(
address user
)
external
view
returns(
uint64[] memory tokenIds,
uint8[] memory pids
)
{
PositionRef[] memory refs = userStakedRefs_[user];
uint256 len = refs.length;
tokenIds = new uint64[](len);
pids = new uint8[](len);
for (uint256 i = 0; i < len;) {
tokenIds[i] = refs[i].tid;
pids[i] = refs[i].pid;
unchecked{ ++i; }
}
}
/// @notice Returns pool ratio diagnostic info.
/// @param pid The pool id to query.
/// @return isHealthy True if the pool ratio is within acceptable bounds.
/// @return currentPrice Current price used for ratio calculation.
/// @return currentTick Current Uniswap-style tick for the pool price.
function getPoolRatioInfo(
uint8 pid
)
external
view
returns(
bool isHealthy,
uint160 currentPrice,
int24 currentTick
)
{
(isHealthy, currentPrice, currentTick) = _poolRatio(pid);
return (
isHealthy,
currentPrice,
currentTick
);
}
/// @notice Returns pending reward amount for a staked position.
/// @param tokenId The NFT position token id.
/// @param pid The pool id (0..2).
/// @return uint256 Pending reward amount (in reward token units).
function pendingRewards(
uint64 tokenId,
uint8 pid
)
external
view
returns(uint256)
{
return _pendingRewards(tokenId, pid);
}
/// @notice Returns the configured target APR (in basis points) for all pools.
/// @dev Pulls values from `targetAprBps[0..4]`.
/// @return apr0 Target APR BPS for pool 0.
/// @return apr1 Target APR BPS for pool 1.
/// @return apr2 Target APR BPS for pool 2.
/// @return apr3 Target APR BPS for pool 3.
/// @return apr4 Target APR BPS for pool 4.
function getAllTargetAPRs()
external
view
returns(
uint16,
uint16,
uint16,
uint16,
uint16
)
{
return (
targetAprBps[0],
targetAprBps[1],
targetAprBps[2],
targetAprBps[3],
targetAprBps[4]
);
}
/// @notice Returns the last auto-update timestamps for all pools.
/// @dev Pulls values from `lastAutoUpdate[0..4]`.
/// @return t0 Last auto-update time for pool 0 (unix timestamp).
/// @return t1 Last auto-update time for pool 1 (unix timestamp).
/// @return t2 Last auto-update time for pool 2 (unix timestamp).
/// @return t3 Last auto-update time for pool 3 (unix timestamp).
/// @return t4 Last auto-update time for pool 4 (unix timestamp).
function getAllLastAutoUpdate()
external
view
returns(
uint40,
uint40,
uint40,
uint40,
uint40
)
{
return (
lastAutoUpdate[0],
lastAutoUpdate[1],
lastAutoUpdate[2],
lastAutoUpdate[3],
lastAutoUpdate[4]
);
}
/* ==============================
:::: EXTERNAL_OWNER ::::
============================== */
/// @notice Initializes single-sided positions for all pools.
/// @dev Can only be called once for core positions. For non-core positions,
/// it can be called multiple times and will also adjust rates.
///
/// This function loops through all pools (0 to MAX_POOL_ID) and creates
/// single-sided positions using the stored token0/token1 configuration.
///
/// The `isCore` flag determines:
/// - Whether rate adjustment is performed after position creation
/// - Whether the `positionsInitialized` flag is set (only for non-core)
///
/// @param isCore If `true`, initializes core positions (no rate adjustment,
/// flag not set). If `false`, initializes normal positions
/// with rate adjustments.
///
/// @custom:security Only callable by contract owner. Protected against reentrancy.
function initializePositions(
bool isCore
)
external
onlyOwner
nonReentrant
{
if (positionsInitialized) revert();
uint256 maxPoolId = BridgeXLib.MAX_POOL_ID;
// === CREATE_ADD_V3_SINGLE_SIDED_POSITIONS ===
for (uint8 i = 0; i <= maxPoolId;) {
_createSingleSided(i, poolInfo[i].token0, poolInfo[i].token1, isCore);
if (!isCore) _adjustRateIfNeeded(i);
unchecked {++i;}
}
if (!isCore) {
positionsInitialized = true;
}
}
/// @notice Collects accrued fees from the protocol's core Uniswap V3 position and sends them to the contract owner.
/// @dev Only callable by the owner. Reverts if poolId or core position id is invalid. Uses max collect amounts to pull all fees.
/// @param poolId The pool id (0..2) whose core LP fees should be collected.
function collectFeesFromCoreLP(
uint8 poolId
)
external
onlyOwner
nonReentrant
{
if (poolId > BridgeXLib.MAX_POOL_ID) revert();
uint256 coreTokenId = initialPositionIds[poolId];
if (coreTokenId == 0) revert();
// === Collect fees from CORE position ===
Iv3PosManager.CollectParams memory params = Iv3PosManager.CollectParams({
tokenId: coreTokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
});
(uint256 amount0, uint256 amount1) = Iv3PosManager(POS_MANAGER).collect(params);
address token0 = poolInfo[poolId].token0;
address token1 = poolInfo[poolId].token1;
if (token0 == address(0) || token1 == address(0)) revert();
// Send fees to current owner
if (amount0 > 0) IERC20(token0).safeTransfer(owner(), amount0);
if (amount1 > 0) IERC20(token1).safeTransfer(owner(), amount1);
}
/// @notice Set a new target APR (in basis points) for a pool.
/// @dev Caps newTargetBps to MAX_NORMAL_APR (MAX_BONUS_APR during the adoption phase). Emits RewardRateUpdated with the updated target APR.
/// @param poolId The pool id (0..2) to update.
/// @param newTargetBps New target APR expressed in basis points.
function setTargetAPR(
uint8 poolId,
uint16 newTargetBps
)
external
onlyOwner
nonReentrant
{
if (poolId > BridgeXLib.MAX_POOL_ID) revert();
uint256 maxApr = block.timestamp > BONUS_APR_ENDS
? MAX_NORMAL_APR
: MAX_BONUS_APR;
if (newTargetBps > maxApr) {
newTargetBps = uint16(maxApr);
} else {
targetAprBps[poolId] = newTargetBps;
}
}
/// @notice Emergency: reduce a pool's reward rate to a lower value immediately.
/// @dev Only callable by the owner. Reverts if newRate is not strictly less than current rate. Updates pool state and lastAutoUpdate timestamp, then //emits RewardRateUpdated.
/// @param poolId The pool id (0..4) to modify.
/// @param newRate New reward rate (per second) which must be lower than the current rate.
function emergencyReduceRewardRate(
uint8 poolId,
uint96 newRate
)
external
onlyOwner
nonReentrant
{
if (poolId > BridgeXLib.MAX_POOL_ID) revert();
uint96 oldRate = poolInfo[poolId].rewardRate;
if (newRate >= oldRate) revert();
_updatePool(poolId);
poolInfo[poolId].rewardRate = newRate;
lastAutoUpdate[poolId] = uint40(block.timestamp);
}
/* ==============================
:::: PRIVATE VIEW / PURE ::::
============================== */
/// @notice Computes the pending (unclaimed) reward amount for a given staked position.
/// @dev Uses the pool's `accRewardPerShare` and `lastRewardTime` to accrue rewards since the last update.
/// If the position has `liquidity == 0`, returns 0. If the pool has no elapsed time or no reward
/// configuration (rewardRate/totalStaked), it returns the value implied by current `accRewardPerShare`.
/// Pending rewards are calculated as: (liquidity * acc / PRECISION) - rewardDebt.
/// @param tokenId The staked NFT/token id whose rewards are being queried.
/// @param pid The pool id/index for which the reward accounting is performed.
/// @return The amount of rewards pending for the position.
function _pendingRewards(
uint64 tokenId,
uint8 pid
)
private
view
returns(uint256)
{
StakedPosition memory pos = stakedPositions_[pid][tokenId];
if (pos.liquidity == 0) return 0;
uint256 acc = poolInfo[pid].accRewardPerShare;
uint256 lastTime = uint256(poolInfo[pid].lastRewardTime);
uint256 elapsed = block.timestamp - lastTime;
if (elapsed > 0 && poolInfo[pid].rewardRate > 0 && poolInfo[pid].totalStaked > 0) {
uint256 rewardRate = uint256(poolInfo[pid].rewardRate);
acc += (elapsed * rewardRate * PRECISION) / poolInfo[pid].totalStaked;
}
return (uint256(pos.liquidity) * acc / PRECISION) - uint256(pos.rewardDebt);
}
/// @notice Checks the health status and retrieves pricing data for a specific liquidity pool.
/// @dev Validates pool existence and configuration, then queries current price and tick from the pool contract.
/// Health is determined by whether the current tick falls within the expected staking position bounds.
/// @param pid The pool identifier (0-4)
/// @return isHealthy True if the current tick is within the valid staking position range, false otherwise
/// @return sqrtPriceX96 The current sqrt price of the pool (Q64.96 fixed-point format)
/// @return currentTick The current tick of the pool
function _poolRatio(
uint8 pid
)
private
view
returns(
bool isHealthy,
uint160 sqrtPriceX96,
int24 currentTick
)
{
if (pid > BridgeXLib.MAX_POOL_ID) return (false, 0, 0);
(address token0, address token1, bool bxl) = BridgeXLib._getPoolTokensAndType(pid, BXUSD_TOKEN, BXETH_TOKEN, BXBTC_TOKEN);
if (token0 == address(0) || token1 == address(0)) return (false, 0, 0);
address pool = Iv3PosManager(FACTORY).getPool(token0, token1, LP_FEE_TIER);
if (pool == address(0)) return (false, 0, 0);
(sqrtPriceX96, currentTick, , , , , ) = ILibertyV3Pool(pool).slot0();
int24 lt = BridgeXLib._getStakeTick(pid, false, bxl);
int24 ut = BridgeXLib._getStakeTick(pid, true, bxl);
isHealthy = (currentTick > lt && currentTick < ut);
return (isHealthy, sqrtPriceX96, currentTick);
}
/// @dev Reverts if the pool ratio is not healthy.
/// @param pid Pool id to check.
function _requireFairRatio(
uint8 pid
)
private
view
{
(bool isHealthy, , ) = _poolRatio(pid);
if (!isHealthy) revert();
}
/// @dev Computes the target reward rate (per second) for a pool given its TVL and configured target APR.
/// @param poolId The pool id (0..2).
/// @param tvl Total value locked (total tokens) for the pool.
/// @return uint256 Target reward rate per second (minimum 1 when tvl == 0).
function _getTargetRewardRate(
uint8 poolId,
uint256 tvl
)
internal
view
returns(uint256)
{
if (tvl == 0) return 1;
uint256 targetBps = targetAprBps[poolId];
uint256 maxApr = block.timestamp > BONUS_APR_ENDS
? MAX_NORMAL_APR
: MAX_BONUS_APR;
if (targetBps > maxApr) targetBps = uint16(maxApr);
return (tvl * targetBps) / (SECONDS_PER_YEAR * 10000);
}
/* ==============================
:::: PRIVATE WRITE ::::
============================== */
/// @dev Checks if automatic rate adjustment cooldown has passed and triggers an auto-adjust if needed.
/// @param poolId The pool id to check and possibly adjust (0..2).
function _adjustRateIfNeeded(
uint8 poolId
)
private
{
uint256 nextUpdate = uint256(lastAutoUpdate[poolId]) + RATE_COOLDOWN;
if (block.timestamp < nextUpdate) return;
_performAutoAdjust(poolId, false);
}
/// @dev Performs automatic reward-rate adjustment for a pool. Caps the target rate by configured maximums and monthly limits, updates pool state, and //emits RewardRateUpdated when changed.
/// @param poolId The pool id to adjust (0..2).
/// @param shouldUpdatePool If true, calls _updatePool(poolId) before calculating adjustments.
function _performAutoAdjust(
uint8 poolId,
bool shouldUpdatePool
)
private
{
if (poolId > BridgeXLib.MAX_POOL_ID) return;
if (shouldUpdatePool) _updatePool(poolId);
PoolInfo storage pool = poolInfo[poolId];
uint256 tvl = pool.totalTokens;
if (tvl == 0) return;
uint256 targetBps = targetAprBps[poolId];
uint256 targetRate = (tvl * targetBps) / (SECONDS_PER_YEAR * 10000);
uint256 maxApr = block.timestamp > BONUS_APR_ENDS
? MAX_NORMAL_APR
: MAX_BONUS_APR;
uint256 maxAllowedRate = (tvl * maxApr) / (SECONDS_PER_YEAR * 10000);
if (targetRate > maxAllowedRate)
targetRate = maxAllowedRate;
uint256 maxMonthly = poolId <= 2 ? MAX_30D_RATE_BXUSD / SECONDS_PER_MONTH :
poolId == 3 ? MAX_30D_RATE_BXETH / SECONDS_PER_MONTH :
MAX_30D_RATE_BXBTC / SECONDS_PER_MONTH;
if (targetRate > maxMonthly) targetRate = maxMonthly;
// Only update if rate actually changes
if (targetRate != uint256(pool.rewardRate)) {
pool.rewardRate = BridgeXLib._safe96(targetRate);
lastAutoUpdate[poolId] = uint40(block.timestamp);
emit RewardRateUpdated(poolId, pool.rewardRate);
}
}
/// @dev Updates internal staking records when a new position is staked: records owner, liquidity, tokens, rewardDebt, stake time, and updates aggregate pool totals. Also appends the position ref to the user's refs.
/// @param poolId Pool id where the position is staked.
/// @param tokenId NFT token id of the staked position.
/// @param liquidity Liquidity amount staked.
/// @param tokens Amount of tokens (principal) corresponding to the stake.
function _updateStakePosition(
uint8 poolId,
uint256 tokenId,
uint256 liquidity,
uint256 tokens
)
private
{
uint256 acc = poolInfo[poolId].accRewardPerShare;
stakedPositions_[poolId][uint64(tokenId)] = StakedPosition({
owner: msg.sender,
liquidity: BridgeXLib._safe128(liquidity),
tokens: BridgeXLib._safe128(tokens),
rewardDebt: BridgeXLib._safe128((acc * liquidity) / PRECISION),
stakeTime: uint40(block.timestamp),
poolId: poolId
});
userStakedRefs_[msg.sender].push(PositionRef({
pid: poolId,
tid: uint64(tokenId)
}));
unchecked {
poolInfo[poolId].totalStaked += liquidity;
poolInfo[poolId].totalTokens += tokens;
}
}
/// @dev Updates pool accumulators: increases accRewardPerShare based on elapsed time and rewardRate, and refreshes lastRewardTime. If totalStaked == 0, sets lastRewardTime to now.
/// @param poolId The pool id to update.
function _updatePool(
uint8 poolId
)
private
{
PoolInfo storage pool = poolInfo[poolId];
if (pool.totalStaked == 0) {
pool.lastRewardTime = uint40(block.timestamp);
return;
}
uint256 elapsed = block.timestamp - uint256(pool.lastRewardTime);
if (elapsed == 0) return;
unchecked {
// Safe: permanent core stake + rewardRate caps + auto-adjust
// Intermediate values stay well below uint256 max
uint256 rew = elapsed * uint256(pool.rewardRate);
uint256 rpl = (rew * PRECISION) / pool.totalStaked;
pool.accRewardPerShare += rpl;
}
pool.lastRewardTime = uint40(block.timestamp);
}
/// @dev Sets a referrer for a sender if none exists, increments the referrer's count, and //emits ReferralRegistered. Uses BX_TREASURY when referrer is zero address.
/// @param referrer The referrer address to register.
/// @param sender The address receiving the referral.
function _setRef(
address referrer,
address sender
)
private
{
if (referrer == address(0)) referrer = BX_TREASURY;
if (userReferrer_[sender] == address(0)) {
userReferrer_[sender] = referrer;
unchecked {
referralsPerUser_[referrer]++;
}
}
}
/// @dev Claims pending rewards for a staked position, mints reward tokens to the user and optionally to their referrer, updates rewardDebt, and //emits Claimed.
/// @param pos Storage reference to the staked position struct.
/// @param sid Staked position id (token id) used in the Claimed event.
/// @param sender The address claiming rewards (position owner).
/// @param pid The pool id the position belongs to.
function _claimRewards(
StakedPosition storage pos,
uint64 sid,
address sender,
uint8 pid
)
private
{
uint256 pending = (uint256(pos.liquidity) * poolInfo[pid].accRewardPerShare / PRECISION) - uint256(pos.rewardDebt);
if (pending == 0) {
pos.rewardDebt = BridgeXLib._safe128((uint256(pos.liquidity) * poolInfo[pid].accRewardPerShare) / PRECISION);
emit StakeEvent(sid, pid, 0, 1);
return;
}
address referrer = userReferrer_[sender];
address rewardTkn = poolInfo[pid].rewardToken;
if (referrer == address(0) || referrer == sender) {
ISecureLayerToken(rewardTkn).mint(sender, pending);
} else {
uint256 referralShare = pending * BridgeXLib.REFERRAL_RATE_BPS / 10000;
if (referralShare > 0) ISecureLayerToken(rewardTkn).mint(referrer, referralShare);
uint256 userShare = pending - referralShare;
if (userShare > 0) ISecureLayerToken(rewardTkn).mint(sender, userShare);
}
pos.rewardDebt = uint128((uint256(pos.liquidity) * poolInfo[pid].accRewardPerShare) / PRECISION);
emit StakeEvent(sid, pid, pending, 1);
}
/* ===================================
:::: PRIVATE_FUNCTIONS
CALLED_DURING_DEPLOYMENT ::::
=====================================*/
/// @notice Initializes the stored pool configuration for a specific pool id.
/// @dev Sets token addresses, reward token, reward rate, and starting reward timestamp.
/// Also stores the target APR in basis points. Intended to be called by internal setup logic.
/// @param id Pool index to initialize.
/// @param t0 Token0 address for the V3 pool/position.
/// @param t1 Token1 address for the V3 pool/position.
/// @param rt Reward token address for the pool.
/// @param apr Target APR in basis points (BPS) for the pool.
/// @param rw Reward rate passed in as `uint256`, stored as `uint96`.
function _initPool(
uint8 id,
address t0,
address t1,
address rt,
uint256 apr,
uint256 rw
)
private
{
poolInfo[id].token0 = t0;
poolInfo[id].token1 = t1;
poolInfo[id].rewardToken = rt;
poolInfo[id].rewardRate = uint96(rw);
poolInfo[id].lastRewardTime = uint40(block.timestamp);
targetAprBps[id] = uint16(apr);
}
/// @notice Creates a single-sided Uniswap V3 liquidity position (used for core and initial staking positions)
/// @dev Called only during deployment. Handles both core (initial pool liquidity)
/// and non-core (initial staked position owned by contract) creation.
/// Uses precomputed sqrt prices and tick ranges.
/// @param poolId The pool to create liquidity for
/// @param token0 First token in the pool
/// @param token1 Second token in the pool
/// @param isCore Whether this is the core protocol-owned position
function _createSingleSided(
uint8 poolId,
address token0,
address token1,
bool isCore
)
private
{
(uint160 sqrtPriceX96, , uint256 amount, , bool bxl) = BridgeXLib._getCoreConfigs(poolId, BXUSD_TOKEN, BXETH_TOKEN, BXBTC_TOKEN, isCore);
uint256 tokenId;
uint256 liquidity;
{
(int24 tl, int24 tu) = BridgeXLib._getCoreTicks(poolId, bxl, isCore);
Iv3PosManager.MintParams memory params = Iv3PosManager.MintParams({
token0: token0,
token1: token1,
fee: LP_FEE_TIER,
tickLower: tl,
tickUpper: tu,
amount0Desired: bxl ? amount : 0,
amount1Desired: !bxl ? amount : 0,
amount0Min: bxl ? amount - 1e15 : 0,
amount1Min: !bxl ? amount - 1e15 : 0,
recipient: address(this),
deadline: block.timestamp + 1 hours
});
if (isCore) Iv3PosManager(POS_MANAGER).createAndInitializePoolIfNecessary(token0, token1, LP_FEE_TIER, sqrtPriceX96);
(tokenId, liquidity, , ) = Iv3PosManager(POS_MANAGER).mint(params);
}
if (isCore) {
initialPositionIds[poolId] = uint64(tokenId);
} else {
userReferrer_[address(this)] = address(this);
_updatePool(poolId);
stakedPositions_[poolId][uint64(tokenId)] = StakedPosition({
owner: address(this),
liquidity: uint128(liquidity),
tokens: uint128(amount),
rewardDebt: uint128((poolInfo[poolId].accRewardPerShare * uint256(liquidity)) / PRECISION),
stakeTime: uint40(block.timestamp),
poolId: poolId
});
userStakedRefs_[address(this)].push(PositionRef({
pid: poolId,
tid: uint64(tokenId)
}));
poolInfo[poolId].totalStaked += liquidity;
poolInfo[poolId].totalTokens += amount;
}
}
/* ==============================
:::: FALLBACK_FUNCTIONS ::::
============================== */
/// @dev Rejects plain Ether transfers; always reverts.
receive() external payable {
revert();
}
/// @dev Rejects calls to nonexistent functions or empty calldata; always reverts.
fallback() external payable {
revert();
}
}