Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SC-1446] support resolver cancellation in ETHOrders #358

Merged
merged 6 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
74 changes: 70 additions & 4 deletions contracts/extensions/ETHOrders.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.23;

import "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol";
import "@1inch/solidity-utils/contracts/mixins/OnlyWethReceiver.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import "../interfaces/IPostInteraction.sol";
import "../OrderLib.sol";
Expand All @@ -20,20 +21,30 @@ contract ETHOrders is IPostInteraction, OnlyWethReceiver {
error InvalidOrder();
error NotEnoughBalance();
error ExistingOrder();
error OrderNotExpired();
error RewardIsTooBig();
error CancelOrderByResolverIsForbidden();

/// @notice ETH order struct.
struct ETHOrder {
address maker;
uint96 balance;
uint16 maximumPremium;
uint32 auctionDuration;
}

uint256 private constant _PREMIUM_BASE = 1000;
uint256 private constant _CANCEL_GAS_LOWER_BOUND = 30000;

address private immutable _LIMIT_ORDER_PROTOCOL;
IWETH private immutable _WETH;
/// @notice Makers and their uint96 ETH balances in single mapping.
IERC20 private immutable _ACCESS_TOKEN;

mapping(bytes32 orderHash => ETHOrder data) public ordersMakersBalances;

event ETHDeposited(bytes32 orderHash, uint256 amount);
event ETHOrderCancelled(bytes32 orderHash, uint256 amount);
event ETHOrderCancelledByThirdParty(bytes32 orderHash, uint256 amount, uint256 reward);

/// @notice Only limit order protocol can call this contract.
modifier onlyLimitOrderProtocol() {
Expand All @@ -42,9 +53,10 @@ contract ETHOrders is IPostInteraction, OnlyWethReceiver {
_;
}

constructor(IWETH weth, address limitOrderProtocol) OnlyWethReceiver(address(weth)) {
constructor(IWETH weth, address limitOrderProtocol, IERC20 accessToken) OnlyWethReceiver(address(weth)) {
_WETH = weth;
_LIMIT_ORDER_PROTOCOL = limitOrderProtocol;
_ACCESS_TOKEN = accessToken;
_WETH.approve(limitOrderProtocol, type(uint256).max);
}

Expand All @@ -61,7 +73,12 @@ contract ETHOrders is IPostInteraction, OnlyWethReceiver {
/*
* @notice Checks if ETH order is valid, makes ETH deposit for an order, saves real maker and wraps ETH into WETH.
*/
function ethOrderDeposit(IOrderMixin.Order calldata order, bytes calldata extension) external payable returns(bytes32 orderHash) {
function ethOrderDeposit(
IOrderMixin.Order calldata order,
bytes calldata extension,
uint16 maximumPremium,
uint32 auctionDuration
) external payable returns(bytes32 orderHash) {
if (!order.makerTraits.needPostInteractionCall()) revert InvalidOrder();
{
(bool valid, bytes4 validationResult) = order.isValidExtension(extension);
Expand All @@ -82,7 +99,9 @@ contract ETHOrders is IPostInteraction, OnlyWethReceiver {
if (ordersMakersBalances[orderHash].maker != address(0)) revert ExistingOrder();
ordersMakersBalances[orderHash] = ETHOrder({
maker: msg.sender,
balance: uint96(msg.value)
balance: uint96(msg.value),
maximumPremium: maximumPremium,
auctionDuration: auctionDuration
});
_WETH.safeDeposit(msg.value);
emit ETHDeposited(orderHash, msg.value);
Expand All @@ -100,6 +119,34 @@ contract ETHOrders is IPostInteraction, OnlyWethReceiver {
emit ETHOrderCancelled(orderHash, refundETHAmount);
}

/**
* @notice Allows third-party to cancel an expired order and receive a reward.
* @param makerTraits The traits of the maker
* @param orderHash Hash of the order to cancel
*/
function cancelOrderByResolver(MakerTraits makerTraits, bytes32 orderHash) external {
unchecked {
if (_ACCESS_TOKEN.balanceOf(msg.sender) == 0) revert AccessDenied();
uint256 expirationTime = MakerTraitsLib.getExpirationTime(makerTraits);
if (block.timestamp < expirationTime) revert OrderNotExpired();
ETHOrder memory ethOrder = ordersMakersBalances[orderHash];
if (ethOrder.maker == address(0)) revert InvalidOrder();
if (ethOrder.maximumPremium == 0) revert CancelOrderByResolverIsForbidden();
IOrderMixin(_LIMIT_ORDER_PROTOCOL).cancelOrder(makerTraits, orderHash);
uint256 reward = _CANCEL_GAS_LOWER_BOUND * block.basefee * (_PREMIUM_BASE + _getCurrentPremiumMultiplier(ethOrder, expirationTime)) / _PREMIUM_BASE;
if (reward > ethOrder.balance) revert RewardIsTooBig();
uint256 refundETHAmount = ethOrder.balance - reward;
ordersMakersBalances[orderHash].balance = 0;
if (reward > 0) {
_WETH.safeWithdrawTo(reward, msg.sender);
}
if (refundETHAmount > 0) {
_WETH.safeWithdrawTo(refundETHAmount, ethOrder.maker);
}
emit ETHOrderCancelledByThirdParty(orderHash, ethOrder.balance, reward);
}
}

/**
* @notice Checks if orderHash signature was signed with real order maker.
*/
Expand Down Expand Up @@ -132,4 +179,23 @@ contract ETHOrders is IPostInteraction, OnlyWethReceiver {
ordersMakersBalances[orderHash].balance -= uint96(makingAmount);
}
}

/**
* @notice Calculates the current premium multiplier based on time since expiration.
* @param order The ETH order
* @param expirationTime The expiration time of the order
* @return The current premium multiplier (scaled by 1e3)
*/
function _getCurrentPremiumMultiplier(ETHOrder memory order, uint256 expirationTime) private view returns (uint256) {
unchecked {
if (block.timestamp <= expirationTime) {
return 0;
}
uint256 timeElapsed = block.timestamp - expirationTime;
if (timeElapsed >= order.auctionDuration) {
return order.maximumPremium;
}
return (timeElapsed * order.maximumPremium) / order.auctionDuration;
}
}
}
9 changes: 9 additions & 0 deletions contracts/libraries/MakerTraitsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ library MakerTraitsLib {
return expiration != 0 && expiration < block.timestamp; // solhint-disable-line not-rely-on-time
}

/**
* @notice Returns the expiration time of the order.
* @param makerTraits The traits of the maker.
* @return result The expiration timestamp of the order.
*/
function getExpirationTime(MakerTraits makerTraits) internal pure returns (uint256) {
return (MakerTraits.unwrap(makerTraits) >> _EXPIRATION_OFFSET) & _EXPIRATION_MASK;
}

/**
* @notice Returns the nonce or epoch of the order.
* @param makerTraits The traits of the maker.
Expand Down
Loading
Loading