Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"contracts/Lockx.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
// Copyright © 2025 Lockx. All rights reserved.
// This software is licensed under the Business Source License 1.1 (BUSL-1.1).
// You may use, modify, and distribute this code for non-commercial purposes only.
// For commercial use, you must obtain a license from Lockx.io.
// On or after January 1, 2029, this code will be made available under the MIT License.
pragma solidity ^0.8.30;
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol';
import '@openzeppelin/contracts/utils/Strings.sol';
import './Withdrawals.sol';
import './SignatureVerification.sol';
/* ───────────────────────────── ERC-5192 / Soulbound standard ──────────────────────────────── */
interface IERC5192 {
/// Emitted exactly once when a Lockbox becomes locked (non-transferable).
event Locked(uint256 tokenId);
/// Must always return true for every existing Lockbox.
function locked(uint256 tokenId) external view returns (bool);
}
/**
* @title Lockx
* @dev Core soul-bound ERC-721 contract for Lockbox creation, key rotation, metadata management, and burning.
* Implements ERC-5192 (soulbound standard) for non-transferability.
* Inherits the Withdrawals smart contract which inherits Deposits and SignatureVerification contracts.
*/
contract Lockx is ERC721, Ownable, Withdrawals, IERC5192 {
/// @dev Next token ID to mint (auto-incremented per mint).
uint256 private _nextId;
/* ─────────── Custom errors ───────── */
error ZeroTokenAddress();
error ArrayLengthMismatch();
error DefaultURIAlreadySet();
error NoURI();
error TransfersDisabled();
error FallbackNotAllowed();
error DirectETHTransferNotAllowed();
error LockboxNotEmpty();
error DuplicateKey();
/* ───────────────────────── Metadata storage ────────────────────────── */
string private _defaultMetadataURI;
mapping(uint256 => string) private _tokenMetadataURIs;
/// Emitted whenever a per-token metadata URI is set/updated.
event TokenMetadataURISet(uint256 indexed tokenId, bytes32 indexed referenceId);
event Minted(uint256 indexed tokenId, bytes32 indexed referenceId);
event LockboxBurned(uint256 indexed tokenId, bytes32 indexed referenceId);
event KeyRotated(uint256 indexed tokenId, bytes32 indexed referenceId);
/* ─────────────────────────── Constructor ───────────────────────────── */
/**
* @dev Deploys the contract and initializes the EIP-712 domain used for
* signature authorization in SignatureVerification.
* Creates the treasury lockbox (tokenId=0) and assigns it to the deployer.
*/
constructor() ERC721('Lockx.io', 'Lockbox') Ownable(msg.sender) SignatureVerification(address(this)) {
// Mint the treasury lockbox (tokenId = 0) to the deployer
uint256 treasuryTokenId = _nextId++;
_initialize(treasuryTokenId, msg.sender, bytes32(0)); // Use deployer address as initial treasury key (can be rotated later)
_mint(msg.sender, treasuryTokenId);
emit Locked(treasuryTokenId);
emit Minted(treasuryTokenId, bytes32(0));
}
/* ───────────────────────── Minting + wrapping flows ───────────────────────── */
/**
* @notice Mint a new Lockbox and deposit ETH.
* @param lockboxPublicKey The public key used for on-chain signature verification.
* @param referenceId An external reference ID for off-chain tracking.
*
* Requirements:
* - `lockboxPublicKey` must not be the zero address.
* - `msg.value` > 0 to deposit ETH.
*/
function createLockboxWithETH(
address lockboxPublicKey,
bytes32 referenceId
) external payable nonReentrant {
if (msg.value == 0) revert ZeroAmount();
uint256 tokenId = _createLockbox(lockboxPublicKey, referenceId);
_depositETH(tokenId, msg.value);
}
/**
* @notice Mint a new Lockbox and deposit ERC20 tokens.
* @param lockboxPublicKey The public key used for off-chain signature verification.
* @param tokenAddress The ERC20 token contract address to deposit.
* @param amount The amount of ERC20 tokens to deposit.
* @param referenceId An external reference ID for off-chain tracking.
*
* Requirements:
* - `lockboxPublicKey` must not be the zero address.
* - `tokenAddress` must not be the zero address.
* - `amount` must be greater than zero.
*/
function createLockboxWithERC20(
address lockboxPublicKey,
address tokenAddress,
uint256 amount,
bytes32 referenceId
) external nonReentrant {
if (tokenAddress == address(0)) revert ZeroTokenAddress();
if (amount == 0) revert ZeroAmount();
uint256 tokenId = _createLockbox(lockboxPublicKey, referenceId);
_depositERC20(tokenId, tokenAddress, amount);
}
/**
* @notice Mint a new Lockbox and deposit a single ERC721.
* @param lockboxPublicKey The public key used for off-chain signature verification.
* @param nftContract The ERC721 contract address to deposit.
* @param externalNftTokenId The token ID of the ERC721 to deposit.
* @param referenceId An external reference ID for off-chain tracking.
*
* Requirements:
* - `lockboxPublicKey` and `nftContract` must not be the zero address.
*/
function createLockboxWithERC721(
address lockboxPublicKey,
address nftContract,
uint256 externalNftTokenId,
bytes32 referenceId
) external nonReentrant {
if (nftContract == address(0)) revert ZeroTokenAddress();
uint256 tokenId = _createLockbox(lockboxPublicKey, referenceId);
_depositERC721(tokenId, nftContract, externalNftTokenId);
}
/**
* @notice Mint a new Lockbox and perform a batch deposit of ETH, ERC20s, and ERC721s.
* @param lockboxPublicKey The public key used for off-chain signature verification.
* @param tokenAddresses ERC20 token contract addresses to deposit.
* @param tokenAmounts Corresponding amounts of each ERC20 to deposit.
* @param nftContracts ERC721 contract addresses to deposit.
* @param nftTokenIds Corresponding token IDs of each ERC721 to deposit.
* @param referenceId An external reference ID for off-chain tracking.
*
* Requirements:
* - `lockboxPublicKey` must not be zero address.
* - `tokenAddresses.length == tokenAmounts.length`.
* - `nftContracts.length == nftTokenIds.length`.
* - ETH deposits can be included via msg.value.
*/
function createLockboxWithBatch(
address lockboxPublicKey,
address[] calldata tokenAddresses,
uint256[] calldata tokenAmounts,
address[] calldata nftContracts,
uint256[] calldata nftTokenIds,
bytes32 referenceId
) external payable nonReentrant {
if (
tokenAddresses.length != tokenAmounts.length ||
nftContracts.length != nftTokenIds.length
) revert ArrayLengthMismatch();
// Prevent empty lockbox creation - at least one asset must be provided
if (msg.value == 0 && tokenAddresses.length == 0 && nftContracts.length == 0) {
revert ZeroAmount();
}
uint256 tokenId = _createLockbox(lockboxPublicKey, referenceId);
_batchDeposit(tokenId, msg.value, tokenAddresses, tokenAmounts, nftContracts, nftTokenIds);
}
/* ──────────────────────── Default metadata management ──────────────────────── */
/**
* @notice Sets the default metadata URI for all Lockboxes (only once).
* @param newDefaultURI The base metadata URI to use for tokens without custom URIs.
* @dev Can only be called by the contract owner, and only once.
*
* Requirements:
* - Default URI must not be already set.
*/
function setDefaultMetadataURI(string memory newDefaultURI) external onlyOwner {
if (bytes(_defaultMetadataURI).length > 0) revert DefaultURIAlreadySet();
_defaultMetadataURI = newDefaultURI;
}
/* ───────────────────────── Token-gated + EIP-712 secured metadata management ────────────────────────── */
/**
* @notice Sets or updates a custom metadata URI for a specific Lockbox.
* @param tokenId The ID of the Lockbox to update.
* @param signature The EIP-712 signature by the active Lockbox key.
* @param newMetadataURI The new metadata URI to assign.
* @param referenceId An external reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp until which the signature is valid.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `signature` must be valid and unexpired.
*/
function setTokenMetadataURI(
uint256 tokenId,
bytes memory signature,
string memory newMetadataURI,
bytes32 referenceId,
uint256 signatureExpiry
) external nonReentrant {
// 1) Checks
if (ownerOf(tokenId) != msg.sender) revert NotOwner();
if (block.timestamp > signatureExpiry) revert SignatureExpired();
_verifyReferenceId(tokenId, referenceId);
bytes memory data = abi.encode(
newMetadataURI,
referenceId,
signatureExpiry
);
_verifySignature(
tokenId,
signature,
address(0),
OperationType.SET_TOKEN_URI,
data
);
// 2) Effects
_tokenMetadataURIs[tokenId] = newMetadataURI;
// 3) Interactions (none in this case, just emit event)
emit TokenMetadataURISet(tokenId, referenceId);
}
/**
* @notice Returns the metadata URI for a Lockbox.
* @param tokenId The ID of the token to query.
* @return The custom URI if set; otherwise the default URI with tokenId appended.
* @dev Reverts if neither custom nor default URI is available.
*/
function tokenURI(uint256 tokenId) public view override(ERC721) returns (string memory) {
if (_ownerOf(tokenId) == address(0)) revert NonexistentToken();
string memory custom = _tokenMetadataURIs[tokenId];
if (bytes(custom).length > 0) return custom;
if (bytes(_defaultMetadataURI).length > 0) {
return string(abi.encodePacked(_defaultMetadataURI, Strings.toString(tokenId)));
}
revert NoURI();
}
/* ─────────────────── Lockbox key rotation ──────────────────── */
/*
* @notice Rotate the off-chain authorization key for a Lockbox.
* @param tokenId The ID of the Lockbox.
* @param signature The EIP-712 signature by the active Lockbox key.
* @param newPublicKey The new authorized Lockbox public key.
* @param referenceId External reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp after which the signature is invalid.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `block.timestamp` must be ≤ `signatureExpiry`.
*/
function rotateLockboxKey(
uint256 tokenId,
bytes memory signature,
address newPublicKey,
bytes32 referenceId,
uint256 signatureExpiry
) external nonReentrant onlyLockboxOwner(tokenId) {
if (block.timestamp > signatureExpiry) revert SignatureExpired();
_verifyReferenceId(tokenId, referenceId);
// Check that the new key is different from the current key
if (_getActiveLockboxPublicKey(tokenId) == newPublicKey) revert DuplicateKey();
bytes memory data = abi.encode(
newPublicKey,
referenceId,
signatureExpiry
);
_verifySignature(
tokenId,
signature,
newPublicKey,
OperationType.ROTATE_KEY,
data
);
emit KeyRotated(tokenId, referenceId);
}
/* ─────────────────── Lockbox burning ──────────────────── */
function _burnLockboxNFT(uint256 tokenId) internal {
_burn(tokenId);
}
/*
* @notice Authenticated burn of a Lockbox, clearing all assets and burning the NFT.
* @param tokenId The ID of the Lockbox.
* @param signature The EIP-712 signature by the active Lockbox key.
* @param referenceId External reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp after which the signature is invalid.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `block.timestamp` must be ≤ `signatureExpiry`.
*/
function burnLockbox(
uint256 tokenId,
bytes memory signature,
bytes32 referenceId,
uint256 signatureExpiry
) external nonReentrant onlyLockboxOwner(tokenId) {
if (block.timestamp > signatureExpiry) revert SignatureExpired();
_verifyReferenceId(tokenId, referenceId);
bytes memory data = abi.encode(referenceId, signatureExpiry);
_verifySignature(
tokenId,
signature,
address(0),
OperationType.BURN_LOCKBOX,
data
);
_finalizeBurn(tokenId);
emit LockboxBurned(tokenId, referenceId);
}
/**
* @dev Internal helper to create a new Lockbox
* @param lockboxPublicKey The public key used for off-chain signature verification
* @param referenceId An external reference ID for off-chain tracking
* @return tokenId The newly minted token ID
*/
function _createLockbox(
address lockboxPublicKey,
bytes32 referenceId
) internal returns (uint256) {
// 1) Checks
if (lockboxPublicKey == address(0)) revert ZeroKey();
// 2) Effects
uint256 tokenId = _nextId++;
_initialize(tokenId, lockboxPublicKey, referenceId);
_mint(msg.sender, tokenId);
emit Locked(tokenId);
emit Minted(tokenId, referenceId);
return tokenId;
}
/**
* @dev Internal helper called by `burnLockbox`.
* - Wipes all ETH / ERC20 / ERC721 bookkeeping for the Lockbox.
* - Delegates the actual ERC-721 burn to `_burnLockboxNFT` (implemented above).
*/
function _finalizeBurn(uint256 tokenId) internal {
// Check if lockbox is empty before burning
if (_ethBalances[tokenId] > 0) revert LockboxNotEmpty();
if (_erc20TokenAddresses[tokenId].length > 0) revert LockboxNotEmpty();
if (_nftKeys[tokenId].length > 0) revert LockboxNotEmpty();
/* ---- ETH ---- */
delete _ethBalances[tokenId];
/* ---- ERC-20 balances ---- */
delete _erc20TokenAddresses[tokenId];
/* ---- ERC-721 bookkeeping ---- */
delete _nftKeys[tokenId];
/* ---- finally burn the NFT itself ---- */
_burnLockboxNFT(tokenId);
_purgeAuth(tokenId);
}
/* ────────────────────── Soul-bound mechanics (ERC-5192) ────────────── */
/**
* @notice Always returns true for existing Lockboxes (soulbound).
* @param tokenId The ID of the Lockbox.
* @return Always true.
* @dev Reverts if token does not exist.
*/
function locked(uint256 tokenId) external view override returns (bool) {
if (_ownerOf(tokenId) == address(0)) revert NonexistentToken();
return true;
}
/// Override _update to enforce soulbound behavior (prevent transfers) and cleanup metadata on burn.
function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
address from = _ownerOf(tokenId);
if (from != address(0) && to != address(0)) {
revert TransfersDisabled();
}
// Clear custom metadata on burn (when to == address(0))
if (to == address(0)) {
delete _tokenMetadataURIs[tokenId];
}
return super._update(to, tokenId, auth);
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721) returns (bool) {
// ERC-5192 soulbound interface
if (interfaceId == type(IERC5192).interfaceId) return true;
// ERC-721 Receiver interface
if (interfaceId == type(IERC721Receiver).interfaceId) return true;
// everything else (ERC-721, ERC-165)
return super.supportsInterface(interfaceId);
}
/* ───────────────────────── Fallback handlers ───────────────────────── */
/**
* @notice Receive ETH only from allowed routers.
* @dev Prevents orphaned ETH from direct transfers.
* Legitimate ETH comes through deposit functions and routers.
*/
receive() external payable {
// Only accept ETH from allowed routers
if (!_isAllowedRouter(msg.sender)) {
revert DirectETHTransferNotAllowed();
}
}
fallback() external {
revert FallbackNotAllowed();
}
}
"
},
"contracts/SignatureVerification.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
// Copyright © 2025 Lockx. All rights reserved.
// This software is licensed under the Business Source License 1.1 (BUSL-1.1).
// You may use, modify, and distribute this code for non-commercial purposes only.
// For commercial use, you must obtain a license from Lockx.io.
// On or after January 1, 2029, this code will be made available under the MIT License.
pragma solidity ^0.8.30;
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';
import '@openzeppelin/contracts/utils/cryptography/EIP712.sol';
/**
* @title SignatureVerification
* @notice Provides signature-based authorization for contract operations using EIP‑712.
* @dev Each Lockbox references an active Lockbox public key that must sign operations.
* The contract stores a nonce to prevent replay attacks.
*/
contract SignatureVerification is EIP712 {
using ECDSA for bytes32;
/// @notice Enumerates the possible operations that require Lockbox key authorization.
enum OperationType {
ROTATE_KEY,
WITHDRAW_ETH,
WITHDRAW_ERC20,
WITHDRAW_NFT,
BURN_LOCKBOX,
SET_TOKEN_URI,
BATCH_WITHDRAW,
SWAP_ASSETS
}
/// @dev Gas-cheap pointer to the Lockbox ERC-721 (set once in constructor).
ERC721 immutable _erc721;
/**
* @dev Stores authorization data for each Lockbox.
* @param nonce Monotonically increasing value to prevent signature replay.
* @param activeLockboxPublicKey The public key currently authorized to sign operations for this Lockbox.
*/
struct TokenAuth {
address activeLockboxPublicKey;
uint96 nonce;
bytes32 referenceId;
}
/// @dev Mapping from Lockbox token ID to its TokenAuth.
mapping(uint256 => TokenAuth) private _tokenAuth;
/* ─────────────────── Errors ────────────────────── */
error NotOwner();
error InvalidSignature();
error AlreadyInitialized();
error ZeroKey();
error InvalidReferenceId();
/* ─────────────────── EIP-712 setup ───────────────────── */
/**
* @dev Typehash for the operation, including tokenId, nonce, opType (as uint8),
* and a bytes32 hash of the data.
*/
bytes32 private constant OPERATION_TYPEHASH =
keccak256('Operation(uint256 tokenId,uint256 nonce,uint8 opType,bytes32 dataHash)');
/**
* @notice Constructor that sets the reference to the ERC721 contract for Lockbox ownership checks.
* @param erc721Address The address of the ERC721 contract that mints/owns the Lockboxs.
*/
constructor(address erc721Address) EIP712('Lockx', '5') {
_erc721 = ERC721(erc721Address);
}
/**
* @notice Initializes the Lockbox data with a public key, nonce, and referenceId.
* @dev Intended to be called once upon minting a new Lockbox.
* @param tokenId The ID of the Lockbox being initialized.
* @param lockboxPublicKey The public key that will sign operations for this Lockbox.
* @param referenceId The off-chain tracking identifier for this Lockbox.
*/
function _initialize(uint256 tokenId, address lockboxPublicKey, bytes32 referenceId) internal {
if (_tokenAuth[tokenId].activeLockboxPublicKey != address(0)) {
revert AlreadyInitialized();
}
_tokenAuth[tokenId].activeLockboxPublicKey = lockboxPublicKey;
_tokenAuth[tokenId].nonce = 0;
_tokenAuth[tokenId].referenceId = referenceId;
}
/**
* @notice Modifier that checks the caller is the owner of the specified token.
* @param tokenId The ID of the Lockbox to check ownership against.
*/
modifier onlyTokenOwner(uint256 tokenId) {
if (_erc721.ownerOf(tokenId) != msg.sender) revert NotOwner();
_;
}
/**
* @notice Verifies an EIP‑712 signature for a specific operation.
* @param tokenId The ID of the Lockbox.
* @param signature The Lockbox private key signature to verify.
* @param newLockboxPublicKey The new Lockbox public key (if rotating the key).
* @param opType The operation being authorized.
* @param data Encoded parameters for the specific operation.
*
* Requirements:
* - The signature must be valid for the current, active Lockbox public key.
* - On successful verification, the nonce increments.
* - If `opType` is `ROTATE_KEY`, the Lockbox public key is updated to `newLockboxPublicKey`.
*/
function _verifySignature(
uint256 tokenId,
bytes memory signature,
address newLockboxPublicKey,
OperationType opType,
bytes memory data
) internal {
TokenAuth storage tokenAuth = _tokenAuth[tokenId];
bytes32 structHash = keccak256(
abi.encode(OPERATION_TYPEHASH, tokenId, tokenAuth.nonce, uint8(opType), keccak256(data))
);
bytes32 expectedHash = _hashTypedDataV4(structHash);
address signer = expectedHash.recover(signature);
if (signer != tokenAuth.activeLockboxPublicKey) {
revert InvalidSignature();
}
// Increment nonce after successful verification.
tokenAuth.nonce++;
// If rotating the key, update the active Lockbox public key.
if (opType == OperationType.ROTATE_KEY && newLockboxPublicKey != address(0)) {
tokenAuth.activeLockboxPublicKey = newLockboxPublicKey;
}
}
function _purgeAuth(uint256 tokenId) internal {
delete _tokenAuth[tokenId];
}
/* ─────────────────── Token-gated view functions ────────────────────── */
/**
* @notice Retrieves the current Lockbox public key for the given Lockbox.
* @param tokenId The ID of the Lockbox.
* @return The currently active Lockbox public key.
*
* Requirements:
* - Caller must be the owner of `tokenId`.
*/
function getActiveLockboxPublicKeyForToken(
uint256 tokenId
) external view onlyTokenOwner(tokenId) returns (address) {
return _tokenAuth[tokenId].activeLockboxPublicKey;
}
/**
* @notice Retrieves the current nonce for the given Lockbox.
* @param tokenId The ID of the Lockbox.
* @return The current nonce used for signature verification.
*
* Requirements:
* - Caller must be the owner of `tokenId`.
*/
function getNonce(uint256 tokenId) external view onlyTokenOwner(tokenId) returns (uint256) {
return uint256(_tokenAuth[tokenId].nonce);
}
/**
* @dev Internal function to verify that the provided referenceId matches the stored one.
* @param tokenId The ID of the Lockbox.
* @param referenceId The referenceId to verify.
*/
function _verifyReferenceId(uint256 tokenId, bytes32 referenceId) internal view {
if (_tokenAuth[tokenId].referenceId != referenceId) {
revert InvalidReferenceId();
}
}
/**
* @dev Internal function to get the stored referenceId for a Lockbox.
* @param tokenId The ID of the Lockbox.
* @return The stored referenceId.
*/
function _getReferenceId(uint256 tokenId) internal view returns (bytes32) {
return _tokenAuth[tokenId].referenceId;
}
/**
* @dev Internal function to get the current active key for a Lockbox.
* @param tokenId The ID of the Lockbox.
* @return The currently active Lockbox public key.
*/
function _getActiveLockboxPublicKey(uint256 tokenId) internal view returns (address) {
return _tokenAuth[tokenId].activeLockboxPublicKey;
}
}
"
},
"contracts/Withdrawals.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
// Copyright © 2025 Lockx. All rights reserved.
// This software is licensed under the Business Source License 1.1 (BUSL-1.1).
// You may use, modify, and distribute this code for non-commercial purposes only.
// For commercial use, you must obtain a license from Lockx.io.
// On or after January 1, 2029, this code will be made available under the MIT License.
pragma solidity ^0.8.30;
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
import '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import './Deposits.sol';
/**
* @title Withdrawals
* @dev Signature-gated withdraw, swap, and view helpers.
* Inherits Deposits for storage & deposit helpers, and
* SignatureVerification for EIP-712 auth.
*/
abstract contract Withdrawals is Deposits {
using SafeERC20 for IERC20;
/* ───────── Enums ───────── */
enum SwapMode {
EXACT_IN, // Specify exact input amount, get variable output
EXACT_OUT // Specify exact output amount, use variable input
}
/* ───────── Treasury Constants ───────── */
uint256 public constant TREASURY_LOCKBOX_ID = 0;
uint256 public constant SWAP_FEE_BP = 10;
uint256 private constant FEE_DIVISOR = 10000;
/* ───────── Events ───────── */
event Withdrawn(uint256 indexed tokenId, bytes32 indexed referenceId);
event SwapExecuted(uint256 indexed tokenId, bytes32 indexed referenceId);
/* ───────── Errors ───────── */
error NoETHBalance();
error InsufficientTokenBalance();
error NFTNotFound();
error EthTransferFailed();
error SignatureExpired();
error SwapCallFailed();
error InvalidSwap();
error SlippageExceeded();
error RouterOverspent();
error InsufficientOutput();
error DuplicateEntry();
error UnsortedArray();
error InvalidRecipient();
error UnauthorizedRouter();
error UnauthorizedSelector();
/* ─────────────────── Lockbox withdrawals ───────────────────── */
/*
* @notice Withdraw ETH from a Lockbox, authorized via EIP-712 signature.
* @param tokenId The ID of the Lockbox.
* @param signature The EIP-712 signature by the active Lockbox key.
* @param amountETH The amount of ETH to withdraw.
* @param recipient The address receiving the ETH.
* @param referenceId External reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp after which the signature is invalid.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `recipient` must not be the zero address.
* - `block.timestamp` must be ≤ `signatureExpiry`.
* - Lockbox must have ≥ `amountETH` ETH
*/
function withdrawETH(
uint256 tokenId,
bytes memory signature,
uint256 amountETH,
address recipient,
bytes32 referenceId,
uint256 signatureExpiry
) external nonReentrant onlyLockboxOwner(tokenId) {
// 1) Checks
if (recipient == address(0)) revert ZeroAddress();
if (recipient == address(this)) revert InvalidRecipient();
if (block.timestamp > signatureExpiry) revert SignatureExpired();
// Check balance before signature verification
uint256 currentBal = _ethBalances[tokenId];
if (currentBal < amountETH) revert NoETHBalance();
_verifyReferenceId(tokenId, referenceId);
// 2) Verify
bytes memory data = abi.encode(
amountETH,
recipient,
referenceId,
signatureExpiry
);
_verifySignature(
tokenId,
signature,
address(0),
OperationType.WITHDRAW_ETH,
data
);
// 3) Effects + Interaction
_withdrawETH(tokenId, amountETH, recipient);
emit Withdrawn(tokenId, referenceId);
}
/*
* @notice Withdraw an ERC-20 token from a Lockbox, authorized via EIP-712 signature.
* @param tokenId The ID of the Lockbox.
* @param signature The EIP-712 signature by the active Lockbox key.
* @param tokenAddress The ERC-20 token address to withdraw.
* @param amount The amount of tokens to withdraw.
* @param recipient The address receiving the tokens.
* @param referenceId External reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp after which the signature is invalid.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `recipient` must not be the zero address.
* - `block.timestamp` must be ≤ `signatureExpiry`.
* - Lockbox must have ≥ `amount` balance of `tokenAddress`.
*/
function withdrawERC20(
uint256 tokenId,
bytes memory signature,
address tokenAddress,
uint256 amount,
address recipient,
bytes32 referenceId,
uint256 signatureExpiry
) external nonReentrant onlyLockboxOwner(tokenId) {
// 1) Checks
if (recipient == address(0)) revert ZeroAddress();
if (recipient == address(this)) revert InvalidRecipient();
if (block.timestamp > signatureExpiry) revert SignatureExpired();
// Check balance before signature verification
mapping(address => uint256) storage balMap = _erc20Balances[tokenId];
uint256 bal = balMap[tokenAddress];
if (bal < amount) revert InsufficientTokenBalance();
_verifyReferenceId(tokenId, referenceId);
// 2) Verify
bytes memory data = abi.encode(
tokenAddress,
amount,
recipient,
referenceId,
signatureExpiry
);
_verifySignature(
tokenId,
signature,
address(0),
OperationType.WITHDRAW_ERC20,
data
);
// 3) Effects + Interaction
_withdrawERC20(tokenId, tokenAddress, amount, recipient);
emit Withdrawn(tokenId, referenceId);
}
/*
* @notice Withdraw an ERC-721 token from a Lockbox, authorized via EIP-712 signature.
* @param tokenId The ID of the Lockbox.
* @param signature The EIP-712 signature by the active Lockbox key.
* @param nftContract The ERC-721 contract address to withdraw.
* @param nftTokenId The token ID of the ERC-721 to withdraw.
* @param recipient The address receiving the NFT.
* @param referenceId External reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp after which the signature is invalid.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `recipient` must not be the zero address.
* - `block.timestamp` must be ≤ `signatureExpiry`.
* - The specified NFT must be deposited in this Lockbox.
*/
function withdrawERC721(
uint256 tokenId,
bytes memory signature,
address nftContract,
uint256 nftTokenId,
address recipient,
bytes32 referenceId,
uint256 signatureExpiry
) external nonReentrant onlyLockboxOwner(tokenId) {
// 1) Checks
if (recipient == address(0)) revert ZeroAddress();
if (recipient == address(this)) revert InvalidRecipient();
if (block.timestamp > signatureExpiry) revert SignatureExpired();
// Check NFT ownership before signature verification
bytes32 key = keccak256(abi.encodePacked(nftContract, nftTokenId));
if (_lockboxNftData[tokenId][key].nftContract == address(0)) revert NFTNotFound();
_verifyReferenceId(tokenId, referenceId);
// 2) Verify
bytes memory data = abi.encode(
nftContract,
nftTokenId,
recipient,
referenceId,
signatureExpiry
);
_verifySignature(
tokenId,
signature,
address(0),
OperationType.WITHDRAW_NFT,
data
);
// 3) Effects + Interaction
_withdrawERC721(tokenId, nftContract, nftTokenId, recipient);
emit Withdrawn(tokenId, referenceId);
}
/*
* @notice Batch withdrawal of ETH, ERC-20s, and ERC-721s with a single signature.
* @param tokenId The ID of the Lockbox.
* @param signature The EIP-712 signature by the active Lockbox key.
* @param amountETH The amount of ETH to withdraw.
* @param tokenAddresses The list of ERC-20 token addresses to withdraw.
* @param tokenAmounts The corresponding amounts of each ERC-20 to withdraw.
* @param nftContracts The list of ERC-721 contract addresses to withdraw.
* @param nftTokenIds The corresponding ERC-721 token IDs to withdraw.
* @param recipient The address receiving all assets.
* @param referenceId External reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp after which the signature is invalid.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `recipient` must not be the zero address.
* - `block.timestamp` must be ≤ `signatureExpiry`.
* - `tokenAddresses.length` must equal `tokenAmounts.length`.
* - `nftContracts.length` must equal `nftTokenIds.length`.
* - `tokenAddresses` must be sorted in strictly ascending order (no duplicates).
* - NFT pairs `(nftContract, nftTokenId)` must be sorted in strictly ascending lexicographic order
* by `(nftContract, nftTokenId)` (no duplicates).
* - Lockbox must have ≥ `amountETH` ETH and sufficient balances for each asset.
*/
function batchWithdraw(
uint256 tokenId,
bytes memory signature,
uint256 amountETH,
address[] calldata tokenAddresses,
uint256[] calldata tokenAmounts,
address[] calldata nftContracts,
uint256[] calldata nftTokenIds,
address recipient,
bytes32 referenceId,
uint256 signatureExpiry
) external nonReentrant onlyLockboxOwner(tokenId) {
// 1) Checks
if (recipient == address(0)) revert ZeroAddress();
if (recipient == address(this)) revert InvalidRecipient();
if (block.timestamp > signatureExpiry) revert SignatureExpired();
if (
tokenAddresses.length != tokenAmounts.length ||
nftContracts.length != nftTokenIds.length
) revert MismatchedInputs();
// Check ETH balance
if (amountETH > 0) {
uint256 currentBal = _ethBalances[tokenId];
if (currentBal < amountETH) revert NoETHBalance();
}
// Check ERC-20 balances
mapping(address => uint256) storage balMap = _erc20Balances[tokenId];
for (uint256 i; i < tokenAddresses.length; ) {
if (balMap[tokenAddresses[i]] < tokenAmounts[i]) revert InsufficientTokenBalance();
unchecked { ++i; }
}
// Check NFT ownership
for (uint256 i; i < nftContracts.length; ) {
bytes32 key = keccak256(abi.encodePacked(nftContracts[i], nftTokenIds[i]));
if (_lockboxNftData[tokenId][key].nftContract == address(0)) revert NFTNotFound();
unchecked { ++i; }
}
_verifyReferenceId(tokenId, referenceId);
// 2) Verify
bytes memory data = abi.encode(
amountETH,
tokenAddresses,
tokenAmounts,
nftContracts,
nftTokenIds,
recipient,
referenceId,
signatureExpiry
);
_verifySignature(
tokenId,
signature,
address(0),
OperationType.BATCH_WITHDRAW,
data
);
// 3) Effects + Interactions for each asset type
if (amountETH > 0) {
_withdrawETH(tokenId, amountETH, recipient);
}
// — ERC-20s — enforce strictly increasing addresses (no duplicates)
mapping(address => uint256) storage lockboxTokenBalances = _erc20Balances[tokenId];
address previousTokenAddress;
bool hasPreviousTokenAddress;
for (uint256 i; i < tokenAddresses.length; ) {
address tokenAddress = tokenAddresses[i];
uint256 tokenAmount = tokenAmounts[i];
if (hasPreviousTokenAddress) {
if (uint256(uint160(tokenAddress)) <= uint256(uint160(previousTokenAddress))) revert UnsortedArray();
} else {
hasPreviousTokenAddress = true;
}
uint256 currentBalance = lockboxTokenBalances[tokenAddress];
if (currentBalance < tokenAmount) revert InsufficientTokenBalance();
unchecked {
lockboxTokenBalances[tokenAddress] = currentBalance - tokenAmount;
}
if (lockboxTokenBalances[tokenAddress] == 0) {
delete lockboxTokenBalances[tokenAddress];
_removeERC20Token(tokenId, tokenAddress);
}
IERC20(tokenAddress).safeTransfer(recipient, tokenAmount);
previousTokenAddress = tokenAddress;
unchecked { ++i; }
}
// — ERC-721s — enforce strictly increasing lexicographic order by (contract, tokenId)
address previousNftContract;
uint256 previousNftTokenId;
bool hasPreviousNft;
for (uint256 i; i < nftContracts.length; ) {
address nftContract = nftContracts[i];
uint256 nftTokenId = nftTokenIds[i];
if (hasPreviousNft) {
if (nftContract < previousNftContract || (nftContract == previousNftContract && nftTokenId <= previousNftTokenId)) revert UnsortedArray();
} else {
hasPreviousNft = true;
}
bytes32 key = keccak256(abi.encodePacked(nftContract, nftTokenId));
if (_lockboxNftData[tokenId][key].nftContract == address(0)) revert NFTNotFound();
delete _lockboxNftData[tokenId][key];
_removeNFTKey(tokenId, key);
IERC721(nftContract).safeTransferFrom(address(this), recipient, nftTokenId);
previousNftContract = nftContract;
previousNftTokenId = nftTokenId;
unchecked { ++i; }
}
emit Withdrawn(tokenId, referenceId);
}
/*
* @notice Execute an asset swap within a Lockbox, authorized via EIP-712 signature.
* @param tokenId The ID of the Lockbox.
* @param signature The EIP-712 signature by the active Lockbox key.
* @param tokenIn The input token address (address(0) for ETH).
* @param tokenOut The output token address (address(0) for ETH).
* @param swapMode Whether this is EXACT_IN or EXACT_OUT swap.
* @param amountSpecified For EXACT_IN: input amount. For EXACT_OUT: desired output amount.
* @param amountLimit For EXACT_IN: min output. For EXACT_OUT: max input allowed.
* @param target The router/aggregator contract address to execute swap.
* @param data The pre-built calldata for the swap execution.
* @param referenceId External reference ID for off-chain tracking.
* @param signatureExpiry UNIX timestamp after which the signature is invalid.
* @param recipient The recipient address for swap output. Use address(0) to credit lockbox.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
* - `block.timestamp` must be < `signatureExpiry`.
* - For EXACT_IN: Lockbox must have ≥ amountSpecified of tokenIn.
* - For EXACT_OUT: Lockbox must have ≥ amountLimit of tokenIn (max you're willing to spend).
* - `target` must be an allowed router.
* - For EXACT_IN: Must receive ≥ amountLimit of tokenOut (min acceptable output).
* - For EXACT_OUT: Must receive ≥ amountSpecified of tokenOut and spend ≤ amountLimit of tokenIn.
* - If `recipient` is address(0), output is credited to lockbox, otherwise sent to recipient.
*/
function swapInLockbox(
uint256 tokenId,
bytes memory signature,
address tokenIn,
address tokenOut,
SwapMode swapMode,
uint256 amountSpecified,
uint256 amountLimit,
address target,
bytes calldata data,
bytes32 referenceId,
uint256 signatureExpiry,
address recipient
) external nonReentrant onlyLockboxOwner(tokenId) {
if (block.timestamp > signatureExpiry) revert SignatureExpired();
if (amountSpecified == 0) revert ZeroAmount();
if (tokenIn == tokenOut) revert InvalidSwap();
// Validate router and calldata selector (also handles zero address)
if (!_isAllowedRouter(target)) revert UnauthorizedRouter();
if (!_isAllowedSelector(data)) revert UnauthorizedSelector();
_verifyReferenceId(tokenId, referenceId);
// 1) For EXACT_IN/EXACT_OUT, check balance sufficiency upfront (deterministic pre-check)
if (swapMode == SwapMode.EXACT_IN) {
if (tokenIn == address(0)) {
if (_ethBalances[tokenId] < amountSpecified) revert NoETHBalance();
} else {
if (_erc20Balances[tokenId][tokenIn] < amountSpecified) revert InsufficientTokenBalance();
}
} else {
// For EXACT_OUT, check maximum input allowed
if (tokenIn == address(0)) {
if (_ethBalances[tokenId] < amountLimit) revert NoETHBalance();
} else {
if (_erc20Balances[tokenId][tokenIn] < amountLimit) revert InsufficientTokenBalance();
}
}
// 2) Verify signature
bytes memory authData = abi.encode(
tokenIn,
tokenOut,
uint8(swapMode),
amountSpecified,
amountLimit,
target,
keccak256(data),
referenceId,
signatureExpiry,
recipient
);
_verifySignature(
tokenId,
signature,
address(0),
OperationType.SWAP_ASSETS,
authData
);
// 3) Measure balances before swap
uint256 balanceInBefore;
if (tokenIn == address(0)) {
balanceInBefore = address(this).balance;
} else {
balanceInBefore = IERC20(tokenIn).balanceOf(address(this));
}
uint256 balanceOutBefore;
if (tokenOut == address(0)) {
balanceOutBefore = address(this).balance;
} else {
balanceOutBefore = IERC20(tokenOut).balanceOf(address(this));
}
// 4) Execute swap with approval
uint256 approvalAmount;
uint256 ethValue;
if (swapMode == SwapMode.EXACT_IN) {
approvalAmount = amountSpecified;
ethValue = (tokenIn == address(0)) ? amountSpecified : 0;
} else {
// For EXACT_OUT, approve the maximum we're willing to spend
approvalAmount = amountLimit;
ethValue = (tokenIn == address(0)) ? amountLimit : 0;
}
if (tokenIn != address(0)) {
IERC20(tokenIn).forceApprove(target, approvalAmount);
}
(bool success,) = target.call{value: ethValue}(data);
// Clean up approval
if (tokenIn != address(0)) {
IERC20(tokenIn).approve(target, 0);
}
if (!success) revert SwapCallFailed();
// 5) Measure actual amounts transferred
uint256 balanceInAfter;
if (tokenIn == address(0)) {
balanceInAfter = address(this).balance;
} else {
balanceInAfter = IERC20(tokenIn).balanceOf(address(this));
}
uint256 balanceOutAfter;
if (tokenOut == address(0)) {
balanceOutAfter = address(this).balance;
} else {
balanceOutAfter = IERC20(tokenOut).balanceOf(address(this));
}
// Calculate actual amounts (handles fee-on-transfer tokens)
uint256 actualAmountIn = balanceInBefore - balanceInAfter;
uint256 actualAmountOut = balanceOutAfter - balanceOutBefore;
// 6) Calculate fee first and derive userAmount (net-of-fee)
uint256 feeAmount = (actualAmountOut * SWAP_FEE_BP + FEE_DIVISOR - 1) / FEE_DIVISOR;
uint256 userAmount = actualAmountOut - feeAmount;
// 7) Validate swap based on mode using net-of-fee output for slippage
if (swapMode == SwapMode.EXACT_IN) {
// For EXACT_IN: verify user receives at least the minimum output after fees
if (userAmount < amountLimit) revert SlippageExceeded();
// Router shouldn't take more than specified
if (actualAmountIn > amountSpecified) revert RouterOverspent();
} else {
// For EXACT_OUT: verify we didn't spend more than maximum
if (actualAmountIn > amountLimit) revert SlippageExceeded();
// User should get at least the specified output after fees
if (userAmount < amountSpecified) revert InsufficientOutput();
}
// 8) Update accounting with actual amounts (handles fee-on-transfer)
// Deduct actual input amount
if (tokenIn == address(0)) {
_ethBalances[tokenId] -= actualAmountIn;
} else {
_erc20Balances[tokenId][tokenIn] -= actualAmountIn;
// Clean up if balance is now 0
if (_erc20Balances[tokenId][tokenIn] == 0) {
delete _erc20Balances[tokenId][tokenIn];
_removeERC20Token(tokenId, tokenIn);
}
}
// Credit fee to treasury lockbox
if (feeAmount > 0) {
_creditToLockbox(TREASURY_LOCKBOX_ID, tokenOut, feeAmount);
}
// Credit user amount to recipient or lockbox
if (recipient != address(0)) {
// Send directly to external recipient
if (tokenOut == address(0)) {
(bool ethSuccess, ) = payable(recipient).call{value: userAmount}('');
if (!ethSuccess) revert EthTransferFailed();
} else {
IERC20(tokenOut).safeTransfer(recipient, userAmount);
}
} else {
// Credit to user's lockbox
_creditToLockbox(tokenId, tokenOut, userAmount);
}
emit SwapExecuted(tokenId, referenceId);
}
/* ─────────────────── View helpers ──────────────────── */
/*
* @notice Returns the full contents of a Lockbox: ETH, ERC-20 balances, and ERC-721s.
* @param tokenId The ID of the Lockbox.
* @return ethBalances The ETH amount held.
* @return erc20Tokens Array of (tokenAddress, balance) for each ERC-20.
* @return nfts Array of nftBalances structs representing each ERC-721.
*
* Requirements:
* - `tokenId` must exist and caller must be its owner.
*/
struct erc20Balances {
address tokenAddress;
uint256 balance;
}
function getFullLockbox(
uint256 tokenId
)
external
view
returns (
uint256 lockboxETH,
erc20Balances[] memory erc20Tokens,
nftBalances[] memory nftContracts
)
{
if (_erc721.ownerOf(tokenId) != msg.sender) revert NotOwner();
lockboxETH = _ethBalances[tokenId];
// ERC-20s
address[] storage tokenAddresses = _erc20TokenAddresses[tokenId];
erc20Tokens = new erc20Balances[](tokenAddresses.length);
for (uint256 i; i < tokenAddresses.length; ) {
erc20Tokens[i] = erc20Balances({
tokenAddress: tokenAddresses[i],
balance: _erc20Balances[tokenId][tokenAddresses[i]]
});
unchecked {
++i;
}
}
// ERC-721s
bytes32[] storage nftList = _nftKeys[tokenId];
uint256 count;
for (uint256 i; i < nftList.length; ) {
if (_lockboxNftData[tokenId][nftList[i]].nftContract != address(0)) count++;
unchecked {
++i;
}
}
nftContracts = new nftBalances[](count);
uint256 index;
for (uint256 i; i < nftList.length; ) {
if (_lockboxNftData[tokenId][nftList[i]].nftContract != address(0)) {
nftContracts[index++] = _lockboxNftData[tokenId][nftList[i]];
}
unchecked {
++i;
}
}
}
/* ─────────────────── Internal helpers ────────────────────── */
/**
* @dev Internal helper to credit tokens to a lockbox
* @param tokenId The lockbox token ID
* @param token The token address (address(0) for ETH)
* @param amount Amount to credit
*/
function _creditToLockbox(
uint256 tokenId,
address token,
uint256 amount
) internal {
if (token == address(0)) {
_ethBalances[tokenId] += amount;
} else {
// Register token if new for lockbox
if (_erc20Balances[tokenId][token] == 0) {
_erc20TokenAddresses[tokenId].push(token);
_erc20Index[tokenId][token] = _erc20TokenAddresses[tokenId].length - 1;
}
_erc20Balances[tokenId][token] += amount;
}
}
/**
* @dev Internal helper to withdraw ETH from a lockbox
* @param tokenId The lockbox token ID
* @param amountETH Amount of ETH to withdraw
* @param recipient Address to receive the ETH
*/
function _withdrawETH(
uint256 tokenId,
uint256 amountETH,
address recipient
) internal {
_ethBalances[tokenId] -= amountETH;
(bool success, ) = payable(recipient).call{value: amountETH}('');
if (!success) revert EthTransferFailed();
}
/**
* @dev Internal helper to withdraw ERC20 tokens from a lockbox
* @param tokenId The lockbox token ID
* @param tokenAddress The ERC20 token address
* @param amount Amount of tokens to withdraw
* @param recipient Address to receive the tokens
*/
function _withdrawERC20(
uint256 tokenId,
address tokenAddress,
uint256 amount,
address recipient
) internal {
mapping(address => uint256) storage balMap = _erc20Balances[tokenId];
unchecked {
balMap[tokenAddress] -= amount;
}
if (balMap[tokenAddress] == 0) {
delete balMap[tokenAddress];
_removeERC20Token(tokenId, tokenAddress);
}
IERC20(tokenAddress).safeTransfer(recipient, amount);
}
/**
* @dev Internal helper to withdraw an ERC721 NFT from a lockbox
* @param tokenId The lockbox token ID
* @param nftContract The ERC721 contract address
* @param nftTokenId The NFT token ID
* @param recipient Address to receive the NFT
*/
function _withdrawERC721(
uint256 tokenId,
address nftContract,
uint256 nftTokenId,
address recipient
) internal {
bytes32 key = keccak256(abi.encodePacked(nftContract, nftTokenId));
delete _lockboxNftData[tokenId][key];
_removeNFTKey(tokenId, key);
IERC721(nftContract).safeTransferFrom(address(this), recipient, nftTokenId);
}
/**
* @dev Check if a router is in the immutable allowlist.
* @param router The router address to check.
* @return bool True if the router is allowed.
*/
function _isAllowedRouter(address router) internal pure returns (bool) {
return
// Uniswap V3 SwapRouter02
router == 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 ||
// Uniswap v4 Router
router == 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af ||
// 1inch Aggregation Router v6
router == 0x111111125421cA6dc452d289314280a0f8842A65 ||
// 0x Exchange Proxy
router == 0xDef1C0ded9bec7F1a1670819833240f027b25EfF ||
// Paraswap Augustus
router == 0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57 ||
// CowSwap GPv2 Settlement
router == 0x9008D19f58AAbD9eD0D60971565AA8510560ab41;
}
/**
* @dev Check if the calldata selector is allowed for swap operations.
* Prevents arbitrary function calls by whitelisting safe swap selectors.
* @param data The calldata to validate
* @return bool True if the selector is allowed for swaps
*/
function _isAllowedSelector(bytes calldata data) private pure returns (bool) {
if (data.length < 4) return false;
bytes4 selector = bytes4(data[:4]);
return
// Uniswap V3 Router02
selector == 0x04e45aaf || // exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))
selector == 0x5023b4df || // exactOutputSingle((address,address,uint24,address,uint256,uint256,uint160))
selector == 0xc04b8d59 || // exactInput((bytes,address,uint256,uint256))
selector == 0xf28c0498 || // exactOutput((bytes,address,uint256,uint256))
selector == 0xac9650d8 || // multicall(bytes[])
selector == 0x5ae401dc || // multicall(uint256,bytes[])
selector == 0x49404b7c || // unwrapWETH9(uint256,address)
selector == 0x12210e8a || // refundETH()
// Uniswap v4 Router
selector == 0x3593564c || // execute(bytes,bytes[],uint256)
selector == 0x24856bc3 || // execute(bytes,bytes[])
// 1inch v6
selector == 0x07ed2379 || // swap(address,(...),bytes)
// 0x Protocol (Exchange Proxy)
selector == 0x415565b0 || // transformERC20
selector == 0xd9627aa4 || // sellToUniswap
// Paraswap Augustus
selector == 0x54e3f31b || // simpleSwap
selector == 0xa94e78ef || // multiSwap
// CowSwap GPv2 Settlement
selector == 0x13d79a0b; // settle
}
/**
* @notice Get list of all allowed routers (for transparency).
* @return address[] Array of allowed router addresses.
*/
function getAllowedRouters() external pure returns (address[] memory) {
address[] memory routers = new address[](6);
routers[0] = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; // Uniswap V3 SwapRouter02
routers[1] = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af; // Uniswap v4 Router
routers[2] = 0x111111125421cA6dc452d289314280a0f8842A65; // 1inch v6
routers[3] = 0xDef1C0ded9bec7F1a1670819833240f027b25EfF; // 0x
routers[4] = 0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57; // Paraswap
routers[5] = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; // Cowswap
return routers;
}
/**
* @notice Check if a router is allowed (public helper).
* @param router The router address to check.
* @return bool True if the router is allowed.
*/
function isAllowedRouter(address router) external pure returns (bool) {
return _isAllowedRouter(router);
}
}
"
},
"@openzeppelin/contracts/utils/Strings.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (utils/Strings.sol)
pragma solidity ^0.8.20;
import {Math} from "./math/Math.sol";
import {SafeCast} from "./math/SafeCast.sol";
import {SignedMath} from "./math/SignedMath.sol";
/**
* @dev String operations.
*/
library Strings {
using SafeCast for *;
bytes16 private constant HEX_DIGITS = "0123456789abcdef";
uint8 private constant ADDRESS_LENGTH = 20;
uint256 private constant SPECIAL_CHARS_LOOKUP =
(1 << 0x08) | // backspace
(1 << 0x09) | // tab
(1 << 0x0a) | // newline
(1 << 0x0c) | // form feed
(1 << 0x0d) | // carriage return
(1 << 0x22) | // double quote
(1 << 0x5c); // backslash
/**
* @dev The `value` string doesn't fit in the specified `length`.
*/
error StringsInsufficientHexLength(uint256 value, uint256 length);
/**
* @dev The string being parsed contains characters that are not in scope of the given base.
*/
error StringsInvalidChar();
/**
* @dev The string being parsed is not a properly formatted address.
*/
error StringsInvalidAddressFormat();
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = Math.log10(value) + 1;
string memory buffer = new string(length);
uint256 ptr;
assembly ("memory-safe") {
ptr := add(add(buffer, 0x20), length)
}
while (true) {
ptr--;
assembly ("memory-safe") {
mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
}
/**
* @dev Converts a `int256` to its ASCII `string` decimal representation.
*/
function toStringSigned(int256 value) internal pure returns (string memory) {
return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value)));
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
unchecked {
return toHexString(value, Math.log256(value) + 1);
}
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
uint256 localValue = value;
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = HEX_DIGITS[localValue & 0xf];
localValue >>= 4;
}
if (localValue != 0) {
revert StringsInsufficientHexLength(value, length);
}
return string(buffer);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal
* representation.
*/
function toHexString(address addr) internal pure returns (string memory) {
return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its checksummed ASCII `string` hexadecimal
* representation, according to EIP-55.
*/
function toChecksumHexString(address addr) internal pure returns (string memory) {
bytes memory buffer = bytes(toHexString(addr));
// hash the hex part of buffer (skip length + 2 bytes, length 40)
uint256 hashValue;
assembly ("memory-safe") {
hashValue := shr(96, keccak256(add(buffer, 0x22), 40))
}
for (uint256 i = 41; i > 1; --i) {
// possible values for buffer[i] are 48 (0) to 57 (9) and 97 (a) to 102 (f)
if (hashValue & 0xf > 7 && uint8(buffer[i]) > 96) {
// case shift by xoring with 0x20
buffer[i] ^= 0x20;
}
hashValue >>= 4;
}
return string(buffer);
}
/**
* @dev Returns true if the two strings are equal.
*/
function equal(string memory a, string memory b) internal pure returns (bool) {
return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b));
}
/**
* @dev Parse a decimal string and returns the value as a `uint256`.
*
* Requirements:
* - The string must be formatted as `[0-9]*`
* - The result must fit into an `uint256` type
*/
function parseUint(string memory input) internal pure returns (uint256) {
return parseUint(input, 0, bytes(input).length);
}
/**
* @dev Variant of {parseUint-string} that parses a substring of `input` located between position `begin` (included) and
* `end` (excluded).
*
* Requirements:
* - The substring must be formatted as `[0-9]*`
* - The result must fit into an `uint256` type
*/
Submitted on: 2025-10-09 09:13:00
Comments
Log in to comment.
No comments yet.