// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.25;
import { FixedPointMathLib } from "@solmate/utils/FixedPointMathLib.sol";
import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol";
import { ERC20 } from "@solmate/tokens/ERC20.sol";
import { ReentrancyGuard } from "@solmate/utils/ReentrancyGuard.sol";
import { IAtomicSolver } from "./IAtomicSolver.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title AtomicQueueUCP
* @notice Allows users to create `AtomicRequests` that specify an ERC20 asset to `offer`
* and an ERC20 asset to `want` in return.
* @notice Making atomic requests where the exchange rate between offer and want is not
* relatively stable is effectively the same as placing a limit order between
* those assets, so requests can be filled at a rate worse than the current market rate.
* @notice It is possible for a user to make multiple requests that use the same offer asset.
* If this is done it is important that the user has approved the queue to spend the
* total amount of assets aggregated from all their requests, and to also have enough
* `offer` asset to cover the aggregate total request of `offerAmount`.
* @custom:security-contact security@molecularlabs.io
*/
contract AtomicQueueUCP is ReentrancyGuard, Ownable {
using SafeTransferLib for ERC20;
using FixedPointMathLib for uint256;
// ========================================= STRUCTS =========================================
/**
* @notice Stores request information needed to fulfill a users atomic request.
* @param deadline unix timestamp for when request is no longer valid
* @param atomicPrice the price in terms of `want` asset the user wants their `offer` assets "sold" at
* @dev atomicPrice MUST be in terms of `want` asset decimals.
* @param offerAmount the amount of `offer` asset the user wants converted to `want` asset
* @param inSolve bool used during solves to prevent duplicate users, and to prevent redoing multiple checks
*/
struct AtomicRequest {
uint64 deadline; // Timestamp when request expires
uint88 atomicPrice; // User's limit price in want asset decimals
uint96 offerAmount; // Amount of offer asset to sell
bool inSolve; // Prevents double-processing in solve
}
/**
* @notice Used in `viewSolveMetaData` helper function to return data in a clean struct.
* @param user the address of the user
* @param flags 8 bits indicating the state of the user. Multiple flags can be set simultaneously.
* Each bit represents a different error condition:
* From right to left:
* - 0: indicates user deadline has passed
* - 1: indicates user request has zero offer amount
* - 2: indicates user does not have enough offer asset in wallet
* - 3: indicates user has not given AtomicQueue approval
* - 4: indicates user's atomic price is above clearing price
* A value of 0 means no errors (user is solvable).
* @param assetsToOffer the amount of offer asset to solve
* @param assetsForWant the amount of assets users want for their offer assets
*/
struct SolveMetaData {
address user; // User's address
uint8 flags; // Bitfield for various error conditions
uint256 assetsToOffer; // Amount of offer asset from this user
uint256 assetsForWant; // Amount of want asset for this user
}
// ========================================= ERRORS =========================================
error AtomicQueue__UserRepeated(address user);
error AtomicQueue__RequestDeadlineExceeded(address user);
error AtomicQueue__UserNotInSolve(address user);
error AtomicQueue__ZeroOfferAmount(address user);
error AtomicQueue__PriceAboveClearing(address user);
error AtomicQueue__UnapprovedSolveCaller(address user);
// ========================================= EVENTS =========================================
event AtomicRequestUpdated(
address user,
address offerToken,
address wantToken,
uint256 amount,
uint256 deadline,
uint256 minPrice,
uint256 timestamp
);
event AtomicRequestFulfilled(
address user,
address offerToken,
address wantToken,
uint256 offerAmountSpent,
uint256 wantAmountReceived,
uint256 timestamp
);
event SolverCallerToggled(address caller, bool isApproved);
// ========================================= STORAGE =========================================
/**
* @notice Maps user address to offer asset to want asset to a AtomicRequest struct.
*/
mapping(address => mapping(ERC20 => mapping(ERC20 => AtomicRequest))) public userAtomicRequest;
mapping(address => bool) public isApprovedSolveCaller;
constructor(address _owner, address[] memory approvedSolveCallers) Ownable(_owner) {
for (uint256 i; i < approvedSolveCallers.length; ++i) {
isApprovedSolveCaller[approvedSolveCallers[i]] = true;
emit SolverCallerToggled(approvedSolveCallers[i], true);
}
}
// ========================================= OWNER FUNCTIONS =========================================
/**
* @notice Allows owner to toggle approved solve callers.
* @param solveCallers an array of addresses to toggle approval for
*/
function toggleApprovedSolveCallers(address[] memory solveCallers) external onlyOwner {
bool isApproved;
for (uint256 i; i < solveCallers.length; ++i) {
isApproved = !isApprovedSolveCaller[solveCallers[i]];
isApprovedSolveCaller[solveCallers[i]] = isApproved;
emit SolverCallerToggled(solveCallers[i], isApproved);
}
}
// ========================================= USER FUNCTIONS =========================================
/**
* @notice Get a users Atomic Request.
* @param user the address of the user to get the request for
* @param offer the ERC0 token they want to exchange for the want
* @param want the ERC20 token they want in exchange for the offer
*/
function getUserAtomicRequest(address user, ERC20 offer, ERC20 want) external view returns (AtomicRequest memory) {
return userAtomicRequest[user][offer][want];
}
/**
* @notice Helper function that returns either
* true: Withdraw request is valid.
* false: Withdraw request is not valid.
* @dev It is possible for a withdraw request to return false from this function, but using the
* request in `updateAtomicRequest` will succeed, but solvers will not be able to include
* the user in `solve` unless some other state is changed.
* @param offer the ERC0 token they want to exchange for the want
* @param user the address of the user making the request
* @param userRequest the request struct to validate
*/
function isAtomicRequestValid(
ERC20 offer,
address user,
AtomicRequest calldata userRequest
)
external
view
returns (bool)
{
// Check user has enough balance
if (userRequest.offerAmount > offer.balanceOf(user)) return false;
// Check request hasn't expired
if (block.timestamp > userRequest.deadline) return false;
// Check sufficient allowance
if (offer.allowance(user, address(this)) < userRequest.offerAmount) return false;
// Check non-zero amounts
if (userRequest.offerAmount == 0) return false;
if (userRequest.atomicPrice == 0) return false;
return true;
}
/**
* @notice Allows user to add/update their withdraw request.
* @notice It is possible for a withdraw request with a zero atomicPrice to be made, and solved.
* If this happens, users will be selling their shares for no assets in return.
* To determine a safe atomicPrice, share.previewRedeem should be used to get
* a good share price, then the user can lower it from there to make their request fill faster.
* @param offer the ERC20 token the user is offering in exchange for the want
* @param want the ERC20 token the user wants in exchange for offer
* @param userRequest the users request
*/
function updateAtomicRequest(ERC20 offer, ERC20 want, AtomicRequest calldata userRequest) external nonReentrant {
// Update user's request in storage
AtomicRequest storage request = userAtomicRequest[msg.sender][offer][want];
request.deadline = userRequest.deadline;
request.atomicPrice = userRequest.atomicPrice;
request.offerAmount = userRequest.offerAmount;
// Emit update event with full request details
emit AtomicRequestUpdated(
msg.sender,
address(offer),
address(want),
userRequest.offerAmount,
userRequest.deadline,
userRequest.atomicPrice,
block.timestamp
);
}
/**
* @notice Called by solvers in order to exchange offer asset for want asset.
* @notice Solvers are optimistically transferred the offer asset, then are required to
* approve this contract to spend enough of want assets to cover all requests.
* @dev It is very likely `solve` TXs will be front run if broadcasted to public mem pools,
* so solvers should use private mem pools.
* @param offer the ERC20 offer token to solve for
* @param want the ERC20 want token to solve for
* @param users an array of user addresses to solve for
* @param runData extra data that is passed back to solver when `finishSolve` is called
* @param solver the address to make `finishSolve` callback to
* @param clearingPrice the uniform clearing price that all requests will be settled at
*/
function solve(
ERC20 offer,
ERC20 want,
address[] calldata users,
bytes calldata runData,
address solver,
uint256 clearingPrice
)
external
nonReentrant
{
if (!isApprovedSolveCaller[msg.sender]) revert AtomicQueue__UnapprovedSolveCaller(msg.sender);
uint8 offerDecimals = offer.decimals();
(uint256 assetsToOffer, uint256 assetsForWant) =
_handleFirstLoop(offer, want, users, clearingPrice, solver, offerDecimals);
IAtomicSolver(solver).finishSolve(runData, msg.sender, offer, want, assetsToOffer, assetsForWant);
_handleSecondLoop(offer, want, users, clearingPrice, solver, offerDecimals);
}
function _handleFirstLoop(
ERC20 offer,
ERC20 want,
address[] calldata users,
uint256 clearingPrice,
address solver,
uint8 offerDecimals
)
internal
returns (uint256 assetsToOffer, uint256 assetsForWant)
{
for (uint256 i = users.length; i > 0;) {
unchecked {
--i;
}
AtomicRequest memory request = _firstLoopHelper(users[i], offer, want, clearingPrice, solver);
assetsToOffer += request.offerAmount;
assetsForWant += _calculateAssetAmount(request.offerAmount, clearingPrice, offerDecimals);
}
}
function _handleSecondLoop(
ERC20 offer,
ERC20 want,
address[] calldata users,
uint256 clearingPrice,
address solver,
uint8 offerDecimals
)
internal
{
for (uint256 i = users.length; i > 0;) {
unchecked {
--i;
}
address user = users[i];
AtomicRequest storage request = userAtomicRequest[users[i]][offer][want];
bytes32 key = keccak256(abi.encode(user, offer, want));
uint256 isInSolve;
assembly {
isInSolve := tload(key)
}
if (isInSolve == 0) revert AtomicQueue__UserNotInSolve(user);
uint256 assetsToUser = _calculateAssetAmount(request.offerAmount, clearingPrice, offerDecimals);
want.safeTransferFrom(solver, user, assetsToUser);
emit AtomicRequestFulfilled(
user, address(offer), address(want), request.offerAmount, assetsToUser, block.timestamp
);
request.offerAmount = 0;
assembly {
tstore(key, 0)
}
}
}
/**
* @notice Helper function solvers can use to determine if users are solvable, and the required amounts to do so.
* @notice Repeated users are not accounted for in this setup, so if solvers have repeat users in their `users`
* array the results can be wrong.
* @dev Since a user can have multiple requests with the same offer asset but different want asset, it is
* possible for `viewSolveMetaData` to report no errors, but for a solve to fail, if any solves were done
* between the time `viewSolveMetaData` and before `solve` is called.
* @param offer the ERC20 offer token to check for solvability
* @param want the ERC20 want token to check for solvability
* @param users an array of user addresses to check for solvability
* @param clearingPrice the uniform clearing price to check requests against
*/
function viewSolveMetaData(
ERC20 offer,
ERC20 want,
address[] calldata users,
uint256 clearingPrice
)
external
view
returns (SolveMetaData[] memory metaData, uint256 totalAssetsForWant, uint256 totalAssetsToOffer)
{
// Cache decimals
uint8 offerDecimals = offer.decimals();
// Initialize return array
metaData = new SolveMetaData[](users.length);
// Check each user's request
for (uint256 i; i < users.length; ++i) {
AtomicRequest memory request = userAtomicRequest[users[i]][offer][want];
metaData[i].user = users[i];
// Set appropriate error flags
if (block.timestamp > request.deadline) {
metaData[i].flags |= uint8(1);
}
if (request.offerAmount == 0) {
metaData[i].flags |= uint8(1) << 1;
}
if (offer.balanceOf(users[i]) < request.offerAmount) {
metaData[i].flags |= uint8(1) << 2;
}
if (offer.allowance(users[i], address(this)) < request.offerAmount) {
metaData[i].flags |= uint8(1) << 3;
}
if (request.atomicPrice > clearingPrice) {
metaData[i].flags |= uint8(1) << 4;
}
// Calculate amounts for this user
metaData[i].assetsToOffer = request.offerAmount;
metaData[i].assetsForWant = _calculateAssetAmount(request.offerAmount, clearingPrice, offerDecimals);
// If no errors, add to totals
if (metaData[i].flags == 0) {
totalAssetsForWant += metaData[i].assetsForWant;
totalAssetsToOffer += request.offerAmount;
}
}
}
/**
* @notice Helper function to calculate the amount of want assets a users wants in exchange for
* `offerAmount` of offer asset.
*/
function _calculateAssetAmount(
uint256 offerAmount,
uint256 clearingPrice,
uint8 offerDecimals
)
internal
pure
returns (uint256)
{
return clearingPrice.mulDivDown(offerAmount, 10 ** offerDecimals);
}
function _firstLoopHelper(
address user,
ERC20 offer,
ERC20 want,
uint256 clearingPrice,
address solver
)
internal
returns (AtomicRequest memory request)
{
request = userAtomicRequest[user][offer][want];
bytes32 key = keccak256(abi.encode(user, offer, want));
uint256 isInSolve;
assembly {
isInSolve := tload(key)
}
if (isInSolve == 1) revert AtomicQueue__UserRepeated(user);
if (block.timestamp > request.deadline) revert AtomicQueue__RequestDeadlineExceeded(user);
if (request.offerAmount == 0) revert AtomicQueue__ZeroOfferAmount(user);
if (request.atomicPrice > clearingPrice) revert AtomicQueue__PriceAboveClearing(user);
assembly {
tstore(key, 1)
}
offer.safeTransferFrom(user, solver, request.offerAmount);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
/*//////////////////////////////////////////////////////////////
METADATA STORAGE
//////////////////////////////////////////////////////////////*/
string public name;
string public symbol;
uint8 public immutable decimals;
/*//////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/*//////////////////////////////////////////////////////////////
EIP-2612 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
}
/*//////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
function approve(address spender, uint256 amount) public virtual returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) public virtual returns (bool) {
balanceOf[msg.sender] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
/*//////////////////////////////////////////////////////////////
EIP-2612 LOGIC
//////////////////////////////////////////////////////////////*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// Unchecked because the only math done is incrementing
// the owner's nonce which cannot realistically overflow.
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
}
function computeDomainSeparator() internal view virtual returns (bytes32) {
return
keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256("1"),
block.chainid,
address(this)
)
);
}
/*//////////////////////////////////////////////////////////////
INTERNAL MINT/BURN LOGIC
//////////////////////////////////////////////////////////////*/
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal virtual {
balanceOf[from] -= amount;
// Cannot underflow because a user's balance
// will never be larger than the total supply.
unchecked {
totalSupply -= amount;
}
emit Transfer(from, address(0), amount);
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Arithmetic library with operations for fixed-point numbers.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol)
/// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol)
library FixedPointMathLib {
/*//////////////////////////////////////////////////////////////
SIMPLIFIED FIXED POINT OPERATIONS
//////////////////////////////////////////////////////////////*/
uint256 internal constant MAX_UINT256 = 2**256 - 1;
uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s.
function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down.
}
function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up.
}
function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down.
}
function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up.
}
/*//////////////////////////////////////////////////////////////
LOW LEVEL FIXED POINT OPERATIONS
//////////////////////////////////////////////////////////////*/
function mulDivDown(
uint256 x,
uint256 y,
uint256 denominator
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
revert(0, 0)
}
// Divide x * y by the denominator.
z := div(mul(x, y), denominator)
}
}
function mulDivUp(
uint256 x,
uint256 y,
uint256 denominator
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
revert(0, 0)
}
// If x * y modulo the denominator is strictly greater than 0,
// 1 is added to round up the division of x * y by the denominator.
z := add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator))
}
}
function rpow(
uint256 x,
uint256 n,
uint256 scalar
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
switch x
case 0 {
switch n
case 0 {
// 0 ** 0 = 1
z := scalar
}
default {
// 0 ** n = 0
z := 0
}
}
default {
switch mod(n, 2)
case 0 {
// If n is even, store scalar in z for now.
z := scalar
}
default {
// If n is odd, store x in z for now.
z := x
}
// Shifting right by 1 is like dividing by 2.
let half := shr(1, scalar)
for {
// Shift n right by 1 before looping to halve it.
n := shr(1, n)
} n {
// Shift n right by 1 each iteration to halve it.
n := shr(1, n)
} {
// Revert immediately if x ** 2 would overflow.
// Equivalent to iszero(eq(div(xx, x), x)) here.
if shr(128, x) {
revert(0, 0)
}
// Store x squared.
let xx := mul(x, x)
// Round to the nearest number.
let xxRound := add(xx, half)
// Revert if xx + half overflowed.
if lt(xxRound, xx) {
revert(0, 0)
}
// Set x to scaled xxRound.
x := div(xxRound, scalar)
// If n is even:
if mod(n, 2) {
// Compute z * x.
let zx := mul(z, x)
// If z * x overflowed:
if iszero(eq(div(zx, x), z)) {
// Revert if x is non-zero.
if iszero(iszero(x)) {
revert(0, 0)
}
}
// Round to the nearest number.
let zxRound := add(zx, half)
// Revert if zx + half overflowed.
if lt(zxRound, zx) {
revert(0, 0)
}
// Return properly scaled zxRound.
z := div(zxRound, scalar)
}
}
}
}
}
/*//////////////////////////////////////////////////////////////
GENERAL NUMBER UTILITIES
//////////////////////////////////////////////////////////////*/
function sqrt(uint256 x) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
let y := x // We start y at x, which will help us make our initial estimate.
z := 181 // The "correct" value is 1, but this saves a multiplication later.
// This segment is to get a reasonable initial estimate for the Babylonian method. With a bad
// start, the correct # of bits increases ~linearly each iteration instead of ~quadratically.
// We check y >= 2^(k + 8) but shift right by k bits
// each branch to ensure that if x >= 256, then y >= 256.
if iszero(lt(y, 0x10000000000000000000000000000000000)) {
y := shr(128, y)
z := shl(64, z)
}
if iszero(lt(y, 0x1000000000000000000)) {
y := shr(64, y)
z := shl(32, z)
}
if iszero(lt(y, 0x10000000000)) {
y := shr(32, y)
z := shl(16, z)
}
if iszero(lt(y, 0x1000000)) {
y := shr(16, y)
z := shl(8, z)
}
// Goal was to get z*z*y within a small factor of x. More iterations could
// get y in a tighter range. Currently, we will have y in [256, 256*2^16).
// We ensured y >= 256 so that the relative difference between y and y+1 is small.
// That's not possible if x < 256 but we can just verify those cases exhaustively.
// Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256.
// Correctness can be checked exhaustively for x < 256, so we assume y >= 256.
// Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps.
// For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range
// (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256.
// Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate
// sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18.
// There is no overflow risk here since y < 2^136 after the first branch above.
z := shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181.
// Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough.
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
// If x+1 is a perfect square, the Babylonian method cycles between
// floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor.
// See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division
// Since the ceil is rare, we save gas on the assignment and repeat division in the rare case.
// If you don't care whether the floor or ceil square root is returned, you can remove this statement.
z := sub(z, lt(div(x, z), z))
}
}
function unsafeMod(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Mod x by y. Note this will return
// 0 instead of reverting if y is zero.
z := mod(x, y)
}
}
function unsafeDiv(uint256 x, uint256 y) internal pure returns (uint256 r) {
/// @solidity memory-safe-assembly
assembly {
// Divide x by y. Note this will return
// 0 instead of reverting if y is zero.
r := div(x, y)
}
}
function unsafeDivUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Add 1 to x * y if x % y > 0. Note this will
// return 0 instead of reverting if y is zero.
z := add(gt(mod(x, y), 0), div(x, y))
}
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.8.0;
import { ERC20 } from "@solmate/tokens/ERC20.sol";
interface IAtomicSolver {
/**
* @notice This function must be implemented in order for an address to be a `solver`
* for the AtomicQueue
* @param runData arbitrary bytes data that is dependent on how each solver is setup
* it could contain swap data, or flash loan data, etc..
* @param initiator the address that initiated a solve
* @param offer the ERC20 asset sent to the solver
* @param want the ERC20 asset the solver must approve the queue for
* @param assetsToOffer the amount of `offer` sent to the solver
* @param assetsForWant the amount of `want` the solver must approve the queue for
*/
function finishSolve(
bytes calldata runData,
address initiator,
ERC20 offer,
ERC20 want,
uint256 assetsToOffer,
uint256 assetsForWant
)
external;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Gas optimized reentrancy protection for smart contracts.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/ReentrancyGuard.sol)
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol)
abstract contract ReentrancyGuard {
uint256 private locked = 1;
modifier nonReentrant() virtual {
require(locked == 1, "REENTRANCY");
locked = 2;
_;
locked = 1;
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
import {ERC20} from "../tokens/ERC20.sol";
/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer.
/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.
library SafeTransferLib {
/*//////////////////////////////////////////////////////////////
ETH OPERATIONS
//////////////////////////////////////////////////////////////*/
function safeTransferETH(address to, uint256 amount) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Transfer the ETH and store if it succeeded or not.
success := call(gas(), to, amount, 0, 0, 0, 0)
}
require(success, "ETH_TRANSFER_FAILED");
}
/*//////////////////////////////////////////////////////////////
ERC20 OPERATIONS
//////////////////////////////////////////////////////////////*/
function safeTransferFrom(
ERC20 token,
address from,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "from" argument.
mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 100 because the length of our calldata totals up like so: 4 + 32 * 3.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 100, 0, 32)
)
}
require(success, "TRANSFER_FROM_FAILED");
}
function safeTransfer(
ERC20 token,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
)
}
require(success, "TRANSFER_FAILED");
}
function safeApprove(
ERC20 token,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
)
}
require(success, "APPROVE_FAILED");
}
}
{
"compilationTarget": {
"src/atomic-queue/AtomicQueueUCP.sol": "AtomicQueueUCP"
},
"evmVersion": "cancun",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": [
":1inch-v2-contracts/=lib/1inch-v2-contracts/contracts/",
":@axelar-network/=node_modules/@axelar-network/",
":@balancer-labs/v2-interfaces/=lib/nucleus-boring-vault/lib/ion-protocol/lib/balancer-v2-monorepo/pkg/interfaces/",
":@balancer-labs/v2-pool-stable/=lib/nucleus-boring-vault/lib/ion-protocol/lib/balancer-v2-monorepo/pkg/pool-stable/",
":@chainlink/=node_modules/@chainlink/",
":@chainlink/contracts/=lib/nucleus-boring-vault/lib/ion-protocol/lib/chainlink/contracts/",
":@ds-test/=lib/forge-std/lib/ds-test/src/",
":@eth-optimism/=node_modules/@eth-optimism/",
":@executooor/=lib/executooor/contracts/",
":@forge-std/=lib/forge-std/src/",
":@ion-protocol/=lib/nucleus-boring-vault/lib/ion-protocol/src/",
":@layerzerolabs/=node_modules/@layerzerolabs/",
":@openzeppelin/=lib/openzeppelin-contracts/",
":@openzeppelin/contracts-upgradeable/=lib/nucleus-boring-vault/lib/ion-protocol/lib/openzeppelin-contracts-upgradeable/contracts/",
":@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
":@solmate/=lib/solmate/src/",
":@uniswap-core/=lib/v3-core/contracts/",
":@uniswap-periphery/=lib/v3-periphery/contracts/",
":@uniswap/v3-core/=lib/nucleus-boring-vault/lib/ion-protocol/lib/v3-core/",
":@uniswap/v3-periphery/=lib/nucleus-boring-vault/lib/ion-protocol/lib/v3-periphery/",
":balancer-v2-monorepo/=lib/nucleus-boring-vault/lib/ion-protocol/lib/",
":chainlink/=lib/nucleus-boring-vault/lib/ion-protocol/lib/chainlink/",
":createx/=lib/nucleus-boring-vault/lib/createx/src/",
":ds-test/=lib/forge-std/lib/ds-test/src/",
":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/",
":executooor/=lib/executooor/contracts/",
":forge-safe/=lib/nucleus-boring-vault/lib/ion-protocol/lib/forge-safe/",
":forge-std/=lib/forge-std/src/",
":halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/",
":hardhat-deploy/=node_modules/hardhat-deploy/",
":ion-protocol/=lib/nucleus-boring-vault/lib/ion-protocol/",
":nucleus-boring-vault/=lib/nucleus-boring-vault/",
":openzeppelin-contracts-upgradeable/=lib/nucleus-boring-vault/lib/ion-protocol/lib/openzeppelin-contracts-upgradeable/",
":openzeppelin-contracts/=lib/openzeppelin-contracts/",
":openzeppelin/=lib/nucleus-boring-vault/lib/createx/lib/openzeppelin-contracts/contracts/",
":pendle-core-v2-public/=lib/nucleus-boring-vault/lib/ion-protocol/lib/pendle-core-v2-public/contracts/",
":solady/=lib/nucleus-boring-vault/lib/ion-protocol/lib/solady/",
":solarray/=lib/nucleus-boring-vault/lib/ion-protocol/lib/solarray/src/",
":solidity-bytes-utils/=node_modules/solidity-bytes-utils/",
":solidity-stringutils/=lib/nucleus-boring-vault/lib/ion-protocol/lib/forge-safe/lib/surl/lib/solidity-stringutils/",
":solmate/=lib/solmate/src/",
":surl/=lib/nucleus-boring-vault/lib/ion-protocol/lib/forge-safe/lib/surl/",
":v3-core/=lib/v3-core/",
":v3-periphery/=lib/v3-periphery/contracts/"
],
"viaIR": true
}
[{"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address[]","name":"approvedSolveCallers","type":"address[]"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"AtomicQueue__PriceAboveClearing","type":"error"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"AtomicQueue__RequestDeadlineExceeded","type":"error"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"AtomicQueue__UnapprovedSolveCaller","type":"error"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"AtomicQueue__UserNotInSolve","type":"error"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"AtomicQueue__UserRepeated","type":"error"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"AtomicQueue__ZeroOfferAmount","type":"error"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"OwnableInvalidOwner","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"OwnableUnauthorizedAccount","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"address","name":"offerToken","type":"address"},{"indexed":false,"internalType":"address","name":"wantToken","type":"address"},{"indexed":false,"internalType":"uint256","name":"offerAmountSpent","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"wantAmountReceived","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"AtomicRequestFulfilled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"address","name":"offerToken","type":"address"},{"indexed":false,"internalType":"address","name":"wantToken","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"deadline","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"minPrice","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"AtomicRequestUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"caller","type":"address"},{"indexed":false,"internalType":"bool","name":"isApproved","type":"bool"}],"name":"SolverCallerToggled","type":"event"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"contract ERC20","name":"offer","type":"address"},{"internalType":"contract ERC20","name":"want","type":"address"}],"name":"getUserAtomicRequest","outputs":[{"components":[{"internalType":"uint64","name":"deadline","type":"uint64"},{"internalType":"uint88","name":"atomicPrice","type":"uint88"},{"internalType":"uint96","name":"offerAmount","type":"uint96"},{"internalType":"bool","name":"inSolve","type":"bool"}],"internalType":"struct AtomicQueueUCP.AtomicRequest","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"isApprovedSolveCaller","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract ERC20","name":"offer","type":"address"},{"internalType":"address","name":"user","type":"address"},{"components":[{"internalType":"uint64","name":"deadline","type":"uint64"},{"internalType":"uint88","name":"atomicPrice","type":"uint88"},{"internalType":"uint96","name":"offerAmount","type":"uint96"},{"internalType":"bool","name":"inSolve","type":"bool"}],"internalType":"struct AtomicQueueUCP.AtomicRequest","name":"userRequest","type":"tuple"}],"name":"isAtomicRequestValid","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract ERC20","name":"offer","type":"address"},{"internalType":"contract ERC20","name":"want","type":"address"},{"internalType":"address[]","name":"users","type":"address[]"},{"internalType":"bytes","name":"runData","type":"bytes"},{"internalType":"address","name":"solver","type":"address"},{"internalType":"uint256","name":"clearingPrice","type":"uint256"}],"name":"solve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"solveCallers","type":"address[]"}],"name":"toggleApprovedSolveCallers","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract ERC20","name":"offer","type":"address"},{"internalType":"contract ERC20","name":"want","type":"address"},{"components":[{"internalType":"uint64","name":"deadline","type":"uint64"},{"internalType":"uint88","name":"atomicPrice","type":"uint88"},{"internalType":"uint96","name":"offerAmount","type":"uint96"},{"internalType":"bool","name":"inSolve","type":"bool"}],"internalType":"struct AtomicQueueUCP.AtomicRequest","name":"userRequest","type":"tuple"}],"name":"updateAtomicRequest","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"contract ERC20","name":"","type":"address"},{"internalType":"contract ERC20","name":"","type":"address"}],"name":"userAtomicRequest","outputs":[{"internalType":"uint64","name":"deadline","type":"uint64"},{"internalType":"uint88","name":"atomicPrice","type":"uint88"},{"internalType":"uint96","name":"offerAmount","type":"uint96"},{"internalType":"bool","name":"inSolve","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract ERC20","name":"offer","type":"address"},{"internalType":"contract ERC20","name":"want","type":"address"},{"internalType":"address[]","name":"users","type":"address[]"},{"internalType":"uint256","name":"clearingPrice","type":"uint256"}],"name":"viewSolveMetaData","outputs":[{"components":[{"internalType":"address","name":"user","type":"address"},{"internalType":"uint8","name":"flags","type":"uint8"},{"internalType":"uint256","name":"assetsToOffer","type":"uint256"},{"internalType":"uint256","name":"assetsForWant","type":"uint256"}],"internalType":"struct AtomicQueueUCP.SolveMetaData[]","name":"metaData","type":"tuple[]"},{"internalType":"uint256","name":"totalAssetsForWant","type":"uint256"},{"internalType":"uint256","name":"totalAssetsToOffer","type":"uint256"}],"stateMutability":"view","type":"function"}]