// 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 {IBondDepository} from "./interfaces/IBondDepository.sol"; import {IGhostAuthority} from "./interfaces/IGhostAuthority.sol"; import {ITreasury} from "./interfaces/ITreasury.sol"; import {IWETH9} from "./interfaces/IWETH9.sol"; import {FullMath} from "./libraries/FullMath.sol"; import {NoteKeeper} from "./types/NoteKeeper.sol"; contract GhostBondDepository is IBondDepository, NoteKeeper { using SafeERC20 for IERC20; address public immutable WETH; address public immutable TREASURY; address public immutable AUTHORITY; 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, address wrapped ) NoteKeeper(ghst, staking, treasury) { IERC20(ftso).approve(staking, type(uint256).max); AUTHORITY = authority; TREASURY = treasury; WETH = wrapped; } function deposit( uint256 id, uint256 amount, uint256 maxPrice, address user ) external override payable 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 = FullMath.mulDiv(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)); // forge-lint: disable-line(unsafe-typecast) IWETH9(WETH).deposit{value: msg.value}(); IERC20(WETH).safeTransfer(TREASURY, msg.value); if (msg.value < amount || market.quoteToken != WETH) { IERC20(market.quoteToken).safeTransferFrom(msg.sender, 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 ? FullMath.mulDiv(market.capacity, 1e18, price) / (10**meta.quoteDecimals) : market.capacity; // forge-lint: disable-next-line(unsafe-typecast) markets[id].maxPayout = uint64(FullMath.mulDiv(capacity, meta.depositInterval, timeRemaining)); uint256 targetDebt = FullMath.mulDiv(capacity, meta.length, timeRemaining); // forge-lint: disable-next-line(unsafe-typecast) uint64 newControlVariable = uint64(FullMath.mulDiv(price, ITreasury(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 != IGhostAuthority(AUTHORITY).governor() && msg.sender != IGhostAuthority(AUTHORITY).policy() ) revert NotGuardianOrPolicy(); uint256 secondsToConclusion = _terms[1] - block.timestamp; uint256 decimals = IERC20Metadata(_quoteToken).decimals(); uint64 targetDebt = uint64(_booleans[0] ? FullMath.mulDiv(_market[0], 1e18, _market[1]) / 10**decimals : _market[0]); // forge-lint: disable-next-line(unsafe-typecast) uint64 maxPayout = uint64(FullMath.mulDiv(targetDebt, _intervals[0], secondsToConclusion)); uint256 maxDebt = targetDebt + FullMath.mulDiv(targetDebt, _market[2], 1e5); uint256 controlVariable = FullMath.mulDiv(_market[1], ITreasury(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, _quoteToken, _market[1]); } function close(uint256 id) external override { if ( msg.sender != IGhostAuthority(AUTHORITY).governor() && msg.sender != IGhostAuthority(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) { return FullMath.mulDiv(currentControlVariable(id), debtRatio(id), 10**metadatas[id].quoteDecimals); } function payoutFor( uint256 id, uint256 amount ) external view override returns (uint256) { uint256 nominator = FullMath.mulDiv(amount, 1e18, marketPrice(id)); return nominator / 10**metadatas[id].quoteDecimals; } function debtRatio(uint256 id) public view override returns (uint256) { return FullMath.mulDiv(currentDebt(id), 10**metadatas[id].quoteDecimals, ITreasury(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(FullMath.mulDiv(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 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) { return FullMath.mulDiv(terms[_id].controlVariable, _debtRatio(_id), 10**metadatas[_id].quoteDecimals); } function _debtRatio(uint256 id) internal view returns (uint256) { return FullMath.mulDiv(markets[id].totalDebt, 10**metadatas[id].quoteDecimals, ITreasury(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 // forge-lint: disable-next-line(unsafe-typecast) ? uint64(FullMath.mulDiv(info.change, secondsSince, info.timeToAdjusted)) : info.change; } }