Downloading
0%

Protocol at a Glance

Real-time metrics from the Plether ecosystem

Collateral
Total Value Locked
APY
splDXY-BULL: 0%
splDXY-BEAR: 0%
24h Volume

Transparent by Design

All Plether contracts are public and verifiable

SyntheticSplitter.sol
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}
View on GitHub →

Independently Audited

Smart contracts reviewed by Cantina's security researchers

CantinaView Audit Report →

Our Commitments

Sovereignty by Design

1

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.

2

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.

3

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.

4

No VC Funding

No outside investors. No board seats. No misaligned incentives. We follow the principle of financial sovereignty by starting with our own.

5

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.

Contact Us