// 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) {