// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* LOGOSVault (skeleton)
* - ERC-4626 share token ($LOGOS), accounting in USDC
* - Deploys USDC into a target basket: WBTC, stETH/rETH, WLD
* - Streams mgmt fee; crystallizes performance fee vs high-water-mark
* - Rebalances within bands; harvests/reinvests yield
*/
import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {ERC4626} from "openzeppelin-contracts/token/ERC20/extensions/ERC4626.sol";
import {Ownable} from "openzeppelin-contracts/access/Ownable.sol";
import {Pausable} from "openzeppelin-contracts/security/Pausable.sol";
import {ReentrancyGuard} from "openzeppelin-contracts/utils/ReentrancyGuard.sol";
interface IAggregatorV3 { // Chainlink-style
function latestRoundData() external view returns (
uint80, int256 answer, uint256, uint256, uint80
);
}
interface ISwapRouter {
// Replace with chosen DEX/aggregator interface (e.g., Uniswap V3 exactInput)
function swap(address tokenIn, address tokenOut, uint256 amountIn, uint256 minOut, address to) external returns (uint256);
}
contract LOGOSVault is ERC4626, Ownable, Pausable, ReentrancyGuard {
// ---------- constants & types ----------
uint256 private constant BPS = 10_000;
uint256 private constant YEAR = 365 days;
struct Target {
address token; // e.g., WBTC, stETH, rETH, WLD
uint16 weightBps; // sum(weights) == 10_000
bool harvestable; // true if we can claim/reinvest rewards
}
// ---------- config ----------
IERC20 public immutable USDC; // vault asset (accounting)
ISwapRouter public router; // whitelisted DEX aggregator
address public keeper; // Gelato/Chainlink caller
// token -> oracle (USDC price with 8 or 18 decimals; normalize inside)
mapping(address => IAggregatorV3) public oracle;
Target[] public targets; // allocation basket
mapping(address => bool) public isAllowedToken;
// ---------- fees ----------
uint256 public mgmtFeeBps; // e.g., 150 = 1.50%/yr
uint256 public perfFeeBps; // e.g., 1000 = 10%
address public feeRecipient;
uint256 public lastFeeAccrual; // timestamp
uint256 public highWaterMarkPPS; // price-per-share at last crystallization (1e18 scale)
// ---------- rebalance ----------
uint16 public bandBps = 500; // +/-5% drift band
uint256 public minTradeUSDC = 5_000e6; // skip tiny trades; adjust per chain
modifier onlyKeeper() {
require(msg.sender == keeper || msg.sender == owner(), "not keeper");
_;
}
constructor(
address _usdc,
string memory _name,
string memory _symbol
) ERC20(_name, _symbol) ERC4626(IERC20(_usdc)) {
USDC = IERC20(_usdc);
feeRecipient = msg.sender;
lastFeeAccrual = block.timestamp;
// set HWM to 1.0 PPS initially so first gains are measured correctly
highWaterMarkPPS = 1e18;
}
// =========================
// ADMIN CONFIG
// =========================
function setRouter(address _router) external onlyOwner { router = ISwapRouter(_router); }
function setKeeper(address _k) external onlyOwner { keeper = _k; }
function setOracles(address[] calldata toks, address[] calldata oracles) external onlyOwner {
require(toks.length == oracles.length, "len");
for (uint i; i<toks.length; ++i) oracle[toks[i]] = IAggregatorV3(oracles[i]);
}
function setFees(uint256 _mgmtBps, uint256 _perfBps, address _recipient) external onlyOwner {
_accrueMgmtFee(); // settle before changing
mgmtFeeBps = _mgmtBps; perfFeeBps = _perfBps; feeRecipient = _recipient;
}
function setBand(uint16 _bandBps, uint256 _minTradeUSDC) external onlyOwner {
bandBps = _bandBps; minTradeUSDC = _minTradeUSDC;
}
function setTargets(Target[] calldata t) external onlyOwner {
delete targets;
uint256 sum;
for (uint i; i<t.length; ++i) {
targets.push(t[i]);
isAllowedToken[t[i].token] = true;
sum += t[i].weightBps;
}
require(sum == BPS, "weights!=100%");
}
// =========================
// ERC4626 OVERRIDES
// =========================
function totalAssets() public view override returns (uint256) {
// value USDC balance + all target tokens at oracle USDC prices
uint256 value = USDC.balanceOf(address(this));
for (uint i; i<targets.length; ++i) {
IERC20 t = IERC20(targets[i].token);
uint256 bal = t.balanceOf(address(this));
if (bal == 0) continue;
value += _toUSDC(targets[i].token, bal);
}
return value;
}
// hook: accrue fees before any share price‑changing action
function _beforeTokenTransfer(address from, address to, uint256) internal override {
if (from != address(0) || to != feeRecipient) { // skip pure fee mints check
_accrueMgmtFee();
}
}
// deposit/withdraw: leave 4626 math, then allocate/liquidate around them
function deposit(uint256 assets, address receiver)
public
override
whenNotPaused
nonReentrant
returns (uint256 shares)
{
shares = super.deposit(assets, receiver); // mints shares based on pre‑swap NAV
_deployToTargets(assets); // convert USDC to basket per weights
}
function mint(uint256 shares, address receiver)
public
override
whenNotPaused
nonReentrant
returns (uint256 assets)
{
assets = super.mint(shares, receiver);
_deployToTargets(assets);
}
function withdraw(uint256 assets, address receiver, address owner_)
public
override
whenNotPaused
nonReentrant
returns (uint256 shares)
{
// ensure we can return USDC: unwind proportionally
_liquidateToUSDC(assets);
shares = super.withdraw(assets, receiver, owner_);
}