// SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @notice Valueless test collateral. Only the protocol can mint; never use as real USDT. contract LostTestUSD is ERC20 { address public immutable protocol; constructor() ERC20("LostTrade Test USDT", "tUSDT") { protocol = msg.sender; } function decimals() public pure override returns (uint8) { return 6; } function mint(address to, uint256 amount) external { require(msg.sender == protocol, "Protocol only"); _mint(to, amount); } } /// @notice TESTNET ONLY. Admin-controlled price, capped synthetic PnL, accelerated vesting. /// Not a production exchange, oracle, anti-sybil system or audited financial product. contract LostTradeTestnet is ERC20, Ownable2Step, ReentrancyGuard { using SafeERC20 for IERC20; uint256 public constant MAX_SUPPLY = 100_000_000 ether; uint256 public constant MAX_LEVERAGE = 1000; uint256 public constant MAX_OPEN_INTEREST = 50_000e6; uint256 public constant LOST_FAUCET_AMOUNT = 1_000 ether; uint256 public constant LOST_FAUCET_CAP = 1_000_000 ether; uint256 public constant TIME_DAILY_CAP = 16_000 ether; uint256 public constant LOSS_DAILY_CAP = 4_000 ether; uint256 public constant MIN_HOLD = 60; uint256 public constant VERIFY_DELAY = 120; uint256 public constant VESTING_DURATION = 600; uint256 public constant STAKE_DELAY = 60; uint256 public constant EXIT_DELAY = 120; uint256 public constant PRICE_MAX_AGE = 1 hours; uint256 public constant MIN_RESERVE = 100_000e6; uint256 public constant ACC = 1e30; LostTestUSD public immutable collateral; uint256 public price = 67_000e8; uint256 public priceUpdatedAt; uint256 public priceVersion = 1; uint256 public reserve; uint256 public lockedProfit; uint256 public totalMargins; uint256 public totalNotional; uint256 public totalFees; uint256 public treasury; uint256 public lossCarryforward; uint256 public dividendVault; uint256 public accDividendPerStake; uint256 public totalStaked; uint256 public totalPendingStake; uint256 public totalExitingStake; uint256 public rewardEscrow; bool public paused; struct Position { uint256 margin; uint256 notional; uint256 entry; uint256 openedAt; uint256 openingFee; bool isLong; } struct Lot { uint256 amount; uint256 claimed; uint256 unlockAt; uint256 timeReward; } struct Stake { uint256 active; uint256 pending; uint256 activateAt; uint256 exiting; uint256 exitAt; uint256 rewardDebt; uint256 accrued; } mapping(address => Position) public positions; mapping(address => Lot[]) private lots; mapping(address => Stake) public stakes; mapping(address => int256) public cumulativePnl; mapping(address => uint256) public lossWatermark; mapping(address => uint256) public nextFaucetAt; mapping(uint256 => uint256) public dailyTimeMinted; mapping(uint256 => uint256) public dailyLossMinted; mapping(address => uint256) public nextLostFaucetAt; uint256 public lostFaucetMinted; event LostFaucetClaimed(address indexed user, uint256 amount, uint256 nextClaimAt); event PriceUpdated(uint256 price, uint256 version); event Opened(address indexed user, bool isLong, uint256 margin, uint256 notional, uint256 price); event Closed(address indexed user, int256 pnl, uint256 payout, uint256 fee, uint256 timeReward, uint256 lossReward); event RewardCreated(address indexed user, uint256 indexed lot, uint256 amount, uint256 unlockAt); event RewardClaimed(address indexed user, uint256 indexed lot, uint256 amount); event FeeAllocated(uint256 fee, uint256 reservePart, uint256 dividendPart, uint256 treasuryPart); event StakeQueued(address indexed user, uint256 amount, uint256 activateAt); event StakeActivated(address indexed user, uint256 amount); event ExitQueued(address indexed user, uint256 amount, uint256 exitAt); event StakeWithdrawn(address indexed user, uint256 amount); event DividendClaimed(address indexed user, uint256 amount); event Paused(bool paused); constructor() ERC20("Lost", "LOST") Ownable(msg.sender) { require(block.chainid == 97 || block.chainid == 31337, "Test chains only"); collateral = new LostTestUSD(); reserve = 1_000_000e6; collateral.mint(address(this), reserve); priceUpdatedAt = block.timestamp; } function version() external pure returns (string memory) { return "LostTrade BSC testnet 0.4.0"; } /// @notice Test-only distribution. Separate from mining quotas; included in MAX_SUPPLY. function lostFaucet() external { require(block.timestamp >= nextLostFaucetAt[msg.sender], "LOST faucet cooldown 24h"); require(lostFaucetMinted + LOST_FAUCET_AMOUNT <= LOST_FAUCET_CAP, "LOST faucet exhausted"); require(totalSupply() + LOST_FAUCET_AMOUNT <= MAX_SUPPLY, "LOST supply cap"); nextLostFaucetAt[msg.sender] = block.timestamp + 1 days; lostFaucetMinted += LOST_FAUCET_AMOUNT; _mint(msg.sender, LOST_FAUCET_AMOUNT); emit LostFaucetClaimed(msg.sender, LOST_FAUCET_AMOUNT, nextLostFaucetAt[msg.sender]); } function faucet() external { require(block.timestamp >= nextFaucetAt[msg.sender], "Faucet cooldown 24h"); nextFaucetAt[msg.sender] = block.timestamp + 1 days; collateral.mint(msg.sender, 1_000e6); } function setPrice(uint256 nextPrice) external onlyOwner { require(nextPrice >= 1_000e8 && nextPrice <= 1_000_000e8, "Price out of bounds"); price = nextPrice; priceUpdatedAt = block.timestamp; priceVersion++; emit PriceUpdated(nextPrice, priceVersion); } function setPaused(bool value) external onlyOwner { paused = value; emit Paused(value); } function _fresh() internal view { require(block.timestamp <= priceUpdatedAt + PRICE_MAX_AGE, "Test price stale"); } function _min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } function _fee(uint256 amount) internal { if (amount == 0) return; totalFees += amount; // Cover realized historical deficits first; do not distribute user deposits or token emissions. uint256 repair = _min(amount, lossCarryforward); lossCarryforward -= repair; uint256 distributable = amount - repair; uint256 div = totalStaked > 0 && reserve >= MIN_RESERVE ? distributable * 20 / 100 : 0; uint256 treas = reserve >= MIN_RESERVE ? distributable * 10 / 100 : 0; uint256 risk = amount - div - treas; reserve += risk; treasury += treas; if (div > 0) { dividendVault += div; accDividendPerStake += div * ACC / totalStaked; } emit FeeAllocated(amount, risk, div, treas); } function openPosition(uint256 margin, uint256 leverage, bool isLong, uint256 expectedVersion) external nonReentrant { require(!paused, "New positions paused"); _fresh(); require(priceVersion == expectedVersion, "Price changed; refresh"); require(positions[msg.sender].margin == 0, "One position per wallet"); require(margin >= 10e6 && margin <= 1000e6 && leverage >= 1 && leverage <= MAX_LEVERAGE, "Invalid size or leverage"); uint256 notional = margin * leverage; require(totalNotional + notional <= MAX_OPEN_INTEREST, "Open interest cap"); require(reserve >= lockedProfit + margin + MIN_RESERVE, "Insufficient free reserve"); uint256 fee = notional * 2 / 10_000; IERC20(address(collateral)).safeTransferFrom(msg.sender, address(this), margin + fee); positions[msg.sender] = Position(margin, notional, price, block.timestamp, fee, isLong); totalMargins += margin; totalNotional += notional; lockedProfit += margin; _fee(fee); emit Opened(msg.sender, isLong, margin, notional, price); } /// @return pnl Capped market PnL excluding fees; each trade's profit and loss are capped at its margin. function quoteClose(address user) public view returns (int256 pnl, uint256 payout, uint256 closeFee, uint256 holdingFee) { Position memory p = positions[user]; if (p.margin == 0) return (0,0,0,0); pnl = (int256(price) - int256(p.entry)) * int256(p.notional) / int256(p.entry); if (!p.isLong) pnl = -pnl; if (pnl > int256(p.margin)) pnl = int256(p.margin); if (pnl < -int256(p.margin)) pnl = -int256(p.margin); uint256 equity = uint256(int256(p.margin) + pnl); holdingFee = _min(p.notional * (block.timestamp - p.openedAt) / (100_000 * 1 hours), equity); closeFee = _min(p.notional * 2 / 10_000, equity - holdingFee); payout = equity - holdingFee - closeFee; } function closePosition(uint256 expectedVersion) external nonReentrant { _fresh(); require(priceVersion == expectedVersion, "Price changed; refresh"); _close(msg.sender); } function liquidate(address user) external nonReentrant { _fresh(); (,uint256 payout,,) = quoteClose(user); require(payout == 0, "Position healthy"); _close(user); } function _close(address user) internal { Position memory p = positions[user]; require(p.margin > 0, "No position"); (int256 pnl, uint256 payout, uint256 closeFee, uint256 holdingFee) = quoteClose(user); delete positions[user]; totalMargins -= p.margin; totalNotional -= p.notional; lockedProfit -= p.margin; if (pnl > 0) { reserve -= uint256(pnl); lossCarryforward += uint256(pnl); } else { uint256 gain = uint256(-pnl); reserve += gain; lossCarryforward -= _min(lossCarryforward, gain); } _fee(closeFee + holdingFee); cumulativePnl[user] += pnl; uint256 netLoss = cumulativePnl[user] < 0 ? uint256(-cumulativePnl[user]) : 0; uint256 newLoss = netLoss > lossWatermark[user] ? netLoss - lossWatermark[user] : 0; if (netLoss > lossWatermark[user]) lossWatermark[user] = netLoss; (uint256 timeReward, uint256 lossReward) = _rewards(user, p, newLoss, closeFee + holdingFee, pnl); if (payout > 0) IERC20(address(collateral)).safeTransfer(user, payout); emit Closed(user, pnl, payout, closeFee + holdingFee, timeReward, lossReward); } function _rewards(address user, Position memory p, uint256 newLoss, uint256 paidCloseFees, int256 pnl) internal returns (uint256 t, uint256 l) { uint256 held = block.timestamp - p.openedAt; if (held < MIN_HOLD) return (0,0); // .05 tUSDT reference price: rewards' bookkeeping value <=20% of paid fees. // No token price guarantee. Fixed quotas, first-settled allocation for this testnet release. uint256 cap = (p.openingFee + paidCloseFees) * 4e12; uint256 effectiveMargin = pnl < 0 ? uint256(int256(p.margin) + pnl) : p.margin; t = _min(effectiveMargin * _min(held, 7 days) * 1e10 / 1 hours, cap * 80 / 100); l = _min(newLoss * 1e11, cap * 20 / 100); uint256 day = block.timestamp / 1 days; t = _min(t, TIME_DAILY_CAP - dailyTimeMinted[day]); l = _min(l, LOSS_DAILY_CAP - dailyLossMinted[day]); uint256 remaining = MAX_SUPPLY - totalSupply(); t = _min(t, remaining); l = _min(l, remaining - t); dailyTimeMinted[day] += t; dailyLossMinted[day] += l; if (t + l == 0) return (t,l); _mint(address(this), t + l); rewardEscrow += t + l; lots[user].push(Lot(t + l, 0, block.timestamp + VERIFY_DELAY, t)); emit RewardCreated(user, lots[user].length - 1, t + l, block.timestamp + VERIFY_DELAY); } function lotCount(address user) external view returns (uint256) { return lots[user].length; } function rewardLot(address user, uint256 id) external view returns (Lot memory) { return lots[user][id]; } function claimable(address user, uint256 id) public view returns (uint256) { Lot memory l = lots[user][id]; if (block.timestamp < l.unlockAt) return 0; uint256 vested = l.amount * 20 / 100 + (l.amount - l.amount * 20 / 100) * _min(block.timestamp - l.unlockAt, VESTING_DURATION) / VESTING_DURATION; return vested - l.claimed; } function claimRewards(uint256[] calldata ids) external nonReentrant { require(ids.length > 0 && ids.length <= 30, "Batch size 1..30"); uint256 amount; for (uint256 i; i < ids.length; ++i) { uint256 earned = claimable(msg.sender, ids[i]); lots[msg.sender][ids[i]].claimed += earned; amount += earned; if (earned > 0) emit RewardClaimed(msg.sender, ids[i], earned); } require(amount > 0, "Nothing unlocked"); rewardEscrow -= amount; _transfer(address(this), msg.sender, amount); } function pendingDividend(address user) public view returns (uint256) { Stake memory s = stakes[user]; return s.accrued + s.active * accDividendPerStake / ACC - s.rewardDebt; } function _checkpoint(address user) internal { Stake storage s = stakes[user]; s.accrued = pendingDividend(user); s.rewardDebt = s.active * accDividendPerStake / ACC; } function queueStake(uint256 amount) external nonReentrant { require(amount > 0, "Zero stake"); Stake storage s = stakes[msg.sender]; require(s.pending == 0, "Activate or cancel pending stake first"); _transfer(msg.sender, address(this), amount); s.pending = amount; s.activateAt = block.timestamp + STAKE_DELAY; totalPendingStake += amount; emit StakeQueued(msg.sender, amount, s.activateAt); } function cancelPendingStake() external nonReentrant { Stake storage s = stakes[msg.sender]; uint256 amount = s.pending; require(amount > 0, "No pending stake"); s.pending = 0; s.activateAt = 0; totalPendingStake -= amount; _transfer(address(this), msg.sender, amount); } function activateStake() external nonReentrant { Stake storage s = stakes[msg.sender]; require(s.pending > 0 && block.timestamp >= s.activateAt, "Stake not ready"); _checkpoint(msg.sender); uint256 amount = s.pending; s.active += amount; totalStaked += amount; totalPendingStake -= amount; s.pending = 0; s.activateAt = 0; s.rewardDebt = s.active * accDividendPerStake / ACC; emit StakeActivated(msg.sender, amount); } function requestUnstake(uint256 amount) external nonReentrant { Stake storage s = stakes[msg.sender]; require(amount > 0 && amount <= s.active && s.exiting == 0, "Invalid unstake or pending exit"); _checkpoint(msg.sender); s.active -= amount; totalStaked -= amount; s.exiting = amount; totalExitingStake += amount; s.exitAt = block.timestamp + EXIT_DELAY; s.rewardDebt = s.active * accDividendPerStake / ACC; emit ExitQueued(msg.sender, amount, s.exitAt); } function withdrawStake() external nonReentrant { Stake storage s = stakes[msg.sender]; require(s.exiting > 0 && block.timestamp >= s.exitAt, "Exit not ready"); uint256 amount = s.exiting; s.exiting = 0; s.exitAt = 0; totalExitingStake -= amount; _transfer(address(this), msg.sender, amount); emit StakeWithdrawn(msg.sender, amount); } function claimDividend() external nonReentrant { _checkpoint(msg.sender); Stake storage s = stakes[msg.sender]; uint256 amount = s.accrued; require(amount > 0, "No dividend"); s.accrued = 0; dividendVault -= amount; IERC20(address(collateral)).safeTransfer(msg.sender, amount); emit DividendClaimed(msg.sender, amount); } function accountedCollateral() public view returns (uint256) { return reserve + totalMargins + dividendVault + treasury; } function accountedLost() public view returns (uint256) { return rewardEscrow + totalStaked + totalPendingStake + totalExitingStake; } }