// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {VaultBaseV3} from "./flap/VaultBaseV3.sol"; import {VaultFactoryBaseV2} from "./flap/VaultFactoryBaseV2.sol"; import {IVaultFactoryValidationV2} from "./flap/IVaultFactory.sol"; import { VaultUISchema, VaultMethodSchema, VaultDataSchema, FieldDescriptor, ApproveAction } from "./flap/IVaultSchemasV1.sol"; import {IPortalTradeV2} from "./flap/IPortal.sol"; import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; /// @title SpaceGoatBuybackSplitVault /// @notice 用税收(BNB 或股票底池报价资产)经 Flap Portal 回购当前税币, /// 再按 bps 把买到的税币分给两个地址。 /// @dev receive() 只做 V3 入账。回购+分发走 buybackAndDistribute()。 contract SpaceGoatBuybackSplitVault is VaultBaseV3 { using SafeERC20 for IERC20; uint16 public constant BPS_DENOM = 10_000; address public immutable taxToken; address internal immutable _quoteToken; address public immutable creator; address public immutable recipientA; address public immutable recipientB; uint16 public immutable bpsA; uint16 public immutable bpsB; uint256 public accountedQuote; uint256 public totalQuoteSpent; uint256 public totalTaxDistributed; uint256 private _status; event RevenueRecognized(uint256 newRevenue, uint256 accountedTotal); event Buyback(uint256 quoteSpent, uint256 taxBought); event DistributedTax(address indexed toA, uint256 amountA, address indexed toB, uint256 amountB); modifier nonReentrant() { require(_status != 2, "reentrant"); _status = 2; _; _status = 1; } constructor( address _taxToken, address quoteToken_, address _creator, address _recipientA, uint16 _bpsA, address _recipientB, uint16 _bpsB ) { require(_taxToken != address(0), "Invalid tax token"); require(_creator != address(0), "Invalid creator"); require(_recipientA != address(0) && _recipientB != address(0), "Zero recipient"); require(_recipientA != _recipientB, "Same recipient"); require(uint256(_bpsA) + uint256(_bpsB) == BPS_DENOM, "bps must sum to 10000"); require(_bpsA > 0 && _bpsB > 0, "bps must be > 0"); taxToken = _taxToken; _quoteToken = quoteToken_; creator = _creator; recipientA = _recipientA; recipientB = _recipientB; bpsA = _bpsA; bpsB = _bpsB; } function vaultQuoteToken() public view override returns (address) { return _quoteToken; } receive() external payable { _syncRevenue(); } fallback() external payable { _syncRevenue(); } function sync() external { _syncRevenue(); } /// @param minTaxOut 最少买到的税币数量,防夹子。测试可填 0。 function buybackAndDistribute(uint256 minTaxOut) external nonReentrant { _syncRevenue(); uint256 quoteAmt = accountedQuote; require(quoteAmt > 0, unicode"没有可回购的税收"); accountedQuote = 0; totalQuoteSpent += quoteAmt; uint256 taxBefore = IERC20(taxToken).balanceOf(address(this)); _buyTax(quoteAmt, minTaxOut); uint256 bought = IERC20(taxToken).balanceOf(address(this)) - taxBefore; emit Buyback(quoteAmt, bought); if (bought == 0) return; uint256 amountA = (bought * bpsA) / BPS_DENOM; uint256 amountB = bought - amountA; totalTaxDistributed += bought; if (amountA > 0) IERC20(taxToken).safeTransfer(recipientA, amountA); if (amountB > 0) IERC20(taxToken).safeTransfer(recipientB, amountB); emit DistributedTax(recipientA, amountA, recipientB, amountB); } function _buyTax(uint256 quoteAmt, uint256 minTaxOut) internal { address portal = _getPortal(); address quoteToken = _quoteToken; IPortalTradeV2.ExactInputParams memory params = IPortalTradeV2.ExactInputParams({ inputToken: quoteToken, outputToken: taxToken, inputAmount: quoteAmt, minOutputAmount: minTaxOut, permitData: "" }); if (quoteToken == address(0)) { IPortalTradeV2(portal).swapExactInput{value: quoteAmt}(params); } else { IERC20(quoteToken).safeApprove(portal, 0); IERC20(quoteToken).safeApprove(portal, quoteAmt); IPortalTradeV2(portal).swapExactInput(params); IERC20(quoteToken).safeApprove(portal, 0); } } function _syncRevenue() internal { uint256 balance = _quoteBalance(); if (balance <= accountedQuote) return; uint256 newRevenue = balance - accountedQuote; accountedQuote = balance; emit RevenueRecognized(newRevenue, balance); } function _quoteBalance() internal view returns (uint256) { address quoteToken = _quoteToken; return quoteToken == address(0) ? address(this).balance : IERC20(quoteToken).balanceOf(address(this)); } function pendingQuote() external view returns (uint256) { uint256 balance = _quoteBalance(); return balance > accountedQuote ? balance : accountedQuote; } function getRecipients() external view returns (address a, uint16 aBps, address b, uint16 bBps, address quote) { return (recipientA, bpsA, recipientB, bpsB, _quoteToken); } function description() public view override returns (string memory) { if (totalTaxDistributed == 0) { return unicode"回购分发金库:税收先囤着,调用 buybackAndDistribute 买回当前币再按比例转出。"; } return unicode"回购分发金库:已执行过回购并分发税币。有新税时再调 buybackAndDistribute。"; } function vaultUISchema() public pure override returns (VaultUISchema memory schema) { schema.vaultType = "SpaceGoatBuybackSplitVault"; schema.description = unicode"税收买回当前发行币,再按两个地址的 bps 分发税币。"; schema.methods = new VaultMethodSchema[](4); schema.methods[0].name = "pendingQuote"; schema.methods[0].description = unicode"待回购的底池余额"; schema.methods[0].inputs = new FieldDescriptor[](0); schema.methods[0].outputs = new FieldDescriptor[](1); schema.methods[0].outputs[0] = FieldDescriptor("amount", "uint256", "Pending quote", 18); schema.methods[0].approvals = new ApproveAction[](0); schema.methods[1].name = "getRecipients"; schema.methods[1].description = unicode"收款地址与比例"; schema.methods[1].inputs = new FieldDescriptor[](0); schema.methods[1].outputs = new FieldDescriptor[](5); schema.methods[1].outputs[0] = FieldDescriptor("a", "address", "Recipient A", 0); schema.methods[1].outputs[1] = FieldDescriptor("aBps", "uint16", "A bps", 0); schema.methods[1].outputs[2] = FieldDescriptor("b", "address", "Recipient B", 0); schema.methods[1].outputs[3] = FieldDescriptor("bBps", "uint16", "B bps", 0); schema.methods[1].outputs[4] = FieldDescriptor("quote", "address", "Quote token", 0); schema.methods[1].approvals = new ApproveAction[](0); schema.methods[2].name = "sync"; schema.methods[2].description = unicode"同步税收入账"; schema.methods[2].isWriteMethod = true; schema.methods[2].inputs = new FieldDescriptor[](0); schema.methods[2].outputs = new FieldDescriptor[](0); schema.methods[2].approvals = new ApproveAction[](0); schema.methods[3].name = "buybackAndDistribute"; schema.methods[3].description = unicode"回购当前币并分发给两个地址"; schema.methods[3].isWriteMethod = true; schema.methods[3].inputs = new FieldDescriptor[](1); schema.methods[3].inputs[0] = FieldDescriptor("minTaxOut", "uint256", "Minimum tax tokens out", 18); schema.methods[3].outputs = new FieldDescriptor[](0); schema.methods[3].approvals = new ApproveAction[](0); } } contract SpaceGoatBuybackSplitVaultFactory is VaultFactoryBaseV2 { constructor() VaultFactoryBaseV2() {} function newVault(address taxToken, address quoteToken, address creator, bytes calldata vaultData) external override returns (address vault) { require(msg.sender == _getVaultPortal(), unicode"仅限 VaultPortal 调用"); (address recipientA, uint16 bpsA, address recipientB, uint16 bpsB) = abi.decode(vaultData, (address, uint16, address, uint16)); require(recipientA != address(0) && recipientB != address(0), "Zero recipient"); require(recipientA != recipientB, "Same recipient"); require(uint256(bpsA) + uint256(bpsB) == 10_000, "bps must sum to 10000"); require(bpsA > 0 && bpsB > 0, "bps must be > 0"); vault = address( new SpaceGoatBuybackSplitVault(taxToken, quoteToken, creator, recipientA, bpsA, recipientB, bpsB) ); } function isQuoteTokenSupported(address) external pure override returns (bool) { return true; } function factorySpecVersion() public pure override returns (string memory) { return "v2.3"; } function _validateBeforeLaunch(IVaultFactoryValidationV2.LaunchValidationDataV1 memory) internal pure override returns (bool success, string memory reason) { return (true, ""); } function vaultDataSchema() public pure override returns (VaultDataSchema memory schema) { schema.description = unicode"税收经 Portal 回购当前发行币,再按 bps 分给两个地址。" unicode"bpsA + bpsB = 10000。发币后需手动或脚本调用 buybackAndDistribute。"; schema.fields = new FieldDescriptor[](4); schema.fields[0] = FieldDescriptor("recipientA", "address", unicode"收款地址 A", 0); schema.fields[1] = FieldDescriptor("bpsA", "uint16", unicode"A 份额(10000=100%)", 0); schema.fields[2] = FieldDescriptor("recipientB", "address", unicode"收款地址 B", 0); schema.fields[3] = FieldDescriptor("bpsB", "uint16", unicode"B 份额(10000=100%)", 0); schema.isArray = false; } }