diff --git a/chains/plasma/PlasmaConstantsLib.sol b/chains/plasma/PlasmaConstantsLib.sol index f908eb87..9ba96a7d 100644 --- a/chains/plasma/PlasmaConstantsLib.sol +++ b/chains/plasma/PlasmaConstantsLib.sol @@ -4,8 +4,13 @@ pragma solidity ^0.8.28; library PlasmaConstantsLib { address public constant MULTISIG = 0xE929438B5B53984FdBABf8562046e141e90E8099; address public constant PLATFORM = 0xd4D6ad656f64E8644AFa18e7CCc9372E0Cd256f0; + address public constant REVENUE_ROUTER = 0xAf95468B1a624605bbFb862B0FB6e9C73Ad847b8; + address public constant XSTBL_XSTAKING = 0x601572b91DC054Be500392A6d3e15c690140998D; + address public constant RECOVERY = 0x046e7a007C331e0d4DafA66104744dB14a52bBBb; // ERC20 + address public constant TOKEN_STBL = 0xfdf91362B7E9330F232e500c0236a02B0DE3e492; + address public constant TOKEN_XSTBL = 0xF40D0724599282CaF9dfb66feB630e936bC0CFBE; address public constant TOKEN_USDT0 = 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb; address public constant TOKEN_USDE = 0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34; address public constant TOKEN_SUSDE = 0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2; diff --git a/guides/AllDeployments.md b/guides/AllDeployments.md index 1e837579..8f21e2f8 100644 --- a/guides/AllDeployments.md +++ b/guides/AllDeployments.md @@ -86,15 +86,20 @@ ### Core -* **Platform** `0xd4d6ad656f64e8644afa18e7ccc9372e0cd256f0` [plasmascan](https://plasmascan.to/address/0xd4D6ad656f64E8644AFa18e7CCc9372E0Cd256f0/contract/9745/readProxyContract) -* **Factory** `0x4c5758e3c454a260d98238706ca6f4802cc52746` [plasmascan](https://plasmascan.to/address/0x4C5758e3c454A260D98238706cA6F4802cc52746) -* **MetaVaultFactory** `0xbd5296dc2603942f116b375c8ee373674be86f56` [plasmascan](https://plasmascan.to/address/0xbd5296dc2603942f116b375c8ee373674be86f56) -* **PriceReader** `0xd4af1b538c826e1e90b695314042ab6fd0e7f4aa` [plasmascan](https://plasmascan.to/address/0xd4af1b538c826e1e90b695314042ab6fd0e7f4aa) -* **Swapper** `0x54f22378e03bea25a05a071b60357d31ce535bb9` [plasmascan](https://plasmascan.to/address/0x54f22378e03bea25a05a071b60357d31ce535bb9) -* **HardWorker** `0x73ae48f75ea304ff229d2a1374654672fa8388e7` [plasmascan](https://plasmascan.to/address/0x73ae48f75ea304ff229d2a1374654672fa8388e7) -* **VaultManager** `0x22d031e45a02d6472786b9d7a4fd78f1733d6990` [plasmascan](https://plasmascan.to/address/0x22d031e45a02d6472786b9d7a4fd78f1733d6990) -* **StrategyLogic** `0x5e670b7a1740e8a11aff06335984d696d537b7bc` [plasmascan](https://plasmascan.to/address/0x5e670b7a1740e8a11aff06335984d696d537b7bc) -* **Zap** `0xbc4ff9766074b53c7a4cb91964836c83188f333d` [plasmascan](https://plasmascan.to/address/0xbc4ff9766074b53c7a4cb91964836c83188f333d) +* **Platform** `0xd4d6ad656f64e8644afa18e7ccc9372e0cd256f0` [plasmascan](https://plasmascan.to/address/0xd4D6ad656f64E8644AFa18e7CCc9372E0Cd256f0#readProxyContract) +* **Factory** `0x4c5758e3c454a260d98238706ca6f4802cc52746` [plasmascan](https://plasmascan.to/address/0x4C5758e3c454A260D98238706cA6F4802cc52746#readProxyContract) +* **MetaVaultFactory** `0xbd5296dc2603942f116b375c8ee373674be86f56` [plasmascan](https://plasmascan.to/address/0xbd5296dc2603942f116b375c8ee373674be86f56#readProxyContract) +* **PriceReader** `0xd4af1b538c826e1e90b695314042ab6fd0e7f4aa` [plasmascan](https://plasmascan.to/address/0xd4af1b538c826e1e90b695314042ab6fd0e7f4aa#readProxyContract) +* **Swapper** `0x54f22378e03bea25a05a071b60357d31ce535bb9` [plasmascan](https://plasmascan.to/address/0x54f22378e03bea25a05a071b60357d31ce535bb9#readProxyContract) +* **HardWorker** `0x73ae48f75ea304ff229d2a1374654672fa8388e7` [plasmascan](https://plasmascan.to/address/0x73ae48f75ea304ff229d2a1374654672fa8388e7#readProxyContract) +* **VaultManager** `0x22d031e45a02d6472786b9d7a4fd78f1733d6990` [plasmascan](https://plasmascan.to/address/0x22d031e45a02d6472786b9d7a4fd78f1733d6990#readProxyContract) +* **StrategyLogic** `0x5e670b7a1740e8a11aff06335984d696d537b7bc` [plasmascan](https://plasmascan.to/address/0x5e670b7a1740e8a11aff06335984d696d537b7bc#readProxyContract) +* **Zap** `0xbc4ff9766074b53c7a4cb91964836c83188f333d` [plasmascan](https://plasmascan.to/address/0xbc4ff9766074b53c7a4cb91964836c83188f333d#readProxyContract) + +### Tokenomics + +* **RevenueRouter** `0xAf95468B1a624605bbFb862B0FB6e9C73Ad847b8` [plasmascan](https://plasmascan.to/address/0xAf95468B1a624605bbFb862B0FB6e9C73Ad847b8#readProxyContract) +* **XStaking** `0x601572b91DC054Be500392A6d3e15c690140998D` [plasmascan](https://plasmascan.to/address/0x601572b91DC054Be500392A6d3e15c690140998D#readProxyContract) ### Periphery diff --git a/guides/Deploy.md b/guides/Deploy.md index 2485d351..498b8746 100644 --- a/guides/Deploy.md +++ b/guides/Deploy.md @@ -6,14 +6,12 @@ It is important to verify during deployment. Otherwise, you will have to manuall ### Plasma -`forge script` now show error: `Error: Chain 9745 not supported`. - -When deploying Platform via `forge create` we see `Error: Dynamic linking not supported in `create` command - deploy the following library contracts first, then provide the address to link at compile time`. - ```shell -forge script --rpc-url plasma --slow --broadcast --verify --etherscan-api-key sonic --verifier-url 'https://api.routescan.io/v2/network/mainnet/evm/9745/etherscan' ./script/deploy-core/Deploy.Plasma.s.sol +forge script --rpc-url plasma --slow --broadcast --verify --etherscan-api-key plasma --verifier-url 'https://api.etherscan.io/v2/api?chainid=9745' ./script/deploy-core/Deploy.Plasma.s.sol +``` -# verify single +``` +# verify on routescan forge verify-contract --rpc-url plasma --etherscan-api-key sonic --verifier-url 'https://api.routescan.io/v2/network/mainnet/evm/9745/etherscan' --watch 0x70e804364175e23F1c30dFa03BFb19d936E5E81c ``` diff --git a/script/deploy-tokenomics/XStaking.s.sol b/script/deploy-tokenomics/XStaking.s.sol new file mode 100644 index 00000000..27b4febd --- /dev/null +++ b/script/deploy-tokenomics/XStaking.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {XStaking} from "../../src/tokenomics/XStaking.sol"; + +contract DeployXStaking is Script { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + new XStaking(); + vm.stopBroadcast(); + } + + function testDeployScript() external {} +} diff --git a/src/interfaces/IRevenueRouter.sol b/src/interfaces/IRevenueRouter.sol index cadb0e4b..394568ef 100644 --- a/src/interfaces/IRevenueRouter.sol +++ b/src/interfaces/IRevenueRouter.sol @@ -14,7 +14,7 @@ interface IRevenueRouter { event UnitEpochRevenue(uint periodEnded, string unitName, uint stblRevenue); event ProcessUnitRevenue(uint unitIndex, uint stblGot); event SetAddresses(address[] addresses); - event SetXShare(uint newShare); + event BuyBackRate(uint bbRate); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CUSTOM ERRORS */ @@ -34,7 +34,7 @@ interface IRevenueRouter { address xToken; address xStaking; address feeTreasury; - uint xShare; + uint __deprecated_xShare; uint activePeriod; uint pendingRevenue; Unit[] units; @@ -43,6 +43,10 @@ interface IRevenueRouter { EnumerableSet.AddressSet assetsAccumulated; EnumerableMap.AddressToUintMap minSwapAmount; EnumerableMap.AddressToUintMap maxSwapAmount; + + // todo use DAO parameter + uint bbRate; + EnumerableMap.AddressToUintMap pendingRevenueAsset; } enum UnitType { @@ -77,12 +81,12 @@ interface IRevenueRouter { /// @notice Set max swap amounts for assets function setMaxSwapAmounts(address[] calldata assets, uint[] calldata maxAmounts) external; - /// @notice Change revenue share for Vaults Unit - function setXShare(uint newShare) external; - /// @notice Set addresses of main-token, xToken, xStaking and feeTreasure token. function setAddresses(address[] memory addresses_) external; + /// @notice Set buy-back rate for rewards + function setBuyBackRate(uint bbRate) external; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* USER ACTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -144,6 +148,13 @@ interface IRevenueRouter { /// @notice Get assets that contract hold on balance function assetsAccumulated() external view returns (address[] memory); - /// @notice Get current xToken revenue share for Vaults Unit - function xShare() external view returns (uint); + /// @notice Buy-back rate for generated revenue + function buyBackRate() external view returns (uint); + + /// @notice Asset with pending revenue for distribution + function pendingRevenueAssets() external view returns (address[] memory); + + /// @notice Pending revenue in form of asset + /// @param asset Allowed asset address + function pendingRevenueAsset(address asset) external view returns (uint); } diff --git a/src/interfaces/IXStaking.sol b/src/interfaces/IXStaking.sol index dde17067..8a1324e7 100644 --- a/src/interfaces/IXStaking.sol +++ b/src/interfaces/IXStaking.sol @@ -11,11 +11,21 @@ interface IXStaking { event Withdraw(address indexed from, uint amount); event NotifyReward(address indexed from, uint amount); - event ClaimRewards(address indexed from, uint amount); event NewDuration(uint oldDuration, uint newDuration); + event RewardTokenAllowed(address indexed token, bool allowed); + event NotifyRewardForToken(address indexed token, address indexed from, uint amount); + event ClaimRewardsForToken(address indexed token, address indexed from, uint amount); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + error DaoNotInitialized(); + error TokenNotAllowed(address token); + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* WRITE FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -48,12 +58,27 @@ interface IXStaking { /// Otherwise, the user receives 1 DAO-token for each 1 xToken staked. function syncDAOBalances(address[] calldata users) external; + /// @notice Allow or disallow reward token + /// @param token Address of reward token + /// @param allowed Allowed state + /// @custom:restricted Only operator + function allowRewardToken(address token, bool allowed) external; + + /// @notice Used to notify pending xToken rebases and platform revenue share + /// @custom:restricted Only RevenueRouter or xToken + /// @param token Address of reward token + /// @param amount The amount of main token to be notified + function notifyRewardAmountToken(address token, uint amount) external; + + /// @notice Claims pending rewards + /// @param token Address of reward token + function getRewardToken(address token) external; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* VIEW FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @notice Returns the last time the reward was modified or periodFinish if the reward has ended - function lastTimeRewardApplicable() external view returns (uint); + // common data /// @notice The address of the xToken token (staking/voting token) /// @return xToken address @@ -62,6 +87,19 @@ interface IXStaking { /// @notice Returns the total voting power (equal to total supply in the XStaking) function totalSupply() external view returns (uint); + /// @notice The duration of notified rewards distribution + function duration() external view returns (uint); + + /// @notice Staked amount + /// @param user the address to check + /// @return The staked balance + function balanceOf(address user) external view returns (uint); + + // legacy data + + /// @notice Returns the last time the reward was modified or periodFinish if the reward has ended + function lastTimeRewardApplicable() external view returns (uint); + /// @notice Last time the rewards system was updated function lastUpdateTime() external view returns (uint); @@ -74,9 +112,6 @@ interface IXStaking { /// @notice Calculates the rewards distributed per second function rewardRate() external view returns (uint); - /// @notice The duration of notified rewards distribution - function duration() external view returns (uint); - /// @dev Current calculated reward per token /// @return The return value is scaled (multiplied) by PRECISION = 10 ** 18 function rewardPerToken() external view returns (uint); @@ -92,8 +127,13 @@ interface IXStaking { /// @notice User's earned reward function earned(address account) external view returns (uint); - /// @notice Voting power - /// @param user the address to check - /// @return The staked balance - function balanceOf(address user) external view returns (uint); + // reward tokens data + + /// @notice Is token allowed for rewards + function isTokenAllowed(address token) external view returns (bool); + + /// @notice User's earned reward of token + /// @param token Address of reward token + /// @param account Address of user + function earnedToken(address token, address account) external view returns (uint); } diff --git a/src/tokenomics/RevenueRouter.sol b/src/tokenomics/RevenueRouter.sol index d9dcc7de..9c93f615 100644 --- a/src/tokenomics/RevenueRouter.sol +++ b/src/tokenomics/RevenueRouter.sol @@ -19,6 +19,7 @@ import {IRecoveryBase} from "../interfaces/IRecoveryBase.sol"; /// @title Platform revenue distributor /// Changelog: +/// 2.0.0: buy-back rate; remove xShare /// 1.8.0: renaming (STBL => main-token, xSTBL => xToken), xShare = 100% by default. /// Add setAddresses, getXShare. RevenueRouter uses IRecoveryBase instead of IRecovery - #426 /// 1.7.1: add addresses() @@ -39,9 +40,11 @@ contract RevenueRouter is Controllable, IRevenueRouter { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.8.0"; + string public constant VERSION = "2.0.0"; + // todo get from DAO parameters uint internal constant RECOVER_PERCENTAGE = 20_000; // 20% + uint internal constant DENOMINATOR = 100_000; // 100% /// @notice Count of addresses in addresses() and setAddresses @@ -62,7 +65,6 @@ contract RevenueRouter is Controllable, IRevenueRouter { $.token = IXToken(xToken_).token(); $.xToken = xToken_; $.xStaking = IXToken(xToken_).xStaking(); - $.xShare = 100_000; } $.feeTreasury = feeTreasury_; $.activePeriod = getPeriod(); @@ -125,14 +127,6 @@ contract RevenueRouter is Controllable, IRevenueRouter { } } - /// @inheritdoc IRevenueRouter - function setXShare(uint newShare) external onlyGovernanceOrMultisig { - RevenueRouterStorage storage $ = _getRevenueRouterStorage(); - $.xShare = newShare; - - emit SetXShare(newShare); - } - /// @inheritdoc IRevenueRouter function setAddresses(address[] memory addresses_) external onlyGovernanceOrMultisig { RevenueRouterStorage storage $ = _getRevenueRouterStorage(); @@ -144,6 +138,13 @@ contract RevenueRouter is Controllable, IRevenueRouter { emit SetAddresses(addresses_); } + /// @inheritdoc IRevenueRouter + function setBuyBackRate(uint bbRate) external onlyOperator { + RevenueRouterStorage storage $ = _getRevenueRouterStorage(); + $.bbRate = bbRate; + emit BuyBackRate(bbRate); + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* USER ACTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -176,13 +177,26 @@ contract RevenueRouter is Controllable, IRevenueRouter { } // put week rewards to XStaking users + address _xStaking = $.xStaking; + + // buy-back rewards if (_pendingRevenue != 0) { - address _xStaking = $.xStaking; IERC20($.token).approve(_xStaking, _pendingRevenue); IXStaking(_xStaking).notifyRewardAmount(_pendingRevenue); } - + // not used now emit EpochFlip(periodEnded, _pendingRevenue); + + // other tokens rewards + address[] memory _pendingRevenueAssets = $.pendingRevenueAsset.keys(); + len = _pendingRevenueAssets.length; + for (uint i; i < len; ++i) { + address asset = _pendingRevenueAssets[i]; + uint revenue = $.pendingRevenueAsset.get(asset); + IERC20(asset).approve(_xStaking, revenue); + IXStaking(_xStaking).notifyRewardAmountToken(asset, revenue); + } + $.pendingRevenueAsset.clear(); } } @@ -252,77 +266,103 @@ contract RevenueRouter is Controllable, IRevenueRouter { } } + struct ProcessAssetVars { + address asset; + uint amountOnBalance; + uint amountPending; + uint amountToProcess; + uint amountToBuyBack; + uint amountToPending; + bool cleanup; + bool finish; + } + /// @inheritdoc IRevenueRouter function processAccumulatedAssets(uint maxAssetsForProcess) external onlyOperatorAgent { RevenueRouterStorage storage $ = _getRevenueRouterStorage(); + uint bbRate = $.bbRate; address[] memory _assetsAccumulated = $.assetsAccumulated.values(); uint len = _assetsAccumulated.length; for (uint i; i < len; ++i) { - address asset = _assetsAccumulated[i]; if (i == maxAssetsForProcess) { break; } - uint amount = IERC20(asset).balanceOf(address(this)); + ProcessAssetVars memory v; + v.asset = _assetsAccumulated[i]; + v.amountOnBalance = IERC20(v.asset).balanceOf(address(this)); + (, v.amountPending) = $.pendingRevenueAsset.tryGet(v.asset); - { - (bool minAmountExist, uint minSwapAmount) = $.minSwapAmount.tryGet(asset); - if (!minAmountExist || amount < minSwapAmount) { - continue; - } - } + v.amountToProcess = v.amountOnBalance - v.amountPending; + v.amountToBuyBack = v.amountToProcess * bbRate / 100; - bool cleanup; - { - (bool maxAmountExist, uint maxSwapAmount) = $.maxSwapAmount.tryGet(asset); - if (!maxAmountExist) { + // when buy-back rate is zero not need to check min/max swap amount settings + if (v.amountToBuyBack != 0) { + (bool minAmountExist, uint minSwapAmount) = $.minSwapAmount.tryGet(v.asset); + (bool maxAmountExist, uint maxSwapAmount) = $.maxSwapAmount.tryGet(v.asset); + if (!minAmountExist || v.amountToBuyBack < minSwapAmount || !maxAmountExist) { continue; } - if (amount > maxSwapAmount) { - amount = maxSwapAmount; + if (v.amountToBuyBack > maxSwapAmount) { + v.amountToBuyBack = maxSwapAmount; + v.amountToProcess = v.amountToBuyBack * 100 / bbRate; } else { - cleanup = true; + v.cleanup = true; } + } else { + v.cleanup = true; } - uint amountToSwap = amount; - address mainToken = $.token; - ISwapper swapper = ISwapper(IPlatform(platform()).swapper()); + if (bbRate < 100 && !IXStaking($.xStaking).isTokenAllowed(v.asset)) { + continue; + } { address _recovery = IPlatform(platform()).recovery(); if (_recovery != address(0)) { - uint toRecovery = amountToSwap * RECOVER_PERCENTAGE / DENOMINATOR; - amountToSwap -= toRecovery; - IERC20(asset).safeTransfer(_recovery, toRecovery); + uint toRecovery = v.amountToProcess * RECOVER_PERCENTAGE / DENOMINATOR; + v.amountToProcess -= toRecovery; + v.amountToBuyBack = v.amountToProcess * bbRate / 100; + IERC20(v.asset).safeTransfer(_recovery, toRecovery); address[] memory assetsToRegister = new address[](1); - assetsToRegister[0] = asset; + assetsToRegister[0] = v.asset; IRecoveryBase(_recovery).registerAssets(assetsToRegister); } } - if (mainToken != address(0)) { + address mainToken = $.token; + require(mainToken != address(0), "SetupMainToken"); + ISwapper swapper = ISwapper(IPlatform(platform()).swapper()); + + if (v.amountToBuyBack != 0) { uint mainTokenBalanceWas = IERC20(mainToken).balanceOf(address(this)); - IERC20(asset).forceApprove(address(swapper), amountToSwap); - try swapper.swap(asset, mainToken, amountToSwap, 20_000) { - uint mainTokenGot = IERC20(mainToken).balanceOf(address(this)) - mainTokenBalanceWas; - uint xGot = mainTokenGot * $.xShare / DENOMINATOR; - if (mainTokenGot > xGot) { - IERC20(mainToken).safeTransfer($.feeTreasury, mainTokenGot - xGot); - } + + IERC20(v.asset).forceApprove(address(swapper), v.amountToBuyBack); + try swapper.swap(v.asset, mainToken, v.amountToBuyBack, 40_000) { + uint xGot = IERC20(mainToken).balanceOf(address(this)) - mainTokenBalanceWas; $.pendingRevenue += xGot; - if (cleanup) { - $.assetsAccumulated.remove(_assetsAccumulated[i]); - } - return; + v.finish = true; } catch {} - } else { - // swap to exchange asset and bridge to sonic - // todo + } + + v.amountToPending = v.amountToProcess - v.amountToBuyBack; + + if (v.amountToPending != 0) { + $.pendingRevenueAsset.set(v.asset, v.amountPending + v.amountToPending); + v.finish = true; + } + + if (v.cleanup) { + $.assetsAccumulated.remove(v.asset); + } + + if (v.finish) { + return; } } + revert CantProcessAction(); } @@ -419,8 +459,19 @@ contract RevenueRouter is Controllable, IRevenueRouter { } /// @inheritdoc IRevenueRouter - function xShare() external view returns (uint) { - return _getRevenueRouterStorage().xShare; + function buyBackRate() external view returns (uint) { + return _getRevenueRouterStorage().bbRate; + } + + /// @inheritdoc IRevenueRouter + function pendingRevenueAssets() external view returns (address[] memory) { + return _getRevenueRouterStorage().pendingRevenueAsset.keys(); + } + + /// @inheritdoc IRevenueRouter + function pendingRevenueAsset(address asset) external view returns (uint) { + (, uint amount) = _getRevenueRouterStorage().pendingRevenueAsset.tryGet(asset); + return amount; } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ diff --git a/src/tokenomics/XStaking.sol b/src/tokenomics/XStaking.sol index fb83615a..4f21c128 100644 --- a/src/tokenomics/XStaking.sol +++ b/src/tokenomics/XStaking.sol @@ -17,6 +17,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol /// @author Alien Deployer (https://github.com/a17) /// @author Jude (https://github.com/iammrjude) /// Changelog: +/// 2.0.0: rewards by any allowed token /// 1.1.2: renaming xSTBL => xToken, StabilityDAO => DAO, syncStabilityDAOBalances => syncDAOBalances /// 1.1.1: syncStabilityDAOBalances - only operator /// 1.1.0: Integration with STBLDAO @@ -29,7 +30,7 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.1.2"; + string public constant VERSION = "2.0.0"; /// @notice decimal precision of 1e18 uint public constant PRECISION = 10 ** 18; @@ -64,9 +65,20 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { mapping(address user => uint rewardPerToken) userRewardPerTokenStored; /// @inheritdoc IXStaking mapping(address user => uint amount) balanceOf; + + // custom token rewards + mapping(address token => TokenRewards) tokenRewards; } - error DaoNotInitialized(); + struct TokenRewards { + bool allowed; + uint lastUpdateTime; + uint rewardPerTokenStored; + uint periodFinish; + uint rewardRate; + mapping(address user => uint rewards) storedRewardsPerUser; + mapping(address user => uint rewardPerToken) userRewardPerTokenStored; + } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* INITIALIZATION */ @@ -90,6 +102,11 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { _; } + modifier updateRewardForToken(address token, address account) { + _updateRewardForToken(token, account); + _; + } + //region ----------------------------------- Restricted actions /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* RESTRICTED ACTIONS */ @@ -118,6 +135,12 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { } } + function allowRewardToken(address token, bool allowed) external onlyOperator { + TokenRewards storage $$ = _getXStakingStorage().tokenRewards[token]; + $$.allowed = allowed; + emit RewardTokenAllowed(token, allowed); + } + //endregion ----------------------------------- Restricted actions //region ----------------------------------- User actions @@ -228,6 +251,57 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { emit NotifyReward(msg.sender, amount); } + /// @inheritdoc IXStaking + function getRewardToken(address token) external updateRewardForToken(token, msg.sender) nonReentrant { + /// @dev claim all the rewards + _claimToken(token, msg.sender); + } + + /// @inheritdoc IXStaking + function notifyRewardAmountToken( + address token, + uint amount + ) external updateRewardForToken(token, address(0)) nonReentrant { + /// @dev ensure > 0 + require(amount != 0, IncorrectZeroArgument()); + + XStakingStorage storage $ = _getXStakingStorage(); + TokenRewards storage $$ = $.tokenRewards[token]; + + require($$.allowed, TokenNotAllowed(token)); + + address _xToken = $.xToken; + + /// @dev only callable by xToken and RevenueRouter contract + require(msg.sender == _xToken || msg.sender == IXToken(_xToken).revenueRouter(), IncorrectMsgSender()); + + /// @dev take the token from a contract to the XStaking + // slither-disable-next-line unchecked-transfer + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + uint _periodFinish = $$.periodFinish; + uint _duration = $.duration; + + if (block.timestamp >= _periodFinish) { + /// @dev the new reward rate being the amount divided by the duration + $$.rewardRate = amount / _duration; + } else { + /// @dev remaining seconds until the period finishes + uint remaining = _periodFinish - block.timestamp; + /// @dev remaining tokens to stream via t * rate + uint _left = remaining * $$.rewardRate; + /// @dev update the rewardRate to the notified amount plus what is left, divided by the duration + $$.rewardRate = (amount + _left) / _duration; + } + + /// @dev update timestamp for the rebase + $$.lastUpdateTime = block.timestamp; + /// @dev update periodFinish (when all rewards are streamed) + $$.periodFinish = block.timestamp + _duration; + + emit NotifyRewardForToken(token, msg.sender, amount); + } + //endregion ----------------------------------- User actions //region ----------------------------------- View functions @@ -320,6 +394,27 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { + $.storedRewardsPerUser[account]; } + // custom token rewards + + /// @inheritdoc IXStaking + function isTokenAllowed(address token) external view returns (bool) { + return _getXStakingStorage().tokenRewards[token].allowed; + } + + function earnedToken(address token, address account) public view returns (uint) { + XStakingStorage storage $ = _getXStakingStorage(); + TokenRewards storage $$ = $.tokenRewards[token]; + return + /// @dev the vote balance of the account + (($.balanceOf[account] + /// @dev current global reward per token, subtracted from the stored reward per token for the user + * (_rewardPerTokenForToken(token) - $$.userRewardPerTokenStored[account])) + /// @dev divide by the 1e18 precision + / PRECISION) + /// @dev add the existing stored rewards for the account to the total + + $$.storedRewardsPerUser[account]; + } + //endregion ----------------------------------- View functions //region ----------------------------------- Internal logic @@ -404,6 +499,77 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { } } + // custom token rewards + function _updateRewardForToken(address token, address account) internal { + TokenRewards storage $$ = _getXStakingStorage().tokenRewards[token]; + + /// @dev fetch and store the new rewardPerToken + $$.rewardPerTokenStored = _rewardPerTokenForToken(token); + + /// @dev fetch and store the new last update time + $$.lastUpdateTime = _lastTimeRewardApplicableForToken(token); + /// @dev check for address(0) calls from notifyRewardAmount + if (account != address(0)) { + /// @dev update the individual account's mapping for stored rewards + $$.storedRewardsPerUser[account] = earnedToken(token, account); + /// @dev update account's mapping for rewardsPerTokenStored + $$.userRewardPerTokenStored[account] = $$.rewardPerTokenStored; + } + } + + function _claimToken(address token, address user) internal { + XStakingStorage storage $ = _getXStakingStorage(); + TokenRewards storage $$ = $.tokenRewards[token]; + + /// @dev fetch the stored rewards (updated by modifier) + uint reward = $$.storedRewardsPerUser[user]; + if (reward > 0) { + /// @dev zero out the stored rewards + $$.storedRewardsPerUser[user] = 0; + + // todo use TokenRewards for xToken too + //address _xToken = $.xToken; + //address mainToken = IXToken(_xToken).token(); + /// @dev approve MainToken to xToken + //IERC20(mainToken).approve(_xToken, reward); + /// @dev convert + //IXToken(_xToken).enter(reward); + /// @dev transfer xToken to the user + //IERC20(_xToken).safeTransfer(user, reward); + + // slither-disable-next-line unchecked-transfer + IERC20(token).safeTransfer(user, reward); + + emit ClaimRewardsForToken(token, user, reward); + } + } + + /// @dev Current calculated reward per token for token + /// @param token Address of reward token + /// @return The return value is scaled (multiplied) by PRECISION = 10 ** 18 + function _rewardPerTokenForToken(address token) internal view returns (uint) { + XStakingStorage storage $ = _getXStakingStorage(); + TokenRewards storage $$ = $.tokenRewards[token]; + uint _totalSupply = $.totalSupply; + return + /// @dev if there's no staked xToken + _totalSupply == 0 + /// @dev return the existing value + ? $$.rewardPerTokenStored + /// @dev else add the existing value + : $$.rewardPerTokenStored + /// @dev to remaining time (since update) multiplied by the current reward rate + /// @dev scaled to precision of 1e18, then divided by the total supply + + (_lastTimeRewardApplicableForToken(token) - $$.lastUpdateTime) * $$.rewardRate * PRECISION + / _totalSupply; + } + + /// @notice Returns the last time the reward for token was modified or periodFinish if the reward has ended + /// @param token Address of reward token + function _lastTimeRewardApplicableForToken(address token) internal view returns (uint) { + return Math.min(block.timestamp, _getXStakingStorage().tokenRewards[token].periodFinish); + } + function _getXStakingStorage() internal pure returns (XStakingStorage storage $) { //slither-disable-next-line assembly assembly { diff --git a/test/tokenomics/RecoveryRelayer.Plasma.t.sol b/test/tokenomics/RecoveryRelayer.Plasma.t.sol index f68dc969..0849430b 100644 --- a/test/tokenomics/RecoveryRelayer.Plasma.t.sol +++ b/test/tokenomics/RecoveryRelayer.Plasma.t.sol @@ -194,9 +194,6 @@ contract RecoveryRelayerPlasmaTest is Test { // ---------------------- Set up revenue router IRevenueRouter revenueRouter = IRevenueRouter(IPlatform(PlasmaConstantsLib.PLATFORM).revenueRouter()); - vm.prank(multisig); - revenueRouter.setXShare(100_000); // no transfers to treasury - // IFactory factory = IFactory(IPlatform(PlasmaConstantsLib.PLATFORM).factory()); // IFactory.Farm memory farm = factory.farm(0); // console.log(farm.strategyLogicId); // Aave Merkl Farm diff --git a/test/tokenomics/RevenueRouter.Upgrade.424.Plasma.t.sol b/test/tokenomics/RevenueRouter.Upgrade.424.Plasma.t.sol index 693a2653..853f3c05 100644 --- a/test/tokenomics/RevenueRouter.Upgrade.424.Plasma.t.sol +++ b/test/tokenomics/RevenueRouter.Upgrade.424.Plasma.t.sol @@ -43,20 +43,6 @@ contract RevenueRouterUpgrade424TestPlasma is Test { assertEq(addr[3], addrAfter[3], "feeTreasure address mismatch"); } - function testXShare() public { - uint xShareBefore = revenueRouter.xShare(); - assertNotEq(xShareBefore, 100_000, "xShare before upgrade mismatch"); - - vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); - vm.prank(makeAddr("not multisig")); - revenueRouter.setXShare(100_000); - - vm.prank(multisig); - revenueRouter.setXShare(100_000); - - assertEq(revenueRouter.xShare(), 100_000, "xShare after upgrade mismatch"); - } - function _upgradeRevenueRouter() internal { address[] memory proxies = new address[](1); proxies[0] = address(revenueRouter); diff --git a/test/tokenomics/RevenueRouter.Upgrade.438.Plasma.t.sol b/test/tokenomics/RevenueRouter.Upgrade.438.Plasma.t.sol new file mode 100644 index 00000000..4cf450b6 --- /dev/null +++ b/test/tokenomics/RevenueRouter.Upgrade.438.Plasma.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +//import {console} from "forge-std/console.sol"; +import {Test} from "forge-std/Test.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {XStaking, IXStaking} from "../../src/tokenomics/XStaking.sol"; +import {IXToken} from "../../src/interfaces/IXToken.sol"; +import {RevenueRouter, IRevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; +import {IRecovery} from "../../src/interfaces/IRecovery.sol"; + +contract RevenueRouterUpgrade438PlasmaTest is Test { + uint public constant FORK_BLOCK = 13214844; // Feb-03-2026 07:23:20 PM +UTC + address public constant PLATFORM = PlasmaConstantsLib.PLATFORM; + address public multisig; + + IXStaking public xStaking; + IXToken public xToken; + IRevenueRouter public revenueRouter; + IRecovery public recovery; + + address public constant USER1 = address(0x1001); + address public constant USER2 = address(0x1002); + address public constant USER3 = address(0x698eDaCD0cc284aB731e1c57662f3d3989E8adB7); + + constructor() { + vm.selectFork(vm.createFork(vm.envString("PLASMA_RPC_URL"), FORK_BLOCK)); + multisig = IPlatform(PLATFORM).multisig(); + + xStaking = IXStaking(PlasmaConstantsLib.XSTBL_XSTAKING); + xToken = IXToken(PlasmaConstantsLib.TOKEN_XSTBL); + revenueRouter = IRevenueRouter(PlasmaConstantsLib.REVENUE_ROUTER); + recovery = IRecovery(PlasmaConstantsLib.RECOVERY); + + _upgradeAndSetup(); + } + + function testZeroBuyBackRate() public { + assertEq(revenueRouter.buyBackRate(), 0); + + _mintAndDepositToStaking(USER1, 50_000e18); + + vm.prank(multisig); + revenueRouter.processAccumulatedAssets(40); + + skip(5 days); + revenueRouter.updatePeriod(); + skip(1 hours); + + uint earnedUser1 = xStaking.earnedToken(PlasmaConstantsLib.TOKEN_WEETH, USER1); + assertGt(earnedUser1, 0); + vm.prank(USER1); + xStaking.getRewardToken(PlasmaConstantsLib.TOKEN_WEETH); + assertEq(IERC20(PlasmaConstantsLib.TOKEN_WEETH).balanceOf(USER1), earnedUser1); + } + + function _mintAndDepositToStaking(address user, uint amount) internal { + deal(PlasmaConstantsLib.TOKEN_STBL, user, amount); + + vm.prank(user); + IERC20(PlasmaConstantsLib.TOKEN_STBL).approve(address(xToken), amount); + + vm.prank(user); + xToken.enter(amount); + + vm.prank(user); + IERC20(address(xToken)).approve(address(xStaking), amount); + + vm.prank(user); + xStaking.deposit(amount); + } + + function _upgradeAndSetup() internal { + _upgradePlatform(); + + // do it on prod + vm.startPrank(multisig); + xStaking.allowRewardToken(PlasmaConstantsLib.TOKEN_WEETH, true); + + address[] memory addresses_ = new address[](4); + addresses_[0] = PlasmaConstantsLib.TOKEN_STBL; + addresses_[1] = PlasmaConstantsLib.TOKEN_XSTBL; + addresses_[2] = PlasmaConstantsLib.XSTBL_XSTAKING; + addresses_[3] = address(0); + revenueRouter.setAddresses(addresses_); + + recovery.changeWhitelist(address(revenueRouter), true); + + vm.stopPrank(); + } + + function _upgradePlatform() internal { + rewind(1 days); + + IPlatform platform = IPlatform(PLATFORM); + + address[] memory proxies = new address[](2); + address[] memory implementations = new address[](2); + + proxies[0] = PlasmaConstantsLib.XSTBL_XSTAKING; + proxies[1] = PlasmaConstantsLib.REVENUE_ROUTER; + //proxies[2] = SonicConstantsLib.REVENUE_ROUTER; + + implementations[0] = address(new XStaking()); + implementations[1] = address(new RevenueRouter()); + //implementations[2] = address(new RevenueRouter()); + + vm.startPrank(platform.multisig()); + platform.announcePlatformUpgrade("2026.02.0-alpha", proxies, implementations); + + skip(1 days); + platform.upgrade(); + vm.stopPrank(); + } +} diff --git a/test/tokenomics/RevenueRouter.Upgrade.438.Sonic.t.sol b/test/tokenomics/RevenueRouter.Upgrade.438.Sonic.t.sol new file mode 100644 index 00000000..2b9cddf0 --- /dev/null +++ b/test/tokenomics/RevenueRouter.Upgrade.438.Sonic.t.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +//import {console} from "forge-std/console.sol"; +import {Test} from "forge-std/Test.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {XStaking, IXStaking} from "../../src/tokenomics/XStaking.sol"; +import {IXToken} from "../../src/interfaces/IXToken.sol"; +import {RevenueRouter, IRevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; + +contract RevenueRouterUpgrade438SonicTest is Test { + uint public constant FORK_BLOCK = 61773869; // Feb-02-2026 01:37:35 PM +UTC + address public constant PLATFORM = SonicConstantsLib.PLATFORM; + address public multisig; + + IXStaking public xStaking; + IXToken public xToken; + IRevenueRouter public revenueRouter; + + address public constant USER1 = address(0x1001); + address public constant USER2 = address(0x1002); + address public constant USER3 = address(0x698eDaCD0cc284aB731e1c57662f3d3989E8adB7); + + constructor() { + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), FORK_BLOCK)); + multisig = IPlatform(PLATFORM).multisig(); + + xStaking = IXStaking(SonicConstantsLib.XSTBL_XSTAKING); + xToken = IXToken(SonicConstantsLib.TOKEN_XSTBL); + revenueRouter = IRevenueRouter(SonicConstantsLib.REVENUE_ROUTER); + + _upgradeAndSetup(); + } + + function testFullBuyBackRate() public { + assertEq(revenueRouter.buyBackRate(), 100); + uint balWas = IERC20(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025).balanceOf(address(revenueRouter)); + vm.prank(multisig); + revenueRouter.processAccumulatedAssets(40); + assertLt(IERC20(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025).balanceOf(address(revenueRouter)), balWas); + + skip(5 days); + assertGt(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(address(revenueRouter)), 0); + revenueRouter.updatePeriod(); + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(address(revenueRouter)), 0); + } + + function testHalfBuyBackRate() public { + // ------------------------------- mint xToken and deposit to staking before upgrade + _mintAndDepositToStaking(USER1, 500_000e18); + _mintAndDepositToStaking(USER2, 3_000e18); + + uint pendingRevenueWas = revenueRouter.pendingRevenue(); + + vm.prank(multisig); + xStaking.allowRewardToken(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025, true); + assertEq(xStaking.isTokenAllowed(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025), true); + assertEq(xStaking.isTokenAllowed(SonicConstantsLib.TOKEN_USDT), false); + + vm.prank(multisig); + revenueRouter.setBuyBackRate(50); + assertEq(revenueRouter.buyBackRate(), 50); + + assertEq(revenueRouter.pendingRevenueAssets().length, 0); + + //console.log(revenueRouter.pendingRevenue()); + + vm.prank(multisig); + revenueRouter.processAccumulatedAssets(40); + + assertEq(revenueRouter.pendingRevenueAssets()[0], SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025); + assertEq(revenueRouter.pendingRevenueAsset(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025), 1600000); + assertGt(revenueRouter.pendingRevenue(), pendingRevenueWas); + + vm.prank(multisig); + revenueRouter.processAccumulatedAssets(40); + assertEq(revenueRouter.pendingRevenueAsset(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025), 3200000); + + vm.startPrank(multisig); + revenueRouter.processAccumulatedAssets(40); + revenueRouter.processAccumulatedAssets(40); + revenueRouter.processAccumulatedAssets(40); + revenueRouter.processAccumulatedAssets(40); + revenueRouter.processAccumulatedAssets(40); + revenueRouter.processAccumulatedAssets(40); + revenueRouter.processAccumulatedAssets(40); + vm.stopPrank(); + + assertEq(IERC20(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025).balanceOf(address(xStaking)), 0); + skip(7 days); + revenueRouter.updatePeriod(); + skip(1 days); + + assertEq(revenueRouter.pendingRevenueAssets().length, 0); + assertGt(IERC20(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025).balanceOf(address(xStaking)), 0); + + uint earnedUser1 = xStaking.earnedToken(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025, USER1); + assertGt(earnedUser1, 0); + vm.prank(USER1); + xStaking.getRewardToken(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025); + assertEq(IERC20(SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025).balanceOf(USER1), earnedUser1); + } + + function _mintAndDepositToStaking(address user, uint amount) internal { + deal(SonicConstantsLib.TOKEN_STBL, user, amount); + + vm.prank(user); + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xToken), amount); + + vm.prank(user); + xToken.enter(amount); + + vm.prank(user); + IERC20(address(xToken)).approve(address(xStaking), amount); + + vm.prank(user); + xStaking.deposit(amount); + } + + function _upgradeAndSetup() internal { + _upgradePlatform(); + + // do it on prod + vm.startPrank(multisig); + revenueRouter.setBuyBackRate(100); + address[] memory assets = new address[](1); + assets[0] = SonicConstantsLib.TOKEN_PT_AUSDC_14AUG2025; + uint[] memory amounts = new uint[](1); + amounts[0] = 1e6; + revenueRouter.setMinSwapAmounts(assets, amounts); + amounts[0] = 2e6; + revenueRouter.setMaxSwapAmounts(assets, amounts); + vm.stopPrank(); + } + + function _upgradePlatform() internal { + rewind(1 days); + + IPlatform platform = IPlatform(PLATFORM); + + address[] memory proxies = new address[](2); + address[] memory implementations = new address[](2); + + proxies[0] = SonicConstantsLib.XSTBL_XSTAKING; + proxies[1] = SonicConstantsLib.REVENUE_ROUTER; + //proxies[2] = SonicConstantsLib.REVENUE_ROUTER; + + implementations[0] = address(new XStaking()); + implementations[1] = address(new RevenueRouter()); + //implementations[2] = address(new RevenueRouter()); + + vm.startPrank(platform.multisig()); + platform.announcePlatformUpgrade("2026.02.0-alpha", proxies, implementations); + + skip(1 days); + platform.upgrade(); + vm.stopPrank(); + } +} diff --git a/test/tokenomics/RevenueRouter.Upgrade4.Sonic.t.sol b/test/tokenomics/RevenueRouter.Upgrade4.Sonic.t.sol index b4ed65f2..6ded0168 100644 --- a/test/tokenomics/RevenueRouter.Upgrade4.Sonic.t.sol +++ b/test/tokenomics/RevenueRouter.Upgrade4.Sonic.t.sol @@ -59,12 +59,6 @@ contract RevenueRouterUpgrade4TestSonic is Test { assertEq(assetsAccumulated[0], asset1); } - // setup xShare - vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); - revenueRouter.setXShare(50_000); - vm.prank(multisig); - revenueRouter.setXShare(50_000); - // test buy-back without setup vm.expectRevert(IControllable.IncorrectMsgSender.selector); revenueRouter.processAccumulatedAssets(50); @@ -106,6 +100,7 @@ contract RevenueRouterUpgrade4TestSonic is Test { IPlatform(PLATFORM).announcePlatformUpgrade("2025.11.0-alpha", proxies, implementations); skip(18 hours); IPlatform(PLATFORM).upgrade(); + revenueRouter.setBuyBackRate(100); vm.stopPrank(); rewind(17 hours); } diff --git a/test/tokenomics/XStaking.Upgrade.404.t.sol b/test/tokenomics/XStaking.Upgrade.404.Sonic.t.sol similarity index 100% rename from test/tokenomics/XStaking.Upgrade.404.t.sol rename to test/tokenomics/XStaking.Upgrade.404.Sonic.t.sol diff --git a/test/tokenomics/XStaking.Upgrade.438.Sonic.t.sol b/test/tokenomics/XStaking.Upgrade.438.Sonic.t.sol new file mode 100644 index 00000000..0f3bacd5 --- /dev/null +++ b/test/tokenomics/XStaking.Upgrade.438.Sonic.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {XStaking} from "../../src/tokenomics/XStaking.sol"; +import {IXToken} from "../../src/interfaces/IXToken.sol"; +import {IXStaking} from "../../src/interfaces/IXStaking.sol"; + +contract XStakingUpgrade438SonicTest is Test { + uint public constant FORK_BLOCK = 61200000; // Jan-26-2026 03:00:37 PM +UTC + address public constant PLATFORM = SonicConstantsLib.PLATFORM; + address public multisig; + + IXStaking public xStaking; + IXToken public xToken; + + address public constant USER1 = address(0x1001); + address public constant USER2 = address(0x1002); + address public constant USER3 = address(0x1003); + + constructor() { + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), FORK_BLOCK)); + multisig = IPlatform(PLATFORM).multisig(); + + xStaking = IXStaking(SonicConstantsLib.XSTBL_XSTAKING); + xToken = IXToken(SonicConstantsLib.TOKEN_XSTBL); + + deal(SonicConstantsLib.TOKEN_USDC, SonicConstantsLib.REVENUE_ROUTER, 100e6); + + _upgradeAndSetup(); + } + + function testRewardTokens() public { + // ------------------------------- mint xToken and deposit to staking before upgrade + _mintAndDepositToStaking(USER1, 500000e18); + _mintAndDepositToStaking(USER2, 3000e18); + + assertEq(xStaking.isTokenAllowed(SonicConstantsLib.TOKEN_USDC), true); + assertEq(xStaking.isTokenAllowed(SonicConstantsLib.TOKEN_USDT), false); + + vm.startPrank(SonicConstantsLib.REVENUE_ROUTER); + IERC20(SonicConstantsLib.TOKEN_USDC).approve(address(xStaking), type(uint).max); + xStaking.notifyRewardAmountToken(SonicConstantsLib.TOKEN_USDC, 100e6); + vm.stopPrank(); + + uint user1Earned = xStaking.earnedToken(SonicConstantsLib.TOKEN_USDC, USER1); + assertEq(user1Earned, 0); + + vm.warp(block.timestamp + 1 hours); + user1Earned = xStaking.earnedToken(SonicConstantsLib.TOKEN_USDC, USER1); + assertGt(user1Earned, 10e6); + + vm.prank(USER1); + xStaking.getRewardToken(SonicConstantsLib.TOKEN_USDC); + assertEq(IERC20(SonicConstantsLib.TOKEN_USDC).balanceOf(USER1), user1Earned); + } + + function _mintAndDepositToStaking(address user, uint amount) internal { + deal(SonicConstantsLib.TOKEN_STBL, user, amount); + + vm.prank(user); + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xToken), amount); + + vm.prank(user); + xToken.enter(amount); + + vm.prank(user); + IERC20(address(xToken)).approve(address(xStaking), amount); + + vm.prank(user); + xStaking.deposit(amount); + } + + function _upgradeAndSetup() internal { + _upgradePlatform(); + + vm.prank(multisig); + xStaking.allowRewardToken(SonicConstantsLib.TOKEN_USDC, true); + } + + function _upgradePlatform() internal { + rewind(1 days); + + IPlatform platform = IPlatform(PLATFORM); + + address[] memory proxies = new address[](1); + address[] memory implementations = new address[](1); + + proxies[0] = SonicConstantsLib.XSTBL_XSTAKING; + //proxies[1] = SonicConstantsLib.TOKEN_XSTBL; + //proxies[2] = SonicConstantsLib.REVENUE_ROUTER; + + implementations[0] = address(new XStaking()); + //implementations[1] = address(new XToken()); + //implementations[2] = address(new RevenueRouter()); + + vm.startPrank(platform.multisig()); + platform.announcePlatformUpgrade("2026.02.0-alpha", proxies, implementations); + + skip(1 days); + platform.upgrade(); + vm.stopPrank(); + } +} diff --git a/test/tokenomics/XStaking.t.sol b/test/tokenomics/XStaking.t.sol index 5906447e..d36e27e9 100644 --- a/test/tokenomics/XStaking.t.sol +++ b/test/tokenomics/XStaking.t.sol @@ -122,7 +122,7 @@ contract XStakingTest is Test, MockSetup { // ------------------------------- Bad paths vm.prank(platform.multisig()); - vm.expectRevert(XStaking.DaoNotInitialized.selector); + vm.expectRevert(IXStaking.DaoNotInitialized.selector); xStaking.syncDAOBalances(users); // ------------------------------- Mint xToken and deposit to staking