019d5666-7204-7199-9428-87c933f7acea
Open this run to answer one question: is this real attacker-relevant behavior with real impact, or just normal contract plumbing?
| ID | Type | Status | Attempts | Heartbeat |
|---|---|---|---|---|
| 42841 | artifact.fetch | complete | 1 / 3 | 1 week ago |
| 42842 | proxy.resolve | complete | 1 / 3 | 1 week ago |
| 42843 | tool.cast_disasm | complete | 1 / 3 | 1 week ago |
| 42844 | analysis.bundle | complete | 1 / 3 | 1 week ago |
| 42845 | capability.graph | complete | 1 / 3 | 1 week ago |
| 42846 | detector.run | complete | 1 / 3 | 1 week ago |
| 42847 | validation.fork | complete | 1 / 3 | 1 week ago |
| 42957 | report.consolidate | complete | 1 / 6 | 1 week ago |
| 42958 | tool.slither | failed | 2 / 2 | 1 week ago |
{
"method": "etherscan+onchain",
"status": "confirmed",
"etherscan_hint": {
"source": "etherscan",
"etherscan": {
"proxy_flag": true,
"implementation": "0x6ac64c4760e0590f88233b2046810e87e0354324"
}
},
"implementation": "0x6ac64c4760e0590f88233b2046810e87e0354324",
"onchain_method": "eip1967"
}
Traceback (most recent call last):
File "/var/www/tripwire/worker/worker.py", line 198, in main
payload = dispatch_job(job_type, job_id, job_meta, run_data, config, laravel_client)
File "/var/www/tripwire/worker/worker.py", line 114, in dispatch_job
return slither_run.run(job_id, job_meta, run_data, config, log, laravel_client)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/var/www/tripwire/worker/jobs/slither_run.py", line 1084, in run
stdout_text, stderr_text, result_json = _run_slither(
~~~~~~~~~~~~^
workspace,
^^^^^^^^^^
...<4 lines>...
target_path=target_path,
^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/var/www/tripwire/worker/jobs/slither_run.py", line 958, in _run_slither
raise RuntimeError(f"slither failed (exit {result.returncode}): {stderr_text or stdout_text}")
RuntimeError: slither failed (exit 1): 'forge clean' running (wd: /tmp/slither-llgbznxh)
'forge config --json' running
'forge build --build-info --skip ./test/** ./script/** --force' running (wd: /tmp/slither-llgbznxh)
Traceback (most recent call last):
File "/var/www/tripwire/.venv/bin/slither", line 10, in <module>
sys.exit(main())
~~~~^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 776, in main
main_impl(all_detector_classes=detectors, all_printer_classes=printers)
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 882, in main_impl
) = process_all(filename, args, detector_classes, printer_classes)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 96, in process_all
compilations = compile_all(target, **vars(args))
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 722, in compile_all
compilations.append(CryticCompile(target, **kwargs))
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 211, in __init__
self._compile(**kwargs)
~~~~~~~~~~~~~^^^^^^^^^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 633, in _compile
self._platform.compile(self, **kwargs)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/platform/foundry.py", line 102, in compile
hardhat_like_parsing(
~~~~~~~~~~~~~~~~~~~~^
crytic_compile, str(self._target), build_directory, str(self._project_root)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/platform/hardhat.py", line 52, in hardhat_like_parsing
raise InvalidCompilation(txt)
crytic_compile.platform.exceptions.InvalidCompilation: Compilation failed. Can you run build command?
/tmp/slither-llgbznxh/out/build-info is not a directory.
{"no_findings":false,"summary":"Found three meaningful issues in the implementation: `fillOffer` is reentrant before state is updated, ERC20 collateral is accounted at nominal rather than received amounts, and the owner can de-whitelist then withdraw live escrowed tokens.","findings":[{"severity":"high","category":"reentrancy","title":"`fillOffer` can be reentered before `filledAmount` is updated, allowing overfilled orders","exploit_scenario":"If a whitelisted collateral token invokes callbacks during `transferFrom` (for example an ERC777-style token or a callback-capable ERC20), an attacker can fill the same offer reentrantly from a contract wallet. Each nested call observes the old `offer.filledAmount`, so multiple full-sized orders are created against one offer. The attacker can then self-settle those duplicated orders and extract more `value + collateral` than was originally escrowed, draining pooled liquidity or leaving the market insolvent.","recommendation":"Make `fillOffer` non-reentrant, or expose only a guarded external entrypoint and keep the core logic internal. Update `offer.filledAmount`/status before any external token transfer, or otherwise re-check remaining capacity after the transfer returns.","evidence":"`fillOffer` performs `iexToken.safeTransferFrom(...)` before calling `_fillOffer(...)`. `_fillOffer` is the only place that increments `offer.filledAmount` and closes the offer.","locations":["src/FrontrunMarket.sol:381","src/FrontrunMarket.sol:410","src/FrontrunMarket.sol:412","src/FrontrunMarket.sol:763","src/FrontrunMarket.sol:770"]},{"severity":"high","category":"other","title":"Nominal ERC20 accounting makes fee-on-transfer collateral tokens insolvent","exploit_scenario":"If governance ever accepts a deflationary / fee-on-transfer token as collateral, the market records `value` and `collateral` using the requested amounts instead of the actual tokens received. An attacker can self-trade using that token so the contract receives less than it promises to pay back on cancel / settle. The shortfall is then paid from unrelated users' escrow if available, or later exits revert because the vault is underfunded.","recommendation":"Only allow plain ERC20 collateral tokens, or account using balance deltas before and after each transfer. Reject deposits where the actual received amount is smaller than the expected amount.","evidence":"Both `newOffer` and `fillOffer` transfer ERC20s and then keep using caller-supplied `value` / derived `collateral`; no post-transfer balance check exists before later payouts in `forceCancelOrder`, `settleFilled`, and `settleCancelled`.","locations":["src/FrontrunMarket.sol:322","src/FrontrunMarket.sol:343","src/FrontrunMarket.sol:381","src/FrontrunMarket.sol:410","src/FrontrunMarket.sol:526","src/FrontrunMarket.sol:575"]},{"severity":"medium","category":"access_control","title":"Owner can confiscate live escrow by de-whitelisting a token before `withdrawStuckToken`","exploit_scenario":"While users still have open offers or orders denominated in token `X`, the owner can call `setAcceptedTokens([X], false)` and immediately follow with `withdrawStuckToken(X, owner)`. Because the withdrawal gate only checks the current whitelist bit, the function transfers the entire balance of `X`, including funds backing active positions. Those positions then remain recorded on-chain but cannot be settled or cancelled successfully.","recommendation":"Track escrowed balances per collateral token and forbid withdrawing any amount backing live offers/orders. `withdrawStuckToken` should not rely only on the current accepted-token flag.","evidence":"`setAcceptedTokens` can flip any token's accepted status at any time, and `withdrawStuckToken` transfers the full contract balance whenever `acceptedTokens[_token]` is false.","locations":["src/FrontrunMarket.sol:656","src/FrontrunMarket.sol:660","src/FrontrunMarket.sol:780","src/FrontrunMarket.sol:782","src/FrontrunMarket.sol:784"]}]}
OpenAI Codex v0.118.0 (research preview)
--------
workdir: /tmp/codex-dgrhxd3v
model: gpt-5.4
provider: openai
approval: never
sandbox: read-only
reasoning effort: high
reasoning summaries: none
session id: 019d568a-c3cb-7493-b929-b368acd8f7d9
--------
user
Context: The user requested the proxy at 0x849f4081899305a1fd24aac84db5174eb60dc28e.
Audit the implementation at 0x6ac64c4760e0590f88233b2046810e87e0354324.
Findings should refer to the implementation unless explicitly about proxy mechanics.
You are a security auditor. Audit this Solidity codebase for exploitable vulnerabilities.
Focus on: reentrancy, access control, upgradeability/proxy issues, signature validation, oracle manipulation,
accounting/invariant breaks, external call patterns, unchecked return values, and dangerous delegatecall.
Scope rules:
- You MUST fully read `AUDIT_BUNDLE.txt` before concluding. It contains the complete contents of all first-party Solidity sources in this workspace.
- Treat `lib/` and `@openzeppelin/` as third-party dependencies; skip them unless there is a concrete misuse angle in first-party code.
- If there are only dependency files and no first-party sources, set no_findings=true and explain in summary.
Output MUST be valid JSON matching the provided schema.
- If you find nothing meaningful: set no_findings=true and findings=[].
- Otherwise: set no_findings=false and include findings with severity/category/title and file/line locations when possible.
Output constraints (IMPORTANT):
- severity MUST be exactly one of: info, low, medium, high, critical (lowercase).
- category MUST be exactly one of: access_control, reentrancy, math, oracle, upgradeability, auth, other.
Keep summary short and concrete.
warning: Codex could not find system bubblewrap on PATH. Please install bubblewrap with your package manager. Codex will use the vendored bubblewrap in the meantime.
exec
/bin/bash -lc pwd in /tmp/codex-dgrhxd3v
succeeded in 0ms:
/tmp/codex-dgrhxd3v
exec
/bin/bash -lc 'rg --files' in /tmp/codex-dgrhxd3v
succeeded in 0ms:
codex-output-schema.json
src/FrontrunMarket.sol
AUDIT_BUNDLE.txt
lib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sol
lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol
lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol
lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol
lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol
lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol
lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol
lib/openzeppelin-contracts-upgradeable/contracts/utils/ContextUpgradeable.sol
lib/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol
lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol
lib/openzeppelin-contracts/contracts/utils/Address.sol
lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol
lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol
lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol
lib/openzeppelin-contracts/contracts/access/IAccessControl.sol
lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol
lib/openzeppelin-contracts-upgradeable/contracts/utils/introspection/ERC165Upgradeable.sol
lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol
exec
/bin/bash -lc "sed -n '1,260p' AUDIT_BUNDLE.txt" in /tmp/codex-dgrhxd3v
succeeded in 0ms:
This file concatenates the full contents of first-party Solidity sources for auditing.
Do not treat dependency code under lib/ or @openzeppelin/ as first-party.
================================================================================
FILE: src/FrontrunMarket.sol
================================================================================
//SPDX-License-Identifier: Unlicense
pragma solidity 0.8.20;
import '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
import '@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol';
import '@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';
struct Token {
address token;
uint48 settleTime;
uint48 settleDuration;
uint152 settleRate; // number of token per point
uint8 status; //
uint256 minAmount;
}
struct Offer {
uint8 offerType;
bytes32 tokenId;
address exToken;
uint256 amount;
uint256 value;
uint256 collateral;
uint256 filledAmount;
uint8 status;
address offeredBy;
uint256 minAmount;
}
struct Order {
uint256 offerId;
uint256 amount;
address seller;
address buyer;
uint8 status;
}
struct Config {
uint256 pledgeRate;
uint256 feeRefund;
uint256 feeSettle;
address feeWallet;
}
contract FrontrunMarket is Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable {
using SafeERC20 for IERC20;
uint256 constant WEI6 = 10 ** 6;
uint8 constant OFFER_BUY = 1;
uint8 constant OFFER_SELL = 2;
// Status
// Offer status
uint8 constant STATUS_OFFER_OPEN = 1;
uint8 constant STATUS_OFFER_FILLED = 2;
uint8 constant STATUS_OFFER_CANCELLED = 3;
// Order Status
uint8 constant STATUS_ORDER_OPEN = 1;
uint8 constant STATUS_ORDER_SETTLE_FILLED = 2;
uint8 constant STATUS_ORDER_SETTLE_CANCELLED = 3;
uint8 constant STATUS_ORDER_CANCELLED = 4;
// token status
uint8 constant STATUS_TOKEN_ACTIVE = 1;
uint8 constant STATUS_TOKEN_INACTIVE = 2;
uint8 constant STATUS_TOKEN_SETTLE = 3;
struct MarketStorage {
mapping(address => bool) acceptedTokens;
mapping(bytes32 => Token) tokens;
mapping(uint256 => Offer) offers;
uint256 lastOfferId;
mapping(uint256 => Order) orders;
uint256 lastOrderId;
Config config;
}
//keccak256(abi.encode(uint256(keccak256("frontrun.Market")) - 1)) & ~bytes32(uint256(0xff));
bytes32 constant MarketStorageLocation = 0x02c326031f2f3dedd13e7fde59c7c20af05a8826f4c07e8d42538c5f2be06800;
function _getOwnStorage() private pure returns (MarketStorage storage $) {
assembly {
$.slot := MarketStorageLocation
}
}
// event
event NewOffer(
uint256 id,
uint8 offerType,
bytes32 tokenId,
address exToken,
uint256 amount,
uint256 value,
uint256 collateral,
uint256 minAmount,
address doer
);
event NewToken(bytes32 tokenId, uint256 settleDuration, uint256 minAmount);
event NewOrder(uint256 id, uint256 offerId, uint256 amount, address seller, address buyer);
event SettleFilled(uint256 orderId, uint256 value, uint256 fee, address doer);
event SettleCancelled(uint256 orderId, uint256 value, uint256 fee, address doer);
event CancelOrder(uint256 orderId, address doer);
event CancelOffer(uint256 offerId, uint256 refundValue, uint256 refundFee, address doer);
event UpdateAcceptedTokens(address[] tokens, bool isAccepted);
event CloseOffer(uint256 offerId, uint256 refundAmount);
event UpdateConfig(
address oldFeeWallet,
uint256 oldFeeSettle,
uint256 oldFeeRefund,
uint256 oldPledgeRate,
address newFeeWallet,
uint256 newFeeSettle,
uint256 newFeeRefund,
uint256 newPledgeRate
);
event TokenToSettlePhase(bytes32 tokenId, address token, uint256 settleRate, uint256 settleTime);
event UpdateTokenStatus(bytes32 tokenId, uint8 oldValue, uint8 newValue);
event TokenForceCancelSettlePhase(bytes32 tokenId);
event Settle2Steps(uint256 orderId, bytes32 hash, address doer);
event UpdateTokenSettleDuration(bytes32 tokenId, uint48 oldValue, uint48 newValue);
function initialize() external initializer {
__Ownable_init(msg.sender);
// init value
MarketStorage storage $ = _getOwnStorage();
$.config.pledgeRate = WEI6; //
$.config.feeWallet = owner();
$.config.feeSettle = WEI6 / 200; // 0.5%
$.config.feeRefund = 0; // 0.0%
}
///////////////////////////
////// SYSTEM ACTION //////
///////////////////////////
/// @notice Creates a market to trade
/// @param tokenId market identifier
/// @param settleDuration Duration after which buyer of token can claim collateral if seller hasnt delivered
/// @param minAmount minAmount that can be filled for this particular market including offers and orders , 6 decimals
// add min order value in market so that in settle2steps we dont have to pay gas to settle low value orders
function createToken(bytes32 tokenId, uint48 settleDuration, uint256 minAmount) external onlyOwner {
MarketStorage storage $ = _getOwnStorage();
Token storage _token = $.tokens[tokenId];
_token.settleDuration = settleDuration;
_token.minAmount = minAmount;
_token.status = STATUS_TOKEN_ACTIVE;
emit NewToken(tokenId, settleDuration, minAmount);
}
/// @notice moves a market to settlement phase
/// @param tokenId market identifier
/// @param tokenAddress contract address for token to be delivered
/// @param settleRate how many tokens per 1M amount traded(if token to be delivered has 18 decimals write 1e12)
function tokenToSettlePhase(
bytes32 tokenId,
address tokenAddress,
uint152 settleRate // how many token for 1M points
) external onlyOwner {
MarketStorage storage $ = _getOwnStorage();
Token storage _token = $.tokens[tokenId];
require(tokenAddress != address(0), 'Invalid Token Address');
require(settleRate > 0, 'Invalid Settle Rate');
require(_token.status == STATUS_TOKEN_ACTIVE || _token.status == STATUS_TOKEN_INACTIVE, 'Invalid Token Status');
_token.token = tokenAddress;
_token.settleRate = settleRate;
// update token settle status & time
_token.status = STATUS_TOKEN_SETTLE;
_token.settleTime = uint48(block.timestamp);
emit TokenToSettlePhase(tokenId, tokenAddress, settleRate, block.timestamp);
}
/// @notice toggle market active inactive
/// @param tokenId market identifier
function tokenToggleActivation(bytes32 tokenId) external onlyOwner {
MarketStorage storage $ = _getOwnStorage();
Token storage _token = $.tokens[tokenId];
uint8 fromStatus = _token.status;
uint8 toStatus = fromStatus == STATUS_TOKEN_ACTIVE ? STATUS_TOKEN_INACTIVE : STATUS_TOKEN_ACTIVE;
require(fromStatus == STATUS_TOKEN_ACTIVE || fromStatus == STATUS_TOKEN_INACTIVE, 'Cannot Change Token Status');
_token.status = toStatus;
emit UpdateTokenStatus(tokenId, fromStatus, toStatus);
}
/// @notice // in case wrong setting for settle
/// @param tokenId market identifier
function tokenForceCancelSettlePhase(bytes32 tokenId) external onlyOwner {
MarketStorage storage $ = _getOwnStorage();
Token storage _token = $.tokens[tokenId];
require(_token.status == STATUS_TOKEN_SETTLE, 'Invalid Token Status');
_token.status = STATUS_TOKEN_INACTIVE;
emit TokenForceCancelSettlePhase(tokenId);
}
/// @notice // updates settleDuration
/// @param tokenId market identifier
/// @param newValue settleDuration value
function updateSettleDuration(bytes32 tokenId, uint48 newValue) external onlyOwner {
MarketStorage storage $ = _getOwnStorage();
Token storage _token = $.tokens[tokenId];
uint48 oldValue = _token.settleDuration;
_token.settleDuration = newValue;
emit UpdateTokenSettleDuration(tokenId, oldValue, newValue);
}
// force cancel order - by Operator/Owner
// refund for both seller & buyer without any fees cut
/// @notice
/// @param orderId order identifier
function forceCancelOrder(uint256 orderId) public nonReentrant onlyOwner {
MarketStorage storage $ = _getOwnStorage();
Order storage order = $.orders[orderId];
Offer storage offer = $.offers[order.offerId];
require(order.status == STATUS_ORDER_OPEN, 'Invalid Order Status');
// calculate refund
uint256 buyerRefundValue = (order.amount * offer.value) / offer.amount; // value
uint256 sellerRefundValue = (order.amount * offer.collateral) / offer.amount; // collateral
address buyer = order.buyer;
address seller = order.seller;
// refund
if (offer.exToken == address(0)) {
// refund ETH
if (buyerRefundValue > 0 && buyer != address(0)) {
(bool success,) = buyer.call{value: buyerRefundValue}('');
require(success, 'Transfer Funds to Seller Fail');
}
if (sellerRefundValue > 0 && seller != address(0)) {
(bool success,) = seller.call{value: sellerRefundValue}('');
require(success, 'Transfer Funds to Seller Fail');
}
} else {
IERC20 iexToken = IERC20(offer.exToken);
if (buyerRefundValue > 0 && buyer != address(0)) {
exec
/bin/bash -lc 'wc -l AUDIT_BUNDLE.txt' in /tmp/codex-dgrhxd3v
succeeded in 0ms:
793 AUDIT_BUNDLE.txt
exec
/bin/bash -lc "sed -n '261,520p' AUDIT_BUNDLE.txt" in /tmp/codex-dgrhxd3v
succeeded in 0ms:
iexToken.safeTransfer(buyer, buyerRefundValue);
}
if (sellerRefundValue > 0 && seller != address(0)) {
iexToken.safeTransfer(seller, sellerRefundValue);
}
}
order.status = STATUS_ORDER_CANCELLED;
emit CancelOrder(orderId, msg.sender);
}
// 2 steps settle:
// Tx1: Seller sending token to system vault/buyer
// Tx2: then Operator/Owner verify and settle to pay seller money+collateral
function settle2Steps(uint256 orderId, bytes32 hash) public nonReentrant onlyOwner {
MarketStorage storage $ = _getOwnStorage();
Order storage order = $.orders[orderId];
Offer storage offer = $.offers[order.offerId];
Token storage token = $.tokens[offer.tokenId];
// check condition
require(token.status == STATUS_TOKEN_SETTLE, 'Invalid Status');
require(token.token != address(0) && token.settleRate > 0, 'Token Not Set');
require(block.timestamp > token.settleTime, 'Settling Time Not Started');
require(order.status == STATUS_ORDER_OPEN, 'Invalid Order Status');
uint256 collateral = (order.amount * offer.collateral) / offer.amount;
uint256 value = (order.amount * offer.value) / offer.amount;
// transfer liquid to seller
uint256 settleFee = (value * $.config.feeSettle) / WEI6;
uint256 totalValue = value + collateral - settleFee;
if (offer.exToken == address(0)) {
// by ETH
(bool success1,) = order.seller.call{value: totalValue}('');
(bool success2,) = $.config.feeWallet.call{value: settleFee}('');
require(success1 && success2, 'Transfer Funds Fail');
} else {
// by exToken
IERC20 iexToken = IERC20(offer.exToken);
iexToken.safeTransfer(order.seller, totalValue);
iexToken.safeTransfer($.config.feeWallet, settleFee);
}
order.status = STATUS_ORDER_SETTLE_FILLED;
emit Settle2Steps(orderId, hash, msg.sender);
emit SettleFilled(orderId, totalValue, settleFee, msg.sender);
}
function settle2StepsBatch(uint256[] calldata orderIds, bytes32[] calldata hashes) external {
require(orderIds.length == hashes.length, 'Invalid Input');
for (uint256 i = 0; i < orderIds.length; i++) {
settle2Steps(orderIds[i], hashes[i]);
}
}
/////////////////////////
////// USER ACTION //////
/////////////////////////
// make a offer request using erc20 collateral
/// @notice
/// @param tokenId market identifier
/// @param amount amount of market asset in 6 decimals
/// @param value amount of collateral being deposited, price = collateral/amount
/// @param exToken collateral token contract addres
/// @param minAmount min amount that must be filled by the counterparty in 6 decimals
function newOffer(
uint8 offerType,
bytes32 tokenId,
uint256 amount, //amount of asset
uint256 value, // amount of collateral
address exToken,
uint256 minAmount
) external nonReentrant {
MarketStorage storage $ = _getOwnStorage();
Token storage token = $.tokens[tokenId];
require(token.status == STATUS_TOKEN_ACTIVE, 'Invalid Token');
require(exToken != address(0) && $.acceptedTokens[exToken], 'Invalid Offer Token');
require(amount > 0 && value > 0, 'Invalid Amount or Value');
require(amount >= minAmount, 'minamount to be filled cant be greater then amount');
require(minAmount >= token.minAmount, 'minamount should be greater then eual to market global minamount');
IERC20 iexToken = IERC20(exToken);
// collateral
uint256 collateral = (value * $.config.pledgeRate) / WEI6;
// transfer offer value (offer buy) or collateral (offer sell)
uint256 _transferAmount = offerType == OFFER_BUY ? value : collateral;
iexToken.safeTransferFrom(msg.sender, address(this), _transferAmount);
// create new offer
_newOffer(offerType, tokenId, exToken, amount, value, collateral, minAmount);
}
// New offer in ETH
/// @notice
/// @param tokenId market identifier
/// @param amount amount of market asset in 6 decimals
/// @param value amount of collateral being deposited, price = collateral/amount
/// @param minAmount min amount that must be filled by the counterparty in 6 decimals
function newOfferETH(
uint8 offerType,
bytes32 tokenId,
uint256 amount,
uint256 value,
uint256 minAmount
) external payable nonReentrant {
MarketStorage storage $ = _getOwnStorage();
Token storage token = $.tokens[tokenId];
require(token.status == STATUS_TOKEN_ACTIVE, 'Invalid Token');
require(amount > 0 && value > 0, 'Invalid Amount or Value');
require(amount >= minAmount, 'minamount to be filled cant be greater then amount');
require(minAmount >= token.minAmount, 'minamount should be greater then eual to market global minamount');
// collateral
uint256 collateral = (value * $.config.pledgeRate) / WEI6;
uint256 _ethAmount = offerType == OFFER_BUY ? value : collateral;
require(_ethAmount == msg.value, 'Insufficient Funds');
// create new offer
_newOffer(offerType, tokenId, address(0), amount, value, collateral, minAmount);
}
/// @notice fill an offer with erc20 collateral and generate an orderid associated with a trade
/// @param offerId offer identifier
/// @param amount amount of market asset in 6 decimals
function fillOffer(uint256 offerId, uint256 amount) public {
MarketStorage storage $ = _getOwnStorage();
Offer storage offer = $.offers[offerId];
Token storage token = $.tokens[offer.tokenId];
require(offer.status == STATUS_OFFER_OPEN, 'Invalid Offer Status');
require(token.status == STATUS_TOKEN_ACTIVE, 'Invalid token Status');
require(amount > 0, 'Invalid Amount');
require(offer.amount - offer.filledAmount >= amount, 'Insufficient Allocations');
if (offer.amount - offer.filledAmount > offer.minAmount) {
require(amount >= offer.minAmount, 'cannot fill less then offer minamount');
} else {
require(amount == offer.amount - offer.filledAmount, 'if less then asset minamount must fill complete order');
}
require(offer.exToken != address(0), 'Invalid Offer Token');
// transfer value or collecteral
IERC20 iexToken = IERC20(offer.exToken);
uint256 _transferAmount;
address buyer;
address seller;
if (offer.offerType == OFFER_BUY) {
_transferAmount = (offer.collateral * amount) / offer.amount;
buyer = offer.offeredBy;
seller = msg.sender;
} else {
_transferAmount = (offer.value * amount) / offer.amount;
buyer = msg.sender;
seller = offer.offeredBy;
}
iexToken.safeTransferFrom(msg.sender, address(this), _transferAmount);
_fillOffer(offerId, amount, buyer, seller);
}
/// @notice fill an offer with eth collateral and generate an orderid associated with a trade
/// @param offerId offer identifier
/// @param amount amount of market asset in 6 decimals
function fillOfferETH(uint256 offerId, uint256 amount) external payable nonReentrant {
uint256 _ethAmount = _fillOfferETH(offerId, amount);
require(msg.value >= _ethAmount, 'Insufficient Funds');
require(_ethAmount + 5 * 1e16 > msg.value, 'Extra Amount Sent');
}
/// @notice batche fill both types of offers
function batchFillOffer(uint256[] calldata offerId, uint256[] calldata amount) external payable nonReentrant {
require(offerId.length == amount.length, 'Invalid Input');
uint256 _ethAmount;
MarketStorage storage $ = _getOwnStorage();
for (uint256 i = 0; i < offerId.length; i++) {
Offer storage offer = $.offers[offerId[i]];
if (offer.exToken == address(0)) {
_ethAmount += _fillOfferETH(offerId[i], amount[i]);
} else {
fillOffer(offerId[i], amount[i]);
}
}
require(msg.value >= _ethAmount, 'Insufficient Funds');
require(_ethAmount + 5 * 1e16 > msg.value, 'Extra Amount Sent');
}
function _fillOfferETH(uint256 offerId, uint256 amount) internal returns (uint256 _ethAmount) {
MarketStorage storage $ = _getOwnStorage();
Offer storage offer = $.offers[offerId];
Token storage token = $.tokens[offer.tokenId];
require(offer.status == STATUS_OFFER_OPEN, 'Invalid Offer Status');
require(token.status == STATUS_TOKEN_ACTIVE, 'Invalid token Status');
require(amount > 0, 'Invalid Amount');
require(offer.amount - offer.filledAmount >= amount, 'Insufficient Allocations');
if (offer.amount - offer.filledAmount > offer.minAmount) {
require(amount >= offer.minAmount, 'cannot fill less then offer minamount or asset minamount');
} else {
require(amount == offer.amount - offer.filledAmount, 'if less then asset minamount must fill complete order');
}
require(offer.exToken == address(0), 'Invalid Offer Token');
// transfer value or collecteral
address buyer;
address seller;
if (offer.offerType == OFFER_BUY) {
_ethAmount = (offer.collateral * amount) / offer.amount;
buyer = offer.offeredBy;
seller = msg.sender;
} else {
_ethAmount = (offer.value * amount) / offer.amount;
buyer = msg.sender;
seller = offer.offeredBy;
}
// new order
_fillOffer(offerId, amount, buyer, seller);
}
/// @notice close unfullfilled offer - by Offer owner
/// @param offerId offer identifier
function cancelOffer(uint256 offerId) public nonReentrant {
MarketStorage storage $ = _getOwnStorage();
Offer storage offer = $.offers[offerId];
require(offer.offeredBy == msg.sender, 'Offer Owner Only');
require(offer.status == STATUS_OFFER_OPEN, 'Invalid Offer Status');
uint256 refundAmount = offer.amount - offer.filledAmount;
require(refundAmount > 0, 'Insufficient Allocations');
// calculate refund
uint256 refundValue;
if (offer.offerType == OFFER_BUY) {
refundValue = (refundAmount * offer.value) / offer.amount;
} else {
refundValue = (refundAmount * offer.collateral) / offer.amount;
}
uint256 refundFee = (refundValue * $.config.feeRefund) / WEI6;
refundValue -= refundFee;
// refund
if (offer.exToken == address(0)) {
// refund ETH
(bool success1,) = offer.offeredBy.call{value: refundValue}('');
(bool success2,) = $.config.feeWallet.call{value: refundFee}('');
require(success1 && success2, 'Transfer Funds Fail');
} else {
IERC20 iexToken = IERC20(offer.exToken);
iexToken.safeTransfer(offer.offeredBy, refundValue);
iexToken.safeTransfer($.config.feeWallet, refundFee);
}
offer.status = STATUS_OFFER_CANCELLED;
emit CancelOffer(offerId, refundValue, refundFee, msg.sender);
}
//
/// @notice settle order - deliver token to finillize the order
/// @param orderId order identifier
function settleFilled(uint256 orderId) public nonReentrant {
MarketStorage storage $ = _getOwnStorage();
exec
/bin/bash -lc "sed -n '521,793p' AUDIT_BUNDLE.txt" in /tmp/codex-dgrhxd3v
succeeded in 0ms:
Order storage order = $.orders[orderId];
Offer storage offer = $.offers[order.offerId];
Token storage token = $.tokens[offer.tokenId];
// check condition
require(token.status == STATUS_TOKEN_SETTLE, 'Invalid Status');
require(token.token != address(0) && token.settleRate > 0, 'Token Not Set');
require(block.timestamp > token.settleTime, 'Settling Time Not Started');
require(order.seller == msg.sender, 'Seller Only');
require(order.status == STATUS_ORDER_OPEN, 'Invalid Order Status');
uint256 collateral = (order.amount * offer.collateral) / offer.amount;
uint256 value = (order.amount * offer.value) / offer.amount;
// transfer token to buyer
IERC20 iToken = IERC20(token.token);
// calculate token amount base on it's decimals
uint256 tokenAmount = (order.amount * token.settleRate) / WEI6;
uint256 tokenAmountFee = (tokenAmount * $.config.feeSettle) / WEI6;
// transfer order fee in token to fee wallet
iToken.safeTransferFrom(order.seller, $.config.feeWallet, tokenAmountFee);
// transfer token after fee to buyer
iToken.safeTransferFrom(order.seller, order.buyer, tokenAmount - tokenAmountFee);
// transfer liquid to seller
uint256 settleFee = (value * $.config.feeSettle) / WEI6;
uint256 totalValue = value + collateral - settleFee;
if (offer.exToken == address(0)) {
// by ETH
(bool success1,) = order.seller.call{value: totalValue}('');
(bool success2,) = $.config.feeWallet.call{value: settleFee}('');
require(success1 && success2, 'Transfer Funds Fail');
} else {
// by exToken
IERC20 iexToken = IERC20(offer.exToken);
iexToken.safeTransfer(order.seller, totalValue);
iexToken.safeTransfer($.config.feeWallet, settleFee);
}
order.status = STATUS_ORDER_SETTLE_FILLED;
emit SettleFilled(orderId, totalValue, settleFee, msg.sender);
}
/// @notice cancel unfilled order by token buyer after settleDuration time frame
/// @notice // token seller lose collateral to token buyer
/// @param orderId order identifier
function settleCancelled(uint256 orderId) public nonReentrant {
MarketStorage storage $ = _getOwnStorage();
Order storage order = $.orders[orderId];
Offer storage offer = $.offers[order.offerId];
Token storage token = $.tokens[offer.tokenId];
// check condition
require(token.status == STATUS_TOKEN_SETTLE, 'Invalid Status');
require(block.timestamp > token.settleTime + token.settleDuration, 'Settling Time Not Ended Yet');
require(order.status == STATUS_ORDER_OPEN, 'Invalid Order Status');
require(order.buyer == msg.sender || owner() == msg.sender, 'Buyer or Operator/Owner Only');
uint256 collateral = (order.amount * offer.collateral) / offer.amount;
uint256 value = (order.amount * offer.value) / offer.amount;
// transfer liquid to buyer
uint256 settleFee = (collateral * $.config.feeSettle * 2) / WEI6;
uint256 totalValue = value + collateral - settleFee;
if (offer.exToken == address(0)) {
// by ETH
(bool success1,) = order.buyer.call{value: totalValue}('');
(bool success2,) = $.config.feeWallet.call{value: settleFee}('');
require(success1 && success2, 'Transfer Funds Fail');
} else {
// by exToken
IERC20 iexToken = IERC20(offer.exToken);
iexToken.safeTransfer(order.buyer, totalValue);
iexToken.safeTransfer($.config.feeWallet, settleFee);
}
order.status = STATUS_ORDER_SETTLE_CANCELLED;
emit SettleCancelled(orderId, totalValue, settleFee, msg.sender);
}
// Batch actions
function batchForceCancelOrders(uint256[] calldata orderIds) external {
for (uint256 i = 0; i < orderIds.length; i++) {
forceCancelOrder(orderIds[i]);
}
}
function batchCancelOffers(uint256[] calldata offerIds) external {
for (uint256 i = 0; i < offerIds.length; i++) {
cancelOffer(offerIds[i]);
}
}
function batchSettleFilleds(uint256[] calldata orderIds) external {
for (uint256 i = 0; i < orderIds.length; i++) {
settleFilled(orderIds[i]);
}
}
function batchSettleCancelleds(uint256[] calldata orderIds) external {
for (uint256 i = 0; i < orderIds.length; i++) {
settleCancelled(orderIds[i]);
}
}
///////////////////////////
///////// SETTER //////////
///////////////////////////
function updateConfig(
address feeWallet_,
uint256 feeSettle_,
uint256 feeRefund_,
uint256 pledgeRate_
) external onlyOwner {
MarketStorage storage $ = _getOwnStorage();
require(feeWallet_ != address(0), 'Invalid Address');
require(feeSettle_ <= WEI6 / 100, 'Settle Fee <= 10%');
require(feeRefund_ <= WEI6 / 100, 'Cancel Fee <= 10%');
emit UpdateConfig(
$.config.feeWallet,
$.config.feeSettle,
$.config.feeRefund,
$.config.pledgeRate,
feeWallet_,
feeSettle_,
feeRefund_,
pledgeRate_
);
// update
$.config.feeWallet = feeWallet_;
$.config.feeSettle = feeSettle_;
$.config.feeRefund = feeRefund_;
$.config.pledgeRate = pledgeRate_;
}
//
/// @notice adding/removing asset to be allowed to use as collateral
function setAcceptedTokens(address[] calldata tokenAddresses, bool isAccepted) external onlyOwner {
MarketStorage storage $ = _getOwnStorage();
for (uint256 i = 0; i < tokenAddresses.length; i++) {
$.acceptedTokens[tokenAddresses[i]] = isAccepted;
}
emit UpdateAcceptedTokens(tokenAddresses, isAccepted);
}
///////////////////////////
///////// GETTER //////////
///////////////////////////
function offerAmount(uint256 offerId) external view returns (uint256) {
MarketStorage storage $ = _getOwnStorage();
return $.offers[offerId].amount;
}
function offerAmountAvailable(uint256 offerId) external view returns (uint256) {
MarketStorage storage $ = _getOwnStorage();
return $.offers[offerId].amount - $.offers[offerId].filledAmount;
}
function offerValue(uint256 offerId) external view returns (uint256) {
MarketStorage storage $ = _getOwnStorage();
return $.offers[offerId].value;
}
function offerExToken(uint256 offerId) external view returns (address) {
MarketStorage storage $ = _getOwnStorage();
return $.offers[offerId].exToken;
}
function isBuyOffer(uint256 offerId) external view returns (bool) {
MarketStorage storage $ = _getOwnStorage();
return $.offers[offerId].offerType == OFFER_BUY;
}
function isSellOffer(uint256 offerId) external view returns (bool) {
MarketStorage storage $ = _getOwnStorage();
return $.offers[offerId].offerType == OFFER_SELL;
}
function offerStatus(uint256 offerId) external view returns (uint256) {
MarketStorage storage $ = _getOwnStorage();
return $.offers[offerId].status;
}
function orderStatus(uint256 orderId) external view returns (uint256) {
MarketStorage storage $ = _getOwnStorage();
return $.orders[orderId].status;
}
function tokens(bytes32 tokenId) external view returns (Token memory) {
MarketStorage storage $ = _getOwnStorage();
return $.tokens[tokenId];
}
function offers(uint256 id) external view returns (Offer memory) {
MarketStorage storage $ = _getOwnStorage();
return $.offers[id];
}
function orders(uint256 id) external view returns (Order memory) {
MarketStorage storage $ = _getOwnStorage();
return $.orders[id];
}
function config() external view returns (Config memory) {
MarketStorage storage $ = _getOwnStorage();
return $.config;
}
function isAcceptedToken(address token) external view returns (bool) {
MarketStorage storage $ = _getOwnStorage();
return $.acceptedTokens[token];
}
function lastOfferId() external view returns (uint256) {
MarketStorage storage $ = _getOwnStorage();
return $.lastOfferId;
}
function lastOrderId() external view returns (uint256) {
MarketStorage storage $ = _getOwnStorage();
return $.lastOrderId;
}
///////////////////////////
//////// INTERNAL /////////
///////////////////////////
function _newOffer(
uint8 offerType,
bytes32 tokenId,
address exToken,
uint256 amount,
uint256 value,
uint256 collateral,
uint256 minAmount
) internal {
MarketStorage storage $ = _getOwnStorage();
// create new offer
$.offers[++$.lastOfferId] =
Offer(offerType, tokenId, exToken, amount, value, collateral, 0, STATUS_OFFER_OPEN, msg.sender, minAmount);
emit NewOffer($.lastOfferId, offerType, tokenId, exToken, amount, value, collateral, minAmount, msg.sender);
}
function _fillOffer(uint256 offerId, uint256 amount, address buyer, address seller) internal {
MarketStorage storage $ = _getOwnStorage();
Offer storage offer = $.offers[offerId];
// new order
$.orders[++$.lastOrderId] = Order(offerId, amount, seller, buyer, STATUS_ORDER_OPEN);
// check if offer is fullfilled
offer.filledAmount += amount;
if (offer.filledAmount == offer.amount) {
offer.status = STATUS_OFFER_FILLED;
emit CloseOffer(offerId, 0);
}
emit NewOrder($.lastOrderId, offerId, amount, seller, buyer);
}
// get stuck token in contract
function withdrawStuckToken(address _token, address _to) external onlyOwner {
MarketStorage storage $ = _getOwnStorage();
require(_token != address(0) && !$.acceptedTokens[_token], 'Invalid Token Address');
uint256 _contractBalance = IERC20(_token).balanceOf(address(this));
IERC20(_token).safeTransfer(_to, _contractBalance);
}
}
exec
/bin/bash -lc "nl -ba src/FrontrunMarket.sol | sed -n '1,260p'" in /tmp/codex-dgrhxd3v
succeeded in 0ms:
1 //SPDX-License-Identifier: Unlicense
2 pragma solidity 0.8.20;
3
4 import '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol';
5 import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
6 import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
7 import '@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol';
8 import '@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol';
9 import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
10 import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
11 import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';
12
13 struct Token {
14 address token;
15 uint48 settleTime;
16 uint48 settleDuration;
17 uint152 settleRate; // number of token per point
18 uint8 status; //
19 uint256 minAmount;
20 }
21
22 struct Offer {
23 uint8 offerType;
24 bytes32 tokenId;
25 address exToken;
26 uint256 amount;
27 uint256 value;
28 uint256 collateral;
29 uint256 filledAmount;
30 uint8 status;
31 address offeredBy;
32 uint256 minAmount;
33 }
34
35 struct Order {
36 uint256 offerId;
37 uint256 amount;
38 address seller;
39 address buyer;
40 uint8 status;
41 }
42
43 struct Config {
44 uint256 pledgeRate;
45 uint256 feeRefund;
46 uint256 feeSettle;
47 address feeWallet;
48 }
49
50 contract FrontrunMarket is Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable {
51 using SafeERC20 for IERC20;
52
53 uint256 constant WEI6 = 10 ** 6;
54 uint8 constant OFFER_BUY = 1;
55 uint8 constant OFFER_SELL = 2;
56
57 // Status
58 // Offer status
59 uint8 constant STATUS_OFFER_OPEN = 1;
60 uint8 constant STATUS_OFFER_FILLED = 2;
61 uint8 constant STATUS_OFFER_CANCELLED = 3;
62
63 // Order Status
64 uint8 constant STATUS_ORDER_OPEN = 1;
65 uint8 constant STATUS_ORDER_SETTLE_FILLED = 2;
66 uint8 constant STATUS_ORDER_SETTLE_CANCELLED = 3;
67 uint8 constant STATUS_ORDER_CANCELLED = 4;
68
69 // token status
70 uint8 constant STATUS_TOKEN_ACTIVE = 1;
71 uint8 constant STATUS_TOKEN_INACTIVE = 2;
72 uint8 constant STATUS_TOKEN_SETTLE = 3;
73
74 struct MarketStorage {
75 mapping(address => bool) acceptedTokens;
76 mapping(bytes32 => Token) tokens;
77 mapping(uint256 => Offer) offers;
78 uint256 lastOfferId;
79 mapping(uint256 => Order) orders;
80 uint256 lastOrderId;
81 Config config;
82 }
83
84 //keccak256(abi.encode(uint256(keccak256("frontrun.Market")) - 1)) & ~bytes32(uint256(0xff));
85 bytes32 constant MarketStorageLocation = 0x02c326031f2f3dedd13e7fde59c7c20af05a8826f4c07e8d42538c5f2be06800;
86
87 function _getOwnStorage() private pure returns (MarketStorage storage $) {
88 assembly {
89 $.slot := MarketStorageLocation
90 }
91 }
92
93 // event
94
95 event NewOffer(
96 uint256 id,
97 uint8 offerType,
98 bytes32 tokenId,
99 address exToken,
100 uint256 amount,
101 uint256 value,
102 uint256 collateral,
103 uint256 minAmount,
104 address doer
105 );
106 event NewToken(bytes32 tokenId, uint256 settleDuration, uint256 minAmount);
107 event NewOrder(uint256 id, uint256 offerId, uint256 amount, address seller, address buyer);
108
109 event SettleFilled(uint256 orderId, uint256 value, uint256 fee, address doer);
110 event SettleCancelled(uint256 orderId, uint256 value, uint256 fee, address doer);
111
112 event CancelOrder(uint256 orderId, address doer);
113 event CancelOffer(uint256 offerId, uint256 refundValue, uint256 refundFee, address doer);
114
115 event UpdateAcceptedTokens(address[] tokens, bool isAccepted);
116
117 event CloseOffer(uint256 offerId, uint256 refundAmount);
118
119 event UpdateConfig(
120 address oldFeeWallet,
121 uint256 oldFeeSettle,
122 uint256 oldFeeRefund,
123 uint256 oldPledgeRate,
124 address newFeeWallet,
125 uint256 newFeeSettle,
126 uint256 newFeeRefund,
127 uint256 newPledgeRate
128 );
129
130 event TokenToSettlePhase(bytes32 tokenId, address token, uint256 settleRate, uint256 settleTime);
131 event UpdateTokenStatus(bytes32 tokenId, uint8 oldValue, uint8 newValue);
132 event TokenForceCancelSettlePhase(bytes32 tokenId);
133
134 event Settle2Steps(uint256 orderId, bytes32 hash, address doer);
135
136 event UpdateTokenSettleDuration(bytes32 tokenId, uint48 oldValue, uint48 newValue);
137
138 function initialize() external initializer {
139 __Ownable_init(msg.sender);
140 // init value
141 MarketStorage storage $ = _getOwnStorage();
142 $.config.pledgeRate = WEI6; //
143 $.config.feeWallet = owner();
144 $.config.feeSettle = WEI6 / 200; // 0.5%
145 $.config.feeRefund = 0; // 0.0%
146 }
147
148 ///////////////////////////
149 ////// SYSTEM ACTION //////
150 ///////////////////////////
151
152 /// @notice Creates a market to trade
153 /// @param tokenId market identifier
154 /// @param settleDuration Duration after which buyer of token can claim collateral if seller hasnt delivered
155 /// @param minAmount minAmount that can be filled for this particular market including offers and orders , 6 decimals
156 // add min order value in market so that in settle2steps we dont have to pay gas to settle low value orders
157 function createToken(bytes32 tokenId, uint48 settleDuration, uint256 minAmount) external onlyOwner {
158 MarketStorage storage $ = _getOwnStorage();
159 Token storage _token = $.tokens[tokenId];
160 _token.settleDuration = settleDuration;
161 _token.minAmount = minAmount;
162 _token.status = STATUS_TOKEN_ACTIVE;
163 emit NewToken(tokenId, settleDuration, minAmount);
164 }
165
166 /// @notice moves a market to settlement phase
167 /// @param tokenId market identifier
168 /// @param tokenAddress contract address for token to be delivered
169 /// @param settleRate how many tokens per 1M amount traded(if token to be delivered has 18 decimals write 1e12)
170 function tokenToSettlePhase(
171 bytes32 tokenId,
172 address tokenAddress,
173 uint152 settleRate // how many token for 1M points
174 ) external onlyOwner {
175 MarketStorage storage $ = _getOwnStorage();
176 Token storage _token = $.tokens[tokenId];
177 require(tokenAddress != address(0), 'Invalid Token Address');
178 require(settleRate > 0, 'Invalid Settle Rate');
179 require(_token.status == STATUS_TOKEN_ACTIVE || _token.status == STATUS_TOKEN_INACTIVE, 'Invalid Token Status');
180 _token.token = tokenAddress;
181 _token.settleRate = settleRate;
182 // update token settle status & time
183 _token.status = STATUS_TOKEN_SETTLE;
184 _token.settleTime = uint48(block.timestamp);
185
186 emit TokenToSettlePhase(tokenId, tokenAddress, settleRate, block.timestamp);
187 }
188
189 /// @notice toggle market active inactive
190 /// @param tokenId market identifier
191 function tokenToggleActivation(bytes32 tokenId) external onlyOwner {
192 MarketStorage storage $ = _getOwnStorage();
193 Token storage _token = $.tokens[tokenId];
194 uint8 fromStatus = _token.status;
195 uint8 toStatus = fromStatus == STATUS_TOKEN_ACTIVE ? STATUS_TOKEN_INACTIVE : STATUS_TOKEN_ACTIVE;
196
197 require(fromStatus == STATUS_TOKEN_ACTIVE || fromStatus == STATUS_TOKEN_INACTIVE, 'Cannot Change Token Status');
198
199 _token.status = toStatus;
200 emit UpdateTokenStatus(tokenId, fromStatus, toStatus);
201 }
202
203 /// @notice // in case wrong setting for settle
204 /// @param tokenId market identifier
205 function tokenForceCancelSettlePhase(bytes32 tokenId) external onlyOwner {
206 MarketStorage storage $ = _getOwnStorage();
207 Token storage _token = $.tokens[tokenId];
208 require(_token.status == STATUS_TOKEN_SETTLE, 'Invalid Token Status');
209 _token.status = STATUS_TOKEN_INACTIVE;
210 emit TokenForceCancelSettlePhase(tokenId);
211 }
212 /// @notice // updates settleDuration
213 /// @param tokenId market identifier
214 /// @param newValue settleDuration value
215
216 function updateSettleDuration(bytes32 tokenId, uint48 newValue) external onlyOwner {
217 MarketStorage storage $ = _getOwnStorage();
218 Token storage _token = $.tokens[tokenId];
219 uint48 oldValue = _token.settleDuration;
220 _token.settleDuration = newValue;
221 emit UpdateTokenSettleDuration(tokenId, oldValue, newValue);
222 }
223
224 // force cancel order - by Operator/Owner
225 // refund for both seller & buyer without any fees cut
226 /// @notice
227 /// @param orderId order identifier
228 function forceCancelOrder(uint256 orderId) public nonReentrant onlyOwner {
229 MarketStorage storage $ = _getOwnStorage();
230 Order storage order = $.orders[orderId];
231 Offer storage offer = $.offers[order.offerId];
232
233 require(order.status == STATUS_ORDER_OPEN, 'Invalid Order Status');
234
235 // calculate refund
236 uint256 buyerRefundValue = (order.amount * offer.value) / offer.amount; // value
237 uint256 sellerRefundValue = (order.amount * offer.collateral) / offer.amount; // collateral
238 address buyer = order.buyer;
239 address seller = order.seller;
240
241 // refund
242 if (offer.exToken == address(0)) {
243 // refund ETH
244 if (buyerRefundValue > 0 && buyer != address(0)) {
245 (bool success,) = buyer.call{value: buyerRefundValue}('');
246 require(success, 'Transfer Funds to Seller Fail');
247 }
248 if (sellerRefundValue > 0 && seller != address(0)) {
249 (bool success,) = seller.call{value: sellerRefundValue}('');
250 require(success, 'Transfer Funds to Seller Fail');
251 }
252 } else {
253 IERC20 iexToken = IERC20(offer.exToken);
254 if (buyerRefundValue > 0 && buyer != address(0)) {
255 iexToken.safeTransfer(buyer, buyerRefundValue);
256 }
257 if (sellerRefundValue > 0 && seller != address(0)) {
258 iexToken.safeTransfer(seller, sellerRefundValue);
259 }
260 }
exec
/bin/bash -lc "nl -ba src/FrontrunMarket.sol | sed -n '261,520p'" in /tmp/codex-dgrhxd3v
succeeded in 0ms:
261 order.status = STATUS_ORDER_CANCELLED;
262 emit CancelOrder(orderId, msg.sender);
263 }
264
265 // 2 steps settle:
266 // Tx1: Seller sending token to system vault/buyer
267 // Tx2: then Operator/Owner verify and settle to pay seller money+collateral
268 function settle2Steps(uint256 orderId, bytes32 hash) public nonReentrant onlyOwner {
269 MarketStorage storage $ = _getOwnStorage();
270 Order storage order = $.orders[orderId];
271 Offer storage offer = $.offers[order.offerId];
272 Token storage token = $.tokens[offer.tokenId];
273
274 // check condition
275 require(token.status == STATUS_TOKEN_SETTLE, 'Invalid Status');
276 require(token.token != address(0) && token.settleRate > 0, 'Token Not Set');
277 require(block.timestamp > token.settleTime, 'Settling Time Not Started');
278 require(order.status == STATUS_ORDER_OPEN, 'Invalid Order Status');
279
280 uint256 collateral = (order.amount * offer.collateral) / offer.amount;
281 uint256 value = (order.amount * offer.value) / offer.amount;
282
283 // transfer liquid to seller
284 uint256 settleFee = (value * $.config.feeSettle) / WEI6;
285 uint256 totalValue = value + collateral - settleFee;
286 if (offer.exToken == address(0)) {
287 // by ETH
288 (bool success1,) = order.seller.call{value: totalValue}('');
289 (bool success2,) = $.config.feeWallet.call{value: settleFee}('');
290 require(success1 && success2, 'Transfer Funds Fail');
291 } else {
292 // by exToken
293 IERC20 iexToken = IERC20(offer.exToken);
294 iexToken.safeTransfer(order.seller, totalValue);
295 iexToken.safeTransfer($.config.feeWallet, settleFee);
296 }
297
298 order.status = STATUS_ORDER_SETTLE_FILLED;
299
300 emit Settle2Steps(orderId, hash, msg.sender);
301 emit SettleFilled(orderId, totalValue, settleFee, msg.sender);
302 }
303
304 function settle2StepsBatch(uint256[] calldata orderIds, bytes32[] calldata hashes) external {
305 require(orderIds.length == hashes.length, 'Invalid Input');
306 for (uint256 i = 0; i < orderIds.length; i++) {
307 settle2Steps(orderIds[i], hashes[i]);
308 }
309 }
310
311 /////////////////////////
312 ////// USER ACTION //////
313 /////////////////////////
314
315 // make a offer request using erc20 collateral
316 /// @notice
317 /// @param tokenId market identifier
318 /// @param amount amount of market asset in 6 decimals
319 /// @param value amount of collateral being deposited, price = collateral/amount
320 /// @param exToken collateral token contract addres
321 /// @param minAmount min amount that must be filled by the counterparty in 6 decimals
322 function newOffer(
323 uint8 offerType,
324 bytes32 tokenId,
325 uint256 amount, //amount of asset
326 uint256 value, // amount of collateral
327 address exToken,
328 uint256 minAmount
329 ) external nonReentrant {
330 MarketStorage storage $ = _getOwnStorage();
331 Token storage token = $.tokens[tokenId];
332 require(token.status == STATUS_TOKEN_ACTIVE, 'Invalid Token');
333 require(exToken != address(0) && $.acceptedTokens[exToken], 'Invalid Offer Token');
334 require(amount > 0 && value > 0, 'Invalid Amount or Value');
335 require(amount >= minAmount, 'minamount to be filled cant be greater then amount');
336 require(minAmount >= token.minAmount, 'minamount should be greater then eual to market global minamount');
337 IERC20 iexToken = IERC20(exToken);
338 // collateral
339 uint256 collateral = (value * $.config.pledgeRate) / WEI6;
340
341 // transfer offer value (offer buy) or collateral (offer sell)
342 uint256 _transferAmount = offerType == OFFER_BUY ? value : collateral;
343 iexToken.safeTransferFrom(msg.sender, address(this), _transferAmount);
344
345 // create new offer
346 _newOffer(offerType, tokenId, exToken, amount, value, collateral, minAmount);
347 }
348
349 // New offer in ETH
350 /// @notice
351 /// @param tokenId market identifier
352 /// @param amount amount of market asset in 6 decimals
353 /// @param value amount of collateral being deposited, price = collateral/amount
354 /// @param minAmount min amount that must be filled by the counterparty in 6 decimals
355 function newOfferETH(
356 uint8 offerType,
357 bytes32 tokenId,
358 uint256 amount,
359 uint256 value,
360 uint256 minAmount
361 ) external payable nonReentrant {
362 MarketStorage storage $ = _getOwnStorage();
363 Token storage token = $.tokens[tokenId];
364 require(token.status == STATUS_TOKEN_ACTIVE, 'Invalid Token');
365 require(amount > 0 && value > 0, 'Invalid Amount or Value');
366 require(amount >= minAmount, 'minamount to be filled cant be greater then amount');
367 require(minAmount >= token.minAmount, 'minamount should be greater then eual to market global minamount');
368
369 // collateral
370 uint256 collateral = (value * $.config.pledgeRate) / WEI6;
371
372 uint256 _ethAmount = offerType == OFFER_BUY ? value : collateral;
373 require(_ethAmount == msg.value, 'Insufficient Funds');
374 // create new offer
375 _newOffer(offerType, tokenId, address(0), amount, value, collateral, minAmount);
376 }
377
378 /// @notice fill an offer with erc20 collateral and generate an orderid associated with a trade
379 /// @param offerId offer identifier
380 /// @param amount amount of market asset in 6 decimals
381 function fillOffer(uint256 offerId, uint256 amount) public {
382 MarketStorage storage $ = _getOwnStorage();
383 Offer storage offer = $.offers[offerId];
384 Token storage token = $.tokens[offer.tokenId];
385 require(offer.status == STATUS_OFFER_OPEN, 'Invalid Offer Status');
386 require(token.status == STATUS_TOKEN_ACTIVE, 'Invalid token Status');
387 require(amount > 0, 'Invalid Amount');
388 require(offer.amount - offer.filledAmount >= amount, 'Insufficient Allocations');
389 if (offer.amount - offer.filledAmount > offer.minAmount) {
390 require(amount >= offer.minAmount, 'cannot fill less then offer minamount');
391 } else {
392 require(amount == offer.amount - offer.filledAmount, 'if less then asset minamount must fill complete order');
393 }
394 require(offer.exToken != address(0), 'Invalid Offer Token');
395
396 // transfer value or collecteral
397 IERC20 iexToken = IERC20(offer.exToken);
398 uint256 _transferAmount;
399 address buyer;
400 address seller;
401 if (offer.offerType == OFFER_BUY) {
402 _transferAmount = (offer.collateral * amount) / offer.amount;
403 buyer = offer.offeredBy;
404 seller = msg.sender;
405 } else {
406 _transferAmount = (offer.value * amount) / offer.amount;
407 buyer = msg.sender;
408 seller = offer.offeredBy;
409 }
410 iexToken.safeTransferFrom(msg.sender, address(this), _transferAmount);
411
412 _fillOffer(offerId, amount, buyer, seller);
413 }
414
415 /// @notice fill an offer with eth collateral and generate an orderid associated with a trade
416 /// @param offerId offer identifier
417 /// @param amount amount of market asset in 6 decimals
418 function fillOfferETH(uint256 offerId, uint256 amount) external payable nonReentrant {
419 uint256 _ethAmount = _fillOfferETH(offerId, amount);
420 require(msg.value >= _ethAmount, 'Insufficient Funds');
421 require(_ethAmount + 5 * 1e16 > msg.value, 'Extra Amount Sent');
422 }
423
424 /// @notice batche fill both types of offers
425 function batchFillOffer(uint256[] calldata offerId, uint256[] calldata amount) external payable nonReentrant {
426 require(offerId.length == amount.length, 'Invalid Input');
427 uint256 _ethAmount;
428 MarketStorage storage $ = _getOwnStorage();
429 for (uint256 i = 0; i < offerId.length; i++) {
430 Offer storage offer = $.offers[offerId[i]];
431 if (offer.exToken == address(0)) {
432 _ethAmount += _fillOfferETH(offerId[i], amount[i]);
433 } else {
434 fillOffer(offerId[i], amount[i]);
435 }
436 }
437 require(msg.value >= _ethAmount, 'Insufficient Funds');
438 require(_ethAmount + 5 * 1e16 > msg.value, 'Extra Amount Sent');
439 }
440
441 function _fillOfferETH(uint256 offerId, uint256 amount) internal returns (uint256 _ethAmount) {
442 MarketStorage storage $ = _getOwnStorage();
443 Offer storage offer = $.offers[offerId];
444 Token storage token = $.tokens[offer.tokenId];
445 require(offer.status == STATUS_OFFER_OPEN, 'Invalid Offer Status');
446 require(token.status == STATUS_TOKEN_ACTIVE, 'Invalid token Status');
447 require(amount > 0, 'Invalid Amount');
448 require(offer.amount - offer.filledAmount >= amount, 'Insufficient Allocations');
449 if (offer.amount - offer.filledAmount > offer.minAmount) {
450 require(amount >= offer.minAmount, 'cannot fill less then offer minamount or asset minamount');
451 } else {
452 require(amount == offer.amount - offer.filledAmount, 'if less then asset minamount must fill complete order');
453 }
454 require(offer.exToken == address(0), 'Invalid Offer Token');
455
456 // transfer value or collecteral
457 address buyer;
458 address seller;
459 if (offer.offerType == OFFER_BUY) {
460 _ethAmount = (offer.collateral * amount) / offer.amount;
461 buyer = offer.offeredBy;
462 seller = msg.sender;
463 } else {
464 _ethAmount = (offer.value * amount) / offer.amount;
465 buyer = msg.sender;
466 seller = offer.offeredBy;
467 }
468 // new order
469 _fillOffer(offerId, amount, buyer, seller);
470 }
471
472 /// @notice close unfullfilled offer - by Offer owner
473 /// @param offerId offer identifier
474 function cancelOffer(uint256 offerId) public nonReentrant {
475 MarketStorage storage $ = _getOwnStorage();
476 Offer storage offer = $.offers[offerId];
477
478 require(offer.offeredBy == msg.sender, 'Offer Owner Only');
479 require(offer.status == STATUS_OFFER_OPEN, 'Invalid Offer Status');
480
481 uint256 refundAmount = offer.amount - offer.filledAmount;
482 require(refundAmount > 0, 'Insufficient Allocations');
483
484 // calculate refund
485 uint256 refundValue;
486 if (offer.offerType == OFFER_BUY) {
487 refundValue = (refundAmount * offer.value) / offer.amount;
488 } else {
489 refundValue = (refundAmount * offer.collateral) / offer.amount;
490 }
491 uint256 refundFee = (refundValue * $.config.feeRefund) / WEI6;
492 refundValue -= refundFee;
493
494 // refund
495 if (offer.exToken == address(0)) {
496 // refund ETH
497 (bool success1,) = offer.offeredBy.call{value: refundValue}('');
498 (bool success2,) = $.config.feeWallet.call{value: refundFee}('');
499 require(success1 && success2, 'Transfer Funds Fail');
500 } else {
501 IERC20 iexToken = IERC20(offer.exToken);
502 iexToken.safeTransfer(offer.offeredBy, refundValue);
503 iexToken.safeTransfer($.config.feeWallet, refundFee);
504 }
505
506 offer.status = STATUS_OFFER_CANCELLED;
507 emit CancelOffer(offerId, refundValue, refundFee, msg.sender);
508 }
509
510 //
511 /// @notice settle order - deliver token to finillize the order
512 /// @param orderId order identifier
513 function settleFilled(uint256 orderId) public nonReentrant {
514 MarketStorage storage $ = _getOwnStorage();
515 Order storage order = $.orders[orderId];
516 Offer storage offer = $.offers[order.offerId];
517 Token storage token = $.tokens[offer.tokenId];
518
519 // check condition
520 require(token.status == STATUS_TOKEN_SETTLE, 'Invalid Status');
exec
/bin/bash -lc "nl -ba src/FrontrunMarket.sol | sed -n '521,760p'" in /tmp/codex-dgrhxd3v
succeeded in 0ms:
521 require(token.token != address(0) && token.settleRate > 0, 'Token Not Set');
522 require(block.timestamp > token.settleTime, 'Settling Time Not Started');
523 require(order.seller == msg.sender, 'Seller Only');
524 require(order.status == STATUS_ORDER_OPEN, 'Invalid Order Status');
525
526 uint256 collateral = (order.amount * offer.collateral) / offer.amount;
527 uint256 value = (order.amount * offer.value) / offer.amount;
528
529 // transfer token to buyer
530 IERC20 iToken = IERC20(token.token);
531 // calculate token amount base on it's decimals
532 uint256 tokenAmount = (order.amount * token.settleRate) / WEI6;
533 uint256 tokenAmountFee = (tokenAmount * $.config.feeSettle) / WEI6;
534 // transfer order fee in token to fee wallet
535 iToken.safeTransferFrom(order.seller, $.config.feeWallet, tokenAmountFee);
536 // transfer token after fee to buyer
537 iToken.safeTransferFrom(order.seller, order.buyer, tokenAmount - tokenAmountFee);
538
539 // transfer liquid to seller
540 uint256 settleFee = (value * $.config.feeSettle) / WEI6;
541 uint256 totalValue = value + collateral - settleFee;
542 if (offer.exToken == address(0)) {
543 // by ETH
544 (bool success1,) = order.seller.call{value: totalValue}('');
545 (bool success2,) = $.config.feeWallet.call{value: settleFee}('');
546 require(success1 && success2, 'Transfer Funds Fail');
547 } else {
548 // by exToken
549 IERC20 iexToken = IERC20(offer.exToken);
550 iexToken.safeTransfer(order.seller, totalValue);
551 iexToken.safeTransfer($.config.feeWallet, settleFee);
552 }
553
554 order.status = STATUS_ORDER_SETTLE_FILLED;
555
556 emit SettleFilled(orderId, totalValue, settleFee, msg.sender);
557 }
558
559 /// @notice cancel unfilled order by token buyer after settleDuration time frame
560 /// @notice // token seller lose collateral to token buyer
561 /// @param orderId order identifier
562
563 function settleCancelled(uint256 orderId) public nonReentrant {
564 MarketStorage storage $ = _getOwnStorage();
565 Order storage order = $.orders[orderId];
566 Offer storage offer = $.offers[order.offerId];
567 Token storage token = $.tokens[offer.tokenId];
568
569 // check condition
570 require(token.status == STATUS_TOKEN_SETTLE, 'Invalid Status');
571 require(block.timestamp > token.settleTime + token.settleDuration, 'Settling Time Not Ended Yet');
572 require(order.status == STATUS_ORDER_OPEN, 'Invalid Order Status');
573 require(order.buyer == msg.sender || owner() == msg.sender, 'Buyer or Operator/Owner Only');
574
575 uint256 collateral = (order.amount * offer.collateral) / offer.amount;
576 uint256 value = (order.amount * offer.value) / offer.amount;
577
578 // transfer liquid to buyer
579 uint256 settleFee = (collateral * $.config.feeSettle * 2) / WEI6;
580 uint256 totalValue = value + collateral - settleFee;
581 if (offer.exToken == address(0)) {
582 // by ETH
583 (bool success1,) = order.buyer.call{value: totalValue}('');
584 (bool success2,) = $.config.feeWallet.call{value: settleFee}('');
585 require(success1 && success2, 'Transfer Funds Fail');
586 } else {
587 // by exToken
588 IERC20 iexToken = IERC20(offer.exToken);
589 iexToken.safeTransfer(order.buyer, totalValue);
590 iexToken.safeTransfer($.config.feeWallet, settleFee);
591 }
592
593 order.status = STATUS_ORDER_SETTLE_CANCELLED;
594
595 emit SettleCancelled(orderId, totalValue, settleFee, msg.sender);
596 }
597
598 // Batch actions
599 function batchForceCancelOrders(uint256[] calldata orderIds) external {
600 for (uint256 i = 0; i < orderIds.length; i++) {
601 forceCancelOrder(orderIds[i]);
602 }
603 }
604
605 function batchCancelOffers(uint256[] calldata offerIds) external {
606 for (uint256 i = 0; i < offerIds.length; i++) {
607 cancelOffer(offerIds[i]);
608 }
609 }
610
611 function batchSettleFilleds(uint256[] calldata orderIds) external {
612 for (uint256 i = 0; i < orderIds.length; i++) {
613 settleFilled(orderIds[i]);
614 }
615 }
616
617 function batchSettleCancelleds(uint256[] calldata orderIds) external {
618 for (uint256 i = 0; i < orderIds.length; i++) {
619 settleCancelled(orderIds[i]);
620 }
621 }
622
623 ///////////////////////////
624 ///////// SETTER //////////
625 ///////////////////////////
626
627 function updateConfig(
628 address feeWallet_,
629 uint256 feeSettle_,
630 uint256 feeRefund_,
631 uint256 pledgeRate_
632 ) external onlyOwner {
633 MarketStorage storage $ = _getOwnStorage();
634 require(feeWallet_ != address(0), 'Invalid Address');
635 require(feeSettle_ <= WEI6 / 100, 'Settle Fee <= 10%');
636 require(feeRefund_ <= WEI6 / 100, 'Cancel Fee <= 10%');
637 emit UpdateConfig(
638 $.config.feeWallet,
639 $.config.feeSettle,
640 $.config.feeRefund,
641 $.config.pledgeRate,
642 feeWallet_,
643 feeSettle_,
644 feeRefund_,
645 pledgeRate_
646 );
647 // update
648 $.config.feeWallet = feeWallet_;
649 $.config.feeSettle = feeSettle_;
650 $.config.feeRefund = feeRefund_;
651 $.config.pledgeRate = pledgeRate_;
652 }
653
654 //
655 /// @notice adding/removing asset to be allowed to use as collateral
656 function setAcceptedTokens(address[] calldata tokenAddresses, bool isAccepted) external onlyOwner {
657 MarketStorage storage $ = _getOwnStorage();
658
659 for (uint256 i = 0; i < tokenAddresses.length; i++) {
660 $.acceptedTokens[tokenAddresses[i]] = isAccepted;
661 }
662 emit UpdateAcceptedTokens(tokenAddresses, isAccepted);
663 }
664
665 ///////////////////////////
666 ///////// GETTER //////////
667 ///////////////////////////
668 function offerAmount(uint256 offerId) external view returns (uint256) {
669 MarketStorage storage $ = _getOwnStorage();
670 return $.offers[offerId].amount;
671 }
672
673 function offerAmountAvailable(uint256 offerId) external view returns (uint256) {
674 MarketStorage storage $ = _getOwnStorage();
675 return $.offers[offerId].amount - $.offers[offerId].filledAmount;
676 }
677
678 function offerValue(uint256 offerId) external view returns (uint256) {
679 MarketStorage storage $ = _getOwnStorage();
680 return $.offers[offerId].value;
681 }
682
683 function offerExToken(uint256 offerId) external view returns (address) {
684 MarketStorage storage $ = _getOwnStorage();
685 return $.offers[offerId].exToken;
686 }
687
688 function isBuyOffer(uint256 offerId) external view returns (bool) {
689 MarketStorage storage $ = _getOwnStorage();
690 return $.offers[offerId].offerType == OFFER_BUY;
691 }
692
693 function isSellOffer(uint256 offerId) external view returns (bool) {
694 MarketStorage storage $ = _getOwnStorage();
695 return $.offers[offerId].offerType == OFFER_SELL;
696 }
697
698 function offerStatus(uint256 offerId) external view returns (uint256) {
699 MarketStorage storage $ = _getOwnStorage();
700 return $.offers[offerId].status;
701 }
702
703 function orderStatus(uint256 orderId) external view returns (uint256) {
704 MarketStorage storage $ = _getOwnStorage();
705 return $.orders[orderId].status;
706 }
707
708 function tokens(bytes32 tokenId) external view returns (Token memory) {
709 MarketStorage storage $ = _getOwnStorage();
710 return $.tokens[tokenId];
711 }
712
713 function offers(uint256 id) external view returns (Offer memory) {
714 MarketStorage storage $ = _getOwnStorage();
715 return $.offers[id];
716 }
717
718 function orders(uint256 id) external view returns (Order memory) {
719 MarketStorage storage $ = _getOwnStorage();
720 return $.orders[id];
721 }
722
723 function config() external view returns (Config memory) {
724 MarketStorage storage $ = _getOwnStorage();
725 return $.config;
726 }
727
728 function isAcceptedToken(address token) external view returns (bool) {
729 MarketStorage storage $ = _getOwnStorage();
730 return $.acceptedTokens[token];
731 }
732
733 function lastOfferId() external view returns (uint256) {
734 MarketStorage storage $ = _getOwnStorage();
735 return $.lastOfferId;
736 }
737
738 function lastOrderId() external view returns (uint256) {
739 MarketStorage storage $ = _getOwnStorage();
740 return $.lastOrderId;
741 }
742
743 ///////////////////////////
744 //////// INTERNAL /////////
745 ///////////////////////////
746 function _newOffer(
747 uint8 offerType,
748 bytes32 tokenId,
749 address exToken,
750 uint256 amount,
751 uint256 value,
752 uint256 collateral,
753 uint256 minAmount
754 ) internal {
755 MarketStorage storage $ = _getOwnStorage();
756 // create new offer
757 $.offers[++$.lastOfferId] =
758 Offer(offerType, tokenId, exToken, amount, value, collateral, 0, STATUS_OFFER_OPEN, msg.sender, minAmount);
759
760 emit NewOffer($.lastOfferId, offerType, tokenId, exToken, amount, value, collateral, minAmount, msg.sender);
exec
/bin/bash -lc "nl -ba src/FrontrunMarket.sol | sed -n '760,820p'" in /tmp/codex-dgrhxd3v
succeeded in 0ms:
760 emit NewOffer($.lastOfferId, offerType, tokenId, exToken, amount, value, collateral, minAmount, msg.sender);
761 }
762
763 function _fillOffer(uint256 offerId, uint256 amount, address buyer, address seller) internal {
764 MarketStorage storage $ = _getOwnStorage();
765 Offer storage offer = $.offers[offerId];
766 // new order
767 $.orders[++$.lastOrderId] = Order(offerId, amount, seller, buyer, STATUS_ORDER_OPEN);
768
769 // check if offer is fullfilled
770 offer.filledAmount += amount;
771 if (offer.filledAmount == offer.amount) {
772 offer.status = STATUS_OFFER_FILLED;
773 emit CloseOffer(offerId, 0);
774 }
775
776 emit NewOrder($.lastOrderId, offerId, amount, seller, buyer);
777 }
778
779 // get stuck token in contract
780 function withdrawStuckToken(address _token, address _to) external onlyOwner {
781 MarketStorage storage $ = _getOwnStorage();
782 require(_token != address(0) && !$.acceptedTokens[_token], 'Invalid Token Address');
783 uint256 _contractBalance = IERC20(_token).balanceOf(address(this));
784 IERC20(_token).safeTransfer(_to, _contractBalance);
785 }
786 }
codex
{"no_findings":false,"summary":"Found three meaningful issues in the implementation: `fillOffer` is reentrant before state is updated, ERC20 collateral is accounted at nominal rather than received amounts, and the owner can de-whitelist then withdraw live escrowed tokens.","findings":[{"severity":"high","category":"reentrancy","title":"`fillOffer` can be reentered before `filledAmount` is updated, allowing overfilled orders","exploit_scenario":"If a whitelisted collateral token invokes callbacks during `transferFrom` (for example an ERC777-style token or a callback-capable ERC20), an attacker can fill the same offer reentrantly from a contract wallet. Each nested call observes the old `offer.filledAmount`, so multiple full-sized orders are created against one offer. The attacker can then self-settle those duplicated orders and extract more `value + collateral` than was originally escrowed, draining pooled liquidity or leaving the market insolvent.","recommendation":"Make `fillOffer` non-reentrant, or expose only a guarded external entrypoint and keep the core logic internal. Update `offer.filledAmount`/status before any external token transfer, or otherwise re-check remaining capacity after the transfer returns.","evidence":"`fillOffer` performs `iexToken.safeTransferFrom(...)` before calling `_fillOffer(...)`. `_fillOffer` is the only place that increments `offer.filledAmount` and closes the offer.","locations":["src/FrontrunMarket.sol:381","src/FrontrunMarket.sol:410","src/FrontrunMarket.sol:412","src/FrontrunMarket.sol:763","src/FrontrunMarket.sol:770"]},{"severity":"high","category":"other","title":"Nominal ERC20 accounting makes fee-on-transfer collateral tokens insolvent","exploit_scenario":"If governance ever accepts a deflationary / fee-on-transfer token as collateral, the market records `value` and `collateral` using the requested amounts instead of the actual tokens received. An attacker can self-trade using that token so the contract receives less than it promises to pay back on cancel / settle. The shortfall is then paid from unrelated users' escrow if available, or later exits revert because the vault is underfunded.","recommendation":"Only allow plain ERC20 collateral tokens, or account using balance deltas before and after each transfer. Reject deposits where the actual received amount is smaller than the expected amount.","evidence":"Both `newOffer` and `fillOffer` transfer ERC20s and then keep using caller-supplied `value` / derived `collateral`; no post-transfer balance check exists before later payouts in `forceCancelOrder`, `settleFilled`, and `settleCancelled`.","locations":["src/FrontrunMarket.sol:322","src/FrontrunMarket.sol:343","src/FrontrunMarket.sol:381","src/FrontrunMarket.sol:410","src/FrontrunMarket.sol:526","src/FrontrunMarket.sol:575"]},{"severity":"medium","category":"access_control","title":"Owner can confiscate live escrow by de-whitelisting a token before `withdrawStuckToken`","exploit_scenario":"While users still have open offers or orders denominated in token `X`, the owner can call `setAcceptedTokens([X], false)` and immediately follow with `withdrawStuckToken(X, owner)`. Because the withdrawal gate only checks the current whitelist bit, the function transfers the entire balance of `X`, including funds backing active positions. Those positions then remain recorded on-chain but cannot be settled or cancelled successfully.","recommendation":"Track escrowed balances per collateral token and forbid withdrawing any amount backing live offers/orders. `withdrawStuckToken` should not rely only on the current accepted-token flag.","evidence":"`setAcceptedTokens` can flip any token's accepted status at any time, and `withdrawStuckToken` transfers the full contract balance whenever `acceptedTokens[_token]` is false.","locations":["src/FrontrunMarket.sol:656","src/FrontrunMarket.sol:660","src/FrontrunMarket.sol:780","src/FrontrunMarket.sol:782","src/FrontrunMarket.sol:784"]}]}
tokens used
53,207
{
"codex": {
"ran_at": "2026-04-04T03:33:51.721265+00:00",
"returncode": 0
},
"project": {
"mode": "source",
"file_count": 19,
"import_count": 22,
"first_party_contracts_count": 1
},
"summary": "Found three meaningful issues in the implementation: `fillOffer` is reentrant before state is updated, ERC20 collateral is accounted at nominal rather than received amounts, and the owner can de-whitelist then withdraw live escrowed tokens.",
"raw_final": {
"summary": "Found three meaningful issues in the implementation: `fillOffer` is reentrant before state is updated, ERC20 collateral is accounted at nominal rather than received amounts, and the owner can de-whitelist then withdraw live escrowed tokens.",
"findings": [
{
"title": "`fillOffer` can be reentered before `filledAmount` is updated, allowing overfilled orders",
"category": "reentrancy",
"evidence": "`fillOffer` performs `iexToken.safeTransferFrom(...)` before calling `_fillOffer(...)`. `_fillOffer` is the only place that increments `offer.filledAmount` and closes the offer.",
"severity": "high",
"locations": [
"src/FrontrunMarket.sol:381",
"src/FrontrunMarket.sol:410",
"src/FrontrunMarket.sol:412",
"src/FrontrunMarket.sol:763",
"src/FrontrunMarket.sol:770"
],
"recommendation": "Make `fillOffer` non-reentrant, or expose only a guarded external entrypoint and keep the core logic internal. Update `offer.filledAmount`/status before any external token transfer, or otherwise re-check remaining capacity after the transfer returns.",
"exploit_scenario": "If a whitelisted collateral token invokes callbacks during `transferFrom` (for example an ERC777-style token or a callback-capable ERC20), an attacker can fill the same offer reentrantly from a contract wallet. Each nested call observes the old `offer.filledAmount`, so multiple full-sized orders are created against one offer. The attacker can then self-settle those duplicated orders and extract more `value + collateral` than was originally escrowed, draining pooled liquidity or leaving the market insolvent."
},
{
"title": "Nominal ERC20 accounting makes fee-on-transfer collateral tokens insolvent",
"category": "other",
"evidence": "Both `newOffer` and `fillOffer` transfer ERC20s and then keep using caller-supplied `value` / derived `collateral`; no post-transfer balance check exists before later payouts in `forceCancelOrder`, `settleFilled`, and `settleCancelled`.",
"severity": "high",
"locations": [
"src/FrontrunMarket.sol:322",
"src/FrontrunMarket.sol:343",
"src/FrontrunMarket.sol:381",
"src/FrontrunMarket.sol:410",
"src/FrontrunMarket.sol:526",
"src/FrontrunMarket.sol:575"
],
"recommendation": "Only allow plain ERC20 collateral tokens, or account using balance deltas before and after each transfer. Reject deposits where the actual received amount is smaller than the expected amount.",
"exploit_scenario": "If governance ever accepts a deflationary / fee-on-transfer token as collateral, the market records `value` and `collateral` using the requested amounts instead of the actual tokens received. An attacker can self-trade using that token so the contract receives less than it promises to pay back on cancel / settle. The shortfall is then paid from unrelated users' escrow if available, or later exits revert because the vault is underfunded."
},
{
"title": "Owner can confiscate live escrow by de-whitelisting a token before `withdrawStuckToken`",
"category": "access_control",
"evidence": "`setAcceptedTokens` can flip any token's accepted status at any time, and `withdrawStuckToken` transfers the full contract balance whenever `acceptedTokens[_token]` is false.",
"severity": "medium",
"locations": [
"src/FrontrunMarket.sol:656",
"src/FrontrunMarket.sol:660",
"src/FrontrunMarket.sol:780",
"src/FrontrunMarket.sol:782",
"src/FrontrunMarket.sol:784"
],
"recommendation": "Track escrowed balances per collateral token and forbid withdrawing any amount backing live offers/orders. `withdrawStuckToken` should not rely only on the current accepted-token flag.",
"exploit_scenario": "While users still have open offers or orders denominated in token `X`, the owner can call `setAcceptedTokens([X], false)` and immediately follow with `withdrawStuckToken(X, owner)`. Because the withdrawal gate only checks the current whitelist bit, the function transfers the entire balance of `X`, including funds backing active positions. Those positions then remain recorded on-chain but cannot be settled or cancelled successfully."
}
],
"no_findings": false
},
"no_findings": false,
"proxy_context": {
"proxy_run_id": "019d5666-7204-7199-9428-87c933f7acea",
"proxy_address": "0x849f4081899305a1fd24aac84db5174eb60dc28e",
"implementation_run_id": "019d5666-ddb2-7138-a85b-4026ca2b3eec",
"implementation_address": "0x6ac64c4760e0590f88233b2046810e87e0354324"
},
"schema_version": 1
}
00000000: PUSH1 0x80 00000002: PUSH1 0x40 00000004: MSTORE 00000005: PUSH2 0x000c 00000008: PUSH2 0x000e 0000000b: JUMP 0000000c: JUMPDEST 0000000d: STOP 0000000e: JUMPDEST 0000000f: PUSH32 0x000000000000000000000000735a07f45ad51548059f6d0a4756a0438bd871c8 00000030: PUSH1 0x01 00000032: PUSH1 0x01 00000034: PUSH1 0xa0 00000036: SHL 00000037: SUB 00000038: AND 00000039: CALLER 0000003a: SUB 0000003b: PUSH2 0x007b 0000003e: JUMPI 0000003f: PUSH1 0x00 00000041: CALLDATALOAD 00000042: PUSH1 0x01 00000044: PUSH1 0x01 00000046: PUSH1 0xe0 00000048: SHL 00000049: SUB 0000004a: NOT 0000004b: AND 0000004c: PUSH4 0x278f7943 00000051: PUSH1 0xe1 00000053: SHL 00000054: EQ 00000055: PUSH2 0x0071 00000058: JUMPI 00000059: PUSH1 0x40 0000005b: MLOAD 0000005c: PUSH4 0x34ad5dbb 00000061: PUSH1 0xe2 00000063: SHL 00000064: DUP2 00000065: MSTORE 00000066: PUSH1 0x04 00000068: ADD 00000069: PUSH1 0x40 0000006b: MLOAD 0000006c: DUP1 0000006d: SWAP2 0000006e: SUB 0000006f: SWAP1 00000070: REVERT 00000071: JUMPDEST 00000072: PUSH2 0x0079 00000075: PUSH2 0x0083 00000078: JUMP 00000079: JUMPDEST 0000007a: JUMP 0000007b: JUMPDEST 0000007c: PUSH2 0x0079 0000007f: PUSH2 0x00b2 00000082: JUMP 00000083: JUMPDEST 00000084: PUSH1 0x00 00000086: DUP1 00000087: PUSH2 0x0093 0000008a: CALLDATASIZE 0000008b: PUSH1 0x04 0000008d: DUP2 0000008e: DUP5 0000008f: PUSH2 0x02e0 00000092: JUMP 00000093: JUMPDEST 00000094: DUP2 00000095: ADD 00000096: SWAP1 00000097: PUSH2 0x00a0 0000009a: SWAP2 0000009b: SWAP1 0000009c: PUSH2 0x0320 0000009f: JUMP 000000a0: JUMPDEST 000000a1: SWAP2 000000a2: POP 000000a3: SWAP2 000000a4: POP 000000a5: PUSH2 0x00ae 000000a8: DUP3 000000a9: DUP3 000000aa: PUSH2 0x00c2 000000ad: JUMP 000000ae: JUMPDEST 000000af: POP 000000b0: POP 000000b1: JUMP 000000b2: JUMPDEST 000000b3: PUSH2 0x0079 000000b6: PUSH2 0x00bd 000000b9: PUSH2 0x011d 000000bc: JUMP 000000bd: JUMPDEST 000000be: PUSH2 0x0143 000000c1: JUMP 000000c2: JUMPDEST 000000c3: PUSH2 0x00cb 000000c6: DUP3 000000c7: PUSH2 0x0167 000000ca: JUMP 000000cb: JUMPDEST 000000cc: PUSH1 0x40 000000ce: MLOAD 000000cf: PUSH1 0x01 000000d1: PUSH1 0x01 000000d3: PUSH1 0xa0 000000d5: SHL 000000d6: SUB 000000d7: DUP4 000000d8: AND 000000d9: SWAP1 000000da: PUSH32 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b 000000fb: SWAP1 000000fc: PUSH1 0x00 000000fe: SWAP1 000000ff: LOG2 00000100: DUP1 00000101: MLOAD 00000102: ISZERO 00000103: PUSH2 0x0115 00000106: JUMPI 00000107: PUSH2 0x0110 0000010a: DUP3 0000010b: DUP3 0000010c: PUSH2 0x01cc 0000010f: JUMP 00000110: JUMPDEST 00000111: POP 00000112: POP 00000113: POP 00000114: JUMP 00000115: JUMPDEST 00000116: PUSH2 0x00ae 00000119: PUSH2 0x0242 0000011c: JUMP 0000011d: JUMPDEST 0000011e: PUSH1 0x00 00000120: PUSH2 0x013e 00000123: PUSH1 0x00 00000125: DUP1 00000126: MLOAD 00000127: PUSH1 0x20 00000129: PUSH2 0x0433 0000012c: DUP4 0000012d: CODECOPY 0000012e: DUP2 0000012f: MLOAD 00000130: SWAP2 00000131: MSTORE 00000132: SLOAD 00000133: PUSH1 0x01 00000135: PUSH1 0x01 00000137: PUSH1 0xa0 00000139: SHL 0000013a: SUB 0000013b: AND 0000013c: SWAP1 0000013d: JUMP 0000013e: JUMPDEST 0000013f: SWAP1 00000140: POP 00000141: SWAP1 00000142: JUMP 00000143: JUMPDEST 00000144: CALLDATASIZE 00000145: PUSH1 0x00 00000147: DUP1 00000148: CALLDATACOPY 00000149: PUSH1 0x00 0000014b: DUP1 0000014c: CALLDATASIZE 0000014d: PUSH1 0x00 0000014f: DUP5 00000150: GAS 00000151: DELEGATECALL 00000152: RETURNDATASIZE 00000153: PUSH1 0x00 00000155: DUP1 00000156: RETURNDATACOPY 00000157: DUP1 00000158: DUP1 00000159: ISZERO 0000015a: PUSH2 0x0162 0000015d: JUMPI 0000015e: RETURNDATASIZE 0000015f: PUSH1 0x00 00000161: RETURN 00000162: JUMPDEST 00000163: RETURNDATASIZE 00000164: PUSH1 0x00 00000166: REVERT 00000167: JUMPDEST 00000168: DUP1 00000169: PUSH1 0x01 0000016b: PUSH1 0x01 0000016d: PUSH1 0xa0 0000016f: SHL 00000170: SUB 00000171: AND 00000172: EXTCODESIZE 00000173: PUSH1 0x00 00000175: SUB 00000176: PUSH2 0x019d 00000179: JUMPI 0000017a: DUP1 0000017b: PUSH1 0x40 0000017d: MLOAD 0000017e: PUSH4 0x4c9c8ce3 00000183: PUSH1 0xe0 00000185: SHL 00000186: DUP2 00000187: MSTORE 00000188: PUSH1 0x04 0000018a: ADD 0000018b: PUSH2 0x0194 0000018e: SWAP2 0000018f: SWAP1 00000190: PUSH2 0x03ef 00000193: JUMP 00000194: JUMPDEST 00000195: PUSH1 0x40 00000197: MLOAD 00000198: DUP1 00000199: SWAP2 0000019a: SUB 0000019b: SWAP1 0000019c: REVERT 0000019d: JUMPDEST 0000019e: PUSH1 0x00 000001a0: DUP1 000001a1: MLOAD 000001a2: PUSH1 0x20 000001a4: PUSH2 0x0433 000001a7: DUP4 000001a8: CODECOPY 000001a9: DUP2 000001aa: MLOAD 000001ab: SWAP2 000001ac: MSTORE 000001ad: DUP1 000001ae: SLOAD 000001af: PUSH1 0x01 000001b1: PUSH1 0x01 000001b3: PUSH1 0xa0 000001b5: SHL 000001b6: SUB 000001b7: NOT 000001b8: AND 000001b9: PUSH1 0x01 000001bb: PUSH1 0x01 000001bd: PUSH1 0xa0 000001bf: SHL 000001c0: SUB 000001c1: SWAP3 000001c2: SWAP1 000001c3: SWAP3 000001c4: AND 000001c5: SWAP2 000001c6: SWAP1 000001c7: SWAP2 000001c8: OR 000001c9: SWAP1 000001ca: SSTORE 000001cb: JUMP 000001cc: JUMPDEST 000001cd: PUSH1 0x60 000001cf: PUSH1 0x00 000001d1: DUP1 000001d2: DUP5 000001d3: PUSH1 0x01 000001d5: PUSH1 0x01 000001d7: PUSH1 0xa0 000001d9: SHL 000001da: SUB 000001db: AND 000001dc: DUP5 000001dd: PUSH1 0x40 000001df: MLOAD 000001e0: PUSH2 0x01e9 000001e3: SWAP2 000001e4: SWAP1 000001e5: PUSH2 0x0403 000001e8: JUMP 000001e9: JUMPDEST 000001ea: PUSH1 0x00 000001ec: PUSH1 0x40 000001ee: MLOAD 000001ef: DUP1 000001f0: DUP4 000001f1: SUB 000001f2: DUP2 000001f3: DUP6 000001f4: GAS 000001f5: DELEGATECALL 000001f6: SWAP2 000001f7: POP 000001f8: POP 000001f9: RETURNDATASIZE 000001fa: DUP1 000001fb: PUSH1 0x00 000001fd: DUP2 000001fe: EQ 000001ff: PUSH2 0x0224 00000202: JUMPI 00000203: PUSH1 0x40 00000205: MLOAD 00000206: SWAP2 00000207: POP 00000208: PUSH1 0x1f 0000020a: NOT 0000020b: PUSH1 0x3f 0000020d: RETURNDATASIZE 0000020e: ADD 0000020f: AND 00000210: DUP3 00000211: ADD 00000212: PUSH1 0x40 00000214: MSTORE 00000215: RETURNDATASIZE 00000216: DUP3 00000217: MSTORE 00000218: RETURNDATASIZE 00000219: PUSH1 0x00 0000021b: PUSH1 0x20 0000021d: DUP5 0000021e: ADD 0000021f: RETURNDATACOPY 00000220: PUSH2 0x0229 00000223: JUMP 00000224: JUMPDEST 00000225: PUSH1 0x60 00000227: SWAP2 00000228: POP 00000229: JUMPDEST 0000022a: POP 0000022b: SWAP2 0000022c: POP 0000022d: SWAP2 0000022e: POP 0000022f: PUSH2 0x0239 00000232: DUP6 00000233: DUP4 00000234: DUP4 00000235: PUSH2 0x0261 00000238: JUMP 00000239: JUMPDEST 0000023a: SWAP6 0000023b: SWAP5 0000023c: POP 0000023d: POP 0000023e: POP 0000023f: POP 00000240: POP 00000241: JUMP 00000242: JUMPDEST 00000243: CALLVALUE 00000244: ISZERO 00000245: PUSH2 0x0079 00000248: JUMPI 00000249: PUSH1 0x40 0000024b: MLOAD 0000024c: PUSH4 0xb398979f 00000251: PUSH1 0xe0 00000253: SHL 00000254: DUP2 00000255: MSTORE 00000256: PUSH1 0x04 00000258: ADD 00000259: PUSH1 0x40 0000025b: MLOAD 0000025c: DUP1 0000025d: SWAP2 0000025e: SUB 0000025f: SWAP1 00000260: REVERT 00000261: JUMPDEST 00000262: PUSH1 0x60 00000264: DUP3 00000265: PUSH2 0x0276 00000268: JUMPI 00000269: PUSH2 0x0271 0000026c: DUP3 0000026d: PUSH2 0x02b7 00000270: JUMP 00000271: JUMPDEST 00000272: PUSH2 0x02b0 00000275: JUMP 00000276: JUMPDEST 00000277: DUP2 00000278: MLOAD 00000279: ISZERO 0000027a: DUP1 0000027b: ISZERO 0000027c: PUSH2 0x028d 0000027f: JUMPI 00000280: POP 00000281: PUSH1 0x01 00000283: PUSH1 0x01 00000285: PUSH1 0xa0 00000287: SHL 00000288: SUB 00000289: DUP5 0000028a: AND 0000028b: EXTCODESIZE 0000028c: ISZERO 0000028d: JUMPDEST 0000028e: ISZERO 0000028f: PUSH2 0x02ad 00000292: JUMPI 00000293: DUP4 00000294: PUSH1 0x40 00000296: MLOAD 00000297: PUSH4 0x9996b315 0000029c: PUSH1 0xe0 0000029e: SHL 0000029f: DUP2 000002a0: MSTORE 000002a1: PUSH1 0x04 000002a3: ADD 000002a4: PUSH2 0x0194 000002a7: SWAP2 000002a8: SWAP1 000002a9: PUSH2 0x03ef 000002ac: JUMP 000002ad: JUMPDEST 000002ae: POP 000002af: DUP1 000002b0: JUMPDEST 000002b1: SWAP4 000002b2: SWAP3 000002b3: POP 000002b4: POP 000002b5: POP 000002b6: JUMP 000002b7: JUMPDEST 000002b8: DUP1 000002b9: MLOAD 000002ba: ISZERO 000002bb: PUSH2 0x02c7 000002be: JUMPI 000002bf: DUP1 000002c0: MLOAD 000002c1: DUP1 000002c2: DUP3 000002c3: PUSH1 0x20 000002c5: ADD 000002c6: REVERT 000002c7: JUMPDEST 000002c8: PUSH1 0x40 000002ca: MLOAD 000002cb: PUSH4 0x0a12f521 000002d0: PUSH1 0xe1 000002d2: SHL 000002d3: DUP2 000002d4: MSTORE 000002d5: PUSH1 0x04 000002d7: ADD 000002d8: PUSH1 0x40 000002da: MLOAD 000002db: DUP1 000002dc: SWAP2 000002dd: SUB 000002de: SWAP1 000002df: REVERT 000002e0: JUMPDEST 000002e1: PUSH1 0x00 000002e3: DUP1 000002e4: DUP6 000002e5: DUP6 000002e6: GT 000002e7: ISZERO 000002e8: PUSH2 0x02f0 000002eb: JUMPI 000002ec: PUSH1 0x00 000002ee: DUP1 000002ef: REVERT 000002f0: JUMPDEST 000002f1: DUP4 000002f2: DUP7 000002f3: GT 000002f4: ISZERO 000002f5: PUSH2 0x02fd 000002f8: JUMPI 000002f9: PUSH1 0x00 000002fb: DUP1 000002fc: REVERT 000002fd: JUMPDEST 000002fe: POP 000002ff: POP 00000300: DUP3 00000301: ADD 00000302: SWAP4 00000303: SWAP2 00000304: SWAP1 00000305: SWAP3 00000306: SUB 00000307: SWAP2 00000308: POP 00000309: JUMP 0000030a: JUMPDEST 0000030b: PUSH4 0x4e487b71 00000310: PUSH1 0xe0 00000312: SHL 00000313: PUSH1 0x00 00000315: MSTORE 00000316: PUSH1 0x41 00000318: PUSH1 0x04 0000031a: MSTORE 0000031b: PUSH1 0x24 0000031d: PUSH1 0x00 0000031f: REVERT 00000320: JUMPDEST 00000321: PUSH1 0x00 00000323: DUP1 00000324: PUSH1 0x40 00000326: DUP4 00000327: DUP6 00000328: SUB 00000329: SLT 0000032a: ISZERO 0000032b: PUSH2 0x0333 0000032e: JUMPI 0000032f: PUSH1 0x00 00000331: DUP1 00000332: REVERT 00000333: JUMPDEST 00000334: DUP3 00000335: CALLDATALOAD 00000336: PUSH1 0x01 00000338: PUSH1 0x01 0000033a: PUSH1 0xa0 0000033c: SHL 0000033d: SUB 0000033e: DUP2 0000033f: AND 00000340: DUP2 00000341: EQ 00000342: PUSH2 0x034a 00000345: JUMPI 00000346: PUSH1 0x00 00000348: DUP1 00000349: REVERT 0000034a: JUMPDEST 0000034b: SWAP2 0000034c: POP 0000034d: PUSH1 0x20 0000034f: DUP4 00000350: ADD 00000351: CALLDATALOAD 00000352: PUSH1 0x01 00000354: PUSH1 0x01 00000356: PUSH1 0x40 00000358: SHL 00000359: SUB 0000035a: DUP1 0000035b: DUP3 0000035c: GT 0000035d: ISZERO 0000035e: PUSH2 0x0366 00000361: JUMPI 00000362: PUSH1 0x00 00000364: DUP1 00000365: REVERT 00000366: JUMPDEST 00000367: DUP2 00000368: DUP6 00000369: ADD 0000036a: SWAP2 0000036b: POP 0000036c: DUP6 0000036d: PUSH1 0x1f 0000036f: DUP4 00000370: ADD 00000371: SLT 00000372: PUSH2 0x037a 00000375: JUMPI 00000376: PUSH1 0x00 00000378: DUP1 00000379: REVERT 0000037a: JUMPDEST 0000037b: DUP2 0000037c: CALLDATALOAD 0000037d: DUP2 0000037e: DUP2 0000037f: GT 00000380: ISZERO 00000381: PUSH2 0x038c 00000384: JUMPI 00000385: PUSH2 0x038c 00000388: PUSH2 0x030a 0000038b: JUMP 0000038c: JUMPDEST 0000038d: PUSH1 0x40 0000038f: MLOAD 00000390: PUSH1 0x1f 00000392: DUP3 00000393: ADD 00000394: PUSH1 0x1f 00000396: NOT 00000397: SWAP1 00000398: DUP2 00000399: AND 0000039a: PUSH1 0x3f 0000039c: ADD 0000039d: AND 0000039e: DUP2 0000039f: ADD 000003a0: SWAP1 000003a1: DUP4 000003a2: DUP3 000003a3: GT 000003a4: DUP2 000003a5: DUP4 000003a6: LT 000003a7: OR 000003a8: ISZERO 000003a9: PUSH2 0x03b4 000003ac: JUMPI 000003ad: PUSH2 0x03b4 000003b0: PUSH2 0x030a 000003b3: JUMP 000003b4: JUMPDEST 000003b5: DUP2 000003b6: PUSH1 0x40 000003b8: MSTORE 000003b9: DUP3 000003ba: DUP2 000003bb: MSTORE 000003bc: DUP9 000003bd: PUSH1 0x20 000003bf: DUP5 000003c0: DUP8 000003c1: ADD 000003c2: ADD 000003c3: GT 000003c4: ISZERO 000003c5: PUSH2 0x03cd 000003c8: JUMPI 000003c9: PUSH1 0x00 000003cb: DUP1 000003cc: REVERT 000003cd: JUMPDEST 000003ce: DUP3 000003cf: PUSH1 0x20 000003d1: DUP7 000003d2: ADD 000003d3: PUSH1 0x20 000003d5: DUP4 000003d6: ADD 000003d7: CALLDATACOPY 000003d8: PUSH1 0x00 000003da: PUSH1 0x20 000003dc: DUP5 000003dd: DUP4 000003de: ADD 000003df: ADD 000003e0: MSTORE 000003e1: DUP1 000003e2: SWAP6 000003e3: POP 000003e4: POP 000003e5: POP 000003e6: POP 000003e7: POP 000003e8: POP 000003e9: SWAP3 000003ea: POP 000003eb: SWAP3 000003ec: SWAP1 000003ed: POP 000003ee: JUMP 000003ef: JUMPDEST 000003f0: PUSH1 0x01 000003f2: PUSH1 0x01 000003f4: PUSH1 0xa0 000003f6: SHL 000003f7: SUB 000003f8: SWAP2 000003f9: SWAP1 000003fa: SWAP2 000003fb: AND 000003fc: DUP2 000003fd: MSTORE 000003fe: PUSH1 0x20 00000400: ADD 00000401: SWAP1 00000402: JUMP 00000403: JUMPDEST 00000404: PUSH1 0x00 00000406: DUP3 00000407: MLOAD 00000408: PUSH1 0x00 0000040a: JUMPDEST 0000040b: DUP2 0000040c: DUP2 0000040d: LT 0000040e: ISZERO 0000040f: PUSH2 0x0424 00000412: JUMPI 00000413: PUSH1 0x20 00000415: DUP2 00000416: DUP7 00000417: ADD 00000418: DUP2 00000419: ADD 0000041a: MLOAD 0000041b: DUP6 0000041c: DUP4 0000041d: ADD 0000041e: MSTORE 0000041f: ADD 00000420: PUSH2 0x040a 00000423: JUMP 00000424: JUMPDEST 00000425: POP 00000426: PUSH1 0x00 00000428: SWAP3 00000429: ADD 0000042a: SWAP2 0000042b: DUP3 0000042c: MSTORE 0000042d: POP 0000042e: SWAP2 0000042f: SWAP1 00000430: POP 00000431: JUMP 00000432: INVALID 00000433: CALLDATASIZE 00000434: ADDMOD 00000435: SWAP5 00000436: LOG1 00000437: EXTCODESIZE 00000438: LOG1 00000439: LOG3 0000043a: UNKNOWN(0x21) 0000043b: MOD 0000043c: PUSH8 0xc828492db98dca3e 00000445: KECCAK256 00000446: PUSH23 0xcc3735a920a3ca505d382bbca264697066735822122092 0000045e: UNKNOWN(0x22) 0000045f: EXTCODEHASH 00000460: UNKNOWN(0xE4) 00000461: UNKNOWN(0xE2) 00000462: SWAP5 00000463: UNKNOWN(0xBC) 00000464: UNKNOWN(0xAE) 00000465: CREATE2 00000466: MSIZE 00000467: SAR 00000468: BALANCE 00000469: SWAP15 0000046a: UNKNOWN(0x0D) 0000046b: PUSH9 0x7141ba94e9cc225e29 00000475: UNKNOWN(0x1F) 00000476: PUSH25
| Severity | Category | Tool | Title | Confidence | Validated | Confirmed | Details |
|---|---|---|---|---|---|---|---|
| critical | upgradeability | detector | Untrusted DELEGATECALL target reachable | 70 | no | no |
viewtrace_id: 0x826da0a762f5fb2615cfa70400928ea66b4a1beab129e3ee3fae5561b8216a8d
call_targets:
0x6ac64c4760e0590f88233b2046810e87e0354324
target_varies:
no
classification:
constant_target
validation_json{
"sink": "DELEGATECALL",
"errors": 0,
"status": "sink_reached",
"attempts": 1,
"trace_id": "0x826da0a762f5fb2615cfa70400928ea66b4a1beab129e3ee3fae5561b8216a8d",
"confirmed": false,
"trace_mode": "callTracer",
"call_targets": [
"0x6ac64c4760e0590f88233b2046810e87e0354324"
],
"matched_probe": null,
"target_varies": false,
"classification": "constant_target"
}
witness_json{
"notes": "heuristic witness; entrypoints are best-effort selectors with zeroed calldata",
"constraints": [],
"entrypoints": [
{
"calldata": "0x278f7943",
"selector": "0x278f7943",
"calldata_variants": [
"0x278f7943",
"0x278f79430000000000000000000000000000000000000000000000000000000000000000"
]
}
]
}
evidence_json{
"examples": [
{
"pc": 337,
"opcode": "DELEGATECALL",
"returndata_checked": null,
"value_expression_category": "n/a",
"target_expression_category": "computed"
},
{
"pc": 501,
"opcode": "DELEGATECALL",
"returndata_checked": null,
"value_expression_category": "n/a",
"target_expression_category": "computed"
}
],
"guards_detected": [],
"delegatecall_count": 2,
"untrusted_delegatecall_count": 2
}
|
| high | reentrancy | codex | `fillOffer` can be reentered before `filledAmount` is updated, allowing overfilled orders | 85 | no | — |
viewevidence_json{
"evidence": "`fillOffer` performs `iexToken.safeTransferFrom(...)` before calling `_fillOffer(...)`. `_fillOffer` is the only place that increments `offer.filledAmount` and closes the offer.",
"locations": [
"src/FrontrunMarket.sol:381",
"src/FrontrunMarket.sol:410",
"src/FrontrunMarket.sol:412",
"src/FrontrunMarket.sol:763",
"src/FrontrunMarket.sol:770"
],
"recommendation": "Make `fillOffer` non-reentrant, or expose only a guarded external entrypoint and keep the core logic internal. Update `offer.filledAmount`/status before any external token transfer, or otherwise re-check remaining capacity after the transfer returns.",
"exploit_scenario": "If a whitelisted collateral token invokes callbacks during `transferFrom` (for example an ERC777-style token or a callback-capable ERC20), an attacker can fill the same offer reentrantly from a contract wallet. Each nested call observes the old `offer.filledAmount`, so multiple full-sized orders are created against one offer. The attacker can then self-settle those duplicated orders and extract more `value + collateral` than was originally escrowed, draining pooled liquidity or leaving the market insolvent."
}
|
| high | other | codex | Nominal ERC20 accounting makes fee-on-transfer collateral tokens insolvent | 85 | no | — |
viewevidence_json{
"evidence": "Both `newOffer` and `fillOffer` transfer ERC20s and then keep using caller-supplied `value` / derived `collateral`; no post-transfer balance check exists before later payouts in `forceCancelOrder`, `settleFilled`, and `settleCancelled`.",
"locations": [
"src/FrontrunMarket.sol:322",
"src/FrontrunMarket.sol:343",
"src/FrontrunMarket.sol:381",
"src/FrontrunMarket.sol:410",
"src/FrontrunMarket.sol:526",
"src/FrontrunMarket.sol:575"
],
"recommendation": "Only allow plain ERC20 collateral tokens, or account using balance deltas before and after each transfer. Reject deposits where the actual received amount is smaller than the expected amount.",
"exploit_scenario": "If governance ever accepts a deflationary / fee-on-transfer token as collateral, the market records `value` and `collateral` using the requested amounts instead of the actual tokens received. An attacker can self-trade using that token so the contract receives less than it promises to pay back on cancel / settle. The shortfall is then paid from unrelated users' escrow if available, or later exits revert because the vault is underfunded."
}
|
| medium | access_control | codex | Owner can confiscate live escrow by de-whitelisting a token before `withdrawStuckToken` | 75 | no | — |
viewevidence_json{
"evidence": "`setAcceptedTokens` can flip any token's accepted status at any time, and `withdrawStuckToken` transfers the full contract balance whenever `acceptedTokens[_token]` is false.",
"locations": [
"src/FrontrunMarket.sol:656",
"src/FrontrunMarket.sol:660",
"src/FrontrunMarket.sol:780",
"src/FrontrunMarket.sol:782",
"src/FrontrunMarket.sol:784"
],
"recommendation": "Track escrowed balances per collateral token and forbid withdrawing any amount backing live offers/orders. `withdrawStuckToken` should not rely only on the current accepted-token flag.",
"exploit_scenario": "While users still have open offers or orders denominated in token `X`, the owner can call `setAcceptedTokens([X], false)` and immediately follow with `withdrawStuckToken(X, owner)`. Because the withdrawal gate only checks the current whitelist bit, the function transfers the entire balance of `X`, including funds backing active positions. Those positions then remain recorded on-chain but cannot be settled or cancelled successfully."
}
|
| medium | upgradeability | cast | DELEGATECALL present | 80 | no | — |
viewevidence_json{
"notes": "delegatecall enables execution in caller context; common in proxies/upgradeable patterns",
"delegatecall_count": 2
}
|
| medium | other | detector | CREATE/CREATE2 reachable | 45 | no | no |
viewtrace_id: 0xa669044104f7a80bf9c8801bf53b24e8789551c80260853b11d82347b90cfc45
validation_json{
"sink": "CREATE",
"errors": 0,
"status": "unknown",
"attempts": 2,
"trace_id": "0xa669044104f7a80bf9c8801bf53b24e8789551c80260853b11d82347b90cfc45",
"confirmed": false,
"trace_mode": "structLogs",
"call_targets": null,
"matched_probe": null,
"target_varies": null,
"classification": "no_sink"
}
witness_json{
"notes": "heuristic witness; entrypoints are best-effort selectors with zeroed calldata",
"constraints": [],
"entrypoints": [
{
"calldata": "0x278f7943",
"selector": "0x278f7943",
"calldata_variants": [
"0x278f7943",
"0x278f79430000000000000000000000000000000000000000000000000000000000000000"
]
}
]
}
evidence_json{
"create_count": 0,
"create2_count": 1
}
|
| low | other | cast | Contract creation opcode present | 60 | no | — |
viewevidence_json{
"notes": "runtime can deploy contracts dynamically via CREATE/CREATE2",
"create_count": 0,
"create2_count": 1
}
|
| info | other | cast | Heavy EXTCODE*/BALANCE usage | 55 | no | — |
viewevidence_json{
"notes": "frequent external code or balance checks; may indicate introspection or validation logic",
"ext_balance_count": 5
}
|