Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions packages/evm-contracts/contracts/perps/AgentPerpEngineNative.sol
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,18 @@ contract AgentPerpEngineNative is AccessControl, ReentrancyGuard {
revert CloseOnlyMode();
}

function _assertLeverage(bytes32 agentId, int256 size, uint256 margin) internal view {
function _assertPositionHealthy(bytes32 agentId, int256 size, uint256 margin, uint256 entryPrice) internal view {
if (size == 0) return;
if (margin == 0) revert Undercollateralized();
MarketConfig memory config = marketConfigs[agentId];
uint256 absSize = _abs(size);
uint256 execPrice = _getExecutionPrice(agentId, 0);
if (Math.mulDiv(absSize, execPrice, margin) > config.maxLeverage) revert MaxLeverageExceeded();
uint256 markPrice = _getExecutionPrice(agentId, 0);
int256 unrealizedPnl = _realizePnl(size, entryPrice, markPrice, absSize);
int256 equity = int256(margin) + unrealizedPnl;
if (equity <= 0) revert Underwater();

uint256 notional = Math.mulDiv(absSize, markPrice, ONE);
if (Math.mulDiv(notional, ONE, uint256(equity)) > config.maxLeverage) revert MaxLeverageExceeded();
}

// ── Position management ──
Expand Down Expand Up @@ -502,7 +507,7 @@ contract AgentPerpEngineNative is AccessControl, ReentrancyGuard {
}
}

_assertLeverage(agentId, pos.size, pos.margin);
_assertPositionHealthy(agentId, pos.size, pos.margin, pos.entryPrice);

// Track open positions counter
if (oldSize == 0 && pos.size != 0) {
Expand All @@ -524,10 +529,30 @@ contract AgentPerpEngineNative is AccessControl, ReentrancyGuard {

function withdrawMargin(bytes32 agentId, uint256 amount) external nonReentrant {
if (tradingPaused) revert TradingPaused();
if (!marketConfigs[agentId].exists) revert MarketNotFound();
_syncOracle(agentId);

MarketState storage market = markets[agentId];
Position storage pos = positions[agentId][msg.sender];

if (pos.size != 0) {
int256 rateDelta = market.cumulativeFundingRate - pos.lastCumulativeFundingRate;
if (rateDelta != 0) {
int256 fundingPayment = (pos.size * rateDelta) / int256(ONE);
if (fundingPayment > 0) {
uint256 loss = uint256(fundingPayment);
if (pos.margin < loss) revert Underwater();
pos.margin -= loss;
} else {
pos.margin += uint256(-fundingPayment);
}
}
pos.lastCumulativeFundingRate = market.cumulativeFundingRate;
}

if (pos.margin < amount) revert InsufficientMargin();
_assertLeverage(agentId, pos.size, pos.margin - amount);
pos.margin -= amount;
_assertPositionHealthy(agentId, pos.size, pos.margin, pos.entryPrice);
emit MarginWithdrawn(agentId, msg.sender, amount);
Address.sendValue(payable(msg.sender), amount);
}
Expand Down
53 changes: 53 additions & 0 deletions packages/evm-contracts/test/perps/AgentPerpEngine.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -750,3 +751,55 @@ contract AgentPerpEngineTest is Test {
engine.createMarket(agentB, 1_000_000 * 1e18, 5 * 1e18, 1_000, 500, 2 minutes, 0, 500, 500);
}
}

contract AgentPerpEngineNativeHealthTest is Test {
SkillOracle oracle;
AgentPerpEngineNative engine;

address admin = address(1);
address operator = address(6);
address pauser = address(7);
address alice = address(2);

bytes32 agentId = keccak256("MODEL_A");

function setUp() public {
vm.txGasPrice(0);
vm.warp(1_000);

vm.startPrank(admin);
oracle = new SkillOracle(100 * 1e18, 2 minutes, admin, admin, pauser);
engine = new AgentPerpEngineNative(oracle, 1_000_000 * 1e18, admin, operator, pauser);
oracle.updateAgentSkill(agentId, 1500, 0);
vm.stopPrank();

vm.prank(operator);
engine.createMarket(agentId);

vm.deal(alice, 1_000 ether);
}

function testNativeWithdrawMarginRejectsNegativeEquityAfterOracleMove() public {
vm.prank(alice);
engine.modifyPosition{value: 200 ether}(agentId, int256(5 ether));

vm.prank(admin);
oracle.updateAgentSkill(agentId, 1500, 100);

vm.expectRevert(AgentPerpEngineNative.Underwater.selector);
vm.prank(alice);
engine.withdrawMargin(agentId, 1 ether);
}

function testNativeModifyPositionRejectsNegativeEquityAfterOracleMove() public {
vm.prank(alice);
engine.modifyPosition{value: 200 ether}(agentId, int256(5 ether));

vm.prank(admin);
oracle.updateAgentSkill(agentId, 1500, 100);

vm.expectRevert(AgentPerpEngineNative.Underwater.selector);
vm.prank(alice);
engine.modifyPosition{value: 1 ether}(agentId, int256(1 ether));
}
}
Loading