// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test, console} from "forge-std/Test.sol"; import {Stabilizer} from "../../../../contracts/bridge/trustless/integration/Stabilizer.sol"; import {PrivatePoolRegistry} from "../../../../contracts/dex/PrivatePoolRegistry.sol"; import {IStablecoinPegManager} from "../../../../contracts/bridge/trustless/integration/IStablecoinPegManager.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MockStablecoinPegManager is IStablecoinPegManager { int256 public deviationBps; function setDeviation(int256 _deviationBps) external { deviationBps = _deviationBps; } function checkUSDpeg(address) external view returns (bool isMaintained, int256 _deviationBps) { _deviationBps = deviationBps; isMaintained = deviationBps >= -50 && deviationBps <= 50; } function checkETHpeg(address) external view returns (bool, int256) { return (true, deviationBps); } function calculateDeviation(address, uint256, uint256) external pure returns (int256) { return 0; } function getPegStatus(address) external view returns (uint256, uint256, int256, bool) { return (1e18, 1e18, deviationBps, true); } function getSupportedAssets() external pure returns (address[] memory) { return new address[](0); } } contract MockDODOPool { address public baseToken; address public quoteToken; uint256 public midPrice = 1e18; uint256 public sellOutAmount = 1e18; constructor(address _base, address _quote) { baseToken = _base; quoteToken = _quote; } function _BASE_TOKEN_() external view returns (address) { return baseToken; } function _QUOTE_TOKEN_() external view returns (address) { return quoteToken; } function getMidPrice() external view returns (uint256) { return midPrice; } function sellBase(address) external returns (uint256) { return sellOutAmount; } function sellQuote(address) external returns (uint256) { return sellOutAmount; } function setSellOutAmount(uint256 v) external { sellOutAmount = v; } function setMidPrice(uint256 v) external { midPrice = v; } } contract MockERC20 { mapping(address => uint256) public balanceOf; function mint(address to, uint256 amount) external { balanceOf[to] += amount; } function transfer(address to, uint256 amount) external returns (bool) { balanceOf[msg.sender] -= amount; balanceOf[to] += amount; return true; } function approve(address, uint256) external pure returns (bool) { return true; } } contract StabilizerTest is Test { Stabilizer public stabilizer; PrivatePoolRegistry public registry; MockStablecoinPegManager public pegManager; MockDODOPool public mockPool; MockERC20 public tokenIn; MockERC20 public tokenOut; address public admin = address(0x1); address public keeper = address(0x2); function setUp() public { registry = new PrivatePoolRegistry(admin); stabilizer = new Stabilizer(admin, address(registry)); pegManager = new MockStablecoinPegManager(); tokenIn = new MockERC20(); tokenOut = new MockERC20(); tokenIn.mint(address(stabilizer), 1000e18); mockPool = new MockDODOPool(address(tokenIn), address(tokenOut)); vm.startPrank(admin); stabilizer.grantRole(stabilizer.STABILIZER_KEEPER_ROLE(), keeper); stabilizer.setStablecoinPegSource(address(pegManager), address(tokenIn)); stabilizer.setMinBlocksBetweenExecution(3); stabilizer.setMaxStabilizationVolumePerBlock(100e18); stabilizer.setThresholdBps(50); stabilizer.setSustainedDeviationBlocks(3); stabilizer.setMaxSlippageBps(100); registry.register(address(tokenIn), address(tokenOut), address(mockPool)); vm.stopPrank(); } function test_checkDeviation_belowThreshold_returnsNoRebalance() public { pegManager.setDeviation(30); // 0.3% < 50 bps (int256 dev, bool shouldRebalance) = stabilizer.checkDeviation(); assertEq(dev, 30); assertFalse(shouldRebalance); } function test_checkDeviation_aboveThreshold_singleBlock_noSustained_returnsNoRebalance() public { pegManager.setDeviation(100); (int256 dev, bool shouldRebalance) = stabilizer.checkDeviation(); assertEq(dev, 100); assertFalse(shouldRebalance); // no samples yet } function test_checkDeviation_aboveThreshold_sustained_returnsRebalance() public { pegManager.setDeviation(100); vm.prank(keeper); stabilizer.recordDeviation(); vm.roll(block.number + 1); stabilizer.recordDeviation(); vm.roll(block.number + 1); stabilizer.recordDeviation(); (int256 dev, bool shouldRebalance) = stabilizer.checkDeviation(); assertEq(dev, 100); assertTrue(shouldRebalance); } function test_executePrivateSwap_revertWhenBlockDelayNotMet() public { pegManager.setDeviation(100); for (uint256 i = 0; i < 3; i++) { stabilizer.recordDeviation(); vm.roll(block.number + 1); } vm.roll(2); // block 2 < lastExecutionBlock(0) + 3 vm.prank(keeper); vm.expectRevert(Stabilizer.BlockDelayNotMet.selector); stabilizer.executePrivateSwap(10e18, address(tokenIn), address(tokenOut)); } function test_executePrivateSwap_revertWhenVolumeCapExceeded() public { pegManager.setDeviation(100); for (uint256 i = 0; i < 4; i++) { stabilizer.recordDeviation(); vm.roll(block.number + 1); } vm.roll(block.number + 10); vm.prank(keeper); vm.expectRevert(Stabilizer.VolumeCapExceeded.selector); stabilizer.executePrivateSwap(200e18, address(tokenIn), address(tokenOut)); } function test_executePrivateSwap_revertWhenSlippageExceeded() public { pegManager.setDeviation(100); for (uint256 i = 0; i < 4; i++) { stabilizer.recordDeviation(); vm.roll(block.number + 1); } vm.roll(block.number + 10); mockPool.setSellOutAmount(1); // very low output => slippage vm.prank(keeper); vm.expectRevert(Stabilizer.SlippageExceeded.selector); stabilizer.executePrivateSwap(10e18, address(tokenIn), address(tokenOut)); } function test_executePrivateSwap_revertWhenNoPrivatePool() public { vm.startPrank(admin); MockERC20 otherIn = new MockERC20(); MockERC20 otherOut = new MockERC20(); otherIn.mint(address(stabilizer), 1000e18); vm.stopPrank(); pegManager.setDeviation(100); for (uint256 i = 0; i < 4; i++) { stabilizer.recordDeviation(); vm.roll(block.number + 1); } vm.roll(block.number + 10); vm.prank(keeper); vm.expectRevert(Stabilizer.NoPrivatePool.selector); stabilizer.executePrivateSwap(10e18, address(otherIn), address(otherOut)); } function test_executePrivateSwap_revertWhenShouldNotRebalance() public { pegManager.setDeviation(30); // below threshold vm.roll(block.number + 10); vm.prank(keeper); vm.expectRevert(Stabilizer.ShouldNotRebalance.selector); stabilizer.executePrivateSwap(10e18, address(tokenIn), address(tokenOut)); } }