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
25 changes: 22 additions & 3 deletions packages/evm-contracts/contracts/GoldClob.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ contract GoldClob is AccessControl, ReentrancyGuard {
uint256 public tradeTreasuryFeeBps;
uint256 public tradeMarketMakerFeeBps;
uint256 public winningsMarketMakerFeeBps;
uint256 public reservedNative;
bool public marketCreationPaused;
bool public orderPlacementPaused;

Expand Down Expand Up @@ -390,6 +391,7 @@ contract GoldClob is AccessControl, ReentrancyGuard {
(progress.executedCost * market.tradeMarketMakerFeeBpsSnapshot) / MAX_FEE_BPS;
uint256 requiredValue = restingCost + progress.executedCost + tradeTreasuryFee + tradeMarketMakerFee;
if (msg.value < requiredValue) revert InsufficientNativeValue();
_reserveNative(restingCost + progress.executedCost);
if (tradeTreasuryFee > 0) payable(treasury).sendValue(tradeTreasuryFee);
if (tradeMarketMakerFee > 0) payable(marketMaker).sendValue(tradeMarketMakerFee);
uint256 traderRefund = msg.value - requiredValue;
Expand Down Expand Up @@ -420,6 +422,7 @@ contract GoldClob is AccessControl, ReentrancyGuard {
order.prevOrderId = 0;
order.nextOrderId = 0;

_releaseNative(refund);
if (refund > 0) payable(msg.sender).sendValue(refund);

emit OrderCancelled(key, orderId);
Expand Down Expand Up @@ -452,6 +455,7 @@ contract GoldClob is AccessControl, ReentrancyGuard {
order.nextOrderId = 0;

emit OrderCancelled(key, orderId);
_releaseNative(refund);
if (refund > 0) payable(msg.sender).sendValue(refund);
}

Expand All @@ -472,11 +476,13 @@ contract GoldClob is AccessControl, ReentrancyGuard {
if (winningShares > 0) {
uint256 fee = (winningShares * market.winningsMarketMakerFeeBpsSnapshot) / MAX_FEE_BPS;
payout = winningShares - fee;
_releaseNative(winningShares);
if (fee > 0) payable(marketMaker).sendValue(fee);
}
} else if (status == MarketStatus.CANCELLED) {
payout = uint256(position.aStake) + uint256(position.bStake);
_clearPosition(position);
_releaseNative(payout);
} else {
revert MarketNotSettled();
}
Expand Down Expand Up @@ -893,6 +899,18 @@ contract GoldClob is AccessControl, ReentrancyGuard {
return orderFlags == ORDER_FLAGS_GTC_POST_ONLY;
}

function _reserveNative(uint256 amount) internal {
if (amount > 0) {
reservedNative += amount;
}
}

function _releaseNative(uint256 amount) internal {
if (amount > 0) {
reservedNative -= amount;
}
}

function _wouldCrossRestingBook(Market storage market, uint8 side, uint16 price) internal view returns (bool) {
if (side == BUY_SIDE) {
return market.bestAsk < MAX_PRICE && market.bestAsk <= price;
Expand All @@ -908,10 +926,11 @@ contract GoldClob is AccessControl, ReentrancyGuard {

function sweepETH(address payable to) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant {
uint256 bal = address(this).balance;
require(bal > 0, "NothingToSweep");
require(bal > reservedNative, "NothingToSweep");
address payable treasuryRecipient = payable(treasury);
if (to == address(0) || to != treasuryRecipient) revert InvalidSweepRecipient();
treasuryRecipient.sendValue(bal);
emit SweepETH(treasuryRecipient, bal);
uint256 excess = bal - reservedNative;
treasuryRecipient.sendValue(excess);
emit SweepETH(treasuryRecipient, excess);
}
}
65 changes: 65 additions & 0 deletions packages/evm-contracts/test/GoldClobSettlement.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,71 @@ contract GoldClobSettlementTest is Test {
clob.claim(duel, MARKET_KIND_DUEL_WINNER);
}

function testSweepCannotDrainRestingOrderCollateral() public {
bytes32 duel = _createOpenMarket("sweep-resting-collateral");
uint128 amount = 1_000;
uint16 price = 600;
uint256 orderValue = _totalOrderValue(SELL_SIDE, price, amount);
uint256 refund = _quoteCost(SELL_SIDE, price, amount);

vm.prank(traderA);
clob.placeOrder{value: orderValue}(duel, MARKET_KIND_DUEL_WINNER, SELL_SIDE, price, amount, ORDER_FLAG_GTC);

assertEq(address(clob).balance, refund, "resting order collateral should be retained");

vm.expectRevert(bytes("NothingToSweep"));
vm.prank(admin);
clob.sweepETH(payable(treasury));

uint256 traderBefore = traderA.balance;
vm.prank(traderA);
clob.cancelOrder(duel, MARKET_KIND_DUEL_WINNER, 1);
assertEq(traderA.balance - traderBefore, refund, "resting collateral must remain refundable");
}

function testSweepCannotDrainResolvedWinnerPayout() public {
bytes32 duel = _createOpenMarket("sweep-resolved-payout");
uint128 amount = 1_000;

_matchTrade(duel, 600, amount);
_resolveDuel(duel, DuelOutcomeOracle.Side.A);

vm.expectRevert(bytes("NothingToSweep"));
vm.prank(admin);
clob.sweepETH(payable(treasury));

uint256 traderBefore = traderB.balance;
vm.prank(traderB);
clob.claim(duel, MARKET_KIND_DUEL_WINNER);

uint256 expectedFee = (uint256(amount) * 200) / 10_000;
assertEq(traderB.balance - traderBefore, uint256(amount) - expectedFee, "winner payout must remain claimable");
}

function testSweepTransfersOnlyForcedExcess() public {
bytes32 duel = _createOpenMarket("sweep-excess-only");
uint128 amount = 1_000;
uint16 price = 600;
uint256 orderValue = _totalOrderValue(SELL_SIDE, price, amount);
uint256 refund = _quoteCost(SELL_SIDE, price, amount);

vm.prank(traderA);
clob.placeOrder{value: orderValue}(duel, MARKET_KIND_DUEL_WINNER, SELL_SIDE, price, amount, ORDER_FLAG_GTC);

uint256 forcedExcess = 1 ether;
vm.deal(admin, forcedExcess);
vm.prank(admin);
(bool sent,) = payable(address(clob)).call{value: forcedExcess}("");
assertTrue(sent, "forced excess transfer failed");

uint256 treasuryBefore = treasury.balance;
vm.prank(admin);
clob.sweepETH(payable(treasury));

assertEq(treasury.balance - treasuryBefore, forcedExcess, "sweep should transfer only excess");
assertEq(address(clob).balance, refund, "escrowed collateral must remain in the CLOB");
}

function _createOpenMarket(string memory label) private returns (bytes32 duel) {
duel = _duelKey(label);
bytes32 participantA = _hashLabel(string.concat(label, "-a"));
Expand Down
Loading