Skip to content

Commit ecf08ad

Browse files
authored
Merge pull request #342 from stabilitydao/maintainance
RevenueRouter upgrades
2 parents 5c7d4fc + 8c79c63 commit ecf08ad

6 files changed

Lines changed: 219 additions & 29 deletions

File tree

chains/sonic/SonicConstantsLib.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ library SonicConstantsLib {
4747
address public constant VAULT_C_USDC_scUSD_ISF_USDC = 0xb773B791F3baDB3b28BC7A2da18E2a012b9116c2;
4848
//address public constant VAULT_C_USDC_Stability_Main = 0x402ae122CaEce6ce57203e3Bd4AF7d1e9Ac446cb;
4949

50+
// leverage vaults
51+
address public constant VAULT_LEV_SiL_stS_S = 0x709833e5B4B98aAb812d175510F94Bc91CFABD89;
52+
address public constant VAULT_LEV_SiL_S_stS = 0x2fBeBA931563feAAB73e8C66d7499c49c8AdA224;
53+
address public constant VAULT_LEV_SiAL_wstkscUSD_USDC = 0x908Db38302177901b10fFa74fA80AdAeB0351Ff1;
54+
5055
// ERC20
5156
address public constant TOKEN_wS = 0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38;
5257
address public constant TOKEN_wETH = 0x50c42dEAcD8Fc9773493ED674b675bE577f2634b;

src/interfaces/IRevenueRouter.sol

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.28;
33

4+
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
5+
46
interface IRevenueRouter {
57
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
68
/* EVENTS */
79
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
810

911
event EpochFlip(uint periodEnded, uint totalStblRevenue);
10-
event NewUnit(uint unitIndex, string name, address feeTreasury);
12+
event AddedUnit(uint unitIndex, UnitType unitType, string name, address feeTreasury);
13+
event UpdatedUnit(uint unitIndex, UnitType unitType, string name, address feeTreasury);
1114
event UnitEpochRevenue(uint periodEnded, string unitName, uint stblRevenue);
1215
event ProcessUnitRevenue(uint unitIndex, uint stblGot);
1316

@@ -32,6 +35,7 @@ interface IRevenueRouter {
3235
uint pendingRevenue;
3336
Unit[] units;
3437
address[] aavePools;
38+
EnumerableSet.AddressSet vaultsAccumulated;
3539
}
3640

3741
enum UnitType {
@@ -54,6 +58,9 @@ interface IRevenueRouter {
5458
/// @notice Add new Unit
5559
function addUnit(UnitType unitType, string calldata name, address feeTreasury) external;
5660

61+
/// @notice Update Unit
62+
function updateUnit(uint unitIndex, UnitType unitType, string calldata name, address feeTreasury) external;
63+
5764
/// @notice Setup Aave pool list
5865
function setAavePools(address[] calldata pools) external;
5966

@@ -77,10 +84,16 @@ interface IRevenueRouter {
7784
/// @notice Claim units fees and swap to STBL
7885
function processUnitsRevenue() external;
7986

87+
/// @notice Withdraw assets from accumulated vaults and swap to STBL
88+
function processAccumulatedVaults(uint maxVaultsForWithdraw) external;
89+
8090
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
8191
/* VIEW FUNCTIONS */
8292
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
8393

94+
/// @notice Show all Units
95+
function units() external view returns (Unit[] memory);
96+
8497
/// @notice The period used for rewarding
8598
/// @return The block.timestamp divided by 1 week in seconds
8699
function getPeriod() external view returns (uint);
@@ -96,4 +109,7 @@ interface IRevenueRouter {
96109

97110
/// @notice Get Aave pool list to mintToTreasury calls
98111
function aavePools() external view returns (address[] memory);
112+
113+
/// @notice Get vault addresses that contract hold on balance, but not withdrew yet
114+
function vaultsAccumulated() external view returns (address[] memory);
99115
}

src/tokenomics/RevenueRouter.sol

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,33 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
55
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
66
import {Controllable, IControllable} from "../core/base/Controllable.sol";
77
import {IXSTBL} from "../interfaces/IXSTBL.sol";
8-
import {IRevenueRouter} from "../interfaces/IRevenueRouter.sol";
8+
import {IRevenueRouter, EnumerableSet} from "../interfaces/IRevenueRouter.sol";
99
import {ISwapper} from "../interfaces/ISwapper.sol";
1010
import {IPlatform} from "../interfaces/IPlatform.sol";
1111
import {IXStaking} from "../interfaces/IXStaking.sol";
1212
import {IFeeTreasury} from "../interfaces/IFeeTreasury.sol";
13+
import {IStabilityVault} from "../interfaces/IStabilityVault.sol";
14+
import {IFactory} from "../interfaces/IFactory.sol";
1315
import {IPool} from "../integrations/aave/IPool.sol";
1416
import {IAToken} from "../integrations/aave/IAToken.sol";
15-
import {IStabilityVault} from "../interfaces/IStabilityVault.sol";
1617

1718
/// @title Platform revenue distributor
1819
/// Changelog:
20+
/// 1.5.0: processAccumulatedVaults
21+
/// 1.4.0: processUnitRevenue use try..catch for Aave aToken withdrawals; view vaultsAccumulated
22+
/// 1.3.0: vaultsAccumulated; updateUnit; units
1923
/// 1.2.0: Units; Aave unit; all revenue via buy-back
2024
/// @author Alien Deployer (https://github.com/a17)
2125
contract RevenueRouter is Controllable, IRevenueRouter {
2226
using SafeERC20 for IERC20;
27+
using EnumerableSet for EnumerableSet.AddressSet;
2328

2429
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
2530
/* CONSTANTS */
2631
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
2732

2833
/// @inheritdoc IControllable
29-
string public constant VERSION = "1.2.0";
34+
string public constant VERSION = "1.5.0";
3035

3136
// keccak256(abi.encode(uint256(keccak256("erc7201:stability.RevenueRouter")) - 1)) & ~bytes32(uint256(0xff))
3237
bytes32 private constant REVENUE_ROUTER_STORAGE_LOCATION =
@@ -58,7 +63,21 @@ contract RevenueRouter is Controllable, IRevenueRouter {
5863
RevenueRouterStorage storage $ = _getRevenueRouterStorage();
5964
uint unitIndex = $.units.length;
6065
$.units.push(Unit({unitType: unitType, name: name, pendingRevenue: 0, feeTreasury: feeTreasury}));
61-
emit NewUnit(unitIndex, name, feeTreasury);
66+
emit AddedUnit(unitIndex, unitType, name, feeTreasury);
67+
}
68+
69+
/// @inheritdoc IRevenueRouter
70+
function updateUnit(
71+
uint unitIndex,
72+
UnitType unitType,
73+
string calldata name,
74+
address feeTreasury
75+
) external onlyGovernanceOrMultisig {
76+
RevenueRouterStorage storage $ = _getRevenueRouterStorage();
77+
$.units[unitIndex].unitType = unitType;
78+
$.units[unitIndex].name = name;
79+
$.units[unitIndex].feeTreasury = feeTreasury;
80+
emit UpdatedUnit(unitIndex, unitType, name, feeTreasury);
6281
}
6382

6483
/// @inheritdoc IRevenueRouter
@@ -138,27 +157,15 @@ contract RevenueRouter is Controllable, IRevenueRouter {
138157

139158
/// @inheritdoc IRevenueRouter
140159
function processFeeVault(address vault, uint amount) external {
141-
RevenueRouterStorage storage $ = _getRevenueRouterStorage();
142-
address stbl = $.stbl;
143-
ISwapper swapper = ISwapper(IPlatform(platform()).swapper());
144-
address[] memory assets = IStabilityVault(vault).assets();
145-
uint len = assets.length;
146-
try IStabilityVault(vault).withdrawAssets(assets, amount, new uint[](len), address(this), msg.sender) {
147-
uint stblBalanceWas = IERC20(stbl).balanceOf(address(this));
148-
for (uint i; i < len; ++i) {
149-
address asset = assets[i];
150-
if (asset != stbl) {
151-
uint threshold = swapper.threshold(asset);
152-
uint amountToSwap = IERC20(asset).balanceOf(address(this));
153-
if (amountToSwap > threshold) {
154-
IERC20(asset).forceApprove(address(swapper), amountToSwap);
155-
try swapper.swap(asset, stbl, amountToSwap, 20_000) {} catch {}
156-
}
157-
}
160+
if (_isDeployedVault(vault)) {
161+
RevenueRouterStorage storage $ = _getRevenueRouterStorage();
162+
if (IStabilityVault(vault).lastBlockDefenseDisabled()) {
163+
_withdrawVaultSharesAndBuyBack($, vault, amount, msg.sender);
164+
} else {
165+
IERC20(vault).safeTransferFrom(msg.sender, address(this), amount);
166+
$.vaultsAccumulated.add(vault);
158167
}
159-
uint stblGot = IERC20(stbl).balanceOf(address(this)) - stblBalanceWas;
160-
IERC20(stbl).safeTransfer($.feeTreasury, stblGot);
161-
} catch {}
168+
}
162169
}
163170

164171
/// @inheritdoc IRevenueRouter
@@ -176,9 +183,12 @@ contract RevenueRouter is Controllable, IRevenueRouter {
176183
for (uint i; i < outAssets.length; ++i) {
177184
address asset = IAToken(outAssets[i]).UNDERLYING_ASSET_ADDRESS();
178185
if (amounts[i] != 0) {
179-
IPool(IAToken(outAssets[i]).POOL()).withdraw(asset, amounts[i], address(this));
180-
IERC20(asset).forceApprove(address(swapper), amounts[i]);
181-
try swapper.swap(asset, stbl, amounts[i], 20_000) {} catch {}
186+
// here we use all because extra amounts from failed withdraw can be
187+
amounts[i] = IERC20(outAssets[i]).balanceOf(address(this));
188+
try IPool(IAToken(outAssets[i]).POOL()).withdraw(asset, amounts[i], address(this)) {
189+
IERC20(asset).forceApprove(address(swapper), amounts[i]);
190+
try swapper.swap(asset, stbl, amounts[i], 20_000) {} catch {}
191+
} catch {}
182192
}
183193
}
184194

@@ -208,10 +218,33 @@ contract RevenueRouter is Controllable, IRevenueRouter {
208218
}
209219
}
210220

221+
/// @inheritdoc IRevenueRouter
222+
function processAccumulatedVaults(uint maxVaultsForWithdraw) external {
223+
RevenueRouterStorage storage $ = _getRevenueRouterStorage();
224+
225+
// process accumulatedVaults
226+
address[] memory _vaultsAccumulated = $.vaultsAccumulated.values();
227+
uint len = _vaultsAccumulated.length;
228+
for (uint i; i < len; ++i) {
229+
if (i == maxVaultsForWithdraw) {
230+
break;
231+
}
232+
_withdrawVaultSharesAndBuyBack(
233+
$, _vaultsAccumulated[i], IERC20(_vaultsAccumulated[i]).balanceOf(address(this)), address(this)
234+
);
235+
$.vaultsAccumulated.remove(_vaultsAccumulated[i]);
236+
}
237+
}
238+
211239
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
212240
/* VIEW FUNCTIONS */
213241
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
214242

243+
/// @inheritdoc IRevenueRouter
244+
function units() external view returns (Unit[] memory) {
245+
return _getRevenueRouterStorage().units;
246+
}
247+
215248
/// @inheritdoc IRevenueRouter
216249
function getPeriod() public view returns (uint) {
217250
return (block.timestamp / 1 weeks);
@@ -237,10 +270,54 @@ contract RevenueRouter is Controllable, IRevenueRouter {
237270
return _getRevenueRouterStorage().aavePools;
238271
}
239272

273+
/// @inheritdoc IRevenueRouter
274+
function vaultsAccumulated() external view returns (address[] memory) {
275+
return _getRevenueRouterStorage().vaultsAccumulated.values();
276+
}
277+
240278
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
241279
/* INTERNAL LOGIC */
242280
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
243281

282+
function _withdrawVaultSharesAndBuyBack(
283+
RevenueRouterStorage storage $,
284+
address vault,
285+
uint amount,
286+
address owner
287+
) internal {
288+
address stbl = $.stbl;
289+
ISwapper swapper = ISwapper(IPlatform(platform()).swapper());
290+
address[] memory assets = IStabilityVault(vault).assets();
291+
uint len = assets.length;
292+
try IStabilityVault(vault).withdrawAssets(assets, amount, new uint[](len), address(this), owner) {
293+
uint stblBalanceWas = IERC20(stbl).balanceOf(address(this));
294+
for (uint i; i < len; ++i) {
295+
address asset = assets[i];
296+
if (asset != stbl) {
297+
uint threshold = swapper.threshold(asset);
298+
uint amountToSwap = IERC20(asset).balanceOf(address(this));
299+
if (amountToSwap > threshold) {
300+
IERC20(asset).forceApprove(address(swapper), amountToSwap);
301+
try swapper.swap(asset, stbl, amountToSwap, 20_000) {} catch {}
302+
}
303+
}
304+
}
305+
uint stblGot = IERC20(stbl).balanceOf(address(this)) - stblBalanceWas;
306+
IERC20(stbl).safeTransfer($.feeTreasury, stblGot);
307+
} catch {}
308+
}
309+
310+
function _isDeployedVault(address vault) internal view returns (bool) {
311+
address[] memory deployedVaults = IFactory(IPlatform(platform()).factory()).deployedVaults();
312+
uint len = deployedVaults.length;
313+
for (uint i; i < len; ++i) {
314+
if (deployedVaults[i] == vault) {
315+
return true;
316+
}
317+
}
318+
return false;
319+
}
320+
244321
function _getRevenueRouterStorage() internal pure returns (RevenueRouterStorage storage $) {
245322
//slither-disable-next-line assembly
246323
assembly {

test/tokenomics/RevenueRouter.Upgrade.Sonic.t.sol

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ contract RevenueRouterUpgradeTestSonic is Test {
2626
revenueRouter.addUnit(IRevenueRouter.UnitType.AaveMarkets, "Lending", SonicConstantsLib.LENDING_FEE_TREASURY);
2727
vm.prank(multisig);
2828
revenueRouter.addUnit(IRevenueRouter.UnitType.AaveMarkets, "Lending", SonicConstantsLib.LENDING_FEE_TREASURY);
29+
vm.prank(multisig);
30+
revenueRouter.updateUnit(
31+
0, IRevenueRouter.UnitType.AaveMarkets, "Lending1", SonicConstantsLib.LENDING_FEE_TREASURY
32+
);
2933

3034
revenueRouter.processUnitRevenue(0);
3135

@@ -45,7 +49,12 @@ contract RevenueRouterUpgradeTestSonic is Test {
4549
vm.startPrank(IPlatform(PLATFORM).hardWorker());
4650
IVault(SonicConstantsLib.VAULT_C_USDC_SiMF_Valmore).doHardWork();
4751
IVault(SonicConstantsLib.VAULT_C_USDC_Stability_Stream).doHardWork();
52+
IVault(SonicConstantsLib.VAULT_LEV_SiL_stS_S).doHardWork();
53+
IVault(SonicConstantsLib.VAULT_LEV_SiL_S_stS).doHardWork();
54+
IVault(SonicConstantsLib.VAULT_LEV_SiAL_wstkscUSD_USDC).doHardWork();
4855
vm.stopPrank();
56+
vm.roll(block.number + 6);
57+
revenueRouter.processUnitsRevenue();
4958
}
5059

5160
function _upgradeRevenueRouter() internal {
@@ -55,8 +64,9 @@ contract RevenueRouterUpgradeTestSonic is Test {
5564
implementations[0] = address(new RevenueRouter());
5665
vm.startPrank(multisig);
5766
IPlatform(PLATFORM).announcePlatformUpgrade("2025.07.0-alpha", proxies, implementations);
58-
skip(1 days);
67+
skip(18 hours);
5968
IPlatform(PLATFORM).upgrade();
6069
vm.stopPrank();
70+
rewind(17 hours);
6171
}
6272
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {console, Test} from "forge-std/Test.sol";
5+
import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol";
6+
import {IPlatform} from "../../src/interfaces/IPlatform.sol";
7+
import {RevenueRouter, IRevenueRouter} from "../../src/tokenomics/RevenueRouter.sol";
8+
import {IVault} from "../../src/interfaces/IVault.sol";
9+
10+
contract RevenueRouterUpgrade2TestSonic is Test {
11+
uint public constant FORK_BLOCK = 38180263; // Jul-12-2025 10:07:41 AM +UTC
12+
address public constant PLATFORM = SonicConstantsLib.PLATFORM;
13+
address public multisig;
14+
IRevenueRouter public revenueRouter;
15+
16+
constructor() {
17+
vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), FORK_BLOCK));
18+
multisig = IPlatform(PLATFORM).multisig();
19+
revenueRouter = IRevenueRouter(IPlatform(PLATFORM).revenueRouter());
20+
_upgradeRevenueRouter();
21+
}
22+
23+
function testUpgrade140() public {
24+
revenueRouter.processUnitsRevenue();
25+
vm.prank(IPlatform(PLATFORM).hardWorker());
26+
IVault(SonicConstantsLib.VAULT_LEV_SiL_stS_S).doHardWork();
27+
address[] memory vaultsAccumulated = revenueRouter.vaultsAccumulated();
28+
assertEq(vaultsAccumulated.length, 2);
29+
}
30+
31+
function _upgradeRevenueRouter() internal {
32+
address[] memory proxies = new address[](1);
33+
proxies[0] = address(revenueRouter);
34+
address[] memory implementations = new address[](1);
35+
implementations[0] = address(new RevenueRouter());
36+
vm.startPrank(multisig);
37+
IPlatform(PLATFORM).announcePlatformUpgrade("2025.07.2-alpha", proxies, implementations);
38+
skip(18 hours);
39+
IPlatform(PLATFORM).upgrade();
40+
vm.stopPrank();
41+
rewind(17 hours);
42+
}
43+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {console, Test} from "forge-std/Test.sol";
5+
import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol";
6+
import {IPlatform} from "../../src/interfaces/IPlatform.sol";
7+
import {RevenueRouter, IRevenueRouter, IControllable} from "../../src/tokenomics/RevenueRouter.sol";
8+
import {IVault} from "../../src/interfaces/IVault.sol";
9+
10+
contract RevenueRouterUpgrade3TestSonic is Test {
11+
uint public constant FORK_BLOCK = 38301556; // Jul-13-2025 09:27:35 AM +UTC
12+
address public constant PLATFORM = SonicConstantsLib.PLATFORM;
13+
address public multisig;
14+
IRevenueRouter public revenueRouter;
15+
16+
constructor() {
17+
vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), FORK_BLOCK));
18+
multisig = IPlatform(PLATFORM).multisig();
19+
revenueRouter = IRevenueRouter(IPlatform(PLATFORM).revenueRouter());
20+
_upgradeRevenueRouter();
21+
}
22+
23+
function testUpgrade150() public {
24+
revenueRouter.processAccumulatedVaults(50);
25+
}
26+
27+
function _upgradeRevenueRouter() internal {
28+
address[] memory proxies = new address[](1);
29+
proxies[0] = address(revenueRouter);
30+
address[] memory implementations = new address[](1);
31+
implementations[0] = address(new RevenueRouter());
32+
vm.startPrank(multisig);
33+
IPlatform(PLATFORM).announcePlatformUpgrade("2025.07.3-alpha", proxies, implementations);
34+
skip(18 hours);
35+
IPlatform(PLATFORM).upgrade();
36+
vm.stopPrank();
37+
rewind(17 hours);
38+
}
39+
}

0 commit comments

Comments
 (0)