Skip to content
Open
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
22 changes: 4 additions & 18 deletions contracts/extensions/NativeOrderFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ contract NativeOrderFactory is Ownable, EIP712Alien {

event NativeOrderCreated(address maker, bytes32 orderHash, address clone, uint256 value);

error OrderReceiverShouldBeSetCorrectly(address receiver);
error OrderMakerShouldBeMsgSender(address expected, address actual);
error OrderMakingAmountShouldBeEqualToMsgValue(uint256 expected, uint256 actual);

address public immutable IMPLEMENTATION;

constructor(
Expand All @@ -50,20 +46,10 @@ contract NativeOrderFactory is Ownable, EIP712Alien {
));
}

function create(IOrderMixin.Order calldata makerOrder) external payable returns (address clone) {
// Validate main order parameters
if (makerOrder.maker.get() != msg.sender) revert OrderMakerShouldBeMsgSender(msg.sender, makerOrder.maker.get());
address receiver = makerOrder.receiver.get();
if (receiver == address(0) || receiver == address(this)) revert OrderReceiverShouldBeSetCorrectly(receiver);
if (msg.value != makerOrder.makingAmount) revert OrderMakingAmountShouldBeEqualToMsgValue(makerOrder.makingAmount, msg.value);

bytes32 makerOrderHash = makerOrder.hash(_domainSeparatorV4());
clone = IMPLEMENTATION.cloneDeterministic(makerOrderHash);
NativeOrderImpl(payable(clone)).depositAndApprove{ value: msg.value }();

IOrderMixin.Order memory order = makerOrder;
order.maker = Address.wrap(uint160(clone));
bytes32 orderHash = order.hashMemory(_domainSeparatorV4());
function create(bytes32 makerSalt, bytes32 orderHash, uint40 expiration) external payable returns (address clone) {
bytes32 salt = keccak256(abi.encode(msg.sender, makerSalt));
clone = IMPLEMENTATION.cloneDeterministic(salt);
NativeOrderImpl(payable(clone)).deposit{ value: msg.value }(msg.sender, orderHash, expiration);
emit NativeOrderCreated(msg.sender, orderHash, clone, msg.value);
}

Expand Down
124 changes: 52 additions & 72 deletions contracts/extensions/NativeOrderImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Errors } from "../libraries/Errors.sol";
import { EIP712Alien } from "../mocks/EIP712Alien.sol";
import { OrderLib, IOrderMixin } from "../OrderLib.sol";

// This contract is owning makers' funds for native orders
contract NativeOrderImpl is IERC1271, EIP712Alien, OnlyWethReceiver {
using Clones for address;
using AddressLib for Address;
Expand All @@ -22,40 +23,45 @@ contract NativeOrderImpl is IERC1271, EIP712Alien, OnlyWethReceiver {
using OrderLib for IOrderMixin.Order;
using MakerTraitsLib for MakerTraits;

event NativeOrderCancelled(bytes32 orderHash, uint256 balance);
event NativeOrderCancelledByResolver(bytes32 orderHash, uint256 balance, uint256 resolverReward);
event NativeOrderCancelled(uint256 balance);
event NativeOrderCancelledByResolver(uint256 balance, uint256 resolverReward);

error OnlyLimitOrderProtocolViolation(address sender, address limitOrderProtocol);
error OnlyFactoryViolation(address sender, address factory);
error OnlyMakerViolation(address sender, address maker);
error OnlyMakerViolation(address sender, uint80 makerTail);
error ResolverAccessTokenMissing(address resolver, address accessToken);
error OrderIsIncorrect(address expected, address actual);
error OrderShouldBeExpired(uint256 currentTime, uint256 expirationTime);
error CanNotCancelForZeroBalance();
error RescueFundsTooMuch(uint256 requested, uint256 available);
error CancellationDelayViolation(uint256 timePassedSinceExpiration, uint256 requiredDelay);
error WrongMakerArgument(address maker, uint80 expectedTail);

uint256 private constant _CANCEL_GAS_LOWER_BOUND = 70_000;

IWETH private immutable _WETH;
address private immutable _LOP;
address private immutable _IMPLEMENTATION = address(this);
address private immutable _FACTORY;
IERC20 private immutable _ACCESS_TOKEN;
uint256 private immutable _CANCELLATION_DELAY;

struct Purpose {
uint80 orderHashTail;
uint80 makerTail;
uint40 expiration;
}

Purpose public purpose;

modifier onlyFactory {
if (msg.sender != _FACTORY) revert OnlyFactoryViolation(msg.sender, _FACTORY);
_;
}

modifier onlyResolver {
if (_ACCESS_TOKEN.balanceOf(msg.sender) == 0) revert ResolverAccessTokenMissing(msg.sender, address(_ACCESS_TOKEN));
modifier onlyMaker {
if (uint80(uint160(msg.sender)) != purpose.makerTail) revert OnlyMakerViolation(msg.sender, purpose.makerTail);
_;
}

modifier onlyMaker(address maker) {
if (msg.sender != maker) revert OnlyMakerViolation(msg.sender, maker);
modifier onlyResolver {
if (_ACCESS_TOKEN.balanceOf(msg.sender) == 0) revert ResolverAccessTokenMissing(msg.sender, address(_ACCESS_TOKEN));
_;
}

Expand All @@ -78,89 +84,63 @@ contract NativeOrderImpl is IERC1271, EIP712Alien, OnlyWethReceiver {
_CANCELLATION_DELAY = cancellationDelay;
}

function depositAndApprove() external payable onlyFactory {
function deposit(address maker, bytes32 orderHash, uint40 expiration) external payable onlyFactory {
purpose = Purpose({
orderHashTail: uint80(uint256(orderHash)),
makerTail: uint80(uint160(maker)),
expiration: expiration
});
_WETH.safeDeposit(msg.value);
_WETH.forceApprove(_LOP, msg.value);
}

function isValidSignature(bytes32 hash, bytes calldata signature) external view returns(bytes4) {
// Extract order from signature via calldata type casting
IOrderMixin.Order calldata makerOrder;
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
makerOrder := signature.offset
function isValidSignature(bytes32 hash, bytes calldata /* signature */) external view returns(bytes4) {
uint80 orderHashTail = purpose.orderHashTail;
uint256 expiration = purpose.expiration;
if (uint80(uint256(hash)) != orderHashTail) {
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparing only the lower 80 bits of the order hash is insufficient for security. Hash collisions become much more likely with truncated hashes, potentially allowing malicious actors to create orders with matching tails. Use the full hash for comparison.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But 80 bits considered as strong enough in crypto

return 0xffffffff;
}

// Check order args by CREATE2 salt validation
bytes32 makerOrderHash = makerOrder.hash(_domainSeparatorV4());
address clone = _IMPLEMENTATION.predictDeterministicAddress(makerOrderHash, _FACTORY);
if (clone != address(this)) {
return bytes4(0);
}

// Check if patched order from signature matches LOP filling order
bytes32 orderHash = _patchOrderMakerAndHash(makerOrder);
if (orderHash != hash) {
return bytes4(0);
if (block.timestamp >= expiration) {
return 0xffffffff;
}

return this.isValidSignature.selector;
}

function cancelOrder(IOrderMixin.Order calldata makerOrder) external onlyMaker(makerOrder.maker.get()) {
uint256 balance = _cancelOrder(makerOrder, 0);
bytes32 orderHash = _patchOrderMakerAndHash(makerOrder);
emit NativeOrderCancelled(orderHash, balance);
function cancelOrder() external onlyMaker {
uint256 balance = _WETH.safeBalanceOf(address(this));
_WETH.safeWithdrawTo(balance, msg.sender);
emit NativeOrderCancelled(balance);
}

function cancelExpiredOrderByResolver(IOrderMixin.Order calldata makerOrder, uint256 rewardLimit) external onlyResolver {
uint256 orderExpiration = makerOrder.makerTraits.getExpirationTime();
if (!makerOrder.makerTraits.isExpired()) revert OrderShouldBeExpired(block.timestamp, orderExpiration);

uint256 resolverReward = 0;
if (rewardLimit > 0) {
if (block.timestamp - orderExpiration < _CANCELLATION_DELAY) revert CancellationDelayViolation(block.timestamp - orderExpiration, _CANCELLATION_DELAY);
resolverReward = Math.min(rewardLimit, block.basefee * _CANCEL_GAS_LOWER_BOUND * 1.1e18 / 1e18);
function rescueFunds(address token, address to, uint256 amount) external onlyMaker {
if (token == address(0)) {
(bool success, ) = to.call{ value: amount }("");
if (!success) revert Errors.ETHTransferFailed();
} else {
IERC20(token).safeTransfer(to, amount);
}
uint256 balance = _cancelOrder(makerOrder, resolverReward);
bytes32 orderHash = _patchOrderMakerAndHash(makerOrder);
emit NativeOrderCancelledByResolver(orderHash, balance, resolverReward);
}

function _cancelOrder(IOrderMixin.Order calldata makerOrder, uint256 resolverReward) private returns(uint256 balance) {
bytes32 makerOrderHash = makerOrder.hash(_domainSeparatorV4());
address clone = _IMPLEMENTATION.predictDeterministicAddress(makerOrderHash, _FACTORY);
if (clone != address(this)) revert OrderIsIncorrect(clone, address(this));
function cancelExpiredOrderByResolver(address maker, uint256 rewardLimit) external onlyResolver {
uint80 makerTail = purpose.makerTail;
uint256 purposeExpiration = purpose.expiration;
if (uint80(uint160(maker)) != makerTail) revert WrongMakerArgument(maker, makerTail);
if (block.timestamp < purposeExpiration) revert OrderShouldBeExpired(block.timestamp, purposeExpiration);

balance = _WETH.safeBalanceOf(address(this));
uint256 balance = _WETH.safeBalanceOf(address(this));
if (balance == 0) revert CanNotCancelForZeroBalance();

_WETH.safeWithdraw(balance);
if (resolverReward > 0) {
uint256 resolverReward;
if (rewardLimit > 0) {
if (block.timestamp - purposeExpiration < _CANCELLATION_DELAY) revert CancellationDelayViolation(block.timestamp - purposeExpiration, _CANCELLATION_DELAY);
resolverReward = Math.min(rewardLimit, block.basefee * _CANCEL_GAS_LOWER_BOUND * 1.1e18 / 1e18); // base fee + 10%
balance -= resolverReward;
(bool success, ) = msg.sender.call{ value: resolverReward }("");
if (!success) revert Errors.ETHTransferFailed();
}
if (balance > 0) {
(bool success, ) = makerOrder.maker.get().call{ value: balance }("");
(bool success, ) = maker.call{ value: balance }("");
if (!success) revert Errors.ETHTransferFailed();
}
}

function rescueFunds(address token, address to, uint256 amount) external onlyResolver {
if (token == address(0)) {
payable(to).transfer(amount);
} else if (IWETH(token) == _WETH) {
uint256 remainingOrderAmount = _WETH.allowance(address(this), _LOP);
uint256 extraAmount = _WETH.safeBalanceOf(address(this)) - remainingOrderAmount;
if (amount > extraAmount) revert RescueFundsTooMuch(amount, extraAmount);
_WETH.safeTransfer(to, amount);
} else {
IERC20(token).safeTransfer(to, amount);
}
}

function _patchOrderMakerAndHash(IOrderMixin.Order memory order) private view returns(bytes32) {
order.maker = Address.wrap(uint160(address(this)));
return order.hashMemory(_domainSeparatorV4());
emit NativeOrderCancelledByResolver(balance, resolverReward);
}
}
Loading