342 lines
12 KiB
Solidity
342 lines
12 KiB
Solidity
// 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;
|
|
}
|
|
}
|