Files
smom-dbis-138/contracts/bridge/trustless/integration/Stabilizer.sol
2026-03-02 12:14:09 -08:00

238 lines
10 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "../../../dex/PrivatePoolRegistry.sol";
import "./IStablecoinPegManager.sol";
import "./ICommodityPegManager.sol";
/**
* @title Minimal DODO PMM pool interface for Stabilizer swaps
*/
interface IDODOPMMPoolStabilizer {
function _BASE_TOKEN_() external view returns (address);
function _QUOTE_TOKEN_() external view returns (address);
function sellBase(uint256 amount) external returns (uint256);
function sellQuote(uint256 amount) external returns (uint256);
function getMidPrice() external view returns (uint256);
}
/**
* @title Stabilizer
* @notice Phase 3: Deviation-triggered private swaps via XAU-anchored pools. Phase 6: TWAP/sustained deviation, per-block cap, flash containment.
* @dev Implements Appendix A of VAULT_SYSTEM_MASTER_TECHNICAL_PLAN. Only STABILIZER_KEEPER_ROLE may call executePrivateSwap.
*/
contract Stabilizer is AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant STABILIZER_KEEPER_ROLE = keccak256("STABILIZER_KEEPER_ROLE");
PrivatePoolRegistry public immutable privatePoolRegistry;
uint256 public lastExecutionBlock;
uint256 public volumeThisBlock;
uint256 public volumeBlockNumber;
uint256 public minBlocksBetweenExecution = 3;
uint256 public maxStabilizationVolumePerBlock;
uint256 public thresholdBps = 50;
uint256 public sustainedDeviationBlocks = 3;
uint256 public maxSlippageBps = 100;
uint256 public maxGasPriceForStabilizer;
IStablecoinPegManager public stablecoinPegManager;
address public stablecoinPegAsset;
ICommodityPegManager public commodityPegManager;
address public commodityPegAsset;
bool public useStablecoinPeg; // true = use stablecoin peg, false = use commodity peg (when set)
struct DeviationSample {
uint256 blockNumber;
int256 deviationBps;
}
uint256 public constant MAX_DEVIATION_SAMPLES = 32;
DeviationSample[MAX_DEVIATION_SAMPLES] private _samples;
uint256 private _sampleIndex;
uint256 private _sampleCount;
event PrivateSwapExecuted(address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut);
event ConfigUpdated(string key, uint256 value);
event PegSourceStablecoinSet(address manager, address asset);
event PegSourceCommoditySet(address manager, address asset);
error NoPrivatePool();
error ShouldNotRebalance();
error BlockDelayNotMet();
error VolumeCapExceeded();
error SlippageExceeded();
error GasPriceTooHigh();
error ZeroAmount();
error NoDeviationSource();
error InsufficientBalance();
constructor(address admin, address _privatePoolRegistry) {
require(admin != address(0), "Stabilizer: zero admin");
require(_privatePoolRegistry != address(0), "Stabilizer: zero registry");
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(STABILIZER_KEEPER_ROLE, admin);
privatePoolRegistry = PrivatePoolRegistry(_privatePoolRegistry);
}
function setStablecoinPegSource(address manager, address asset) external onlyRole(DEFAULT_ADMIN_ROLE) {
stablecoinPegManager = IStablecoinPegManager(manager);
stablecoinPegAsset = asset;
useStablecoinPeg = true;
emit PegSourceStablecoinSet(manager, asset);
}
function setCommodityPegSource(address manager, address asset) external onlyRole(DEFAULT_ADMIN_ROLE) {
commodityPegManager = ICommodityPegManager(manager);
commodityPegAsset = asset;
useStablecoinPeg = false;
emit PegSourceCommoditySet(manager, asset);
}
function setMinBlocksBetweenExecution(uint256 v) external onlyRole(DEFAULT_ADMIN_ROLE) {
minBlocksBetweenExecution = v;
emit ConfigUpdated("minBlocksBetweenExecution", v);
}
function setMaxStabilizationVolumePerBlock(uint256 v) external onlyRole(DEFAULT_ADMIN_ROLE) {
maxStabilizationVolumePerBlock = v;
emit ConfigUpdated("maxStabilizationVolumePerBlock", v);
}
function setThresholdBps(uint256 v) external onlyRole(DEFAULT_ADMIN_ROLE) {
thresholdBps = v;
emit ConfigUpdated("thresholdBps", v);
}
function setSustainedDeviationBlocks(uint256 v) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(v <= MAX_DEVIATION_SAMPLES, "Stabilizer: sample limit");
sustainedDeviationBlocks = v;
emit ConfigUpdated("sustainedDeviationBlocks", v);
}
function setMaxSlippageBps(uint256 v) external onlyRole(DEFAULT_ADMIN_ROLE) {
maxSlippageBps = v;
emit ConfigUpdated("maxSlippageBps", v);
}
function setMaxGasPriceForStabilizer(uint256 v) external onlyRole(DEFAULT_ADMIN_ROLE) {
maxGasPriceForStabilizer = v;
emit ConfigUpdated("maxGasPriceForStabilizer", v);
}
/**
* @notice Get current deviation in bps and whether rebalance is recommended (sustained over threshold).
* @return deviationBps Current peg deviation in basis points.
* @return shouldRebalance True if over threshold for sustainedDeviationBlocks samples.
*/
function checkDeviation() external view returns (int256 deviationBps, bool shouldRebalance) {
deviationBps = _getDeviationBps();
// forge-lint: disable-next-line unsafe-typecast -- thresholdBps is uint256, casting to int256 for comparison with _abs result; thresholdBps is small (basis points)
if (_abs(deviationBps) <= int256(uint256(thresholdBps))) {
return (deviationBps, false);
}
shouldRebalance = _sustainedOverThreshold();
return (deviationBps, shouldRebalance);
}
/**
* @notice Record current deviation for sustained-deviation check (call from keeper each block or before executePrivateSwap).
*/
function recordDeviation() external {
int256 d = _getDeviationBps();
_pushSample(block.number, d);
}
/**
* @notice Execute a private swap via the private pool registry (only keeper when shouldRebalance).
* @param tradeSize Amount of tokenIn to swap.
* @param tokenIn Token to sell (must have balance on this contract).
* @param tokenOut Token to buy.
* @return amountOut Amount of tokenOut received (reverts on slippage or volume cap).
*/
function executePrivateSwap(
uint256 tradeSize,
address tokenIn,
address tokenOut
) external nonReentrant onlyRole(STABILIZER_KEEPER_ROLE) returns (uint256 amountOut) {
if (tradeSize == 0) revert ZeroAmount();
if (maxGasPriceForStabilizer != 0 && block.basefee > maxGasPriceForStabilizer) revert GasPriceTooHigh();
if (block.number < lastExecutionBlock + minBlocksBetweenExecution) revert BlockDelayNotMet();
_pushSample(block.number, _getDeviationBps());
(int256 deviationBps, bool shouldRebalance) = this.checkDeviation();
if (!shouldRebalance) revert ShouldNotRebalance();
if (block.number != volumeBlockNumber) {
volumeBlockNumber = block.number;
volumeThisBlock = 0;
}
if (volumeThisBlock + tradeSize > maxStabilizationVolumePerBlock) revert VolumeCapExceeded();
volumeThisBlock += tradeSize;
lastExecutionBlock = block.number;
address pool = privatePoolRegistry.getPrivatePool(tokenIn, tokenOut);
if (pool == address(0)) revert NoPrivatePool();
address base = IDODOPMMPoolStabilizer(pool)._BASE_TOKEN_();
address quote = IDODOPMMPoolStabilizer(pool)._QUOTE_TOKEN_();
uint256 midPrice = IDODOPMMPoolStabilizer(pool).getMidPrice();
uint256 expectedOut = tokenIn == base
? (tradeSize * midPrice) / 1e18
: (tradeSize * 1e18) / midPrice;
uint256 minAmountOut = (expectedOut * (10000 - maxSlippageBps)) / 10000;
if (IERC20(tokenIn).balanceOf(address(this)) < tradeSize) revert InsufficientBalance();
IERC20(tokenIn).safeTransfer(pool, tradeSize);
amountOut = tokenIn == base
? IDODOPMMPoolStabilizer(pool).sellBase(tradeSize)
: IDODOPMMPoolStabilizer(pool).sellQuote(tradeSize);
if (amountOut < minAmountOut) revert SlippageExceeded();
emit PrivateSwapExecuted(tokenIn, tokenOut, tradeSize, amountOut);
return amountOut;
}
function _getDeviationBps() internal view returns (int256) {
if (useStablecoinPeg && address(stablecoinPegManager) != address(0) && stablecoinPegAsset != address(0)) {
(, int256 d) = stablecoinPegManager.checkUSDpeg(stablecoinPegAsset);
return d;
}
if (!useStablecoinPeg && address(commodityPegManager) != address(0) && commodityPegAsset != address(0)) {
(, int256 d) = commodityPegManager.checkCommodityPeg(commodityPegAsset);
return d;
}
return 0;
}
function _sustainedOverThreshold() internal view returns (bool) {
if (sustainedDeviationBlocks == 0) return true;
if (_sampleCount < sustainedDeviationBlocks) return false;
uint256 n = sustainedDeviationBlocks;
for (uint256 i = 0; i < n; i++) {
uint256 idx = (_sampleIndex + MAX_DEVIATION_SAMPLES - 1 - i) % MAX_DEVIATION_SAMPLES;
int256 d = _samples[idx].deviationBps;
// forge-lint: disable-next-line unsafe-typecast -- d is deviationBps (small int256), safe to cast to uint256 for abs
uint256 absD = d < 0 ? uint256(-d) : uint256(d);
if (absD <= thresholdBps) return false;
}
return true;
}
function _pushSample(uint256 blockNum, int256 deviationBps) internal {
uint256 idx = _sampleIndex % MAX_DEVIATION_SAMPLES;
_samples[idx] = DeviationSample({ blockNumber: blockNum, deviationBps: deviationBps });
_sampleIndex = ( _sampleIndex + 1) % MAX_DEVIATION_SAMPLES;
if (_sampleCount < MAX_DEVIATION_SAMPLES) _sampleCount++;
}
function _abs(int256 x) internal pure returns (int256) {
return x < 0 ? -x : x;
}
}