From 7babe7482d8d1601a59f2c908def93e4cf03f6f6 Mon Sep 17 00:00:00 2001 From: rndrntwrk <180591682+rndrntwrk@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:33:51 -0500 Subject: [PATCH] fix(perps): enforce stale oracle gate on liquidation --- .../contracts/perps/AgentPerpEngine.sol | 5 +- .../contracts/perps/AgentPerpEngineNative.sol | 7 +- .../test/perps/AgentPerpEngine.t.sol | 66 +++++++++++++++++++ 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/packages/evm-contracts/contracts/perps/AgentPerpEngine.sol b/packages/evm-contracts/contracts/perps/AgentPerpEngine.sol index 21da022a..55d66d61 100644 --- a/packages/evm-contracts/contracts/perps/AgentPerpEngine.sol +++ b/packages/evm-contracts/contracts/perps/AgentPerpEngine.sol @@ -132,7 +132,6 @@ contract AgentPerpEngine is AccessControl, ReentrancyGuard { uint256 public immutable defaultSkewScale; bool public tradingPaused; bool public marketCreationPaused; - bool private _isLiquidationContext; event MarketCreated( bytes32 indexed agentId, @@ -540,9 +539,7 @@ contract AgentPerpEngine is AccessControl, ReentrancyGuard { Position storage position = positions[agentId][trader]; if (position.size == 0) revert NoPosition(); - _isLiquidationContext = true; _syncOracle(agentId); - _isLiquidationContext = false; int256 fundingPayment = _settleFunding(position, market, true); uint256 markPrice = _markPrice(market, config); @@ -814,7 +811,7 @@ contract AgentPerpEngine is AccessControl, ReentrancyGuard { (uint256 mu, uint256 sigma, uint256 lastUpdate) = oracle.agentSkills(agentId); if (lastUpdate == 0) revert UnknownOracleAgent(); - if (!_isLiquidationContext && block.timestamp - lastUpdate > config.maxOracleDelay) revert StaleOracle(); + if (block.timestamp - lastUpdate > config.maxOracleDelay) revert StaleOracle(); _accrueFunding(market, config); diff --git a/packages/evm-contracts/contracts/perps/AgentPerpEngineNative.sol b/packages/evm-contracts/contracts/perps/AgentPerpEngineNative.sol index beabda64..b99a4811 100644 --- a/packages/evm-contracts/contracts/perps/AgentPerpEngineNative.sol +++ b/packages/evm-contracts/contracts/perps/AgentPerpEngineNative.sol @@ -102,7 +102,6 @@ contract AgentPerpEngineNative is AccessControl, ReentrancyGuard { uint256 public immutable fundingVelocity; uint256 public immutable maxLeverage; bool public tradingPaused; - bool private _isLiquidationContext; event PositionOpened( bytes32 indexed agentId, @@ -249,9 +248,9 @@ contract AgentPerpEngineNative is AccessControl, ReentrancyGuard { } MarketConfig memory config = marketConfigs[agentId]; - (uint256 mu,, uint256 lastUpdate) = oracle.agentSkills(agentId); + (,, uint256 lastUpdate) = oracle.agentSkills(agentId); if (lastUpdate == 0) revert UnknownOracleAgent(); - if (!_isLiquidationContext && block.timestamp - lastUpdate > config.maxOracleDelay) revert StaleOracle(); + if (block.timestamp - lastUpdate > config.maxOracleDelay) revert StaleOracle(); _updateFunding(agentId); price = oracle.getIndexPrice(agentId); @@ -536,9 +535,7 @@ contract AgentPerpEngineNative is AccessControl, ReentrancyGuard { function liquidate(bytes32 agentId, address trader) external nonReentrant { if (!marketConfigs[agentId].exists) revert MarketNotFound(); - _isLiquidationContext = true; _syncOracle(agentId); - _isLiquidationContext = false; MarketConfig memory config = marketConfigs[agentId]; MarketState storage market = markets[agentId]; diff --git a/packages/evm-contracts/test/perps/AgentPerpEngine.t.sol b/packages/evm-contracts/test/perps/AgentPerpEngine.t.sol index dee412b1..59485244 100644 --- a/packages/evm-contracts/test/perps/AgentPerpEngine.t.sol +++ b/packages/evm-contracts/test/perps/AgentPerpEngine.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../../contracts/perps/SkillOracle.sol"; import "../../contracts/perps/AgentPerpEngine.sol"; +import "../../contracts/perps/AgentPerpEngineNative.sol"; import "../../contracts/MockERC20.sol"; contract AgentPerpEngineTest is Test { @@ -94,6 +95,71 @@ contract AgentPerpEngineTest is Test { engine.syncOracle(agentId); } + function testOracleStalenessBlocksLiquidation() public { + bytes32 staleAgent = keccak256("STALE_AGENT"); + bytes32 peerAgent = keccak256("STALE_PEER"); + + vm.prank(admin); + oracle.updateAgentSkill(staleAgent, 1500, 0); + vm.prank(admin); + oracle.updateAgentSkill(peerAgent, 1500, 0); + + vm.prank(operator); + engine.createMarket( + staleAgent, + 1_000_000 * 1e18, + 5 * 1e18, + 1_000, + 500, + 1 minutes, + 0, + 0, + 0 + ); + + vm.prank(alice); + engine.modifyPosition(staleAgent, 100 * 1e18, 4 * 1e18); + + vm.prank(admin); + oracle.updateAgentSkill(staleAgent, 1000, 0); + + vm.warp(block.timestamp + 1 minutes + 1); + + vm.prank(bob); + vm.expectRevert(AgentPerpEngine.StaleOracle.selector); + engine.liquidate(staleAgent, alice); + } + + function testNativeOracleStalenessBlocksLiquidation() public { + bytes32 staleAgent = keccak256("NATIVE_STALE_AGENT"); + bytes32 peerAgent = keccak256("NATIVE_STALE_PEER"); + + SkillOracle nativeOracle = new SkillOracle(100 * 1e18, 2 hours, admin, admin, pauser); + vm.prank(admin); + nativeOracle.updateAgentSkill(staleAgent, 1500, 0); + vm.prank(admin); + nativeOracle.updateAgentSkill(peerAgent, 1500, 0); + + AgentPerpEngineNative nativeEngine = + new AgentPerpEngineNative(nativeOracle, 1_000_000 * 1e18, admin, operator, pauser); + + vm.prank(operator); + nativeEngine.createMarket(staleAgent); + + vm.deal(alice, 1_000 ether); + vm.prank(alice); + nativeEngine.modifyPosition{value: 100 ether}(staleAgent, 4 * 1e18); + + vm.prank(admin); + nativeOracle.updateAgentSkill(staleAgent, 1000, 0); + + vm.warp(block.timestamp + nativeEngine.DEFAULT_MAX_ORACLE_DELAY() + 1); + + vm.prank(bob); + vm.expectRevert(AgentPerpEngineNative.StaleOracle.selector); + nativeEngine.liquidate(staleAgent, alice); + } + function testOracleStalenessBlocksGetIndexPrice() public { vm.warp(block.timestamp + 3 minutes); vm.expectRevert(SkillOracle.StaleOracle.selector);