SKIP TO CONTENT

Demo build — mock protocol data. No chain, no ABIs yet.

  • SYNCING: ……

K401FeeSplitter.sol

Splits the 5% fee stream 50/30/15/5 and decays the team share linearly to zero over 30 days.

146 lines5.6 KBSolidity
Source of src/K401FeeSplitter.sol, 146 lines of Solidity
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.24;
3 
4import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
6import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8import {IUniswapV2Router} from "./interfaces/IK401Interfaces.sol";
9 
10/**
11 * @title K401FeeSplitter
12 * @notice Receives the 5% pair fee in 401K, converts it to USDG, and splits it.
13 *
14 * StockDesk (buys equity for Vested Seats) 50%
15 * Treasury (backing) 30%
16 * LP / POL 15%
17 * Buyback 5%
18 *
19 * Team: starts at 40% of the whole stream and decays LINEARLY to zero over 30 days,
20 * taken pro-rata from the four buckets. After day 30 the team share is 0 forever —
21 * there is no setter, no extension and no admin path that can change it.
22 */
23contract K401FeeSplitter is Ownable, ReentrancyGuard {
24 using SafeERC20 for IERC20;
25 
26 uint256 public constant BPS = 10_000;
27 uint256 public constant STOCKDESK_BPS = 5_000;
28 uint256 public constant TREASURY_BPS = 3_000;
29 uint256 public constant LP_BPS = 1_500;
30 uint256 public constant BUYBACK_BPS = 500;
31 
32 /// @notice Team share at t = 0, as a fraction of the whole fee stream.
33 uint256 public constant TEAM_START_BPS = 4_000;
34 /// @notice Team share reaches exactly 0 here and stays there permanently.
35 uint256 public constant TEAM_DECAY_PERIOD = 30 days;
36 
37 IERC20 public immutable k401;
38 IERC20 public immutable usdg;
39 /// @notice Start of the team decay. Immutable — the clock can never be reset.
40 uint256 public immutable teamStartTs;
41 
42 IUniswapV2Router public router;
43 address public stockDesk;
44 address public treasury;
45 address public lpReceiver;
46 address public buyback;
47 address public team;
48 bool public wiringLocked;
49 
50 event Distributed(
51 uint256 k401In,
52 uint256 usdgOut,
53 uint256 toStockDesk,
54 uint256 toTreasury,
55 uint256 toLp,
56 uint256 toBuyback,
57 uint256 toTeam
58 );
59 event Wired(address router, address stockDesk, address treasury, address lpReceiver, address buyback, address team);
60 
61 error AlreadySet();
62 error NotWired();
63 error ZeroAddress();
64 error NothingToDistribute();
65 error SlippageExceeded();
66 
67 constructor(address k401_, address usdg_, address owner_) Ownable(owner_) {
68 if (k401_ == address(0) || usdg_ == address(0)) revert ZeroAddress();
69 k401 = IERC20(k401_);
70 usdg = IERC20(usdg_);
71 teamStartTs = block.timestamp;
72 }
73 
74 /// @notice One-shot wiring. Once called, destinations are frozen forever.
75 function wire(
76 address router_,
77 address stockDesk_,
78 address treasury_,
79 address lpReceiver_,
80 address buyback_,
81 address team_
82 ) external onlyOwner {
83 if (wiringLocked) revert AlreadySet();
84 if (
85 router_ == address(0) || stockDesk_ == address(0) || treasury_ == address(0) || lpReceiver_ == address(0)
86 || buyback_ == address(0) || team_ == address(0)
87 ) revert ZeroAddress();
88 router = IUniswapV2Router(router_);
89 stockDesk = stockDesk_;
90 treasury = treasury_;
91 lpReceiver = lpReceiver_;
92 buyback = buyback_;
93 team = team_;
94 wiringLocked = true;
95 emit Wired(router_, stockDesk_, treasury_, lpReceiver_, buyback_, team_);
96 }
97 
98 /// @notice Current team share of the fee stream, in bps. Linear decay to zero over 30 days.
99 function teamShareBps() public view returns (uint256) {
100 uint256 elapsed = block.timestamp - teamStartTs;
101 if (elapsed >= TEAM_DECAY_PERIOD) return 0;
102 return (TEAM_START_BPS * (TEAM_DECAY_PERIOD - elapsed)) / TEAM_DECAY_PERIOD;
103 }
104 
105 function pendingFees() external view returns (uint256) {
106 return k401.balanceOf(address(this));
107 }
108 
109 /**
110 * @notice Permissionless. Sells the accumulated 401K fee for USDG and splits it.
111 * @param minUsdgOut Caller-supplied slippage bound for the fee sale.
112 */
113 function distribute(uint256 minUsdgOut) external nonReentrant returns (uint256 usdgOut) {
114 if (!wiringLocked) revert NotWired();
115 uint256 amountIn = k401.balanceOf(address(this));
116 if (amountIn == 0) revert NothingToDistribute();
117 
118 address[] memory path = new address[](2);
119 path[0] = address(k401);
120 path[1] = address(usdg);
121 
122 uint256 before = usdg.balanceOf(address(this));
123 k401.forceApprove(address(router), amountIn);
124 router.swapExactTokensForTokens(amountIn, minUsdgOut, path, address(this), block.timestamp);
125 usdgOut = usdg.balanceOf(address(this)) - before;
126 if (usdgOut < minUsdgOut) revert SlippageExceeded();
127 
128 uint256 total = usdg.balanceOf(address(this));
129 uint256 toTeam = (total * teamShareBps()) / BPS;
130 uint256 rest = total - toTeam;
131 
132 uint256 toStockDesk = (rest * STOCKDESK_BPS) / BPS;
133 uint256 toTreasury = (rest * TREASURY_BPS) / BPS;
134 uint256 toLp = (rest * LP_BPS) / BPS;
135 uint256 toBuyback = rest - toStockDesk - toTreasury - toLp; // absorbs rounding dust
136 
137 if (toTeam != 0) usdg.safeTransfer(team, toTeam);
138 if (toStockDesk != 0) usdg.safeTransfer(stockDesk, toStockDesk);
139 if (toTreasury != 0) usdg.safeTransfer(treasury, toTreasury);
140 if (toLp != 0) usdg.safeTransfer(lpReceiver, toLp);
141 if (toBuyback != 0) usdg.safeTransfer(buyback, toBuyback);
142 
143 emit Distributed(amountIn, usdgOut, toStockDesk, toTreasury, toLp, toBuyback, toTeam);
144 }
145}
146 

Click any line number to deep-link to it — the target line highlights on load.