Protocol at a Glance
Real-time metrics from the Plether ecosystem
Transparent by Design
All Plether contracts are public and verifiable
1// SPDX-License-Identifier: AGPL-3.0
2pragma solidity 0.8.33;
3
4import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
6import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
7import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
8import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
9import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
10import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
11import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
12import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
13
14import {SyntheticToken} from "./SyntheticToken.sol";
15import {AggregatorV3Interface} from "./interfaces/AggregatorV3Interface.sol";
16import {ISyntheticSplitter} from "./interfaces/ISyntheticSplitter.sol";
17import {OracleLib} from "./libraries/OracleLib.sol";
18
19/// @title SyntheticSplitter
20/// @notice Core protocol contract for minting/burning synthetic plDXY tokens.
21/// @dev Accepts USDC collateral to mint equal amounts of plDXY-BEAR + plDXY-BULL tokens.
22/// Maintains 10% liquidity buffer locally, 90% deployed to yield adapters.
23/// Three lifecycle states: ACTIVE → PAUSED → SETTLED (liquidated).
24contract SyntheticSplitter is ISyntheticSplitter, Ownable2Step, Pausable, ReentrancyGuard {
25
26 using SafeERC20 for IERC20;
27
28 // ==========================================
29 // STATE
30 // ==========================================
31
32 // Assets
33 SyntheticToken public immutable TOKEN_A; // Bear
34 SyntheticToken public immutable TOKEN_B; // Bull
35 IERC20 public immutable USDC;
36
37 // Logic
38 AggregatorV3Interface public immutable ORACLE;
39 uint256 public immutable CAP;
40 uint256 public immutable USDC_MULTIPLIER; // Cached math scaler
41 uint256 public constant BUFFER_PERCENT = 10; // Keep 10% liquid in Splitter
42
43 // The Bank
44 IERC4626 public yieldAdapter;
45
46 // Governance: Adapter Migration
47 address public pendingAdapter;
48 uint256 public adapterActivationTime;
49
50 // Governance: Fee Receivers
51 address public treasury;
52 address public staking;
53
54 struct FeeConfig {
55 address treasuryAddr;
56 address stakingAddr;
57 }
58 FeeConfig public pendingFees;
59 uint256 public feesActivationTime;
60
61 uint256 public constant ORACLE_TIMEOUT = 8 hours;
62 uint256 public constant TIMELOCK_DELAY = 7 days;
63 uint256 public lastUnpauseTime;
64
65 uint256 public constant HARVEST_REWARD_BPS = 10;
66 uint256 public constant MIN_SURPLUS_THRESHOLD = 50 * 1e6;
67
68 // Liquidation State
69 bool public isLiquidated;
70
71 // Sequencer Feed
72 AggregatorV3Interface public immutable SEQUENCER_UPTIME_FEED;
73 uint256 public constant SEQUENCER_GRACE_PERIOD = 1 hours;
74
75 // Events
76 event Minted(address indexed user, uint256 amount);
77 event Burned(address indexed user, uint256 amount);
78 event AdapterProposed(address indexed newAdapter, uint256 activationTime);
79 event AdapterMigrated(address indexed oldAdapter, address indexed newAdapter, uint256 transferredAmount);
80 event LiquidationTriggered(uint256 price);
81 event EmergencyRedeemed(address indexed user, uint256 amount);
82 event YieldHarvested(uint256 totalSurplus, uint256 treasuryAmt, uint256 stakingAmt);
83 event FeesProposed(address indexed treasury, address indexed staking, uint256 activationTime);
84 event FeesUpdated(address indexed treasury, address indexed staking);
85 event EmergencyEjected(uint256 amountRecovered);
86 event AdapterWithdrawn(uint256 requested, uint256 withdrawn);
87 event TokenRescued(address indexed token, address indexed to, uint256 amount);
88
89 error Splitter__ZeroAddress();
90 error Splitter__InvalidCap();
91 error Splitter__ZeroAmount();
92 error Splitter__ZeroRefund();
93 error Splitter__AdapterNotSet();
94 error Splitter__AdapterInsufficientLiquidity();
95 error Splitter__LiquidationActive();
96 error Splitter__NotLiquidated();
97 error Splitter__TimelockActive();
98 error Splitter__InvalidProposal();
99 error Splitter__NoSurplus();
100 error Splitter__GovernanceLocked();
101 error Splitter__InsufficientHarvest();
102 error Splitter__AdapterWithdrawFailed();
103 error Splitter__Insolvent();
104 error Splitter__NotPaused();
105 error Splitter__CannotRescueCoreAsset();
106 error Splitter__MigrationLostFunds();
107
108 // Structs for Views
109 struct SystemStatus {
110 uint256 currentPrice;
111 uint256 capPrice;
112 bool liquidated;
113 bool isPaused;
114 uint256 totalAssets; // Local + Adapter
115 uint256 totalLiabilities; // Bear Supply * CAP
116 uint256 collateralRatio; // Basis points
117 uint256 adapterAssets; // USDC value held in yield adapter
118 }
119
120 /// @notice Deploys the SyntheticSplitter and creates plDXY-BEAR and plDXY-BULL tokens.
121 /// @param _oracle Chainlink-compatible price feed for plDXY basket.
122 /// @param _usdc USDC token address (6 decimals).
123 /// @param _yieldAdapter ERC4626-compliant yield adapter for USDC deposits.
124 /// @param _cap Maximum plDXY price (8 decimals). Triggers liquidation when breached.
125 /// @param _treasury Treasury address for fee distribution.
126 /// @param _sequencerUptimeFeed L2 sequencer uptime feed (address(0) for L1/testnets).
127 constructor(
128 address _oracle,
129 address _usdc,
130 address _yieldAdapter,
131 uint256 _cap,
132 address _treasury,
133 address _sequencerUptimeFeed
134 ) Ownable(msg.sender) {
135 if (_oracle == address(0)) {
136 revert Splitter__ZeroAddress();
137 }
138 if (_usdc == address(0)) {
139 revert Splitter__ZeroAddress();
140 }
141 if (_yieldAdapter == address(0)) {
142 revert Splitter__ZeroAddress();
143 }
144 if (_cap == 0) {
145 revert Splitter__InvalidCap();
146 }
147 if (_treasury == address(0)) {
148 revert Splitter__ZeroAddress();
149 }
150
151 ORACLE = AggregatorV3Interface(_oracle);
152 USDC = IERC20(_usdc);
153 yieldAdapter = IERC4626(_yieldAdapter);
154 CAP = _cap;
155 treasury = _treasury;
156 SEQUENCER_UPTIME_FEED = AggregatorV3Interface(_sequencerUptimeFeed);
157
158 TOKEN_A = new SyntheticToken("Plether Dollar Index Bear", "plDXY-BEAR", address(this));
159 TOKEN_B = new SyntheticToken("Plether Dollar Index Bull", "plDXY-BULL", address(this));
160
161 uint256 decimals = ERC20(_usdc).decimals();
162 USDC_MULTIPLIER = 10 ** (18 + 8 - decimals);
163 }
164
165 // ==========================================
166 // 1. MINTING (With Buffer)
167 // ==========================================
168
169 function previewMint(
170 uint256 mintAmount
171 ) external view returns (uint256 usdcRequired, uint256 depositToAdapter, uint256 keptInBuffer) {
172 if (mintAmount == 0) {
173 return (0, 0, 0);
174 }
175 if (isLiquidated) {
176 revert Splitter__LiquidationActive();
177 }
178
179 uint256 price = _getOraclePrice();
180 if (price >= CAP) {
181 revert Splitter__LiquidationActive();
182 }
183
184 usdcRequired = Math.mulDiv(mintAmount, CAP, USDC_MULTIPLIER, Math.Rounding.Ceil);
185
186 keptInBuffer = (usdcRequired * BUFFER_PERCENT) / 100;
187 depositToAdapter = usdcRequired - keptInBuffer;
188 }
189
190 /// @notice Mint plDXY-BEAR and plDXY-BULL tokens by depositing USDC collateral.
191 /// @param amount The amount of token pairs to mint (18 decimals).
192 function mint(
193 uint256 amount
194 ) external nonReentrant whenNotPaused {
195 if (amount == 0) {
196 revert Splitter__ZeroAmount();
197 }
198 if (isLiquidated) {
199 revert Splitter__LiquidationActive();
200 }
201 if (address(yieldAdapter) == address(0)) {
202 revert Splitter__AdapterNotSet();
203 }
204
205 uint256 price = _getOraclePrice();
206 if (price >= CAP) {
207 revert Splitter__LiquidationActive();
208 }
209
210 uint256 usdcNeeded = Math.mulDiv(amount, CAP, USDC_MULTIPLIER, Math.Rounding.Ceil);
211 if (usdcNeeded == 0) {
212 revert Splitter__ZeroAmount();
213 }
214
215 USDC.safeTransferFrom(msg.sender, address(this), usdcNeeded);
216
217 uint256 keepAmount = (usdcNeeded * BUFFER_PERCENT) / 100;
218 uint256 depositAmount = usdcNeeded - keepAmount;
219
220 if (depositAmount > 0) {
221 USDC.forceApprove(address(yieldAdapter), depositAmount);
222 yieldAdapter.deposit(depositAmount, address(this));
223 }
224
225 TOKEN_A.mint(msg.sender, amount);
226 TOKEN_B.mint(msg.sender, amount);
227
228 emit Minted(msg.sender, amount);
229 }
230
231 // ==========================================
232 // 2. BURNING (Smart Withdrawal)
233 // ==========================================
234
235 function previewBurn(
236 uint256 burnAmount
237 ) external view returns (uint256 usdcRefund, uint256 withdrawnFromAdapter) {
238 if (burnAmount == 0) {
239 return (0, 0);
240 }
241
242 _requireSolventIfPaused();
243
244 usdcRefund = (burnAmount * CAP) / USDC_MULTIPLIER;
245 if (usdcRefund == 0) {
246 revert Splitter__ZeroRefund();
247 }
248
249 uint256 localBalance = USDC.balanceOf(address(this));
250
251 if (localBalance < usdcRefund) {
252 withdrawnFromAdapter = usdcRefund - localBalance;
253
254 uint256 maxWithdraw = yieldAdapter.maxWithdraw(address(this));
255 if (maxWithdraw < withdrawnFromAdapter) {
256 revert Splitter__AdapterInsufficientLiquidity();
257 }
258 } else {
259 withdrawnFromAdapter = 0;
260 }
261 }
262
263 /// @notice Burn plDXY-BEAR and plDXY-BULL tokens to redeem USDC collateral.
264 /// @param amount The amount of token pairs to burn (18 decimals).
265 function burn(
266 uint256 amount
267 ) external nonReentrant {
268 if (amount == 0) {
269 revert Splitter__ZeroAmount();
270 }
271
272 _requireSolventIfPaused();
273
274 uint256 usdcRefund = (amount * CAP) / USDC_MULTIPLIER;
275 if (usdcRefund == 0) {
276 revert Splitter__ZeroRefund();
277 }
278
279 uint256 localBalance = USDC.balanceOf(address(this));
280
281 if (localBalance < usdcRefund) {
282 uint256 shortage = usdcRefund - localBalance;
283 if (address(yieldAdapter) == address(0)) {
284 revert Splitter__AdapterNotSet();
285 }
286
287 _withdrawFromAdapter(shortage);
288 }
289
290 USDC.safeTransfer(msg.sender, usdcRefund);
291
292 TOKEN_A.burn(msg.sender, amount);
293 TOKEN_B.burn(msg.sender, amount);
294
295 emit Burned(msg.sender, amount);
296 }
297
298 /// @dev Withdraws USDC from yield adapter with redeem fallback.
299 function _withdrawFromAdapter(
300 uint256 amount
301 ) internal {
302 try yieldAdapter.withdraw(amount, address(this), address(this)) {
303 }
304 catch {
305 uint256 sharesToRedeem = yieldAdapter.convertToShares(amount);
306 if (sharesToRedeem > 0) {
307 uint256 maxShares = yieldAdapter.maxRedeem(address(this));
308 sharesToRedeem = sharesToRedeem + 1 > maxShares ? maxShares : sharesToRedeem + 1;
309 }
310 try yieldAdapter.redeem(sharesToRedeem, address(this), address(this)) {}
311 catch {
312 revert Splitter__AdapterWithdrawFailed();
313 }
314 }
315 }
316
317 // ==========================================
318 // 3. LIQUIDATION & EMERGENCY
319 // ==========================================
320
321 /// @notice Locks the protocol into liquidated state when price >= CAP.
322 function triggerLiquidation() external nonReentrant {
323 uint256 price = _getOraclePrice();
324 if (price < CAP) {
325 revert Splitter__NotLiquidated();
326 }
327 if (isLiquidated) {
328 revert Splitter__LiquidationActive();
329 }
330
331 isLiquidated = true;
332 emit LiquidationTriggered(price);
333 }
334
335 /// @notice Emergency redemption when protocol is liquidated (price >= CAP).
336 function emergencyRedeem(
337 uint256 amount
338 ) external nonReentrant {
339 if (!isLiquidated) {
340 uint256 price = _getOraclePrice();
341 if (price >= CAP) {
342 isLiquidated = true;
343 emit LiquidationTriggered(price);
344 } else {
345 revert Splitter__NotLiquidated();
346 }
347 }
348 if (amount == 0) {
349 revert Splitter__ZeroAmount();
350 }
351
352 uint256 usdcRefund = (amount * CAP) / USDC_MULTIPLIER;
353 if (usdcRefund == 0) {
354 revert Splitter__ZeroRefund();
355 }
356
357 uint256 localBalance = USDC.balanceOf(address(this));
358 if (localBalance < usdcRefund) {
359 uint256 shortage = usdcRefund - localBalance;
360 _withdrawFromAdapter(shortage);
361 }
362
363 USDC.safeTransfer(msg.sender, usdcRefund);
364 TOKEN_A.burn(msg.sender, amount);
365
366 emit EmergencyRedeemed(msg.sender, amount);
367 }
368
369 /// @notice Emergency exit: withdraws all funds from yield adapter.
370 function ejectLiquidity() external onlyOwner {
371 if (address(yieldAdapter) == address(0)) {
372 revert Splitter__AdapterNotSet();
373 }
374
375 uint256 shares = yieldAdapter.balanceOf(address(this));
376 uint256 recovered = 0;
377
378 if (shares > 0) {
379 recovered = yieldAdapter.redeem(shares, address(this), address(this));
380 }
381
382 _pause();
383
384 emit EmergencyEjected(recovered);
385 }
386
387 /// @notice Withdraws a specific amount from yield adapter while paused.
388 function withdrawFromAdapter(
389 uint256 amount
390 ) external nonReentrant onlyOwner {
391 if (!paused()) {
392 revert Splitter__NotPaused();
393 }
394 if (address(yieldAdapter) == address(0)) {
395 revert Splitter__AdapterNotSet();
396 }
397 if (amount == 0) {
398 revert Splitter__ZeroAmount();
399 }
400
401 uint256 maxAvailable = yieldAdapter.maxWithdraw(address(this));
402 uint256 toWithdraw = amount > maxAvailable ? maxAvailable : amount;
403
404 if (toWithdraw > 0) {
405 yieldAdapter.withdraw(toWithdraw, address(this), address(this));
406 }
407
408 emit AdapterWithdrawn(amount, toWithdraw);
409 }
410
411 // ==========================================
412 // 4. HARVEST (Permissionless)
413 // ==========================================
414
415 function previewHarvest()
416 external
417 view
418 returns (
419 bool canHarvest,
420 uint256 totalSurplus,
421 uint256 callerReward,
422 uint256 treasuryShare,
423 uint256 stakingShare
424 )
425 {
426 if (address(yieldAdapter) == address(0)) {
427 return (false, 0, 0, 0, 0);
428 }
429
430 uint256 myShares = yieldAdapter.balanceOf(address(this));
431 uint256 adapterAssets = yieldAdapter.convertToAssets(myShares);
432 uint256 localBuffer = USDC.balanceOf(address(this));
433 uint256 totalHoldings = adapterAssets + localBuffer;
434
435 uint256 requiredBacking = (TOKEN_A.totalSupply() * CAP) / USDC_MULTIPLIER;
436
437 if (totalHoldings > requiredBacking) {
438 totalSurplus = totalHoldings - requiredBacking;
439 } else {
440 return (false, 0, 0, 0, 0);
441 }
442
443 if (totalSurplus < MIN_SURPLUS_THRESHOLD) {
444 return (false, totalSurplus, 0, 0, 0);
445 }
446
447 uint256 harvestableAmount = (adapterAssets > totalSurplus) ? totalSurplus : adapterAssets;
448
449 callerReward = (harvestableAmount * HARVEST_REWARD_BPS) / 10_000;
450 uint256 remaining = harvestableAmount - callerReward;
451 treasuryShare = (remaining * 20) / 100;
452 stakingShare = remaining - treasuryShare;
453
454 canHarvest = true;
455 }
456
457 /// @notice Permissionless yield harvesting from the adapter.
458 function harvestYield() external nonReentrant whenNotPaused {
459 if (address(yieldAdapter) == address(0)) {
460 revert Splitter__AdapterNotSet();
461 }
462
463 uint256 myShares = yieldAdapter.balanceOf(address(this));
464 uint256 totalAssets = yieldAdapter.convertToAssets(myShares);
465 uint256 localBuffer = USDC.balanceOf(address(this));
466 uint256 totalHoldings = totalAssets + localBuffer;
467
468 uint256 requiredBacking = (TOKEN_A.totalSupply() * CAP) / USDC_MULTIPLIER;
469
470 if (totalHoldings <= requiredBacking + MIN_SURPLUS_THRESHOLD) {
471 revert Splitter__NoSurplus();
472 }
473
474 uint256 surplus = totalHoldings - requiredBacking;
475
476 uint256 expectedPull = totalAssets > surplus ? surplus : totalAssets;
477 uint256 balanceBefore = USDC.balanceOf(address(this));
478 if (totalAssets > surplus) {
479 yieldAdapter.withdraw(surplus, address(this), address(this));
480 } else {
481 yieldAdapter.redeem(myShares, address(this), address(this));
482 }
483 uint256 harvested = USDC.balanceOf(address(this)) - balanceBefore;
484
485 if (harvested < (expectedPull * 90) / 100) {
486 revert Splitter__InsufficientHarvest();
487 }
488
489 uint256 callerCut = (harvested * HARVEST_REWARD_BPS) / 10_000;
490 uint256 remaining = harvested - callerCut;
491 uint256 treasuryShare = (remaining * 20) / 100;
492 uint256 stakingShare = remaining - treasuryShare;
493
494 emit YieldHarvested(harvested, treasuryShare, stakingShare);
495
496 if (callerCut > 0) {
497 USDC.safeTransfer(msg.sender, callerCut);
498 }
499 USDC.safeTransfer(treasury, treasuryShare);
500 if (staking != address(0)) {
501 USDC.safeTransfer(staking, stakingShare);
502 } else {
503 USDC.safeTransfer(treasury, stakingShare);
504 }
505 }
506
507 // ==========================================
508 // 5. GOVERNANCE
509 // ==========================================
510
511 function _checkLiveness() internal view {
512 if (paused()) {
513 revert Splitter__GovernanceLocked();
514 }
515 if (block.timestamp < lastUnpauseTime + TIMELOCK_DELAY) {
516 revert Splitter__GovernanceLocked();
517 }
518 }
519
520 /// @notice Propose new fee receiver addresses (7-day timelock).
521 function proposeFeeReceivers(
522 address _treasury,
523 address _staking
524 ) external onlyOwner {
525 if (_treasury == address(0)) {
526 revert Splitter__ZeroAddress();
527 }
528 pendingFees = FeeConfig(_treasury, _staking);
529 feesActivationTime = block.timestamp + TIMELOCK_DELAY;
530 emit FeesProposed(_treasury, _staking, feesActivationTime);
531 }
532
533 /// @notice Finalize pending fee receiver change after timelock expires.
534 function finalizeFeeReceivers() external onlyOwner {
535 if (feesActivationTime == 0) {
536 revert Splitter__InvalidProposal();
537 }
538 if (block.timestamp < feesActivationTime) {
539 revert Splitter__TimelockActive();
540 }
541
542 _checkLiveness();
543
544 treasury = pendingFees.treasuryAddr;
545 staking = pendingFees.stakingAddr;
546
547 delete pendingFees;
548 feesActivationTime = 0;
549 emit FeesUpdated(treasury, staking);
550 }
551
552 /// @notice Propose a new yield adapter (7-day timelock).
553 function proposeAdapter(
554 address _newAdapter
555 ) external onlyOwner {
556 if (_newAdapter == address(0)) {
557 revert Splitter__ZeroAddress();
558 }
559 pendingAdapter = _newAdapter;
560 adapterActivationTime = block.timestamp + TIMELOCK_DELAY;
561 emit AdapterProposed(_newAdapter, adapterActivationTime);
562 }
563
564 /// @notice Finalize adapter migration after timelock.
565 function finalizeAdapter() external nonReentrant onlyOwner {
566 if (pendingAdapter == address(0)) {
567 revert Splitter__InvalidProposal();
568 }
569 if (block.timestamp < adapterActivationTime) {
570 revert Splitter__TimelockActive();
571 }
572
573 _checkLiveness();
574
575 uint256 assetsBefore = _getTotalAssets();
576
577 IERC4626 oldAdapter = yieldAdapter;
578 IERC4626 newAdapter = IERC4626(pendingAdapter);
579
580 uint256 movedAmount = 0;
581 if (address(oldAdapter) != address(0)) {
582 uint256 shares = oldAdapter.balanceOf(address(this));
583 if (shares > 0) {
584 movedAmount = oldAdapter.redeem(shares, address(this), address(this));
585 }
586 }
587 if (movedAmount > 0) {
588 USDC.forceApprove(address(newAdapter), movedAmount);
589 newAdapter.deposit(movedAmount, address(this));
590 }
591
592 yieldAdapter = newAdapter;
593
594 uint256 assetsAfter = _getTotalAssets();
595 if (assetsAfter < (assetsBefore * 99_999) / 100_000) {
596 revert Splitter__MigrationLostFunds();
597 }
598
599 pendingAdapter = address(0);
600 adapterActivationTime = 0;
601 emit AdapterMigrated(address(oldAdapter), address(newAdapter), movedAmount);
602 }
603
604 // ==========================================
605 // ADMIN HELPERS
606 // ==========================================
607
608 function pause() external onlyOwner {
609 _pause();
610 }
611
612 function unpause() external onlyOwner {
613 lastUnpauseTime = block.timestamp;
614 _unpause();
615 }
616
617 /// @notice Rescue accidentally sent tokens.
618 function rescueToken(
619 address token,
620 address to
621 ) external onlyOwner {
622 if (token == address(USDC) || token == address(TOKEN_A) || token == address(TOKEN_B)) {
623 revert Splitter__CannotRescueCoreAsset();
624 }
625 uint256 balance = IERC20(token).balanceOf(address(this));
626 IERC20(token).safeTransfer(to, balance);
627 emit TokenRescued(token, to, balance);
628 }
629
630 // ==========================================
631 // VIEW HELPERS (DASHBOARD)
632 // ==========================================
633
634 function currentStatus() external view override returns (Status) {
635 if (isLiquidated) {
636 return Status.SETTLED;
637 }
638 if (paused()) {
639 return Status.PAUSED;
640 }
641 return Status.ACTIVE;
642 }
643
644 /// @notice Returns comprehensive system metrics for dashboards.
645 function getSystemStatus() external view returns (SystemStatus memory status) {
646 status.capPrice = CAP;
647 status.liquidated = isLiquidated;
648 status.isPaused = paused();
649
650 try ORACLE.latestRoundData() returns (uint80, int256 price, uint256, uint256, uint80) {
651 status.currentPrice = price > 0 ? uint256(price) : 0;
652 } catch {
653 status.currentPrice = 0;
654 }
655
656 status.totalAssets = _getTotalAssets();
657 status.totalLiabilities = _getTotalLiabilities();
658
659 if (status.totalLiabilities > 0) {
660 status.collateralRatio = (status.totalAssets * 1e4) / status.totalLiabilities;
661 } else {
662 status.collateralRatio = 0;
663 }
664
665 if (address(yieldAdapter) != address(0)) {
666 uint256 myShares = yieldAdapter.balanceOf(address(this));
667 status.adapterAssets = yieldAdapter.convertToAssets(myShares);
668 }
669 }
670
671 // ==========================================
672 // INTERNAL HELPERS
673 // ==========================================
674
675 function _getTotalLiabilities() internal view returns (uint256) {
676 return (TOKEN_A.totalSupply() * CAP) / USDC_MULTIPLIER;
677 }
678
679 function _getTotalAssets() internal view returns (uint256) {
680 uint256 adapterValue = 0;
681 if (address(yieldAdapter) != address(0)) {
682 uint256 myShares = yieldAdapter.balanceOf(address(this));
683 adapterValue = yieldAdapter.convertToAssets(myShares);
684 }
685 return USDC.balanceOf(address(this)) + adapterValue;
686 }
687
688 function _requireSolventIfPaused() internal view {
689 if (paused()) {
690 uint256 totalAssets = _getTotalAssets();
691 uint256 totalLiabilities = _getTotalLiabilities();
692 if (totalAssets < totalLiabilities) {
693 revert Splitter__Insolvent();
694 }
695 }
696 }
697
698 function _getOraclePrice() internal view returns (uint256) {
699 return OracleLib.getValidatedPrice(ORACLE, SEQUENCER_UPTIME_FEED, SEQUENCER_GRACE_PERIOD, ORACLE_TIMEOUT);
700 }
701
702}Our Commitments
Sovereignty by Design
Non-Custodial
Your funds. Your keys. Your exit. There is no admin switch that can freeze your position or block your withdrawal. You can close and redeem at any time—no permission required.
0% Protocol Fees
No entry fees. No exit fees. No tribute for the privilege of using the protocol. Plether only benefits when it delivers real yield—not by extracting rent from your trades. Our interests are aligned: we grow only when your collateral grows.
Open Source
We don't believe in security by obscurity. All Plether code—contracts, front-end, infrastructure—is open source under the AGPL license. Fork us. Contribute. Compete. Make us better.
No VC Funding
No outside investors. No board seats. No misaligned incentives. We follow the principle of financial sovereignty by starting with our own.
Passes Walkaway Test
Built to last, not to be maintained. No upgradeable proxies. No off-chain dependencies. Admin keys exist for emergencies, not operations. The protocol runs indefinitely without intervention.