// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20Metadata} from "@openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {NoteKeeper} from "./types/NoteKeeper.sol"; import {IBondDepository} from "./interfaces/IBondDepository.sol"; contract GhostBondDepository is IBondDepository, NoteKeeper { using SafeERC20 for IERC20; Market[] public markets; Term[] public terms; Metadata[] public metadatas; mapping(uint256 => Adjustment) public adjustments; mapping(address => uint256[]) public marketsForQuote; constructor( address authority, address ftso, address ghst, address staking, address treasury ) NoteKeeper(authority, ftso, ghst, staking, treasury) { _ftso.approve(staking, type(uint256).max); } function deposit( uint256 id, uint256 amount, uint256 maxPrice, address user, address referral ) external override returns ( uint256 payout, uint256 expiry, uint256 index ) { Market storage market = markets[id]; Term memory term = terms[id]; uint48 currentTime = uint48(block.timestamp); if (currentTime >= term.conclusion) revert DepositoryConcluded(term.conclusion); _decay(id, currentTime); uint256 price = _marketPrice(id); if (price > maxPrice) revert DepositoryMaxPrice(price, maxPrice); expiry = term.fixedTerm ? term.vesting + currentTime : term.vesting; payout = ((amount * 1e18) / price) / (10**metadatas[id].quoteDecimals); if (payout > market.maxPayout) revert DepositoryMaxSize(payout, market.maxPayout); market.capacity -= market.capacityInQuote ? amount : payout; market.purchased += amount; market.sold += uint64(payout); // forge-lint: disable-line(unsafe-typecast) market.totalDebt += uint64(payout); // forge-lint: disable-line(unsafe-typecast) emit Bond(id, amount, price); index = addNote(user, payout, uint48(expiry), uint48(id), referral); // forge-lint: disable-line(unsafe-typecast) IERC20(market.quoteToken).safeTransferFrom(msg.sender, address(_treasury), amount); if (term.maxDebt < market.totalDebt) { market.capacity = 0; emit MarketClosed(id); } else { _tune(id, currentTime); } } function _decay(uint256 id, uint48 time) internal { markets[id].totalDebt -= debtDecay(id); metadatas[id].lastDecay = time; if (adjustments[id].active) { Adjustment storage adjustment = adjustments[id]; (uint64 adjustBy, uint48 secondsSince, bool stillActive) = _controlDecay(id); terms[id].controlVariable -= adjustBy; if (stillActive) { adjustment.change -= adjustBy; adjustment.timeToAdjusted -= secondsSince; adjustment.lastAdjustment = time; } else { adjustment.active = false; } } } function _tune(uint256 id, uint48 time) internal { Metadata memory meta = metadatas[id]; if (time >= meta.lastTune + meta.tuneInterval) { Market memory market = markets[id]; uint256 timeRemaining = terms[id].conclusion - time; uint256 price = _marketPrice(id); uint256 capacity = market.capacityInQuote ? ((market.capacity * 1e18) / price) / (10**meta.quoteDecimals) : market.capacity; // forge-lint: disable-next-line(unsafe-typecast) markets[id].maxPayout = uint64((capacity * meta.depositInterval) / timeRemaining); uint256 targetDebt = (capacity * meta.length) / timeRemaining; // forge-lint: disable-next-line(unsafe-typecast) uint64 newControlVariable = uint64((price * _treasury.baseSupply()) / targetDebt); emit Tuned(id, terms[id].controlVariable, newControlVariable); if (newControlVariable >= terms[id].controlVariable) { terms[id].controlVariable = newControlVariable; } else { uint64 change = terms[id].controlVariable - newControlVariable; adjustments[id] = Adjustment({ change: change, lastAdjustment: time, timeToAdjusted: meta.tuneInterval, active: true }); } metadatas[id].lastTune = time; } } function create( uint256[3] calldata _market, uint256[2] calldata _terms, address _quoteToken, uint32[2] calldata _intervals, bool[2] calldata _booleans ) external override returns (uint256 id) { if ( msg.sender != authority.governor() && msg.sender != authority.policy() ) revert NotGuardianOrPolicy(); uint256 secondsToConclusion = _terms[1] - block.timestamp; uint256 decimals = IERC20Metadata(_quoteToken).decimals(); uint64 targetDebt = uint64(_booleans[0] ? ((_market[0] * 1e18) / _market[1]) / 10**decimals : _market[0]); // forge-lint: disable-next-line(unsafe-typecast) uint64 maxPayout = uint64((targetDebt * _intervals[0]) / secondsToConclusion); uint256 maxDebt = targetDebt + ((targetDebt * _market[2]) / 1e5); uint256 controlVariable = (_market[1] * _treasury.baseSupply()) / targetDebt; id = markets.length; markets.push( Market({ quoteToken: _quoteToken, capacityInQuote: _booleans[0], capacity: _market[0], totalDebt: targetDebt, maxPayout: maxPayout, purchased: 0, sold: 0 }) ); terms.push( Term({ fixedTerm: _booleans[1], controlVariable: uint64(controlVariable), // forge-lint: disable-line(unsafe-typecast) vesting: uint48(_terms[0]), // forge-lint: disable-line(unsafe-typecast) conclusion: uint48(_terms[1]), // forge-lint: disable-line(unsafe-typecast) maxDebt: uint64(maxDebt) // forge-lint: disable-line(unsafe-typecast) }) ); metadatas.push( Metadata({ lastTune: uint48(block.timestamp), lastDecay: uint48(block.timestamp), // forge-lint: disable-next-line(unsafe-typecast) length: uint48(secondsToConclusion), depositInterval: _intervals[0], tuneInterval: _intervals[1], // forge-lint: disable-next-line(unsafe-typecast) quoteDecimals: uint8(decimals) }) ); marketsForQuote[_quoteToken].push(id); emit MarketCreated(id, address(_ftso), _quoteToken, _market[1]); } function close(uint256 id) external override { if ( msg.sender != authority.governor() && msg.sender != authority.policy() ) revert NotGuardianOrPolicy(); terms[id].conclusion = uint48(block.timestamp); markets[id].capacity = 0; emit MarketClosed(id); } function marketPrice(uint256 id) public view override returns (uint256) { uint256 nominator = currentControlVariable(id) * debtRatio(id); return nominator / (10**metadatas[id].quoteDecimals); } function payoutFor( uint256 id, uint256 amount ) external view override returns (uint256) { uint256 nominator = (amount * 1e18) / marketPrice(id); return nominator / 10**metadatas[id].quoteDecimals; } function debtRatio(uint256 id) public view override returns (uint256) { uint256 nominator = currentDebt(id) * (10**metadatas[id].quoteDecimals); return nominator / _treasury.baseSupply(); } function currentDebt(uint256 id) public view override returns (uint256) { return markets[id].totalDebt - debtDecay(id); } function debtDecay(uint256 id) public view override returns (uint64) { Metadata memory meta = metadatas[id]; uint256 secondsSince = block.timestamp - meta.lastDecay; return uint64((markets[id].totalDebt * secondsSince) / meta.length); } function currentControlVariable(uint256 id) public view override returns (uint256) { (uint64 decay, , ) = _controlDecay(id); return terms[id].controlVariable - decay; } function isLive(uint256 id) public view override returns (bool) { return (markets[id].capacity > 0 && terms[id].conclusion > block.timestamp); } function liveMarkets() external view override returns (uint256[] memory) { uint256 number; uint256 i; uint256 j; for (; i < markets.length; ) { if (isLive(i)) { unchecked { ++number; } } unchecked { ++i; } } uint256[] memory ids = new uint256[](number); i = 0; for (; i < markets.length; ) { if (isLive(i)) { ids[j] = i; unchecked { ++j; } } unchecked { ++i; } } return ids; } function liveMarketsFor(address token) external view override returns (uint256[] memory) { uint256[] memory mkts = marketsForQuote[token]; uint256 number; uint256 i; uint256 j; for (; i < mkts.length; ) { if (isLive(mkts[i])) { unchecked { ++number; } } unchecked { ++i; } } uint256[] memory ids = new uint256[](number); i = 0; for (; i < mkts.length; ) { if (isLive(mkts[i])) { ids[j] = mkts[i]; unchecked { ++j; } } unchecked { ++i; } } return ids; } function _marketPrice(uint256 _id) internal view returns (uint256) { uint256 nominator = terms[_id].controlVariable * _debtRatio(_id); return nominator / (10**metadatas[_id].quoteDecimals); } function _debtRatio(uint256 id) internal view returns (uint256) { uint256 nominator = markets[id].totalDebt * (10**metadatas[id].quoteDecimals); return nominator / _treasury.baseSupply(); } function _controlDecay(uint256 id) internal view returns ( uint64 decay, uint48 secondsSince, bool active ) { Adjustment memory info = adjustments[id]; if (!info.active) return (0, 0, false); secondsSince = uint48(block.timestamp) - info.lastAdjustment; active = secondsSince < info.timeToAdjusted; decay = active ? (info.change * secondsSince) / info.timeToAdjusted : info.change; } }