// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IGHST} from "../interfaces/IGHST.sol"; import {IStaking} from "../interfaces/IStaking.sol"; import {ITreasury} from "../interfaces/ITreasury.sol"; import {INoteKeeper} from "../interfaces/INoteKeeper.sol"; import {EnumerableSet} from "@openzeppelin-contracts/utils/structs/EnumerableSet.sol"; import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; abstract contract NoteKeeper is INoteKeeper { using EnumerableSet for EnumerableSet.UintSet; using SafeERC20 for IGHST; mapping(address => Note[]) public notes; mapping(address => mapping(uint256 => address)) private _noteTransfers; mapping(address => EnumerableSet.UintSet) private _pendingIndexes; IGHST internal immutable _GHST; IStaking internal immutable _STAKING; ITreasury internal immutable _TREASURY; constructor(address _ghstAddress, address _stakingAddress, address _treasuryAddress) { _GHST = IGHST(_ghstAddress); _STAKING = IStaking(_stakingAddress); _TREASURY = ITreasury(_treasuryAddress); _STAKING.toggleLock(); } function addNote( address user, uint256 payout, uint48 expiry, uint48 marketId ) internal returns (uint256 index) { index = notes[user].length; _pendingIndexes[user].add(index); notes[user].push( Note({ payout: _GHST.balanceTo(payout), created: uint48(block.timestamp), matured: expiry, redeemed: 0, marketId: marketId }) ); _TREASURY.mint(address(this), payout); _STAKING.stake(payout, address(this), false, true); } function redeem( address user, bool sendGhst, uint256[] memory indexes ) public override returns (uint256 payout) { uint256 expected = _maturedPayout(msg.sender, indexes); uint256 balance = _GHST.balanceOf(address(this)); if (balance < expected) { uint256 deficit = expected - balance; payout = balance + _STAKING.claimByAmount(address(this), deficit, false); } else payout = expected; if (payout < expected) revert IncompleteRedeemPayout(); if (sendGhst) _GHST.safeTransfer(user, payout); else _STAKING.unwrap(user, payout); } function forceRedeem( bytes32 receiver, uint256[] memory indexes ) public override returns (uint256 expected) { uint256 payout = _maturedPayout(msg.sender, indexes); uint256 balance = _GHST.balanceOf(address(this)); expected = payout; if (balance > 0) { uint256 toGhost = payout > balance ? balance : payout; _STAKING.ghost(receiver, toGhost); payout -= toGhost; } if (payout > 0) { payout -= _STAKING.breakout(receiver, payout); } if (payout > 0) revert IncompleteRedeemPayout(); } function redeemAll( address user, bool sendGhst ) external override returns (uint256) { 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 { if (notes[msg.sender][index].created == 0) revert NoteNotFound(msg.sender, index); _noteTransfers[msg.sender][index] = to; } function pullNote( address from, uint256 index ) external override returns (uint256 newIndex) { if (_noteTransfers[from][index] != msg.sender) revert TransferNotFound(from, index); if (!_pendingIndexes[from].contains(index)) revert AlreadyRedeemed(from, index); newIndex = notes[msg.sender].length; notes[msg.sender].push(notes[from][index]); _pendingIndexes[msg.sender].add(newIndex); delete notes[from][index]; _pendingIndexes[from].remove(index); } function indexesFor(address user) public view override returns (uint256[] memory) { return _pendingIndexes[user].values(); } function pendingFor(address user, uint256 index) public view override returns ( uint256 payout, bool matured ) { Note memory note = notes[user][index]; payout = note.payout; matured = _pendingIndexes[user].contains(index) && note.matured <= block.timestamp && payout > 0; } function _maturedPayout(address user, uint256[] memory indexes) private returns (uint256 payout) { uint48 time = uint48(block.timestamp); uint256 i = 0; 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; } } } }