019d5666-721c-732b-98f2-8fef0dd24f32
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 |
|---|---|---|---|---|
| 42869 | artifact.fetch | complete | 1 / 3 | 1 week ago |
| 42870 | proxy.resolve | complete | 1 / 3 | 1 week ago |
| 42871 | tool.cast_disasm | complete | 1 / 3 | 1 week ago |
| 42872 | analysis.bundle | complete | 1 / 3 | 1 week ago |
| 42873 | capability.graph | complete | 1 / 3 | 1 week ago |
| 42874 | detector.run | complete | 1 / 3 | 1 week ago |
| 42875 | validation.fork | complete | 1 / 3 | 1 week ago |
| 42978 | report.consolidate | complete | 1 / 6 | 1 week ago |
| 42979 | tool.slither | failed | 2 / 2 | 1 week ago |
{
"method": "etherscan+onchain",
"status": "confirmed",
"etherscan_hint": {
"source": "etherscan",
"etherscan": {
"proxy_flag": true,
"implementation": "0x00000000d681e85e5783588f87a9573cb97eda01"
}
},
"implementation": "0x00000000d681e85e5783588f87a9573cb97eda01",
"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-q3mbt37d)
'forge config --json' running
'forge build --build-info --skip ./test/** ./script/** --force' running (wd: /tmp/slither-q3mbt37d)
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-q3mbt37d/out/build-info is not a directory.
{"no_findings":false,"summary":"Read `AUDIT_BUNDLE.txt` in full. Found 2 material issues in the implementation: proxy upgrades can strand state because storage slots are version/name-dependent, and CoWSwap orders do not bound `feeAmount`, allowing approved sell tokens to be overspent beyond `params.amountIn`.","findings":[{"severity":"high","category":"upgradeability","title":"Version-dependent storage slots can brick the proxy and strand assets on upgrade","exploit_scenario":"The proxy points at this implementation today. If the proxy admin later upgrades to a new `SwapModule` implementation deployed with a different `name_` or `version_`, the immutables `_swapModuleStorageSlot` and `_mellowACLStorageSlot` change. After the upgrade, `subvault()`, `oracle()`, multipliers, and supported-role bookkeeping are read from empty storage. Because `subvault` has no setter and `initialize` is one-shot, the proxy can no longer satisfy `onlySubvault`, leaving assets held by the proxy permanently stuck.","recommendation":"Do not derive live storage slots from constructor-time version/name inputs for proxy state. Use fixed ERC-7201/ERC-1967-style slots that remain identical across implementation versions, or enforce that every upgrade reuses the exact same slot constants.","evidence":"`SlotLibrary.getSlot` mixes `contractName`, `name`, and `version` into the slot seed. `MellowACL` and `SwapModule` cache those derived slots in immutables during construction and use them for all reads/writes of first-party state, so a new implementation with different constructor args silently points the proxy at a different storage region.","locations":["src/libraries/SlotLibrary.sol:13","src/permissions/MellowACL.sol:11","src/permissions/MellowACL.sol:58","src/utils/SwapModule.sol:37","src/utils/SwapModule.sol:62","src/utils/SwapModule.sol:197","src/utils/SwapModule.sol:306"]},{"severity":"medium","category":"access_control","title":"`createLimitOrder` does not constrain CoWSwap `feeAmount`, so CALLER_ROLE can spend more than `params.amountIn`","exploit_scenario":"A compromised or over-scoped `CALLER_ROLE` can first call `setCowswapApproval(asset, type(uint256).max)`, then create a pre-signed CoWSwap order with `sellAmount == params.amountIn` but an arbitrarily large `feeAmount`. The order passes `checkParams` and `checkCowswapOrder`, yet CoWSwap settlement will pull `sellAmount + feeAmount` from the module. The extra sell token is outside the min-out/slippage checks, so approved balances can be drained far beyond the amount the module intended to trade.","recommendation":"Reject orders with nonzero `feeAmount`, or explicitly include fee in the validated parameters and in the balance/allowance accounting. If partial fills are not needed, also require `order.partiallyFillable == false` and scope approval to the exact live exposure instead of a reusable allowance.","evidence":"`checkCowswapOrder` validates token addresses, `sellAmount`, `buyAmount`, deadline, kind, and balance modes, but never checks `order.feeAmount` (or `order.partiallyFillable`). Separately, `setCowswapApproval` allows arbitrary allowance to `cowswapVaultRelayer`, so settlement can legally transfer more sell token than `params.amountIn`.","locations":["src/utils/SwapModule.sol:137","src/utils/SwapModule.sol:150","src/utils/SwapModule.sol:153","src/utils/SwapModule.sol:277","src/utils/SwapModule.sol:286"]}]}
OpenAI Codex v0.118.0 (research preview)
--------
workdir: /tmp/codex-ky1rccd0
model: gpt-5.4
provider: openai
approval: never
sandbox: read-only
reasoning effort: high
reasoning summaries: none
session id: 019d569a-dc65-7581-b53e-c6f89aa6ec96
--------
user
Context: The user requested the proxy at 0xc95b806ac073df930014ac476d26c8ad918f14e0.
Audit the implementation at 0x00000000d681e85e5783588f87a9573cb97eda01.
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-ky1rccd0
succeeded in 0ms:
/tmp/codex-ky1rccd0
exec
/bin/bash -lc 'rg --files' in /tmp/codex-ky1rccd0
succeeded in 0ms:
AUDIT_BUNDLE.txt
codex-output-schema.json
src/permissions/MellowACL.sol
src/libraries/TransferLibrary.sol
src/libraries/SlotLibrary.sol
src/interfaces/permissions/IMellowACL.sol
src/utils/SwapModule.sol
src/interfaces/factories/IFactoryEntity.sol
src/interfaces/external/aave/IAaveOracle.sol
src/interfaces/utils/ISwapModule.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC1363.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC165.sol
lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol
lib/contracts/src/contracts/GPv2Settlement.sol
lib/contracts/src/contracts/GPv2VaultRelayer.sol
lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol
lib/openzeppelin-contracts-upgradeable/contracts/utils/ContextUpgradeable.sol
lib/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol
lib/openzeppelin-contracts-upgradeable/contracts/access/extensions/AccessControlEnumerableUpgradeable.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/access/IAccessControl.sol
lib/contracts/src/contracts/interfaces/GPv2Authentication.sol
lib/contracts/src/contracts/interfaces/GPv2EIP1271.sol
lib/contracts/src/contracts/interfaces/IERC20.sol
lib/contracts/src/contracts/interfaces/IVault.sol
lib/openzeppelin-contracts-upgradeable/contracts/utils/introspection/ERC165Upgradeable.sol
lib/contracts/src/contracts/mixins/GPv2Signing.sol
lib/contracts/src/contracts/mixins/ReentrancyGuard.sol
lib/contracts/src/contracts/mixins/StorageAccessible.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/access/extensions/IAccessControlEnumerable.sol
lib/contracts/src/contracts/libraries/GPv2SafeERC20.sol
lib/contracts/src/contracts/libraries/GPv2Transfer.sol
lib/contracts/src/contracts/libraries/GPv2Interaction.sol
lib/contracts/src/contracts/libraries/GPv2Order.sol
lib/contracts/src/contracts/libraries/SafeCast.sol
lib/contracts/src/contracts/libraries/SafeMath.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol
lib/contracts/src/contracts/libraries/GPv2Trade.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Errors.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/SlotDerivation.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Arrays.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Comparators.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Address.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/math/SafeCast.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/math/Math.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Panic.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol
lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol
exec
/bin/bash -lc 'wc -l AUDIT_BUNDLE.txt' in /tmp/codex-ky1rccd0
succeeded in 0ms:
658 AUDIT_BUNDLE.txt
exec
/bin/bash -lc "sed -n '1,260p' AUDIT_BUNDLE.txt" in /tmp/codex-ky1rccd0
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/interfaces/external/aave/IAaveOracle.sol
================================================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
interface IAaveOracle {
function getAssetPrice(address asset) external view returns (uint256);
}
================================================================================
FILE: src/interfaces/factories/IFactoryEntity.sol
================================================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
/// @title IFactoryEntity
interface IFactoryEntity {
/// @notice Initializes the factory-created entity with arbitrary initialization data.
/// @param initParams The initialization parameters.
function initialize(bytes calldata initParams) external;
/// @notice Emitted once the entity has been initialized.
/// @param initParams The initialization parameters.
event Initialized(bytes initParams);
}
================================================================================
FILE: src/interfaces/permissions/IMellowACL.sol
================================================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
/// @notice Interface for the MellowACL contract, which extends OpenZeppelin's AccessControlEnumerable
/// @dev Adds tracking of which roles are actively in use (i.e., assigned to at least one address)
interface IMellowACL is IAccessControlEnumerable {
/// @notice Storage layout used to track actively assigned roles
struct MellowACLStorage {
EnumerableSet.Bytes32Set supportedRoles; // Set of roles that have at least one assigned member
}
/// @notice Returns the total number of unique roles that are currently assigned
function supportedRoles() external view returns (uint256);
/// @notice Returns the role at the specified index in the set of active roles
/// @param index Index within the supported role set
/// @return role The bytes32 identifier of the role
function supportedRoleAt(uint256 index) external view returns (bytes32);
/// @notice Checks whether a given role is currently active (i.e., has at least one member)
/// @param role The bytes32 identifier of the role to check
/// @return isActive True if the role has any members assigned
function hasSupportedRole(bytes32 role) external view returns (bool);
/// @notice Emitted when a new role is granted for the first time
event RoleAdded(bytes32 indexed role);
/// @notice Emitted when a role loses its last member
event RoleRemoved(bytes32 indexed role);
}
================================================================================
FILE: src/interfaces/utils/ISwapModule.sol
================================================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import "../external/aave/IAaveOracle.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import {GPv2Settlement} from "@cowswap/contracts/GPv2Settlement.sol";
import {GPv2Order} from "@cowswap/contracts/libraries/GPv2Order.sol";
import "../factories/IFactoryEntity.sol";
interface ISwapModule is IFactoryEntity {
// Errors
error Forbidden(string reason);
error ZeroValue();
// Structs
struct SwapModuleStorage {
address subvault;
address oracle;
uint256 defaultMultiplier;
mapping(address => mapping(address => uint256)) customMultiplier;
}
struct Params {
address tokenIn;
address tokenOut;
uint256 amountIn;
uint256 minAmountOut;
uint256 deadline;
}
// View functions
function MIN_MULTIPLIER() external view returns (uint256);
function MAX_MULTIPLIER() external view returns (uint256);
function BASE_MULTIPLIER() external view returns (uint256);
function TOKEN_IN_ROLE() external view returns (bytes32);
function TOKEN_OUT_ROLE() external view returns (bytes32);
function ROUTER_ROLE() external view returns (bytes32);
function CALLER_ROLE() external view returns (bytes32);
function SET_SLIPPAGE_ROLE() external view returns (bytes32);
function cowswapSettlement() external view returns (address);
function cowswapVaultRelayer() external view returns (address);
function weth() external view returns (address);
function subvault() external view returns (address);
function oracle() external view returns (address);
function checkMultiplier(uint256 multiplier) external pure;
function defaultMultiplier() external view returns (uint256);
function customMultiplier(address tokenIn, address tokenOut) external view returns (uint256);
function evaluate(address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256);
function checkParams(Params calldata params) external view;
function checkCowswapOrder(Params calldata params, GPv2Order.Data calldata order, bytes calldata orderUid)
external
view;
// Mutable functions
function setOracle(address oracle_) external;
function setDefaultMultiplier(uint256 multiplier) external;
function setCustomMultiplier(address tokenIn, address tokenOut, uint256 multiplier) external;
function pushAssets(address asset, uint256 value) external payable;
function pullAssets(address asset, uint256 value) external;
function swap(Params calldata params, address router, bytes calldata data)
external
returns (bytes memory response);
function setCowswapApproval(address asset, uint256 amount) external;
function createLimitOrder(Params calldata params, GPv2Order.Data calldata order, bytes calldata orderUid)
external;
function invalidateOrder(bytes calldata orderUid) external;
// Events
event AssetsPulled(address indexed asset, uint256 value);
event AssetsPushed(address indexed asset, uint256 value);
event DefaultMultiplierSet(uint256 indexed multiplier);
event CustomMultiplierSet(address indexed tokenIn, address indexed tokenOut, uint256 indexed multiplier);
event CowswapApprovalSet(address indexed asset, uint256 amount);
event LimitOrderCreated(Params params, bytes orderUid);
event LimitOrderInvalidated(bytes orderUid);
event Swap(Params params, address router, uint256 amountOut);
event OracleSet(address indexed oracle);
}
================================================================================
FILE: src/libraries/SlotLibrary.sol
================================================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
/// @title SlotLibrary
/// @notice Library for computing deterministic and collision-resistant storage slots
/// @dev Used to generate unique storage slots for upgradeable modules using string identifiers
library SlotLibrary {
/// @notice Computes a unique storage slot based on the module's identifiers
/// @param contractName Logical contract/module name (e.g., "ShareModule")
/// @param name Human-readable instance name (e.g., "Mellow")
/// @param version Version number for the module configuration
/// @return A bytes32 value representing the derived storage slot
function getSlot(string memory contractName, string memory name, uint256 version) internal pure returns (bytes32) {
return keccak256(
abi.encode(
uint256(keccak256(abi.encodePacked("mellow.flexible-vaults.storage.", contractName, name, version))) - 1
)
) & ~bytes32(uint256(0xff));
}
}
================================================================================
FILE: src/libraries/TransferLibrary.sol
================================================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
/// @title TransferLibrary
/// @notice Library for unified handling of native ETH and ERC20 asset transfers.
/// @dev Provides safe and abstracted methods for sending and receiving both ETH and ERC20 tokens.
///
/// # ETH Convention
/// Uses the constant `ETH = 0xEeee...EeE` to distinguish native ETH from ERC20 tokens.
library TransferLibrary {
using SafeERC20 for IERC20;
/// @notice Error thrown when `msg.value` does not match expected ETH amount
error InvalidValue();
/// @dev Placeholder address used to represent native ETH transfers
address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
/// @notice Safely sends assets (ETH or ERC20) to a recipient
/// @param asset Address of the asset to send (use `ETH` constant for native ETH)
/// @param to Recipient address
/// @param assets Amount of assets to send
/// @dev Uses `Address.sendValue` for ETH and `safeTransfer` for ERC20
function sendAssets(address asset, address to, uint256 assets) internal {
if (asset == ETH) {
Address.sendValue(payable(to), assets);
} else {
IERC20(asset).safeTransfer(to, assets);
}
}
/// @notice Safely receives assets (ETH or ERC20) from a sender
/// @param asset Address of the asset to receive (use `ETH` constant for native ETH)
/// @param from Sender address (only used for ERC20)
/// @param assets Amount of assets expected to receive
/// @dev Reverts if `msg.value` is incorrect for ETH or uses `safeTransferFrom` for ERC20
function receiveAssets(address asset, address from, uint256 assets) internal {
if (asset == ETH) {
if (msg.value != assets) {
revert InvalidValue();
}
} else {
IERC20(asset).safeTransferFrom(from, address(this), assets);
}
}
exec
/bin/bash -lc "sed -n '261,520p' AUDIT_BUNDLE.txt" in /tmp/codex-ky1rccd0
succeeded in 0ms:
/// @notice Returns the balance of an account for a given asset
/// @param asset Address of the asset to check the balance of (use `ETH` constant for native ETH)
/// @param account Address of the account to check the balance of
/// @return Balance of the account for the given asset
function balanceOf(address asset, address account) internal view returns (uint256) {
if (asset == ETH) {
return account.balance;
} else {
return IERC20(asset).balanceOf(account);
}
}
}
================================================================================
FILE: src/permissions/MellowACL.sol
================================================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import "../interfaces/permissions/IMellowACL.sol";
import "../libraries/SlotLibrary.sol";
abstract contract MellowACL is IMellowACL, AccessControlEnumerableUpgradeable {
using EnumerableSet for EnumerableSet.Bytes32Set;
bytes32 private immutable _mellowACLStorageSlot;
constructor(string memory name_, uint256 version_) {
_mellowACLStorageSlot = SlotLibrary.getSlot("MellowACL", name_, version_);
_disableInitializers();
}
// View functions
/// @inheritdoc IMellowACL
function supportedRoles() external view returns (uint256) {
return _mellowACLStorage().supportedRoles.length();
}
/// @inheritdoc IMellowACL
function supportedRoleAt(uint256 index) external view returns (bytes32) {
return _mellowACLStorage().supportedRoles.at(index);
}
/// @inheritdoc IMellowACL
function hasSupportedRole(bytes32 role) external view returns (bool) {
return _mellowACLStorage().supportedRoles.contains(role);
}
// Internal functions
function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
if (super._grantRole(role, account)) {
if (_mellowACLStorage().supportedRoles.add(role)) {
emit RoleAdded(role);
}
return true;
}
return false;
}
function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
if (super._revokeRole(role, account)) {
if (getRoleMemberCount(role) == 0) {
_mellowACLStorage().supportedRoles.remove(role);
emit RoleRemoved(role);
}
return true;
}
return false;
}
function _mellowACLStorage() private view returns (MellowACLStorage storage $) {
bytes32 slot = _mellowACLStorageSlot;
assembly {
$.slot := slot
}
}
}
================================================================================
FILE: src/utils/SwapModule.sol
================================================================================
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import "../interfaces/utils/ISwapModule.sol";
import "../libraries/TransferLibrary.sol";
import "../permissions/MellowACL.sol";
contract SwapModule is ISwapModule, MellowACL, ReentrancyGuardUpgradeable {
using SafeERC20 for IERC20;
/// @inheritdoc ISwapModule
uint256 public constant MIN_MULTIPLIER = 0.9e8;
/// @inheritdoc ISwapModule
uint256 public constant MAX_MULTIPLIER = 1.1e8;
/// @inheritdoc ISwapModule
uint256 public constant BASE_MULTIPLIER = 1e8;
/// @inheritdoc ISwapModule
bytes32 public constant TOKEN_IN_ROLE = keccak256("utils.SwapModule.TOKEN_IN_ROLE");
/// @inheritdoc ISwapModule
bytes32 public constant TOKEN_OUT_ROLE = keccak256("utils.SwapModule.TOKEN_OUT_ROLE");
/// @inheritdoc ISwapModule
bytes32 public constant ROUTER_ROLE = keccak256("utils.SwapModule.ROUTER_ROLE");
/// @inheritdoc ISwapModule
bytes32 public constant CALLER_ROLE = keccak256("utils.SwapModule.CALLER_ROLE");
/// @inheritdoc ISwapModule
bytes32 public constant SET_SLIPPAGE_ROLE = keccak256("utils.SwapModule.SET_SLIPPAGE_ROLE");
/// @inheritdoc ISwapModule
address public immutable cowswapSettlement;
/// @inheritdoc ISwapModule
address public immutable cowswapVaultRelayer;
/// @inheritdoc ISwapModule
address public immutable weth;
bytes32 private immutable _swapModuleStorageSlot;
constructor(
string memory name_,
uint256 version_,
address cowswapSettlement_,
address cowswapVaultRelayer_,
address weth_
) MellowACL(name_, version_) {
cowswapSettlement = cowswapSettlement_;
cowswapVaultRelayer = cowswapVaultRelayer_;
weth = weth_;
_swapModuleStorageSlot = SlotLibrary.getSlot("SwapModule", name_, version_);
}
// View functions
modifier onlySubvault() {
if (_msgSender() != subvault()) {
revert Forbidden("msg.sender != subvault");
}
_;
}
/// @inheritdoc ISwapModule
function subvault() public view returns (address) {
return _swapModuleStorage().subvault;
}
/// @inheritdoc ISwapModule
function oracle() public view returns (address) {
return _swapModuleStorage().oracle;
}
/// @inheritdoc ISwapModule
function checkMultiplier(uint256 multiplier) public pure {
if (multiplier < MIN_MULTIPLIER || multiplier > MAX_MULTIPLIER) {
revert Forbidden("multiplier out of expected range");
}
}
/// @inheritdoc ISwapModule
function defaultMultiplier() public view returns (uint256) {
return _swapModuleStorage().defaultMultiplier;
}
/// @inheritdoc ISwapModule
function customMultiplier(address tokenIn, address tokenOut) public view returns (uint256) {
return _swapModuleStorage().customMultiplier[tokenIn][tokenOut];
}
/// @inheritdoc ISwapModule
function evaluate(address tokenIn, address tokenOut, uint256 amountIn) public view returns (uint256) {
tokenIn = tokenIn == TransferLibrary.ETH ? weth : tokenIn;
tokenOut = tokenOut == TransferLibrary.ETH ? weth : tokenOut;
uint8 decimalsIn = IERC20Metadata(tokenIn).decimals();
uint8 decimalsOut = IERC20Metadata(tokenOut).decimals();
IAaveOracle oracle_ = IAaveOracle(oracle());
uint256 tokenInPrice = oracle_.getAssetPrice(tokenIn);
uint256 tokenOutPrice = oracle_.getAssetPrice(tokenOut);
if (decimalsIn == decimalsOut) {
return Math.mulDiv(amountIn, tokenInPrice, tokenOutPrice);
} else if (decimalsIn < decimalsOut) {
return Math.mulDiv(amountIn, tokenInPrice * 10 ** (decimalsOut - decimalsIn), tokenOutPrice);
} else {
return Math.mulDiv(amountIn, tokenInPrice, tokenOutPrice * 10 ** (decimalsIn - decimalsOut));
}
}
/// @inheritdoc ISwapModule
function checkParams(Params calldata params) public view {
if (!hasRole(TOKEN_IN_ROLE, params.tokenIn)) {
revert Forbidden("tokenIn");
}
if (!hasRole(TOKEN_OUT_ROLE, params.tokenOut)) {
revert Forbidden("tokenOut");
}
if (TransferLibrary.balanceOf(params.tokenIn, address(this)) < params.amountIn || params.amountIn == 0) {
revert Forbidden("amountIn");
}
SwapModuleStorage storage $ = _swapModuleStorage();
uint256 multiplier = $.customMultiplier[params.tokenIn][params.tokenOut];
if (multiplier == 0) {
multiplier = $.defaultMultiplier;
}
uint256 oracleMinAmount =
Math.mulDiv(evaluate(params.tokenIn, params.tokenOut, params.amountIn), multiplier, BASE_MULTIPLIER);
if (params.minAmountOut < oracleMinAmount) {
revert Forbidden("minAmountOut < oracleMinAmount");
}
if (params.deadline < block.timestamp) {
revert Forbidden("deadline");
}
if (params.tokenIn == params.tokenOut) {
revert Forbidden("tokenIn == tokenOut");
}
}
/// @inheritdoc ISwapModule
function checkCowswapOrder(Params calldata params, GPv2Order.Data calldata order, bytes calldata orderUid)
public
view
{
if (params.tokenIn != address(order.sellToken) || params.tokenIn == TransferLibrary.ETH) {
revert Forbidden("sellToken");
}
if (params.tokenOut != address(order.buyToken) || params.tokenOut == TransferLibrary.ETH) {
revert Forbidden("buyToken");
}
if (address(this) != order.receiver) {
revert Forbidden("receiver");
}
if (params.amountIn != order.sellAmount) {
revert Forbidden("amountIn != sellAmount");
}
if (params.minAmountOut != order.buyAmount) {
revert Forbidden("minAmountOut != buyAmount");
}
if (params.deadline != order.validTo) {
revert Forbidden("deadline != validTo");
}
if (order.kind != GPv2Order.KIND_SELL) {
revert Forbidden("kind != KIND_SELL");
}
if (order.sellTokenBalance != GPv2Order.BALANCE_ERC20) {
revert Forbidden("sellTokenBalance != BALANCE_ERC20");
}
if (order.buyTokenBalance != GPv2Order.BALANCE_ERC20) {
revert Forbidden("buyTokenBalance != BALANCE_ERC20");
}
bytes memory calculatedOrderUid = new bytes(56);
GPv2Order.packOrderUidParams(
calculatedOrderUid,
GPv2Order.hash(order, GPv2Settlement(payable(cowswapSettlement)).domainSeparator()),
address(this),
order.validTo
);
exec
/bin/bash -lc "sed -n '521,760p' AUDIT_BUNDLE.txt" in /tmp/codex-ky1rccd0
succeeded in 0ms:
if (keccak256(orderUid) != keccak256(calculatedOrderUid)) {
revert Forbidden("orderUid != calculatedOrderUid");
}
}
// Mutable functions
/// @inheritdoc IFactoryEntity
function initialize(bytes calldata data) external initializer {
(
address admin,
address subvault_,
address oracle_,
uint256 defaultMultiplier_,
address[] memory holders,
bytes32[] memory roles
) = abi.decode(data, (address, address, address, uint256, address[], bytes32[]));
if (admin == address(0) || subvault_ == address(0) || oracle_ == address(0)) {
revert ZeroValue();
}
checkMultiplier(defaultMultiplier_);
SwapModuleStorage storage $ = _swapModuleStorage();
_grantRole(DEFAULT_ADMIN_ROLE, admin);
$.subvault = subvault_;
$.oracle = oracle_;
$.defaultMultiplier = defaultMultiplier_;
for (uint256 i = 0; i < holders.length; i++) {
if (holders[i] == address(0) || roles[i] == bytes32(0)) {
revert ZeroValue();
}
_grantRole(roles[i], holders[i]);
}
emit Initialized(data);
}
/// @inheritdoc ISwapModule
function setOracle(address oracle_) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (oracle_ == address(0)) {
revert ZeroValue();
}
_swapModuleStorage().oracle = oracle_;
emit OracleSet(oracle_);
}
/// @inheritdoc ISwapModule
function setDefaultMultiplier(uint256 multiplier) external onlyRole(SET_SLIPPAGE_ROLE) {
checkMultiplier(multiplier);
_swapModuleStorage().defaultMultiplier = multiplier;
emit DefaultMultiplierSet(multiplier);
}
/// @inheritdoc ISwapModule
function setCustomMultiplier(address tokenIn, address tokenOut, uint256 multiplier)
external
onlyRole(SET_SLIPPAGE_ROLE)
{
checkMultiplier(multiplier);
_swapModuleStorage().customMultiplier[tokenIn][tokenOut] = multiplier;
emit CustomMultiplierSet(tokenIn, tokenOut, multiplier);
}
/// @inheritdoc ISwapModule
function pushAssets(address asset, uint256 value) external payable onlySubvault {
TransferLibrary.receiveAssets(asset, _msgSender(), value);
emit AssetsPushed(asset, value);
}
/// @inheritdoc ISwapModule
function pullAssets(address asset, uint256 value) external onlySubvault {
TransferLibrary.sendAssets(asset, _msgSender(), value);
emit AssetsPulled(asset, value);
}
/// @inheritdoc ISwapModule
function swap(Params calldata params, address router, bytes calldata data)
external
nonReentrant
onlyRole(CALLER_ROLE)
returns (bytes memory response)
{
checkParams(params);
if (!hasRole(ROUTER_ROLE, router)) {
revert Forbidden("router");
}
uint256 balanceBefore = TransferLibrary.balanceOf(params.tokenOut, address(this));
if (params.tokenIn != TransferLibrary.ETH) {
IERC20(params.tokenIn).forceApprove(router, params.amountIn);
response = Address.functionCall(router, data);
IERC20(params.tokenIn).forceApprove(router, 0);
} else {
response = Address.functionCallWithValue(router, data, params.amountIn);
}
uint256 amountOut = TransferLibrary.balanceOf(params.tokenOut, address(this)) - balanceBefore;
if (amountOut < params.minAmountOut) {
revert Forbidden("amountOut < minAmountOut");
}
emit Swap(params, router, amountOut);
}
/// @inheritdoc ISwapModule
function setCowswapApproval(address asset, uint256 amount) external onlyRole(CALLER_ROLE) {
if (asset == TransferLibrary.ETH || !hasRole(TOKEN_IN_ROLE, asset)) {
revert Forbidden("asset");
}
IERC20(asset).forceApprove(cowswapVaultRelayer, amount);
emit CowswapApprovalSet(asset, amount);
}
/// @inheritdoc ISwapModule
function createLimitOrder(Params calldata params, GPv2Order.Data calldata order, bytes calldata orderUid)
external
onlyRole(CALLER_ROLE)
{
checkParams(params);
checkCowswapOrder(params, order, orderUid);
GPv2Settlement(payable(cowswapSettlement)).setPreSignature(orderUid, true);
emit LimitOrderCreated(params, orderUid);
}
/// @inheritdoc ISwapModule
function invalidateOrder(bytes calldata orderUid) external onlyRole(CALLER_ROLE) {
GPv2Settlement(payable(cowswapSettlement)).invalidateOrder(orderUid);
emit LimitOrderInvalidated(orderUid);
}
receive() external payable {}
// Internal functions
function _swapModuleStorage() internal view returns (SwapModuleStorage storage $) {
bytes32 slot = _swapModuleStorageSlot;
assembly {
$.slot := slot
}
}
}
exec
/bin/bash -lc "nl -ba src/utils/SwapModule.sol | sed -n '1,260p'" in /tmp/codex-ky1rccd0
succeeded in 0ms:
1 // SPDX-License-Identifier: BUSL-1.1
2 pragma solidity 0.8.25;
3
4 import "../interfaces/utils/ISwapModule.sol";
5
6 import "../libraries/TransferLibrary.sol";
7 import "../permissions/MellowACL.sol";
8
9 contract SwapModule is ISwapModule, MellowACL, ReentrancyGuardUpgradeable {
10 using SafeERC20 for IERC20;
11
12 /// @inheritdoc ISwapModule
13 uint256 public constant MIN_MULTIPLIER = 0.9e8;
14 /// @inheritdoc ISwapModule
15 uint256 public constant MAX_MULTIPLIER = 1.1e8;
16 /// @inheritdoc ISwapModule
17 uint256 public constant BASE_MULTIPLIER = 1e8;
18
19 /// @inheritdoc ISwapModule
20 bytes32 public constant TOKEN_IN_ROLE = keccak256("utils.SwapModule.TOKEN_IN_ROLE");
21 /// @inheritdoc ISwapModule
22 bytes32 public constant TOKEN_OUT_ROLE = keccak256("utils.SwapModule.TOKEN_OUT_ROLE");
23 /// @inheritdoc ISwapModule
24 bytes32 public constant ROUTER_ROLE = keccak256("utils.SwapModule.ROUTER_ROLE");
25 /// @inheritdoc ISwapModule
26 bytes32 public constant CALLER_ROLE = keccak256("utils.SwapModule.CALLER_ROLE");
27 /// @inheritdoc ISwapModule
28 bytes32 public constant SET_SLIPPAGE_ROLE = keccak256("utils.SwapModule.SET_SLIPPAGE_ROLE");
29
30 /// @inheritdoc ISwapModule
31 address public immutable cowswapSettlement;
32 /// @inheritdoc ISwapModule
33 address public immutable cowswapVaultRelayer;
34 /// @inheritdoc ISwapModule
35 address public immutable weth;
36
37 bytes32 private immutable _swapModuleStorageSlot;
38
39 constructor(
40 string memory name_,
41 uint256 version_,
42 address cowswapSettlement_,
43 address cowswapVaultRelayer_,
44 address weth_
45 ) MellowACL(name_, version_) {
46 cowswapSettlement = cowswapSettlement_;
47 cowswapVaultRelayer = cowswapVaultRelayer_;
48 weth = weth_;
49 _swapModuleStorageSlot = SlotLibrary.getSlot("SwapModule", name_, version_);
50 }
51
52 // View functions
53
54 modifier onlySubvault() {
55 if (_msgSender() != subvault()) {
56 revert Forbidden("msg.sender != subvault");
57 }
58 _;
59 }
60
61 /// @inheritdoc ISwapModule
62 function subvault() public view returns (address) {
63 return _swapModuleStorage().subvault;
64 }
65
66 /// @inheritdoc ISwapModule
67 function oracle() public view returns (address) {
68 return _swapModuleStorage().oracle;
69 }
70
71 /// @inheritdoc ISwapModule
72 function checkMultiplier(uint256 multiplier) public pure {
73 if (multiplier < MIN_MULTIPLIER || multiplier > MAX_MULTIPLIER) {
74 revert Forbidden("multiplier out of expected range");
75 }
76 }
77
78 /// @inheritdoc ISwapModule
79 function defaultMultiplier() public view returns (uint256) {
80 return _swapModuleStorage().defaultMultiplier;
81 }
82
83 /// @inheritdoc ISwapModule
84 function customMultiplier(address tokenIn, address tokenOut) public view returns (uint256) {
85 return _swapModuleStorage().customMultiplier[tokenIn][tokenOut];
86 }
87
88 /// @inheritdoc ISwapModule
89 function evaluate(address tokenIn, address tokenOut, uint256 amountIn) public view returns (uint256) {
90 tokenIn = tokenIn == TransferLibrary.ETH ? weth : tokenIn;
91 tokenOut = tokenOut == TransferLibrary.ETH ? weth : tokenOut;
92 uint8 decimalsIn = IERC20Metadata(tokenIn).decimals();
93 uint8 decimalsOut = IERC20Metadata(tokenOut).decimals();
94 IAaveOracle oracle_ = IAaveOracle(oracle());
95 uint256 tokenInPrice = oracle_.getAssetPrice(tokenIn);
96 uint256 tokenOutPrice = oracle_.getAssetPrice(tokenOut);
97 if (decimalsIn == decimalsOut) {
98 return Math.mulDiv(amountIn, tokenInPrice, tokenOutPrice);
99 } else if (decimalsIn < decimalsOut) {
100 return Math.mulDiv(amountIn, tokenInPrice * 10 ** (decimalsOut - decimalsIn), tokenOutPrice);
101 } else {
102 return Math.mulDiv(amountIn, tokenInPrice, tokenOutPrice * 10 ** (decimalsIn - decimalsOut));
103 }
104 }
105
106 /// @inheritdoc ISwapModule
107 function checkParams(Params calldata params) public view {
108 if (!hasRole(TOKEN_IN_ROLE, params.tokenIn)) {
109 revert Forbidden("tokenIn");
110 }
111 if (!hasRole(TOKEN_OUT_ROLE, params.tokenOut)) {
112 revert Forbidden("tokenOut");
113 }
114 if (TransferLibrary.balanceOf(params.tokenIn, address(this)) < params.amountIn || params.amountIn == 0) {
115 revert Forbidden("amountIn");
116 }
117 SwapModuleStorage storage $ = _swapModuleStorage();
118 uint256 multiplier = $.customMultiplier[params.tokenIn][params.tokenOut];
119 if (multiplier == 0) {
120 multiplier = $.defaultMultiplier;
121 }
122 uint256 oracleMinAmount =
123 Math.mulDiv(evaluate(params.tokenIn, params.tokenOut, params.amountIn), multiplier, BASE_MULTIPLIER);
124
125 if (params.minAmountOut < oracleMinAmount) {
126 revert Forbidden("minAmountOut < oracleMinAmount");
127 }
128 if (params.deadline < block.timestamp) {
129 revert Forbidden("deadline");
130 }
131 if (params.tokenIn == params.tokenOut) {
132 revert Forbidden("tokenIn == tokenOut");
133 }
134 }
135
136 /// @inheritdoc ISwapModule
137 function checkCowswapOrder(Params calldata params, GPv2Order.Data calldata order, bytes calldata orderUid)
138 public
139 view
140 {
141 if (params.tokenIn != address(order.sellToken) || params.tokenIn == TransferLibrary.ETH) {
142 revert Forbidden("sellToken");
143 }
144 if (params.tokenOut != address(order.buyToken) || params.tokenOut == TransferLibrary.ETH) {
145 revert Forbidden("buyToken");
146 }
147 if (address(this) != order.receiver) {
148 revert Forbidden("receiver");
149 }
150 if (params.amountIn != order.sellAmount) {
151 revert Forbidden("amountIn != sellAmount");
152 }
153 if (params.minAmountOut != order.buyAmount) {
154 revert Forbidden("minAmountOut != buyAmount");
155 }
156 if (params.deadline != order.validTo) {
157 revert Forbidden("deadline != validTo");
158 }
159 if (order.kind != GPv2Order.KIND_SELL) {
160 revert Forbidden("kind != KIND_SELL");
161 }
162 if (order.sellTokenBalance != GPv2Order.BALANCE_ERC20) {
163 revert Forbidden("sellTokenBalance != BALANCE_ERC20");
164 }
165 if (order.buyTokenBalance != GPv2Order.BALANCE_ERC20) {
166 revert Forbidden("buyTokenBalance != BALANCE_ERC20");
167 }
168
169 bytes memory calculatedOrderUid = new bytes(56);
170 GPv2Order.packOrderUidParams(
171 calculatedOrderUid,
172 GPv2Order.hash(order, GPv2Settlement(payable(cowswapSettlement)).domainSeparator()),
173 address(this),
174 order.validTo
175 );
176 if (keccak256(orderUid) != keccak256(calculatedOrderUid)) {
177 revert Forbidden("orderUid != calculatedOrderUid");
178 }
179 }
180
181 // Mutable functions
182
183 /// @inheritdoc IFactoryEntity
184 function initialize(bytes calldata data) external initializer {
185 (
186 address admin,
187 address subvault_,
188 address oracle_,
189 uint256 defaultMultiplier_,
190 address[] memory holders,
191 bytes32[] memory roles
192 ) = abi.decode(data, (address, address, address, uint256, address[], bytes32[]));
193 if (admin == address(0) || subvault_ == address(0) || oracle_ == address(0)) {
194 revert ZeroValue();
195 }
196 checkMultiplier(defaultMultiplier_);
197 SwapModuleStorage storage $ = _swapModuleStorage();
198
199 _grantRole(DEFAULT_ADMIN_ROLE, admin);
200 $.subvault = subvault_;
201 $.oracle = oracle_;
202 $.defaultMultiplier = defaultMultiplier_;
203 for (uint256 i = 0; i < holders.length; i++) {
204 if (holders[i] == address(0) || roles[i] == bytes32(0)) {
205 revert ZeroValue();
206 }
207 _grantRole(roles[i], holders[i]);
208 }
209 emit Initialized(data);
210 }
211
212 /// @inheritdoc ISwapModule
213 function setOracle(address oracle_) external onlyRole(DEFAULT_ADMIN_ROLE) {
214 if (oracle_ == address(0)) {
215 revert ZeroValue();
216 }
217 _swapModuleStorage().oracle = oracle_;
218 emit OracleSet(oracle_);
219 }
220
221 /// @inheritdoc ISwapModule
222 function setDefaultMultiplier(uint256 multiplier) external onlyRole(SET_SLIPPAGE_ROLE) {
223 checkMultiplier(multiplier);
224 _swapModuleStorage().defaultMultiplier = multiplier;
225 emit DefaultMultiplierSet(multiplier);
226 }
227
228 /// @inheritdoc ISwapModule
229 function setCustomMultiplier(address tokenIn, address tokenOut, uint256 multiplier)
230 external
231 onlyRole(SET_SLIPPAGE_ROLE)
232 {
233 checkMultiplier(multiplier);
234 _swapModuleStorage().customMultiplier[tokenIn][tokenOut] = multiplier;
235 emit CustomMultiplierSet(tokenIn, tokenOut, multiplier);
236 }
237
238 /// @inheritdoc ISwapModule
239 function pushAssets(address asset, uint256 value) external payable onlySubvault {
240 TransferLibrary.receiveAssets(asset, _msgSender(), value);
241 emit AssetsPushed(asset, value);
242 }
243
244 /// @inheritdoc ISwapModule
245 function pullAssets(address asset, uint256 value) external onlySubvault {
246 TransferLibrary.sendAssets(asset, _msgSender(), value);
247 emit AssetsPulled(asset, value);
248 }
249
250 /// @inheritdoc ISwapModule
251 function swap(Params calldata params, address router, bytes calldata data)
252 external
253 nonReentrant
254 onlyRole(CALLER_ROLE)
255 returns (bytes memory response)
256 {
257 checkParams(params);
258 if (!hasRole(ROUTER_ROLE, router)) {
259 revert Forbidden("router");
260 }
exec
/bin/bash -lc "sed -n '1,220p' lib/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol" in /tmp/codex-ky1rccd0
succeeded in 0ms:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)
pragma solidity ^0.8.20;
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
* consider using {ReentrancyGuardTransient} instead.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuardUpgradeable is Initializable {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
/// @custom:storage-location erc7201:openzeppelin.storage.ReentrancyGuard
struct ReentrancyGuardStorage {
uint256 _status;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ReentrancyGuardStorageLocation = 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;
function _getReentrancyGuardStorage() private pure returns (ReentrancyGuardStorage storage $) {
assembly {
$.slot := ReentrancyGuardStorageLocation
}
}
/**
* @dev Unauthorized reentrant call.
*/
error ReentrancyGuardReentrantCall();
function __ReentrancyGuard_init() internal onlyInitializing {
__ReentrancyGuard_init_unchained();
}
function __ReentrancyGuard_init_unchained() internal onlyInitializing {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
$._status = NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
// On the first call to nonReentrant, _status will be NOT_ENTERED
if ($._status == ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Any calls to nonReentrant after this point will fail
$._status = ENTERED;
}
function _nonReentrantAfter() private {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
$._status = NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
return $._status == ENTERED;
}
}
exec
/bin/bash -lc "sed -n '1,220p' lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol" in /tmp/codex-ky1rccd0
succeeded in 0ms:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (access/AccessControl.sol)
pragma solidity ^0.8.20;
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
import {ContextUpgradeable} from "../utils/ContextUpgradeable.sol";
import {ERC165Upgradeable} from "../utils/introspection/ERC165Upgradeable.sol";
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Contract module that allows children to implement role-based access
* control mechanisms. This is a lightweight version that doesn't allow enumerating role
* members except through off-chain means by accessing the contract event logs. Some
* applications may benefit from on-chain enumerability, for those cases see
* {AccessControlEnumerable}.
*
* Roles are referred to by their `bytes32` identifier. These should be exposed
* in the external API and be unique. The best way to achieve this is by
* using `public constant` hash digests:
*
* ```solidity
* bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
* ```
*
* Roles can be used to represent a set of permissions. To restrict access to a
* function call, use {hasRole}:
*
* ```solidity
* function foo() public {
* require(hasRole(MY_ROLE, msg.sender));
* ...
* }
* ```
*
* Roles can be granted and revoked dynamically via the {grantRole} and
* {revokeRole} functions. Each role has an associated admin role, and only
* accounts that have a role's admin role can call {grantRole} and {revokeRole}.
*
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
* that only accounts with this role will be able to grant or revoke other
* roles. More complex role relationships can be created by using
* {_setRoleAdmin}.
*
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
* grant and revoke this role. Extra precautions should be taken to secure
* accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
* to enforce additional security measures for this role.
*/
abstract contract AccessControlUpgradeable is Initializable, ContextUpgradeable, IAccessControl, ERC165Upgradeable {
struct RoleData {
mapping(address account => bool) hasRole;
bytes32 adminRole;
}
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/// @custom:storage-location erc7201:openzeppelin.storage.AccessControl
struct AccessControlStorage {
mapping(bytes32 role => RoleData) _roles;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.AccessControl")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant AccessControlStorageLocation = 0x02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800;
function _getAccessControlStorage() private pure returns (AccessControlStorage storage $) {
assembly {
$.slot := AccessControlStorageLocation
}
}
/**
* @dev Modifier that checks that an account has a specific role. Reverts
* with an {AccessControlUnauthorizedAccount} error including the required role.
*/
modifier onlyRole(bytes32 role) {
_checkRole(role);
_;
}
function __AccessControl_init() internal onlyInitializing {
}
function __AccessControl_init_unchained() internal onlyInitializing {
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) public view virtual returns (bool) {
AccessControlStorage storage $ = _getAccessControlStorage();
return $._roles[role].hasRole[account];
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
* is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
*/
function _checkRole(bytes32 role) internal view virtual {
_checkRole(role, _msgSender());
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
* is missing `role`.
*/
function _checkRole(bytes32 role, address account) internal view virtual {
if (!hasRole(role, account)) {
revert AccessControlUnauthorizedAccount(account, role);
}
}
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
AccessControlStorage storage $ = _getAccessControlStorage();
return $._roles[role].adminRole;
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleGranted} event.
*/
function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleRevoked} event.
*/
function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been revoked `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*
* May emit a {RoleRevoked} event.
*/
function renounceRole(bytes32 role, address callerConfirmation) public virtual {
if (callerConfirmation != _msgSender()) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}
/**
* @dev Sets `adminRole` as ``role``'s admin role.
*
* Emits a {RoleAdminChanged} event.
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
AccessControlStorage storage $ = _getAccessControlStorage();
bytes32 previousAdminRole = getRoleAdmin(role);
$._roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
*
* Internal function without access restriction.
*
* May emit a {RoleGranted} event.
*/
function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
AccessControlStorage storage $ = _getAccessControlStorage();
if (!hasRole(role, account)) {
$._roles[role].hasRole[account] = true;
emit RoleGranted(role, account, _msgSender());
return true;
} else {
return false;
}
}
/**
* @dev Attempts to revoke `role` from `account` and returns a boolean indicating if `role` was revoked.
*
* Internal function without access restriction.
*
exec
/bin/bash -lc "sed -n '1,260p' lib/contracts/src/contracts/libraries/GPv2Order.sol" in /tmp/codex-ky1rccd0
succeeded in 0ms:
// SPDX-License-Identifier: LGPL-3.0-or-later
pragma solidity >=0.7.6 <0.9.0;
import "../interfaces/IERC20.sol";
/// @title Gnosis Protocol v2 Order Library
/// @author Gnosis Developers
library GPv2Order {
/// @dev The complete data for a Gnosis Protocol order. This struct contains
/// all order parameters that are signed for submitting to GP.
struct Data {
IERC20 sellToken;
IERC20 buyToken;
address receiver;
uint256 sellAmount;
uint256 buyAmount;
uint32 validTo;
bytes32 appData;
uint256 feeAmount;
bytes32 kind;
bool partiallyFillable;
bytes32 sellTokenBalance;
bytes32 buyTokenBalance;
}
/// @dev The order EIP-712 type hash for the [`GPv2Order.Data`] struct.
///
/// This value is pre-computed from the following expression:
/// ```
/// keccak256(
/// "Order(" +
/// "address sellToken," +
/// "address buyToken," +
/// "address receiver," +
/// "uint256 sellAmount," +
/// "uint256 buyAmount," +
/// "uint32 validTo," +
/// "bytes32 appData," +
/// "uint256 feeAmount," +
/// "string kind," +
/// "bool partiallyFillable," +
/// "string sellTokenBalance," +
/// "string buyTokenBalance" +
/// ")"
/// )
/// ```
bytes32 internal constant TYPE_HASH =
hex"d5a25ba2e97094ad7d83dc28a6572da797d6b3e7fc6663bd93efb789fc17e489";
/// @dev The marker value for a sell order for computing the order struct
/// hash. This allows the EIP-712 compatible wallets to display a
/// descriptive string for the order kind (instead of 0 or 1).
///
/// This value is pre-computed from the following expression:
/// ```
/// keccak256("sell")
/// ```
bytes32 internal constant KIND_SELL =
hex"f3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775";
/// @dev The OrderKind marker value for a buy order for computing the order
/// struct hash.
///
/// This value is pre-computed from the following expression:
/// ```
/// keccak256("buy")
/// ```
bytes32 internal constant KIND_BUY =
hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc";
/// @dev The TokenBalance marker value for using direct ERC20 balances for
/// computing the order struct hash.
///
/// This value is pre-computed from the following expression:
/// ```
/// keccak256("erc20")
/// ```
bytes32 internal constant BALANCE_ERC20 =
hex"5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9";
/// @dev The TokenBalance marker value for using Balancer Vault external
/// balances (in order to re-use Vault ERC20 approvals) for computing the
/// order struct hash.
///
/// This value is pre-computed from the following expression:
/// ```
/// keccak256("external")
/// ```
bytes32 internal constant BALANCE_EXTERNAL =
hex"abee3b73373acd583a130924aad6dc38cfdc44ba0555ba94ce2ff63980ea0632";
/// @dev The TokenBalance marker value for using Balancer Vault internal
/// balances for computing the order struct hash.
///
/// This value is pre-computed from the following expression:
/// ```
/// keccak256("internal")
/// ```
bytes32 internal constant BALANCE_INTERNAL =
hex"4ac99ace14ee0a5ef932dc609df0943ab7ac16b7583634612f8dc35a4289a6ce";
/// @dev Marker address used to indicate that the receiver of the trade
/// proceeds should the owner of the order.
///
/// This is chosen to be `address(0)` for gas efficiency as it is expected
/// to be the most common case.
address internal constant RECEIVER_SAME_AS_OWNER = address(0);
/// @dev The byte length of an order unique identifier.
uint256 internal constant UID_LENGTH = 56;
/// @dev Returns the actual receiver for an order. This function checks
/// whether or not the [`receiver`] field uses the marker value to indicate
/// it is the same as the order owner.
///
/// @return receiver The actual receiver of trade proceeds.
function actualReceiver(
Data memory order,
address owner
) internal pure returns (address receiver) {
if (order.receiver == RECEIVER_SAME_AS_OWNER) {
receiver = owner;
} else {
receiver = order.receiver;
}
}
/// @dev Return the EIP-712 signing hash for the specified order.
///
/// @param order The order to compute the EIP-712 signing hash for.
/// @param domainSeparator The EIP-712 domain separator to use.
/// @return orderDigest The 32 byte EIP-712 struct hash.
function hash(
Data memory order,
bytes32 domainSeparator
) internal pure returns (bytes32 orderDigest) {
bytes32 structHash;
// NOTE: Compute the EIP-712 order struct hash in place. As suggested
// in the EIP proposal, noting that the order struct has 12 fields, and
// prefixing the type hash `(1 + 12) * 32 = 416` bytes to hash.
// <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#rationale-for-encodedata>
// solhint-disable-next-line no-inline-assembly
assembly {
let dataStart := sub(order, 32)
let temp := mload(dataStart)
mstore(dataStart, TYPE_HASH)
structHash := keccak256(dataStart, 416)
mstore(dataStart, temp)
}
// NOTE: Now that we have the struct hash, compute the EIP-712 signing
// hash using scratch memory past the free memory pointer. The signing
// hash is computed from `"\x19\x01" || domainSeparator || structHash`.
// <https://docs.soliditylang.org/en/v0.7.6/internals/layout_in_memory.html#layout-in-memory>
// <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#specification>
// solhint-disable-next-line no-inline-assembly
assembly {
let freeMemoryPointer := mload(0x40)
mstore(freeMemoryPointer, "\x19\x01")
mstore(add(freeMemoryPointer, 2), domainSeparator)
mstore(add(freeMemoryPointer, 34), structHash)
orderDigest := keccak256(freeMemoryPointer, 66)
}
}
/// @dev Packs order UID parameters into the specified memory location. The
/// result is equivalent to `abi.encodePacked(...)` with the difference that
/// it allows re-using the memory for packing the order UID.
///
/// This function reverts if the order UID buffer is not the correct size.
///
/// @param orderUid The buffer pack the order UID parameters into.
/// @param orderDigest The EIP-712 struct digest derived from the order
/// parameters.
/// @param owner The address of the user who owns this order.
/// @param validTo The epoch time at which the order will stop being valid.
function packOrderUidParams(
bytes memory orderUid,
bytes32 orderDigest,
address owner,
uint32 validTo
) internal pure {
require(orderUid.length == UID_LENGTH, "GPv2: uid buffer overflow");
// NOTE: Write the order UID to the allocated memory buffer. The order
// parameters are written to memory in **reverse order** as memory
// operations write 32-bytes at a time and we want to use a packed
// encoding. This means, for example, that after writing the value of
// `owner` to bytes `20:52`, writing the `orderDigest` to bytes `0:32`
// will **overwrite** bytes `20:32`. This is desirable as addresses are
// only 20 bytes and `20:32` should be `0`s:
//
// | 1111111111222222222233333333334444444444555555
// byte | 01234567890123456789012345678901234567890123456789012345
// -------+---------------------------------------------------------
// field | [.........orderDigest..........][......owner.......][vT]
// -------+---------------------------------------------------------
// mstore | [000000000000000000000000000.vT]
// | [00000000000.......owner.......]
// | [.........orderDigest..........]
//
// Additionally, since Solidity `bytes memory` are length prefixed,
// 32 needs to be added to all the offsets.
//
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(add(orderUid, 56), validTo)
mstore(add(orderUid, 52), owner)
mstore(add(orderUid, 32), orderDigest)
}
}
/// @dev Extracts specific order information from the standardized unique
/// order id of the protocol.
///
/// @param orderUid The unique identifier used to represent an order in
/// the protocol. This uid is the packed concatenation of the order digest,
/// the validTo order parameter and the address of the user who created the
/// order. It is used by the user to interface with the contract directly,
/// and not by calls that are triggered by the solvers.
/// @return orderDigest The EIP-712 signing digest derived from the order
/// parameters.
/// @return owner The address of the user who owns this order.
/// @return validTo The epoch time at which the order will stop being valid.
function extractOrderUidParams(
bytes calldata orderUid
)
internal
pure
returns (bytes32 orderDigest, address owner, uint32 validTo)
{
require(orderUid.length == UID_LENGTH, "GPv2: invalid uid");
// Use assembly to efficiently decode packed calldata.
// solhint-disable-next-line no-inline-assembly
assembly {
orderDigest := calldataload(orderUid.offset)
owner := shr(96, calldataload(add(orderUid.offset, 32)))
validTo := shr(224, calldataload(add(orderUid.offset, 52)))
}
}
}
exec
/bin/bash -lc 'rg -n "struct Data|feeAmount|partiallyFillable" lib/contracts/src/contracts/libraries/GPv2Order.sol' in /tmp/codex-ky1rccd0
succeeded in 0ms:
11: struct Data {
19: uint256 feeAmount;
21: bool partiallyFillable;
39: /// "uint256 feeAmount," +
41: /// "bool partiallyFillable," +
exec
/bin/bash -lc "rg -n \"feeAmount|sellAmount \\+|partiallyFillable\" lib/contracts/src/contracts -g '"'!**/test/**'"'" in /tmp/codex-ky1rccd0
succeeded in 0ms:
lib/contracts/src/contracts/GPv2Settlement.sol:56: uint256 feeAmount,
lib/contracts/src/contracts/GPv2Settlement.sol:192: feeTransfer.amount = order.feeAmount;
lib/contracts/src/contracts/GPv2Settlement.sol:238: order.feeAmount,
lib/contracts/src/contracts/GPv2Settlement.sol:379: if (order.partiallyFillable) {
lib/contracts/src/contracts/GPv2Settlement.sol:381: executedFeeAmount = order.feeAmount.mul(executedSellAmount).div(
lib/contracts/src/contracts/GPv2Settlement.sol:386: executedFeeAmount = order.feeAmount;
lib/contracts/src/contracts/GPv2Settlement.sol:401: if (order.partiallyFillable) {
lib/contracts/src/contracts/GPv2Settlement.sol:403: executedFeeAmount = order.feeAmount.mul(executedBuyAmount).div(
lib/contracts/src/contracts/GPv2Settlement.sol:408: executedFeeAmount = order.feeAmount;
lib/contracts/src/contracts/libraries/GPv2Order.sol:19: uint256 feeAmount;
lib/contracts/src/contracts/libraries/GPv2Order.sol:21: bool partiallyFillable;
lib/contracts/src/contracts/libraries/GPv2Order.sol:39: /// "uint256 feeAmount," +
lib/contracts/src/contracts/libraries/GPv2Order.sol:41: /// "bool partiallyFillable," +
lib/contracts/src/contracts/libraries/GPv2Trade.sol:24: uint256 feeAmount;
lib/contracts/src/contracts/libraries/GPv2Trade.sol:48: order.feeAmount = trade.feeAmount;
lib/contracts/src/contracts/libraries/GPv2Trade.sol:51: order.partiallyFillable,
lib/contracts/src/contracts/libraries/GPv2Trade.sol:102: bool partiallyFillable,
lib/contracts/src/contracts/libraries/GPv2Trade.sol:113: partiallyFillable = flags & 0x02 != 0;
exec
/bin/bash -lc 'rg -n "filledAmounts|executedAmount|feeAmount" lib/contracts/src/contracts/GPv2Settlement.sol' in /tmp/codex-ky1rccd0
succeeded in 0ms:
56: uint256 feeAmount,
151: /// order will always be fully executed, so the trade's `executedAmount`
175: uint256 limitAmount = trade.executedAmount;
192: feeTransfer.amount = order.feeAmount;
238: order.feeAmount,
314: trade.executedAmount,
331: /// @param executedAmount The portion of the order to execute. This will be
341: uint256 executedAmount,
380: executedSellAmount = executedAmount;
381: executedFeeAmount = order.feeAmount.mul(executedSellAmount).div(
386: executedFeeAmount = order.feeAmount;
402: executedBuyAmount = executedAmount;
403: executedFeeAmount = order.feeAmount.mul(executedBuyAmount).div(
408: executedFeeAmount = order.feeAmount;
exec
/bin/bash -lc "sed -n '330,420p' lib/contracts/src/contracts/GPv2Settlement.sol" in /tmp/codex-ky1rccd0
succeeded in 0ms:
/// @param buyPrice The price of the order's buy token.
/// @param executedAmount The portion of the order to execute. This will be
/// ignored for fill-or-kill orders.
/// @param inTransfer Memory location for computed executed sell amount
/// transfer.
/// @param outTransfer Memory location for computed executed buy amount
/// transfer.
function computeTradeExecution(
RecoveredOrder memory recoveredOrder,
uint256 sellPrice,
uint256 buyPrice,
uint256 executedAmount,
GPv2Transfer.Data memory inTransfer,
GPv2Transfer.Data memory outTransfer
) internal {
GPv2Order.Data memory order = recoveredOrder.data;
bytes memory orderUid = recoveredOrder.uid;
// solhint-disable-next-line not-rely-on-time
require(order.validTo >= block.timestamp, "GPv2: order expired");
// NOTE: The following computation is derived from the equation:
// ```
// amount_x * price_x = amount_y * price_y
// ```
// Intuitively, if a chocolate bar is 0,50€ and a beer is 4€, 1 beer
// is roughly worth 8 chocolate bars (`1 * 4 = 8 * 0.5`). From this
// equation, we can derive:
// - The limit price for selling `x` and buying `y` is respected iff
// ```
// limit_x * price_x >= limit_y * price_y
// ```
// - The executed amount of token `y` given some amount of `x` and
// clearing prices is:
// ```
// amount_y = amount_x * price_x / price_y
// ```
require(
order.sellAmount.mul(sellPrice) >= order.buyAmount.mul(buyPrice),
"GPv2: limit price not respected"
);
uint256 executedSellAmount;
uint256 executedBuyAmount;
uint256 executedFeeAmount;
uint256 currentFilledAmount;
if (order.kind == GPv2Order.KIND_SELL) {
if (order.partiallyFillable) {
executedSellAmount = executedAmount;
executedFeeAmount = order.feeAmount.mul(executedSellAmount).div(
order.sellAmount
);
} else {
executedSellAmount = order.sellAmount;
executedFeeAmount = order.feeAmount;
}
executedBuyAmount = executedSellAmount.mul(sellPrice).ceilDiv(
buyPrice
);
currentFilledAmount = filledAmount[orderUid].add(
executedSellAmount
);
require(
currentFilledAmount <= order.sellAmount,
"GPv2: order filled"
);
} else {
if (order.partiallyFillable) {
executedBuyAmount = executedAmount;
executedFeeAmount = order.feeAmount.mul(executedBuyAmount).div(
order.buyAmount
);
} else {
executedBuyAmount = order.buyAmount;
executedFeeAmount = order.feeAmount;
}
executedSellAmount = executedBuyAmount.mul(buyPrice).div(sellPrice);
currentFilledAmount = filledAmount[orderUid].add(executedBuyAmount);
require(
currentFilledAmount <= order.buyAmount,
"GPv2: order filled"
);
}
executedSellAmount = executedSellAmount.add(executedFeeAmount);
exec
/bin/bash -lc "sed -n '160,205p' lib/contracts/src/contracts/GPv2Settlement.sol" in /tmp/codex-ky1rccd0
succeeded in 0ms:
recoverOrderFromTrade(recoveredOrder, tokens, trade);
IVault.SwapKind kind = order.kind == GPv2Order.KIND_SELL
? IVault.SwapKind.GIVEN_IN
: IVault.SwapKind.GIVEN_OUT;
IVault.FundManagement memory funds;
funds.sender = recoveredOrder.owner;
funds.fromInternalBalance =
order.sellTokenBalance == GPv2Order.BALANCE_INTERNAL;
funds.recipient = payable(recoveredOrder.receiver);
funds.toInternalBalance =
order.buyTokenBalance == GPv2Order.BALANCE_INTERNAL;
int256[] memory limits = new int256[](tokens.length);
uint256 limitAmount = trade.executedAmount;
// NOTE: Array allocation initializes elements to 0, so we only need to
// set the limits we care about. This ensures that the swap will respect
// the order's limit price.
if (order.kind == GPv2Order.KIND_SELL) {
require(limitAmount >= order.buyAmount, "GPv2: limit too low");
limits[trade.sellTokenIndex] = order.sellAmount.toInt256();
limits[trade.buyTokenIndex] = -limitAmount.toInt256();
} else {
require(limitAmount <= order.sellAmount, "GPv2: limit too high");
limits[trade.sellTokenIndex] = limitAmount.toInt256();
limits[trade.buyTokenIndex] = -order.buyAmount.toInt256();
}
GPv2Transfer.Data memory feeTransfer;
feeTransfer.account = recoveredOrder.owner;
feeTransfer.token = order.sellToken;
feeTransfer.amount = order.feeAmount;
feeTransfer.balance = order.sellTokenBalance;
int256[] memory tokenDeltas = vaultRelayer.batchSwapWithFee(
kind,
swaps,
tokens,
funds,
limits,
// NOTE: Specify a deadline to ensure that an expire order
// cannot be used to trade.
order.validTo,
feeTransfer
);
exec
/bin/bash -lc 'rg -n "feeTransfer|transfer fee|fee token|feeAmount" lib/contracts/src/contracts/GPv2VaultRelayer.sol lib/contracts/src/contracts/GPv2Settlement.sol' in /tmp/codex-ky1rccd0
succeeded in 0ms:
lib/contracts/src/contracts/GPv2Settlement.sol:56: uint256 feeAmount,
lib/contracts/src/contracts/GPv2Settlement.sol:189: GPv2Transfer.Data memory feeTransfer;
lib/contracts/src/contracts/GPv2Settlement.sol:190: feeTransfer.account = recoveredOrder.owner;
lib/contracts/src/contracts/GPv2Settlement.sol:191: feeTransfer.token = order.sellToken;
lib/contracts/src/contracts/GPv2Settlement.sol:192: feeTransfer.amount = order.feeAmount;
lib/contracts/src/contracts/GPv2Settlement.sol:193: feeTransfer.balance = order.sellTokenBalance;
lib/contracts/src/contracts/GPv2Settlement.sol:204: feeTransfer
lib/contracts/src/contracts/GPv2Settlement.sol:238: order.feeAmount,
lib/contracts/src/contracts/GPv2Settlement.sol:381: executedFeeAmount = order.feeAmount.mul(executedSellAmount).div(
lib/contracts/src/contracts/GPv2Settlement.sol:386: executedFeeAmount = order.feeAmount;
lib/contracts/src/contracts/GPv2Settlement.sol:403: executedFeeAmount = order.feeAmount.mul(executedBuyAmount).div(
lib/contracts/src/contracts/GPv2Settlement.sol:408: executedFeeAmount = order.feeAmount;
lib/contracts/src/contracts/GPv2VaultRelayer.sol:64: /// @param feeTransfer The transfer data for the caller fee.
lib/contracts/src/contracts/GPv2VaultRelayer.sol:73: GPv2Transfer.Data calldata feeTransfer
lib/contracts/src/contracts/GPv2VaultRelayer.sol:83: vault.fastTransferFromAccount(feeTransfer, msg.sender);
exec
/bin/bash -lc "sed -n '1,260p' lib/contracts/src/contracts/GPv2VaultRelayer.sol" in /tmp/codex-ky1rccd0
succeeded in 0ms:
// SPDX-License-Identifier: LGPL-3.0-or-later
pragma solidity >=0.7.6 <0.9.0;
pragma abicoder v2;
import "./interfaces/IERC20.sol";
import "./interfaces/IVault.sol";
import "./libraries/GPv2Transfer.sol";
/// @title Gnosis Protocol v2 Vault Relayer Contract
/// @author Gnosis Developers
contract GPv2VaultRelayer {
using GPv2Transfer for IVault;
/// @dev The creator of the contract which has special permissions. This
/// value is set at creation time and cannot change.
address private immutable creator;
/// @dev The vault this relayer is for.
IVault private immutable vault;
constructor(IVault vault_) {
creator = msg.sender;
vault = vault_;
}
/// @dev Modifier that ensures that a function can only be called by the
/// creator of this contract.
modifier onlyCreator() {
require(msg.sender == creator, "GPv2: not creator");
_;
}
/// @dev Transfers all sell amounts for the executed trades from their
/// owners to the caller.
///
/// This function reverts if:
/// - The caller is not the creator of the vault relayer
/// - Any ERC20 transfer fails
///
/// @param transfers The transfers to execute.
function transferFromAccounts(
GPv2Transfer.Data[] calldata transfers
) external onlyCreator {
vault.transferFromAccounts(transfers, msg.sender);
}
/// @dev Performs a Balancer batched swap on behalf of a user and sends a
/// fee to the caller.
///
/// This function reverts if:
/// - The caller is not the creator of the vault relayer
/// - The swap fails
/// - The fee transfer fails
///
/// @param kind The Balancer swap kind, this can either be `GIVEN_IN` for
/// sell orders or `GIVEN_OUT` for buy orders.
/// @param swaps The swaps to perform.
/// @param tokens The tokens for the swaps. Swaps encode to and from tokens
/// as indices into this array.
/// @param funds The fund management settings, specifying the user the swap
/// is being performed for as well as the recipient of the proceeds.
/// @param limits Swap limits for encoding limit prices.
/// @param deadline The deadline for the swap.
/// @param feeTransfer The transfer data for the caller fee.
/// @return tokenDeltas The executed swap amounts.
function batchSwapWithFee(
IVault.SwapKind kind,
IVault.BatchSwapStep[] calldata swaps,
IERC20[] memory tokens,
IVault.FundManagement memory funds,
int256[] memory limits,
uint256 deadline,
GPv2Transfer.Data calldata feeTransfer
) external onlyCreator returns (int256[] memory tokenDeltas) {
tokenDeltas = vault.batchSwap(
kind,
swaps,
tokens,
funds,
limits,
deadline
);
vault.fastTransferFromAccount(feeTransfer, msg.sender);
}
}
exec
/bin/bash -lc "nl -ba src/permissions/MellowACL.sol | sed -n '1,220p'" in /tmp/codex-ky1rccd0
succeeded in 0ms:
1 // SPDX-License-Identifier: BUSL-1.1
2 pragma solidity 0.8.25;
3
4 import "../interfaces/permissions/IMellowACL.sol";
5
6 import "../libraries/SlotLibrary.sol";
7
8 abstract contract MellowACL is IMellowACL, AccessControlEnumerableUpgradeable {
9 using EnumerableSet for EnumerableSet.Bytes32Set;
10
11 bytes32 private immutable _mellowACLStorageSlot;
12
13 constructor(string memory name_, uint256 version_) {
14 _mellowACLStorageSlot = SlotLibrary.getSlot("MellowACL", name_, version_);
15 _disableInitializers();
16 }
17
18 // View functions
19
20 /// @inheritdoc IMellowACL
21 function supportedRoles() external view returns (uint256) {
22 return _mellowACLStorage().supportedRoles.length();
23 }
24
25 /// @inheritdoc IMellowACL
26 function supportedRoleAt(uint256 index) external view returns (bytes32) {
27 return _mellowACLStorage().supportedRoles.at(index);
28 }
29
30 /// @inheritdoc IMellowACL
31 function hasSupportedRole(bytes32 role) external view returns (bool) {
32 return _mellowACLStorage().supportedRoles.contains(role);
33 }
34
35 // Internal functions
36
37 function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
38 if (super._grantRole(role, account)) {
39 if (_mellowACLStorage().supportedRoles.add(role)) {
40 emit RoleAdded(role);
41 }
42 return true;
43 }
44 return false;
45 }
46
47 function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
48 if (super._revokeRole(role, account)) {
49 if (getRoleMemberCount(role) == 0) {
50 _mellowACLStorage().supportedRoles.remove(role);
51 emit RoleRemoved(role);
52 }
53 return true;
54 }
55 return false;
56 }
57
58 function _mellowACLStorage() private view returns (MellowACLStorage storage $) {
59 bytes32 slot = _mellowACLStorageSlot;
60 assembly {
61 $.slot := slot
62 }
63 }
64 }
exec
/bin/bash -lc "nl -ba src/libraries/SlotLibrary.sol | sed -n '1,120p'" in /tmp/codex-ky1rccd0
succeeded in 0ms:
1 // SPDX-License-Identifier: BUSL-1.1
2 pragma solidity 0.8.25;
3
4 /// @title SlotLibrary
5 /// @notice Library for computing deterministic and collision-resistant storage slots
6 /// @dev Used to generate unique storage slots for upgradeable modules using string identifiers
7 library SlotLibrary {
8 /// @notice Computes a unique storage slot based on the module's identifiers
9 /// @param contractName Logical contract/module name (e.g., "ShareModule")
10 /// @param name Human-readable instance name (e.g., "Mellow")
11 /// @param version Version number for the module configuration
12 /// @return A bytes32 value representing the derived storage slot
13 function getSlot(string memory contractName, string memory name, uint256 version) internal pure returns (bytes32) {
14 return keccak256(
15 abi.encode(
16 uint256(keccak256(abi.encodePacked("mellow.flexible-vaults.storage.", contractName, name, version))) - 1
17 )
18 ) & ~bytes32(uint256(0xff));
19 }
20 }
exec
/bin/bash -lc "nl -ba src/interfaces/utils/ISwapModule.sol | sed -n '1,220p'" in /tmp/codex-ky1rccd0
succeeded in 0ms:
1 // SPDX-License-Identifier: BUSL-1.1
2 pragma solidity 0.8.25;
3
4 import "../external/aave/IAaveOracle.sol";
5
6 import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
7 import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
8 import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9 import "@openzeppelin/contracts/utils/math/Math.sol";
10
11 import {GPv2Settlement} from "@cowswap/contracts/GPv2Settlement.sol";
12 import {GPv2Order} from "@cowswap/contracts/libraries/GPv2Order.sol";
13
14 import "../factories/IFactoryEntity.sol";
15
16 interface ISwapModule is IFactoryEntity {
17 // Errors
18
19 error Forbidden(string reason);
20 error ZeroValue();
21
22 // Structs
23
24 struct SwapModuleStorage {
25 address subvault;
26 address oracle;
27 uint256 defaultMultiplier;
28 mapping(address => mapping(address => uint256)) customMultiplier;
29 }
30
31 struct Params {
32 address tokenIn;
33 address tokenOut;
34 uint256 amountIn;
35 uint256 minAmountOut;
36 uint256 deadline;
37 }
38
39 // View functions
40
41 function MIN_MULTIPLIER() external view returns (uint256);
42 function MAX_MULTIPLIER() external view returns (uint256);
43 function BASE_MULTIPLIER() external view returns (uint256);
44
45 function TOKEN_IN_ROLE() external view returns (bytes32);
46 function TOKEN_OUT_ROLE() external view returns (bytes32);
47 function ROUTER_ROLE() external view returns (bytes32);
48 function CALLER_ROLE() external view returns (bytes32);
49 function SET_SLIPPAGE_ROLE() external view returns (bytes32);
50
51 function cowswapSettlement() external view returns (address);
52 function cowswapVaultRelayer() external view returns (address);
53 function weth() external view returns (address);
54
55 function subvault() external view returns (address);
56
57 function oracle() external view returns (address);
58
59 function checkMultiplier(uint256 multiplier) external pure;
60
61 function defaultMultiplier() external view returns (uint256);
62
63 function customMultiplier(address tokenIn, address tokenOut) external view returns (uint256);
64
65 function evaluate(address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256);
66
67 function checkParams(Params calldata params) external view;
68
69 function checkCowswapOrder(Params calldata params, GPv2Order.Data calldata order, bytes calldata orderUid)
70 external
71 view;
72
73 // Mutable functions
74
75 function setOracle(address oracle_) external;
76
77 function setDefaultMultiplier(uint256 multiplier) external;
78
79 function setCustomMultiplier(address tokenIn, address tokenOut, uint256 multiplier) external;
80
81 function pushAssets(address asset, uint256 value) external payable;
82
83 function pullAssets(address asset, uint256 value) external;
84
85 function swap(Params calldata params, address router, bytes calldata data)
86 external
87 returns (bytes memory response);
88
89 function setCowswapApproval(address asset, uint256 amount) external;
90
91 function createLimitOrder(Params calldata params, GPv2Order.Data calldata order, bytes calldata orderUid)
92 external;
93
94 function invalidateOrder(bytes calldata orderUid) external;
95
96 // Events
97
98 event AssetsPulled(address indexed asset, uint256 value);
99
100 event AssetsPushed(address indexed asset, uint256 value);
101
102 event DefaultMultiplierSet(uint256 indexed multiplier);
103
104 event CustomMultiplierSet(address indexed tokenIn, address indexed tokenOut, uint256 indexed multiplier);
105
106 event CowswapApprovalSet(address indexed asset, uint256 amount);
107
108 event LimitOrderCreated(Params params, bytes orderUid);
109
110 event LimitOrderInvalidated(bytes orderUid);
111
112 event Swap(Params params, address router, uint256 amountOut);
113
114 event OracleSet(address indexed oracle);
115 }
exec
/bin/bash -lc "nl -ba src/utils/SwapModule.sol | sed -n '260,340p'" in /tmp/codex-ky1rccd0
succeeded in 0ms:
260 }
261 uint256 balanceBefore = TransferLibrary.balanceOf(params.tokenOut, address(this));
262 if (params.tokenIn != TransferLibrary.ETH) {
263 IERC20(params.tokenIn).forceApprove(router, params.amountIn);
264 response = Address.functionCall(router, data);
265 IERC20(params.tokenIn).forceApprove(router, 0);
266 } else {
267 response = Address.functionCallWithValue(router, data, params.amountIn);
268 }
269 uint256 amountOut = TransferLibrary.balanceOf(params.tokenOut, address(this)) - balanceBefore;
270 if (amountOut < params.minAmountOut) {
271 revert Forbidden("amountOut < minAmountOut");
272 }
273 emit Swap(params, router, amountOut);
274 }
275
276 /// @inheritdoc ISwapModule
277 function setCowswapApproval(address asset, uint256 amount) external onlyRole(CALLER_ROLE) {
278 if (asset == TransferLibrary.ETH || !hasRole(TOKEN_IN_ROLE, asset)) {
279 revert Forbidden("asset");
280 }
281 IERC20(asset).forceApprove(cowswapVaultRelayer, amount);
282 emit CowswapApprovalSet(asset, amount);
283 }
284
285 /// @inheritdoc ISwapModule
286 function createLimitOrder(Params calldata params, GPv2Order.Data calldata order, bytes calldata orderUid)
287 external
288 onlyRole(CALLER_ROLE)
289 {
290 checkParams(params);
291 checkCowswapOrder(params, order, orderUid);
292 GPv2Settlement(payable(cowswapSettlement)).setPreSignature(orderUid, true);
293 emit LimitOrderCreated(params, orderUid);
294 }
295
296 /// @inheritdoc ISwapModule
297 function invalidateOrder(bytes calldata orderUid) external onlyRole(CALLER_ROLE) {
298 GPv2Settlement(payable(cowswapSettlement)).invalidateOrder(orderUid);
299 emit LimitOrderInvalidated(orderUid);
300 }
301
302 receive() external payable {}
303
304 // Internal functions
305
306 function _swapModuleStorage() internal view returns (SwapModuleStorage storage $) {
307 bytes32 slot = _swapModuleStorageSlot;
308 assembly {
309 $.slot := slot
310 }
311 }
312 }
codex
{"no_findings":false,"summary":"Read `AUDIT_BUNDLE.txt` in full. Found 2 material issues in the implementation: proxy upgrades can strand state because storage slots are version/name-dependent, and CoWSwap orders do not bound `feeAmount`, allowing approved sell tokens to be overspent beyond `params.amountIn`.","findings":[{"severity":"high","category":"upgradeability","title":"Version-dependent storage slots can brick the proxy and strand assets on upgrade","exploit_scenario":"The proxy points at this implementation today. If the proxy admin later upgrades to a new `SwapModule` implementation deployed with a different `name_` or `version_`, the immutables `_swapModuleStorageSlot` and `_mellowACLStorageSlot` change. After the upgrade, `subvault()`, `oracle()`, multipliers, and supported-role bookkeeping are read from empty storage. Because `subvault` has no setter and `initialize` is one-shot, the proxy can no longer satisfy `onlySubvault`, leaving assets held by the proxy permanently stuck.","recommendation":"Do not derive live storage slots from constructor-time version/name inputs for proxy state. Use fixed ERC-7201/ERC-1967-style slots that remain identical across implementation versions, or enforce that every upgrade reuses the exact same slot constants.","evidence":"`SlotLibrary.getSlot` mixes `contractName`, `name`, and `version` into the slot seed. `MellowACL` and `SwapModule` cache those derived slots in immutables during construction and use them for all reads/writes of first-party state, so a new implementation with different constructor args silently points the proxy at a different storage region.","locations":["src/libraries/SlotLibrary.sol:13","src/permissions/MellowACL.sol:11","src/permissions/MellowACL.sol:58","src/utils/SwapModule.sol:37","src/utils/SwapModule.sol:62","src/utils/SwapModule.sol:197","src/utils/SwapModule.sol:306"]},{"severity":"medium","category":"access_control","title":"`createLimitOrder` does not constrain CoWSwap `feeAmount`, so CALLER_ROLE can spend more than `params.amountIn`","exploit_scenario":"A compromised or over-scoped `CALLER_ROLE` can first call `setCowswapApproval(asset, type(uint256).max)`, then create a pre-signed CoWSwap order with `sellAmount == params.amountIn` but an arbitrarily large `feeAmount`. The order passes `checkParams` and `checkCowswapOrder`, yet CoWSwap settlement will pull `sellAmount + feeAmount` from the module. The extra sell token is outside the min-out/slippage checks, so approved balances can be drained far beyond the amount the module intended to trade.","recommendation":"Reject orders with nonzero `feeAmount`, or explicitly include fee in the validated parameters and in the balance/allowance accounting. If partial fills are not needed, also require `order.partiallyFillable == false` and scope approval to the exact live exposure instead of a reusable allowance.","evidence":"`checkCowswapOrder` validates token addresses, `sellAmount`, `buyAmount`, deadline, kind, and balance modes, but never checks `order.feeAmount` (or `order.partiallyFillable`). Separately, `setCowswapApproval` allows arbitrary allowance to `cowswapVaultRelayer`, so settlement can legally transfer more sell token than `params.amountIn`.","locations":["src/utils/SwapModule.sol:137","src/utils/SwapModule.sol:150","src/utils/SwapModule.sol:153","src/utils/SwapModule.sol:277","src/utils/SwapModule.sol:286"]}]}
tokens used
56,429
{
"codex": {
"ran_at": "2026-04-04T03:51:38.608562+00:00",
"returncode": 0
},
"project": {
"mode": "source",
"file_count": 49,
"import_count": 60,
"first_party_contracts_count": 8
},
"summary": "Read `AUDIT_BUNDLE.txt` in full. Found 2 material issues in the implementation: proxy upgrades can strand state because storage slots are version/name-dependent, and CoWSwap orders do not bound `feeAmount`, allowing approved sell tokens to be overspent beyond `params.amountIn`.",
"raw_final": {
"summary": "Read `AUDIT_BUNDLE.txt` in full. Found 2 material issues in the implementation: proxy upgrades can strand state because storage slots are version/name-dependent, and CoWSwap orders do not bound `feeAmount`, allowing approved sell tokens to be overspent beyond `params.amountIn`.",
"findings": [
{
"title": "Version-dependent storage slots can brick the proxy and strand assets on upgrade",
"category": "upgradeability",
"evidence": "`SlotLibrary.getSlot` mixes `contractName`, `name`, and `version` into the slot seed. `MellowACL` and `SwapModule` cache those derived slots in immutables during construction and use them for all reads/writes of first-party state, so a new implementation with different constructor args silently points the proxy at a different storage region.",
"severity": "high",
"locations": [
"src/libraries/SlotLibrary.sol:13",
"src/permissions/MellowACL.sol:11",
"src/permissions/MellowACL.sol:58",
"src/utils/SwapModule.sol:37",
"src/utils/SwapModule.sol:62",
"src/utils/SwapModule.sol:197",
"src/utils/SwapModule.sol:306"
],
"recommendation": "Do not derive live storage slots from constructor-time version/name inputs for proxy state. Use fixed ERC-7201/ERC-1967-style slots that remain identical across implementation versions, or enforce that every upgrade reuses the exact same slot constants.",
"exploit_scenario": "The proxy points at this implementation today. If the proxy admin later upgrades to a new `SwapModule` implementation deployed with a different `name_` or `version_`, the immutables `_swapModuleStorageSlot` and `_mellowACLStorageSlot` change. After the upgrade, `subvault()`, `oracle()`, multipliers, and supported-role bookkeeping are read from empty storage. Because `subvault` has no setter and `initialize` is one-shot, the proxy can no longer satisfy `onlySubvault`, leaving assets held by the proxy permanently stuck."
},
{
"title": "`createLimitOrder` does not constrain CoWSwap `feeAmount`, so CALLER_ROLE can spend more than `params.amountIn`",
"category": "access_control",
"evidence": "`checkCowswapOrder` validates token addresses, `sellAmount`, `buyAmount`, deadline, kind, and balance modes, but never checks `order.feeAmount` (or `order.partiallyFillable`). Separately, `setCowswapApproval` allows arbitrary allowance to `cowswapVaultRelayer`, so settlement can legally transfer more sell token than `params.amountIn`.",
"severity": "medium",
"locations": [
"src/utils/SwapModule.sol:137",
"src/utils/SwapModule.sol:150",
"src/utils/SwapModule.sol:153",
"src/utils/SwapModule.sol:277",
"src/utils/SwapModule.sol:286"
],
"recommendation": "Reject orders with nonzero `feeAmount`, or explicitly include fee in the validated parameters and in the balance/allowance accounting. If partial fills are not needed, also require `order.partiallyFillable == false` and scope approval to the exact live exposure instead of a reusable allowance.",
"exploit_scenario": "A compromised or over-scoped `CALLER_ROLE` can first call `setCowswapApproval(asset, type(uint256).max)`, then create a pre-signed CoWSwap order with `sellAmount == params.amountIn` but an arbitrarily large `feeAmount`. The order passes `checkParams` and `checkCowswapOrder`, yet CoWSwap settlement will pull `sellAmount + feeAmount` from the module. The extra sell token is outside the min-out/slippage checks, so approved balances can be drained far beyond the amount the module intended to trade."
}
],
"no_findings": false
},
"no_findings": false,
"proxy_context": {
"proxy_run_id": "019d5666-721c-732b-98f2-8fef0dd24f32",
"proxy_address": "0xc95b806ac073df930014ac476d26c8ad918f14e0",
"implementation_run_id": "019d5666-f542-7208-84bc-d2e3db83e367",
"implementation_address": "0x00000000d681e85e5783588f87a9573cb97eda01"
},
"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 0x000000000000000000000000b1c296d637866c309443f4dd77fd59146a925daf 00000030: PUSH1 0x01 00000032: PUSH1 0x01 00000034: PUSH1 0xa0 00000036: SHL 00000037: SUB 00000038: AND 00000039: CALLER 0000003a: SUB 0000003b: PUSH2 0x007a 0000003e: JUMPI 0000003f: PUSH0 00000040: CALLDATALOAD 00000041: PUSH1 0x01 00000043: PUSH1 0x01 00000045: PUSH1 0xe0 00000047: SHL 00000048: SUB 00000049: NOT 0000004a: AND 0000004b: PUSH4 0x278f7943 00000050: PUSH1 0xe1 00000052: SHL 00000053: EQ 00000054: PUSH2 0x0070 00000057: JUMPI 00000058: PUSH1 0x40 0000005a: MLOAD 0000005b: PUSH4 0x34ad5dbb 00000060: PUSH1 0xe2 00000062: SHL 00000063: DUP2 00000064: MSTORE 00000065: PUSH1 0x04 00000067: ADD 00000068: PUSH1 0x40 0000006a: MLOAD 0000006b: DUP1 0000006c: SWAP2 0000006d: SUB 0000006e: SWAP1 0000006f: REVERT 00000070: JUMPDEST 00000071: PUSH2 0x0078 00000074: PUSH2 0x0082 00000077: JUMP 00000078: JUMPDEST 00000079: JUMP 0000007a: JUMPDEST 0000007b: PUSH2 0x0078 0000007e: PUSH2 0x00b0 00000081: JUMP 00000082: JUMPDEST 00000083: PUSH0 00000084: DUP1 00000085: PUSH2 0x0091 00000088: CALLDATASIZE 00000089: PUSH1 0x04 0000008b: DUP2 0000008c: DUP5 0000008d: PUSH2 0x0303 00000090: JUMP 00000091: JUMPDEST 00000092: DUP2 00000093: ADD 00000094: SWAP1 00000095: PUSH2 0x009e 00000098: SWAP2 00000099: SWAP1 0000009a: PUSH2 0x033e 0000009d: JUMP 0000009e: JUMPDEST 0000009f: SWAP2 000000a0: POP 000000a1: SWAP2 000000a2: POP 000000a3: PUSH2 0x00ac 000000a6: DUP3 000000a7: DUP3 000000a8: PUSH2 0x00c0 000000ab: JUMP 000000ac: JUMPDEST 000000ad: POP 000000ae: POP 000000af: JUMP 000000b0: JUMPDEST 000000b1: PUSH2 0x0078 000000b4: PUSH2 0x00bb 000000b7: PUSH2 0x011a 000000ba: JUMP 000000bb: JUMPDEST 000000bc: PUSH2 0x0151 000000bf: JUMP 000000c0: JUMPDEST 000000c1: PUSH2 0x00c9 000000c4: DUP3 000000c5: PUSH2 0x016f 000000c8: JUMP 000000c9: JUMPDEST 000000ca: PUSH1 0x40 000000cc: MLOAD 000000cd: PUSH1 0x01 000000cf: PUSH1 0x01 000000d1: PUSH1 0xa0 000000d3: SHL 000000d4: SUB 000000d5: DUP4 000000d6: AND 000000d7: SWAP1 000000d8: PUSH32 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b 000000f9: SWAP1 000000fa: PUSH0 000000fb: SWAP1 000000fc: LOG2 000000fd: DUP1 000000fe: MLOAD 000000ff: ISZERO 00000100: PUSH2 0x0112 00000103: JUMPI 00000104: PUSH2 0x010d 00000107: DUP3 00000108: DUP3 00000109: PUSH2 0x01ea 0000010c: JUMP 0000010d: JUMPDEST 0000010e: POP 0000010f: POP 00000110: POP 00000111: JUMP 00000112: JUMPDEST 00000113: PUSH2 0x00ac 00000116: PUSH2 0x025c 00000119: JUMP 0000011a: JUMPDEST 0000011b: PUSH0 0000011c: PUSH2 0x014c 0000011f: PUSH32 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc 00000140: SLOAD 00000141: PUSH1 0x01 00000143: PUSH1 0x01 00000145: PUSH1 0xa0 00000147: SHL 00000148: SUB 00000149: AND 0000014a: SWAP1 0000014b: JUMP 0000014c: JUMPDEST 0000014d: SWAP1 0000014e: POP 0000014f: SWAP1 00000150: JUMP 00000151: JUMPDEST 00000152: CALLDATASIZE 00000153: PUSH0 00000154: DUP1 00000155: CALLDATACOPY 00000156: PUSH0 00000157: DUP1 00000158: CALLDATASIZE 00000159: PUSH0 0000015a: DUP5 0000015b: GAS 0000015c: DELEGATECALL 0000015d: RETURNDATASIZE 0000015e: PUSH0 0000015f: DUP1 00000160: RETURNDATACOPY 00000161: DUP1 00000162: DUP1 00000163: ISZERO 00000164: PUSH2 0x016b 00000167: JUMPI 00000168: RETURNDATASIZE 00000169: PUSH0 0000016a: RETURN 0000016b: JUMPDEST 0000016c: RETURNDATASIZE 0000016d: PUSH0 0000016e: REVERT 0000016f: JUMPDEST 00000170: DUP1 00000171: PUSH1 0x01 00000173: PUSH1 0x01 00000175: PUSH1 0xa0 00000177: SHL 00000178: SUB 00000179: AND 0000017a: EXTCODESIZE 0000017b: PUSH0 0000017c: SUB 0000017d: PUSH2 0x01a9 00000180: JUMPI 00000181: PUSH1 0x40 00000183: MLOAD 00000184: PUSH4 0x4c9c8ce3 00000189: PUSH1 0xe0 0000018b: SHL 0000018c: DUP2 0000018d: MSTORE 0000018e: PUSH1 0x01 00000190: PUSH1 0x01 00000192: PUSH1 0xa0 00000194: SHL 00000195: SUB 00000196: DUP3 00000197: AND 00000198: PUSH1 0x04 0000019a: DUP3 0000019b: ADD 0000019c: MSTORE 0000019d: PUSH1 0x24 0000019f: ADD 000001a0: JUMPDEST 000001a1: PUSH1 0x40 000001a3: MLOAD 000001a4: DUP1 000001a5: SWAP2 000001a6: SUB 000001a7: SWAP1 000001a8: REVERT 000001a9: JUMPDEST 000001aa: PUSH32 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc 000001cb: DUP1 000001cc: SLOAD 000001cd: PUSH1 0x01 000001cf: PUSH1 0x01 000001d1: PUSH1 0xa0 000001d3: SHL 000001d4: SUB 000001d5: NOT 000001d6: AND 000001d7: PUSH1 0x01 000001d9: PUSH1 0x01 000001db: PUSH1 0xa0 000001dd: SHL 000001de: SUB 000001df: SWAP3 000001e0: SWAP1 000001e1: SWAP3 000001e2: AND 000001e3: SWAP2 000001e4: SWAP1 000001e5: SWAP2 000001e6: OR 000001e7: SWAP1 000001e8: SSTORE 000001e9: JUMP 000001ea: JUMPDEST 000001eb: PUSH1 0x60 000001ed: PUSH0 000001ee: DUP1 000001ef: DUP5 000001f0: PUSH1 0x01 000001f2: PUSH1 0x01 000001f4: PUSH1 0xa0 000001f6: SHL 000001f7: SUB 000001f8: AND 000001f9: DUP5 000001fa: PUSH1 0x40 000001fc: MLOAD 000001fd: PUSH2 0x0206 00000200: SWAP2 00000201: SWAP1 00000202: PUSH2 0x0407 00000205: JUMP 00000206: JUMPDEST 00000207: PUSH0 00000208: PUSH1 0x40 0000020a: MLOAD 0000020b: DUP1 0000020c: DUP4 0000020d: SUB 0000020e: DUP2 0000020f: DUP6 00000210: GAS 00000211: DELEGATECALL 00000212: SWAP2 00000213: POP 00000214: POP 00000215: RETURNDATASIZE 00000216: DUP1 00000217: PUSH0 00000218: DUP2 00000219: EQ 0000021a: PUSH2 0x023e 0000021d: JUMPI 0000021e: PUSH1 0x40 00000220: MLOAD 00000221: SWAP2 00000222: POP 00000223: PUSH1 0x1f 00000225: NOT 00000226: PUSH1 0x3f 00000228: RETURNDATASIZE 00000229: ADD 0000022a: AND 0000022b: DUP3 0000022c: ADD 0000022d: PUSH1 0x40 0000022f: MSTORE 00000230: RETURNDATASIZE 00000231: DUP3 00000232: MSTORE 00000233: RETURNDATASIZE 00000234: PUSH0 00000235: PUSH1 0x20 00000237: DUP5 00000238: ADD 00000239: RETURNDATACOPY 0000023a: PUSH2 0x0243 0000023d: JUMP 0000023e: JUMPDEST 0000023f: PUSH1 0x60 00000241: SWAP2 00000242: POP 00000243: JUMPDEST 00000244: POP 00000245: SWAP2 00000246: POP 00000247: SWAP2 00000248: POP 00000249: PUSH2 0x0253 0000024c: DUP6 0000024d: DUP4 0000024e: DUP4 0000024f: PUSH2 0x027b 00000252: JUMP 00000253: JUMPDEST 00000254: SWAP6 00000255: SWAP5 00000256: POP 00000257: POP 00000258: POP 00000259: POP 0000025a: POP 0000025b: JUMP 0000025c: JUMPDEST 0000025d: CALLVALUE 0000025e: ISZERO 0000025f: PUSH2 0x0078 00000262: JUMPI 00000263: PUSH1 0x40 00000265: MLOAD 00000266: PUSH4 0xb398979f 0000026b: PUSH1 0xe0 0000026d: SHL 0000026e: DUP2 0000026f: MSTORE 00000270: PUSH1 0x04 00000272: ADD 00000273: PUSH1 0x40 00000275: MLOAD 00000276: DUP1 00000277: SWAP2 00000278: SUB 00000279: SWAP1 0000027a: REVERT 0000027b: JUMPDEST 0000027c: PUSH1 0x60 0000027e: DUP3 0000027f: PUSH2 0x0290 00000282: JUMPI 00000283: PUSH2 0x028b 00000286: DUP3 00000287: PUSH2 0x02da 0000028a: JUMP 0000028b: JUMPDEST 0000028c: PUSH2 0x02d3 0000028f: JUMP 00000290: JUMPDEST 00000291: DUP2 00000292: MLOAD 00000293: ISZERO 00000294: DUP1 00000295: ISZERO 00000296: PUSH2 0x02a7 00000299: JUMPI 0000029a: POP 0000029b: PUSH1 0x01 0000029d: PUSH1 0x01 0000029f: PUSH1 0xa0 000002a1: SHL 000002a2: SUB 000002a3: DUP5 000002a4: AND 000002a5: EXTCODESIZE 000002a6: ISZERO 000002a7: JUMPDEST 000002a8: ISZERO 000002a9: PUSH2 0x02d0 000002ac: JUMPI 000002ad: PUSH1 0x40 000002af: MLOAD 000002b0: PUSH4 0x9996b315 000002b5: PUSH1 0xe0 000002b7: SHL 000002b8: DUP2 000002b9: MSTORE 000002ba: PUSH1 0x01 000002bc: PUSH1 0x01 000002be: PUSH1 0xa0 000002c0: SHL 000002c1: SUB 000002c2: DUP6 000002c3: AND 000002c4: PUSH1 0x04 000002c6: DUP3 000002c7: ADD 000002c8: MSTORE 000002c9: PUSH1 0x24 000002cb: ADD 000002cc: PUSH2 0x01a0 000002cf: JUMP 000002d0: JUMPDEST 000002d1: POP 000002d2: DUP1 000002d3: JUMPDEST 000002d4: SWAP4 000002d5: SWAP3 000002d6: POP 000002d7: POP 000002d8: POP 000002d9: JUMP 000002da: JUMPDEST 000002db: DUP1 000002dc: MLOAD 000002dd: ISZERO 000002de: PUSH2 0x02ea 000002e1: JUMPI 000002e2: DUP1 000002e3: MLOAD 000002e4: DUP1 000002e5: DUP3 000002e6: PUSH1 0x20 000002e8: ADD 000002e9: REVERT 000002ea: JUMPDEST 000002eb: PUSH1 0x40 000002ed: MLOAD 000002ee: PUSH4 0xd6bda275 000002f3: PUSH1 0xe0 000002f5: SHL 000002f6: DUP2 000002f7: MSTORE 000002f8: PUSH1 0x04 000002fa: ADD 000002fb: PUSH1 0x40 000002fd: MLOAD 000002fe: DUP1 000002ff: SWAP2 00000300: SUB 00000301: SWAP1 00000302: REVERT 00000303: JUMPDEST 00000304: PUSH0 00000305: DUP1 00000306: DUP6 00000307: DUP6 00000308: GT 00000309: ISZERO 0000030a: PUSH2 0x0311 0000030d: JUMPI 0000030e: PUSH0 0000030f: DUP1 00000310: REVERT 00000311: JUMPDEST 00000312: DUP4 00000313: DUP7 00000314: GT 00000315: ISZERO 00000316: PUSH2 0x031d 00000319: JUMPI 0000031a: PUSH0 0000031b: DUP1 0000031c: REVERT 0000031d: JUMPDEST 0000031e: POP 0000031f: POP 00000320: DUP3 00000321: ADD 00000322: SWAP4 00000323: SWAP2 00000324: SWAP1 00000325: SWAP3 00000326: SUB 00000327: SWAP2 00000328: POP 00000329: JUMP 0000032a: JUMPDEST 0000032b: PUSH4 0x4e487b71 00000330: PUSH1 0xe0 00000332: SHL 00000333: PUSH0 00000334: MSTORE 00000335: PUSH1 0x41 00000337: PUSH1 0x04 00000339: MSTORE 0000033a: PUSH1 0x24 0000033c: PUSH0 0000033d: REVERT 0000033e: JUMPDEST 0000033f: PUSH0 00000340: DUP1 00000341: PUSH1 0x40 00000343: DUP4 00000344: DUP6 00000345: SUB 00000346: SLT 00000347: ISZERO 00000348: PUSH2 0x034f 0000034b: JUMPI 0000034c: PUSH0 0000034d: DUP1 0000034e: REVERT 0000034f: JUMPDEST 00000350: DUP3 00000351: CALLDATALOAD 00000352: PUSH1 0x01 00000354: PUSH1 0x01 00000356: PUSH1 0xa0 00000358: SHL 00000359: SUB 0000035a: DUP2 0000035b: AND 0000035c: DUP2 0000035d: EQ 0000035e: PUSH2 0x0365 00000361: JUMPI 00000362: PUSH0 00000363: DUP1 00000364: REVERT 00000365: JUMPDEST 00000366: SWAP2 00000367: POP 00000368: PUSH1 0x20 0000036a: DUP4 0000036b: ADD 0000036c: CALLDATALOAD 0000036d: PUSH8 0xffffffffffffffff 00000376: DUP1 00000377: DUP3 00000378: GT 00000379: ISZERO 0000037a: PUSH2 0x0381 0000037d: JUMPI 0000037e: PUSH0 0000037f: DUP1 00000380: REVERT 00000381: JUMPDEST 00000382: DUP2 00000383: DUP6 00000384: ADD 00000385: SWAP2 00000386: POP 00000387: DUP6 00000388: PUSH1 0x1f 0000038a: DUP4 0000038b: ADD 0000038c: SLT 0000038d: PUSH2 0x0394 00000390: JUMPI 00000391: PUSH0 00000392: DUP1 00000393: REVERT 00000394: JUMPDEST 00000395: DUP2 00000396: CALLDATALOAD 00000397: DUP2 00000398: DUP2 00000399: GT 0000039a: ISZERO 0000039b: PUSH2 0x03a6 0000039e: JUMPI 0000039f: PUSH2 0x03a6 000003a2: PUSH2 0x032a 000003a5: JUMP 000003a6: JUMPDEST 000003a7: PUSH1 0x40 000003a9: MLOAD 000003aa: PUSH1 0x1f 000003ac: DUP3 000003ad: ADD 000003ae: PUSH1 0x1f 000003b0: NOT 000003b1: SWAP1 000003b2: DUP2 000003b3: AND 000003b4: PUSH1 0x3f 000003b6: ADD 000003b7: AND 000003b8: DUP2 000003b9: ADD 000003ba: SWAP1 000003bb: DUP4 000003bc: DUP3 000003bd: GT 000003be: DUP2 000003bf: DUP4 000003c0: LT 000003c1: OR 000003c2: ISZERO 000003c3: PUSH2 0x03ce 000003c6: JUMPI 000003c7: PUSH2 0x03ce 000003ca: PUSH2 0x032a 000003cd: JUMP 000003ce: JUMPDEST 000003cf: DUP2 000003d0: PUSH1 0x40 000003d2: MSTORE 000003d3: DUP3 000003d4: DUP2 000003d5: MSTORE 000003d6: DUP9 000003d7: PUSH1 0x20 000003d9: DUP5 000003da: DUP8 000003db: ADD 000003dc: ADD 000003dd: GT 000003de: ISZERO 000003df: PUSH2 0x03e6 000003e2: JUMPI 000003e3: PUSH0 000003e4: DUP1 000003e5: REVERT 000003e6: JUMPDEST 000003e7: DUP3 000003e8: PUSH1 0x20 000003ea: DUP7 000003eb: ADD 000003ec: PUSH1 0x20 000003ee: DUP4 000003ef: ADD 000003f0: CALLDATACOPY 000003f1: PUSH0 000003f2: PUSH1 0x20 000003f4: DUP5 000003f5: DUP4 000003f6: ADD 000003f7: ADD 000003f8: MSTORE 000003f9: DUP1 000003fa: SWAP6 000003fb: POP 000003fc: POP 000003fd: POP 000003fe: POP 000003ff: POP 00000400: POP 00000401: SWAP3 00000402: POP 00000403: SWAP3 00000404: SWAP1 00000405: POP 00000406: JUMP 00000407: JUMPDEST 00000408: PUSH0 00000409: DUP3 0000040a: MLOAD 0000040b: DUP1 0000040c: PUSH1 0x20 0000040e: DUP6 0000040f: ADD 00000410: DUP5 00000411: MCOPY 00000412: PUSH0 00000413: SWAP3 00000414: ADD 00000415: SWAP2 00000416: DUP3 00000417: MSTORE 00000418: POP 00000419: SWAP2 0000041a: SWAP1 0000041b: POP 0000041c: JUMP 0000041d: INVALID 0000041e: LOG2 0000041f: PUSH5 0x6970667358 00000425: UNKNOWN(0x22) 00000426: SLT 00000427: KECCAK256 00000428: UNKNOWN(0xD9) 00000429: PUSH31 0xaf6413661ec158cd95206e2d8b2d570512e88949f67b8e1a4f4c3cb9964464 00000449: PUSH20
| Severity | Category | Tool | Title | Confidence | Validated | Confirmed | Details |
|---|---|---|---|---|---|---|---|
| critical | upgradeability | detector | Untrusted DELEGATECALL target reachable | 70 | no | no |
viewtrace_id: 0x3fd0961cdc3a05ee1b14246cb5c532448b199b12800baca3f3ded73cf35eadf3
call_targets:
0x00000000d681e85e5783588f87a9573cb97eda01
target_varies:
no
classification:
constant_target
validation_json{
"sink": "DELEGATECALL",
"errors": 0,
"status": "sink_reached",
"attempts": 1,
"trace_id": "0x3fd0961cdc3a05ee1b14246cb5c532448b199b12800baca3f3ded73cf35eadf3",
"confirmed": false,
"trace_mode": "callTracer",
"call_targets": [
"0x00000000d681e85e5783588f87a9573cb97eda01"
],
"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": 348,
"opcode": "DELEGATECALL",
"returndata_checked": null,
"value_expression_category": "n/a",
"target_expression_category": "computed"
},
{
"pc": 529,
"opcode": "DELEGATECALL",
"returndata_checked": null,
"value_expression_category": "n/a",
"target_expression_category": "computed"
}
],
"guards_detected": [],
"delegatecall_count": 2,
"untrusted_delegatecall_count": 2
}
|
| high | upgradeability | codex | Version-dependent storage slots can brick the proxy and strand assets on upgrade | 85 | no | — |
viewevidence_json{
"evidence": "`SlotLibrary.getSlot` mixes `contractName`, `name`, and `version` into the slot seed. `MellowACL` and `SwapModule` cache those derived slots in immutables during construction and use them for all reads/writes of first-party state, so a new implementation with different constructor args silently points the proxy at a different storage region.",
"locations": [
"src/libraries/SlotLibrary.sol:13",
"src/permissions/MellowACL.sol:11",
"src/permissions/MellowACL.sol:58",
"src/utils/SwapModule.sol:37",
"src/utils/SwapModule.sol:62",
"src/utils/SwapModule.sol:197",
"src/utils/SwapModule.sol:306"
],
"recommendation": "Do not derive live storage slots from constructor-time version/name inputs for proxy state. Use fixed ERC-7201/ERC-1967-style slots that remain identical across implementation versions, or enforce that every upgrade reuses the exact same slot constants.",
"exploit_scenario": "The proxy points at this implementation today. If the proxy admin later upgrades to a new `SwapModule` implementation deployed with a different `name_` or `version_`, the immutables `_swapModuleStorageSlot` and `_mellowACLStorageSlot` change. After the upgrade, `subvault()`, `oracle()`, multipliers, and supported-role bookkeeping are read from empty storage. Because `subvault` has no setter and `initialize` is one-shot, the proxy can no longer satisfy `onlySubvault`, leaving assets held by the proxy permanently stuck."
}
|
| 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 | access_control | codex | `createLimitOrder` does not constrain CoWSwap `feeAmount`, so CALLER_ROLE can spend more than `params.amountIn` | 75 | no | — |
viewevidence_json{
"evidence": "`checkCowswapOrder` validates token addresses, `sellAmount`, `buyAmount`, deadline, kind, and balance modes, but never checks `order.feeAmount` (or `order.partiallyFillable`). Separately, `setCowswapApproval` allows arbitrary allowance to `cowswapVaultRelayer`, so settlement can legally transfer more sell token than `params.amountIn`.",
"locations": [
"src/utils/SwapModule.sol:137",
"src/utils/SwapModule.sol:150",
"src/utils/SwapModule.sol:153",
"src/utils/SwapModule.sol:277",
"src/utils/SwapModule.sol:286"
],
"recommendation": "Reject orders with nonzero `feeAmount`, or explicitly include fee in the validated parameters and in the balance/allowance accounting. If partial fills are not needed, also require `order.partiallyFillable == false` and scope approval to the exact live exposure instead of a reusable allowance.",
"exploit_scenario": "A compromised or over-scoped `CALLER_ROLE` can first call `setCowswapApproval(asset, type(uint256).max)`, then create a pre-signed CoWSwap order with `sellAmount == params.amountIn` but an arbitrarily large `feeAmount`. The order passes `checkParams` and `checkCowswapOrder`, yet CoWSwap settlement will pull `sellAmount + feeAmount` from the module. The extra sell token is outside the min-out/slippage checks, so approved balances can be drained far beyond the amount the module intended to trade."
}
|