- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
200 lines
7.3 KiB
Solidity
200 lines
7.3 KiB
Solidity
// 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));
|
|
}
|
|
}
|