2026-03-02 12:14:09 -08:00
// 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 ) ;
feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs
- 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
2026-04-07 23:40:52 -07:00
function sellBase ( address to ) external returns ( uint256 ) ;
function sellQuote ( address to ) external returns ( uint256 ) ;
2026-03-02 12:14:09 -08:00
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 ) / 1 e18
: ( tradeSize * 1 e18 ) / midPrice ;
uint256 minAmountOut = ( expectedOut * ( 10000 - maxSlippageBps ) ) / 10000 ;
if ( IERC20 ( tokenIn ) . balanceOf ( address ( this ) ) < tradeSize ) revert InsufficientBalance ( ) ;
IERC20 ( tokenIn ) . safeTransfer ( pool , tradeSize ) ;
amountOut = tokenIn == base
feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs
- 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
2026-04-07 23:40:52 -07:00
? IDODOPMMPoolStabilizer ( pool ) . sellBase ( address ( this ) )
: IDODOPMMPoolStabilizer ( pool ) . sellQuote ( address ( this ) ) ;
2026-03-02 12:14:09 -08:00
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 ;
}
}