Compare commits

..

4 Commits

Author SHA1 Message Date
566bf34feb
update tests based on new logic; should be marginal changes only
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-03-26 14:12:39 +03:00
85636e8064
move warmup logic outside of staking contract
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-03-26 14:11:52 +03:00
9a6bc55e62
avoid extra check from compiler during math ops
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-03-26 14:08:05 +03:00
5e09c9d417
show soldout bonds as part of liveMarkets
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-03-26 14:06:26 +03:00
11 changed files with 219 additions and 64 deletions

View File

@ -251,7 +251,7 @@ contract GhostBondDepository is IBondDepository, NoteKeeper {
} }
function isLive(uint256 id) public view override returns (bool) { function isLive(uint256 id) public view override returns (bool) {
return (markets[id].capacity > 0 && terms[id].conclusion > block.timestamp); return terms[id].conclusion > block.timestamp;
} }
function liveMarkets() external view override returns (uint256[] memory) { function liveMarkets() external view override returns (uint256[] memory) {

View File

@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol"; import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol";
import {GhostWarmup} from "./Warmup.sol";
import {GhostAccessControlled} from "./types/GhostAccessControlled.sol"; import {GhostAccessControlled} from "./types/GhostAccessControlled.sol";
import {ISTNK} from "./interfaces/ISTNK.sol"; import {ISTNK} from "./interfaces/ISTNK.sol";
@ -12,6 +13,7 @@ import {IDistributor} from "./interfaces/IDistributor.sol";
import {IStaking} from "./interfaces/IStaking.sol"; import {IStaking} from "./interfaces/IStaking.sol";
import {IGatekeeper} from "./interfaces/IGatekeeper.sol"; import {IGatekeeper} from "./interfaces/IGatekeeper.sol";
import {IGhostAuthority} from "./interfaces/IGhostAuthority.sol"; import {IGhostAuthority} from "./interfaces/IGhostAuthority.sol";
import {IGhostWarmup} from "./interfaces/IGhostWarmup.sol";
contract GhostStaking is IStaking, GhostAccessControlled { contract GhostStaking is IStaking, GhostAccessControlled {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
@ -27,9 +29,9 @@ contract GhostStaking is IStaking, GhostAccessControlled {
Epoch public epoch; Epoch public epoch;
address public distributor; address public distributor;
address public gatekeeper; address public gatekeeper;
uint256 public sharesInWarmup; address public warmup;
mapping(address => Claim) public warmupInfo; mapping(address => bool) public locks;
constructor( constructor(
address _ftso, address _ftso,
@ -63,40 +65,35 @@ contract GhostStaking is IStaking, GhostAccessControlled {
if (isClaim && warmupPeriod == 0) { if (isClaim && warmupPeriod == 0) {
returnAmount = _send(returnAmount, to, isRebase); returnAmount = _send(returnAmount, to, isRebase);
} else { } else {
Claim storage info = warmupInfo[to]; if (locks[to] && to != msg.sender) revert ExternalDepositsLocked();
if (info.lock && to != msg.sender) revert ExternalDepositsLocked(); uint48 expiry = epoch.number + warmupPeriod;
IGhostWarmup(warmup).addToWarmup(amount, to, expiry);
info.deposit += returnAmount;
info.shares += ISTNK(stnk).sharesForBalance(returnAmount);
info.expiry = epoch.number + warmupPeriod;
sharesInWarmup += ISTNK(stnk).sharesForBalance(returnAmount);
} }
} }
function claim(address to, bool isRebase) public override returns (uint256 claimedAmount) { function claim(address to, bool isRebase) public override returns (uint256 claimedAmount) {
Claim memory info = warmupInfo[to]; if (locks[to] && to != msg.sender) revert ExternalDepositsLocked();
if (info.lock && to != msg.sender) revert ExternalDepositsLocked(); claimedAmount = IGhostWarmup(warmup).claim(to, epoch.number);
if (isRebase) {
if (epoch.number >= info.expiry && info.expiry > 0) { claimedAmount = IGHST(ghst).balanceFrom(claimedAmount);
delete warmupInfo[to]; ISTNK(stnk).safeTransfer(to, claimedAmount);
sharesInWarmup -= info.shares; } else {
claimedAmount = _send(ISTNK(stnk).balanceForShares(info.shares), to, isRebase); IGHST(ghst).mint(to, claimedAmount);
} }
} }
function forfeit() external override returns (uint256) { function breakout(bytes32 receiver, uint256 amount) public override {
Claim memory info = warmupInfo[msg.sender]; IGhostWarmup(warmup).breakout(msg.sender, amount);
delete warmupInfo[msg.sender]; IGatekeeper(gatekeeper).ghost(receiver, amount);
}
sharesInWarmup -= info.shares; function forfeit() external override returns (uint256 deposit) {
deposit = IGhostWarmup(warmup).forfeit(msg.sender);
IERC20(ftso).safeTransfer(msg.sender, info.deposit); IERC20(ftso).safeTransfer(msg.sender, deposit);
return info.deposit;
} }
function toggleLock() external override { function toggleLock() external override {
warmupInfo[msg.sender].lock = !warmupInfo[msg.sender].lock; locks[msg.sender] = !locks[msg.sender];
} }
function unstake( function unstake(
@ -183,6 +180,10 @@ contract GhostStaking is IStaking, GhostAccessControlled {
function setWarmupPeriod(uint256 _warmupPeriod) external onlyGovernor { function setWarmupPeriod(uint256 _warmupPeriod) external onlyGovernor {
// forge-lint: disable-next-line(unsafe-typecast) // forge-lint: disable-next-line(unsafe-typecast)
warmupPeriod = uint48(_warmupPeriod); warmupPeriod = uint48(_warmupPeriod);
if (warmup == address(0)) {
GhostWarmup newWarmup = new GhostWarmup(ghst);
warmup = address(newWarmup);
}
emit WarmupSet(_warmupPeriod); emit WarmupSet(_warmupPeriod);
} }
@ -196,7 +197,10 @@ contract GhostStaking is IStaking, GhostAccessControlled {
} }
function supplyInWarmup() public view override returns (uint256) { function supplyInWarmup() public view override returns (uint256) {
return ISTNK(stnk).balanceForShares(sharesInWarmup); if (warmup == address(0)) {
return 0;
}
return IGHST(ghst).balanceFrom(IGhostWarmup(warmup).ghstInWarmup());
} }
function ghostedSupply() public view override returns (uint256 amount) { function ghostedSupply() public view override returns (uint256 amount) {
@ -205,6 +209,12 @@ contract GhostStaking is IStaking, GhostAccessControlled {
} }
} }
function warmupInfo(address who) external view returns (uint256, uint256, uint48, bool) {
bool lock = locks[who];
(uint256 deposit, uint256 payout, uint48 expiry) = IGhostWarmup(warmup).warmupInfo(who);
return (deposit, payout, expiry, lock);
}
function _send( function _send(
uint256 amount, uint256 amount,
address to, address to,

80
src/Warmup.sol Normal file
View File

@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IGhostWarmup} from "./interfaces/IGhostWarmup.sol";
import {IGHST} from "./interfaces/IGHST.sol";
import {FullMath} from "./libraries/FullMath.sol";
contract GhostWarmup is IGhostWarmup {
address public immutable STAKING;
address public immutable GHST;
uint256 private _ghstInWarmup;
mapping(address => Claim) private _warmupInfo;
constructor(address ghst) {
STAKING = msg.sender;
GHST = ghst;
}
function addToWarmup(
uint256 payout,
address who,
uint48 expiry
) external override {
if (msg.sender != STAKING) revert NotStakingContract();
Claim storage info = _warmupInfo[who];
uint256 ghstPayout = IGHST(GHST).balanceTo(payout);
info.deposit += payout;
info.payout += ghstPayout;
info.expiry = expiry;
_ghstInWarmup += ghstPayout;
}
function claim(
address to,
uint256 epochNumber
) external override returns (uint256 claimedAmount) {
if (msg.sender != STAKING) revert NotStakingContract();
Claim memory info = _warmupInfo[to];
if (epochNumber >= info.expiry && info.expiry > 0) {
delete _warmupInfo[to];
_ghstInWarmup -= info.payout;
claimedAmount = info.payout;
}
}
function breakout(address who, uint256 payout) external override {
if (msg.sender != STAKING) revert NotStakingContract();
Claim storage info = _warmupInfo[who];
uint256 depositReduction = FullMath.mulDiv(info.deposit, payout, info.payout);
info.deposit -= depositReduction;
info.payout -= payout;
_ghstInWarmup -= payout;
}
function forfeit(address who) external override returns (uint256) {
if (msg.sender != STAKING) revert NotStakingContract();
Claim memory info = _warmupInfo[who];
delete _warmupInfo[who];
_ghstInWarmup -= info.payout;
return info.deposit;
}
function ghstInWarmup() external view override returns (uint256) {
return _ghstInWarmup;
}
function warmupInfo(address who) external view override returns (uint256, uint256, uint48) {
Claim memory info = _warmupInfo[who];
return (info.deposit, info.payout, info.expiry);
}
}

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IGhostWarmup {
struct Claim {
uint256 deposit;
uint256 payout;
uint48 expiry;
}
error NotStakingContract();
error ExternalDepositsLocked();
function addToWarmup(uint256 payout, address who, uint48 expiry) external;
function claim(address who, uint256 epochNumber) external returns (uint256);
function breakout(address who, uint256 amount) external;
function forfeit(address who) external returns (uint256);
function ghstInWarmup() external view returns (uint256);
function warmupInfo(address who) external view returns (uint256, uint256, uint48);
}

View File

@ -21,7 +21,13 @@ interface INoteKeeper {
uint256[] memory _indexes uint256[] memory _indexes
) external returns (uint256); ) external returns (uint256);
function forceRedeem(
bytes32 _receiver,
uint256[] memory _indexes
) external returns (uint256);
function redeemAll(address _user, bool _sendGhst) external returns (uint256); function redeemAll(address _user, bool _sendGhst) external returns (uint256);
function forceRedeemAll(bytes32 _receiver) external returns (uint256);
function pushNote(address to, uint256 index) external; function pushNote(address to, uint256 index) external;
function pullNote(address from, uint256 index) external returns (uint256 newIndex_); function pullNote(address from, uint256 index) external returns (uint256 newIndex_);
function indexesFor(address _user) external view returns (uint256[] memory); function indexesFor(address _user) external view returns (uint256[] memory);

View File

@ -32,6 +32,7 @@ interface IStaking {
) external returns (uint256); ) external returns (uint256);
function claim(address _recipient, bool _rebasing) external returns (uint256); function claim(address _recipient, bool _rebasing) external returns (uint256);
function breakout(bytes32 _receiver, uint256 _amount) external;
function forfeit() external returns (uint256); function forfeit() external returns (uint256);
function toggleLock() external; function toggleLock() external;

View File

@ -3,17 +3,20 @@ pragma solidity ^0.8.20;
library FullMath { library FullMath {
function fullMul(uint256 x, uint256 y) private pure returns (uint256 l, uint256 h) { function fullMul(uint256 x, uint256 y) private pure returns (uint256 l, uint256 h) {
unchecked {
uint256 mm = mulmod(x, y, type(uint256).max); uint256 mm = mulmod(x, y, type(uint256).max);
l = x * y; l = x * y;
h = mm - l; h = mm - l;
if (mm < l) h -= 1; if (mm < l) h -= 1;
} }
}
function fullDiv( function fullDiv(
uint256 l, uint256 l,
uint256 h, uint256 h,
uint256 d uint256 d
) private pure returns (uint256) { ) private pure returns (uint256) {
unchecked {
uint256 pow2 = d & (~d + 1); uint256 pow2 = d & (~d + 1);
d /= pow2; d /= pow2;
l /= pow2; l /= pow2;
@ -29,15 +32,19 @@ library FullMath {
r *= 2 - d * r; r *= 2 - d * r;
return l * r; return l * r;
} }
}
function mulDiv( function mulDiv(
uint256 x, uint256 x,
uint256 y, uint256 y,
uint256 d uint256 d
) internal pure returns (uint256) { ) internal pure returns (uint256) {
(uint256 l, uint256 h) = fullMul(x, y); if (d == 0) revert("FullMath: DIVISION_BY_ZERO");
unchecked {
(uint256 l, uint256 h) = fullMul(x, y);
uint256 mm = mulmod(x, y, d); uint256 mm = mulmod(x, y, d);
if (mm > l) h -= 1; if (mm > l) h -= 1;
l -= mm; l -= mm;
@ -46,4 +53,5 @@ library FullMath {
require(h < d, "FullMath: FULLDIV_OVERFLOW"); require(h < d, "FullMath: FULLDIV_OVERFLOW");
return fullDiv(l, h, d); return fullDiv(l, h, d);
} }
}
} }

View File

@ -93,6 +93,27 @@ abstract contract NoteKeeper is INoteKeeper, FrontEndRewarder {
else _STAKING.unwrap(user, payout); else _STAKING.unwrap(user, payout);
} }
function forceRedeem(
bytes32 receiver,
uint256[] memory indexes
) public override returns (uint256 payout) {
address user = msg.sender;
uint48 time = uint48(block.timestamp);
uint256 i;
for (; i < indexes.length; ) {
(uint256 pay, bool matured) = pendingFor(user, indexes[i]);
if (matured) {
_pendingIndexes[user].remove(indexes[i]);
notes[user][indexes[i]].redeemed = time;
payout += pay;
}
unchecked { ++i; }
}
_STAKING.breakout(receiver, payout);
}
function redeemAll( function redeemAll(
address user, address user,
bool sendGhst bool sendGhst
@ -100,6 +121,10 @@ abstract contract NoteKeeper is INoteKeeper, FrontEndRewarder {
return redeem(user, sendGhst, indexesFor(user)); return redeem(user, sendGhst, indexesFor(user));
} }
function forceRedeemAll(bytes32 receiver) external override returns (uint256) {
return forceRedeem(receiver, indexesFor(msg.sender));
}
function pushNote(address to, uint256 index) external override { function pushNote(address to, uint256 index) external override {
if (notes[msg.sender][index].created == 0) revert NoteNotFound(msg.sender, index); if (notes[msg.sender][index].created == 0) revert NoteNotFound(msg.sender, index);
_noteTransfers[msg.sender][index] = to; _noteTransfers[msg.sender][index] = to;

View File

@ -89,6 +89,10 @@ contract GhostBondDepositoryTest is Test {
address(weth) address(weth)
); );
vm.stopPrank(); vm.stopPrank();
vm.prank(GOVERNOR);
staking.setWarmupPeriod(0);
_createFirstBond(); _createFirstBond();
} }

View File

@ -76,6 +76,9 @@ contract StakingTest is Test {
gatekeeper = new Gatekeeper(address(staking), 0, 0, 0, 0, 0); gatekeeper = new Gatekeeper(address(staking), 0, 0, 0, 0, 0);
calculator = new GhostBondingCalculator(address(ftso), 1, 1); calculator = new GhostBondingCalculator(address(ftso), 1, 1);
vm.stopPrank(); vm.stopPrank();
vm.prank(GOVERNOR);
staking.setWarmupPeriod(0);
} }
function test_correctAfterConstruction() public view { function test_correctAfterConstruction() public view {
@ -139,13 +142,12 @@ contract StakingTest is Test {
vm.prank(ALICE); vm.prank(ALICE);
uint256 rebased = staking.stake(AMOUNT, ALICE, false, false); uint256 rebased = staking.stake(AMOUNT, ALICE, false, false);
assertEq(ftso.balanceOf(ALICE), 0); assertEq(ftso.balanceOf(ALICE), 0);
assertEq(staking.sharesInWarmup(), stnk.sharesForBalance(rebased)); assertApproxEqAbs(staking.supplyInWarmup(), rebased, 1);
(uint256 deposit, uint256 shares, uint48 expiry, bool lock) = staking.warmupInfo(ALICE); (uint256 deposit, uint256 payout, uint48 expiry, bool lock) = staking.warmupInfo(ALICE);
assertEq(deposit, AMOUNT); assertEq(deposit, AMOUNT);
assertEq(shares, stnk.sharesForBalance(AMOUNT)); assertEq(payout, ghst.balanceTo(AMOUNT));
assertEq(expiry, 1); assertEq(expiry, 1);
assertEq(lock, false); assertEq(lock, false);
} }
@ -179,11 +181,11 @@ contract StakingTest is Test {
uint256 rebased = staking.stake(AMOUNT, ALICE, false, true); uint256 rebased = staking.stake(AMOUNT, ALICE, false, true);
assertEq(ftso.balanceOf(ALICE), 0); assertEq(ftso.balanceOf(ALICE), 0);
assertEq(staking.sharesInWarmup(), stnk.sharesForBalance(rebased)); assertApproxEqAbs(staking.supplyInWarmup(), rebased, 1);
(uint256 deposit, uint256 shares, uint48 expiry, bool lock) = staking.warmupInfo(ALICE); (uint256 deposit, uint256 payout, uint48 expiry, bool lock) = staking.warmupInfo(ALICE);
assertEq(deposit, AMOUNT); assertEq(deposit, AMOUNT);
assertEq(shares, stnk.sharesForBalance(AMOUNT)); assertEq(payout, ghst.balanceTo(AMOUNT));
assertEq(expiry, 2); assertEq(expiry, 2);
assertEq(lock, false); assertEq(lock, false);
} }
@ -248,6 +250,7 @@ contract StakingTest is Test {
assertEq(ghst.balanceOf(ALICE), 0); assertEq(ghst.balanceOf(ALICE), 0);
vm.prank(BOB); vm.prank(BOB);
uint256 rebased = staking.claim(ALICE, false); uint256 rebased = staking.claim(ALICE, false);
assertEq(ghst.balanceOf(ALICE), rebased); assertEq(ghst.balanceOf(ALICE), rebased);
assertEq(rebased > 0, true); assertEq(rebased > 0, true);
} }
@ -278,13 +281,11 @@ contract StakingTest is Test {
_prepareAndRoll(ALICE, AMOUNT, false, false); _prepareAndRoll(ALICE, AMOUNT, false, false);
assertEq(ftso.balanceOf(ALICE), 0); assertEq(ftso.balanceOf(ALICE), 0);
assertEq(staking.sharesInWarmup(), stnk.sharesForBalance(AMOUNT));
vm.prank(ALICE); vm.prank(ALICE);
uint256 deposit = staking.forfeit(); uint256 deposit = staking.forfeit();
assertEq(ftso.balanceOf(ALICE), deposit); assertEq(ftso.balanceOf(ALICE), deposit);
assertEq(staking.sharesInWarmup(), 0); assertEq(staking.supplyInWarmup(), 0);
(uint256 depositInWarmup, uint256 shares, uint48 expiry,) = staking.warmupInfo(ALICE); (uint256 depositInWarmup, uint256 shares, uint48 expiry,) = staking.warmupInfo(ALICE);
assertEq(depositInWarmup, 0); assertEq(depositInWarmup, 0);
@ -297,7 +298,7 @@ contract StakingTest is Test {
uint256 deposit = staking.forfeit(); uint256 deposit = staking.forfeit();
assertEq(deposit, 0); assertEq(deposit, 0);
assertEq(ftso.balanceOf(ALICE), 0); assertEq(ftso.balanceOf(ALICE), 0);
assertEq(staking.sharesInWarmup(), 0); assertEq(staking.supplyInWarmup(), 0);
} }
function test_unstake_canRedeemStinkyToFatso() public { function test_unstake_canRedeemStinkyToFatso() public {

View File

@ -150,7 +150,7 @@ contract StinkyTest is Test, ERC20PermitTest, ERC20AllowanceTest, ERC20TransferT
staking.stake(AMOUNT, ALICE, true, true); staking.stake(AMOUNT, ALICE, true, true);
vm.stopPrank(); vm.stopPrank();
assertEq(stnk.circulatingSupply(), AMOUNT); assertApproxEqAbs(stnk.circulatingSupply(), AMOUNT, 1);
assertEq(ftso.totalSupply(), AMOUNT); assertEq(ftso.totalSupply(), AMOUNT);
assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY); assertEq(stnk.totalSupply(), TOTAL_INITIAL_SUPPLY);
assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY); assertEq(stnk.balanceOf(address(staking)), TOTAL_INITIAL_SUPPLY);